mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 07:46:24 +00:00
Document user accounts: README section, config table, changelog, .env.example
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ws7xj5Ej623hh4GXQCYYR
This commit is contained in:
+24
-1
@@ -5,9 +5,32 @@
|
|||||||
# Postgres: postgresql+psycopg://user:pass@host:5432/handler
|
# Postgres: postgresql+psycopg://user:pass@host:5432/handler
|
||||||
DATABASE_URL=sqlite:////var/lib/handler/handler.db
|
DATABASE_URL=sqlite:////var/lib/handler/handler.db
|
||||||
|
|
||||||
# Single global bearer token gating every API route. Required for the API to start.
|
# Legacy/machine bearer token gating every API route. Human operators now sign in with
|
||||||
|
# email + password (user accounts — first sign-up becomes the admin); this token remains
|
||||||
|
# for scripts/CI and as a break-glass credential. Optional once accounts exist.
|
||||||
AUTH_TOKEN=change-me-to-a-long-random-string
|
AUTH_TOKEN=change-me-to-a-long-random-string
|
||||||
|
|
||||||
|
# ---- User accounts (email + password sign-in for the dashboard/API) ----
|
||||||
|
# Browser session lifetime and one-shot link validity. Defaults shown.
|
||||||
|
# SESSION_TTL_DAYS=30
|
||||||
|
# RESET_TOKEN_TTL_HOURS=2
|
||||||
|
# INVITE_TOKEN_TTL_HOURS=168
|
||||||
|
|
||||||
|
# Outbound email for invite + password-reset links. Leave SMTP_HOST unset to run without
|
||||||
|
# email: invite/reset links are then shown to the admin in the dashboard instead of
|
||||||
|
# mailed, and self-serve "forgot password" tells users to ask an admin.
|
||||||
|
# SMTP_HOST=smtp.example.com
|
||||||
|
# SMTP_PORT=587
|
||||||
|
# SMTP_USERNAME=
|
||||||
|
# SMTP_PASSWORD=
|
||||||
|
# SMTP_FROM=handler@example.com
|
||||||
|
# SMTP_STARTTLS=true # STARTTLS on port 587 (the common setup)
|
||||||
|
# SMTP_SSL=false # implicit TLS on port 465 instead
|
||||||
|
|
||||||
|
# Base URL the emailed links point at, e.g. https://handler.example.com. Falls back to
|
||||||
|
# each request's own origin when unset (right for the same-origin bundled UI).
|
||||||
|
# PUBLIC_BASE_URL=
|
||||||
|
|
||||||
# Optional higher-trust token gating PUT /shared/context/:key.
|
# Optional higher-trust token gating PUT /shared/context/:key.
|
||||||
# Falls back to AUTH_TOKEN if unset.
|
# Falls back to AUTH_TOKEN if unset.
|
||||||
# SHARED_CONTEXT_WRITE_TOKEN=
|
# SHARED_CONTEXT_WRITE_TOKEN=
|
||||||
|
|||||||
@@ -6,6 +6,52 @@ the image workflows publish (plus `latest` from every push to `main`).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added — user accounts: email sign-in, invites, resets, per-user separation
|
||||||
|
|
||||||
|
- **Email + password accounts** replace "know the API key" for humans. First run shows
|
||||||
|
a setup form and the **first account created is the admin**; every later account is
|
||||||
|
**invited by an admin** through a one-shot set-password link. Passwords are scrypt
|
||||||
|
(stdlib, self-describing hashes); sessions are opaque bearer tokens stored only as
|
||||||
|
SHA-256 with a configurable TTL.
|
||||||
|
- **Password reset by email** (`POST /auth/forgot` → short-lived link, silent about
|
||||||
|
account existence) via plain SMTP (`SMTP_*` settings). **Email is optional**: with
|
||||||
|
SMTP unset, invite/reset links are shown to the admin in the dashboard to hand over
|
||||||
|
out-of-band. Spending a link revokes the account's existing sessions.
|
||||||
|
- **Per-user separation of projects, skills, and tools.** Projects, skills, MCP
|
||||||
|
connectors, plugins, and model backends gain an owner; users see **shared + their
|
||||||
|
own** (foreign resources 404 — existence isn't leaked), owners operate their own
|
||||||
|
projects end-to-end without admin, shared (unowned) rows stay admin-managed and
|
||||||
|
visible to all. Launches materialize only the project owner's skills/connectors, and
|
||||||
|
private model backends can't be picked for someone else's spawns or schedules.
|
||||||
|
Deleting a user reassigns their resources to shared; admins can reassign owners.
|
||||||
|
- **Users page** in the dashboard (admin-only): invite, admin/disable toggles, reset
|
||||||
|
links, delete. Sign-in page gains first-run setup, forgot-password, and a raw
|
||||||
|
API-token fallback; `/reset` is the public landing page for invite/reset links.
|
||||||
|
- **Admin safety rails**: the last active admin can't be demoted/disabled/deleted; no
|
||||||
|
self-deletion. `AUTH_TOKEN`/`ADMIN_TOKEN`/`SHARED_CONTEXT_WRITE_TOKEN` keep their
|
||||||
|
exact historical semantics for scripts/CI and break-glass.
|
||||||
|
- 24 new tests (auth flows + separation matrix; 397 total).
|
||||||
|
|
||||||
|
### Database (user accounts)
|
||||||
|
|
||||||
|
- Migration **`0016_user_accounts`**: new `users`, `auth_sessions`, `auth_tokens`
|
||||||
|
tables plus a nullable `owner_user_id` on `projects`, `claude_skills`,
|
||||||
|
`claude_connectors`, `claude_plugins`, `claude_models`. Purely additive; existing
|
||||||
|
rows have no owner (= shared) so an upgraded deployment behaves exactly as before
|
||||||
|
until accounts are created.
|
||||||
|
|
||||||
|
### Deployment notes (user accounts rollout)
|
||||||
|
|
||||||
|
1. Apply migrations as usual (the API container runs them on start).
|
||||||
|
2. Optionally set `SMTP_HOST`/`SMTP_PORT`/`SMTP_USERNAME`/`SMTP_PASSWORD`/`SMTP_FROM`
|
||||||
|
(+ `SMTP_STARTTLS`/`SMTP_SSL`) and `PUBLIC_BASE_URL` for emailed links; without
|
||||||
|
them, invite/reset links appear in the dashboard instead.
|
||||||
|
3. Open the dashboard and create the first account — it becomes the admin. Existing
|
||||||
|
`AUTH_TOKEN`-based scripts keep working unchanged; the token can be rotated or
|
||||||
|
dropped once accounts exist (keep one as break-glass if you like).
|
||||||
|
4. New TTL knobs (optional): `SESSION_TTL_DAYS=30`, `RESET_TOKEN_TTL_HOURS=2`,
|
||||||
|
`INVITE_TOKEN_TTL_HOURS=168`.
|
||||||
|
|
||||||
### Added — the pi harness for local models ([#29](https://github.com/0xWheatyz/handler/pull/29))
|
### Added — the pi harness for local models ([#29](https://github.com/0xWheatyz/handler/pull/29))
|
||||||
|
|
||||||
- **`harness` on model backends** (`claude` | `pi`, default `claude`). A backend row can
|
- **`harness` on model backends** (`claude` | `pi`, default `claude`). A backend row can
|
||||||
|
|||||||
@@ -126,9 +126,12 @@ Configuration is entirely environment-driven (see [`.env.example`](.env.example)
|
|||||||
| Variable | Purpose | Default |
|
| Variable | Purpose | Default |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `DATABASE_URL` | `sqlite:////abs/path.db` or `postgresql+psycopg://…` | `sqlite:///./handler.db` |
|
| `DATABASE_URL` | `sqlite:////abs/path.db` or `postgresql+psycopg://…` | `sqlite:///./handler.db` |
|
||||||
| `AUTH_TOKEN` | Global bearer token gating every API route | *(required for the API)* |
|
| `AUTH_TOKEN` | Legacy/machine bearer token (scripts, CI, break-glass) — humans sign in with email + password instead ([user accounts](#user-accounts--sign-in)) | unset → env-token auth off |
|
||||||
| `SHARED_CONTEXT_WRITE_TOKEN` | Higher-trust token gating `PUT /shared/context/:key` | falls back to `AUTH_TOKEN` |
|
| `SHARED_CONTEXT_WRITE_TOKEN` | Higher-trust token gating `PUT /shared/context/:key` | falls back to `AUTH_TOKEN` |
|
||||||
| `ADMIN_TOKEN` | Gates the web control surface (enqueue commands, project/host CRUD, credential edits) | falls back to `AUTH_TOKEN` |
|
| `ADMIN_TOKEN` | Admin-level env token (enqueue commands, project/host CRUD, credential edits) | falls back to `AUTH_TOKEN` |
|
||||||
|
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USERNAME` / `SMTP_PASSWORD` / `SMTP_FROM` / `SMTP_STARTTLS` / `SMTP_SSL` | Outbound email for invite + password-reset links | unset → links shown to the admin instead of mailed |
|
||||||
|
| `PUBLIC_BASE_URL` | Base URL emailed links point at | unset → the request's own origin |
|
||||||
|
| `SESSION_TTL_DAYS` / `RESET_TOKEN_TTL_HOURS` / `INVITE_TOKEN_TTL_HOURS` | Session and one-shot-link lifetimes | `30` / `2` / `168` |
|
||||||
| `WEBHOOK_URL` | Generic target for the `Notification` hook (ntfy, Slack, …) | unset → no-op |
|
| `WEBHOOK_URL` | Generic target for the `Notification` hook (ntfy, Slack, …) | unset → no-op |
|
||||||
| `SEARXNG_URL` / `BRAVE_SEARCH_API_KEY` | Provider for the agents' `web_search` tool (pi harness) | unset → DuckDuckGo fallback |
|
| `SEARXNG_URL` / `BRAVE_SEARCH_API_KEY` | Provider for the agents' `web_search` tool (pi harness) | unset → DuckDuckGo fallback |
|
||||||
| `HANDLER_SECRET_KEY` | Fernet key encrypting git-server tokens + SSH keys at rest (set the same value on API and control) | unset → secret store disabled |
|
| `HANDLER_SECRET_KEY` | Fernet key encrypting git-server tokens + SSH keys at rest (set the same value on API and control) | unset → secret store disabled |
|
||||||
@@ -204,6 +207,56 @@ docker compose run --rm control handler list
|
|||||||
docker compose run --rm control handler spawn --project leeworks-api --name junior --task "…"
|
docker compose run --rm control handler spawn --project leeworks-api --name junior --task "…"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## User accounts & sign-in
|
||||||
|
|
||||||
|
Humans no longer need to know an API key. The dashboard signs in with **email +
|
||||||
|
password**, and the accounts model is deliberately small-team-shaped:
|
||||||
|
|
||||||
|
- **First run**: with zero accounts, the sign-in page becomes a setup form. The first
|
||||||
|
account created **is the admin**. (`POST /auth/setup` refuses once any account exists.)
|
||||||
|
- **Everyone else is invited by an admin** (Users page / `POST /auth/users`): creating a
|
||||||
|
user mints a one-shot **invite link** through which the invitee sets their own
|
||||||
|
password. With SMTP configured the link is emailed; either way it is shown to the
|
||||||
|
admin, so email is optional infrastructure, not a requirement.
|
||||||
|
- **Password reset by email**: "Forgot password?" mails a short-lived reset link
|
||||||
|
(`POST /auth/forgot` — silent about whether the address exists). Without SMTP, an
|
||||||
|
admin mints a reset link from the Users page instead. Spending a link revokes every
|
||||||
|
existing session for that account.
|
||||||
|
- **Sessions** are opaque bearer tokens (only their SHA-256 is stored), sent exactly
|
||||||
|
like the old token: `Authorization: Bearer …`. `POST /auth/logout` revokes one;
|
||||||
|
changing a password revokes the rest.
|
||||||
|
- **Admin safety rails**: the last active admin can't be demoted, disabled, or deleted;
|
||||||
|
you can't delete your own account.
|
||||||
|
|
||||||
|
### Per-user separation
|
||||||
|
|
||||||
|
Every project, skill, MCP connector, plugin, and model backend is either **owned** by
|
||||||
|
one user or **shared** (no owner). The rules, everywhere:
|
||||||
|
|
||||||
|
- A user sees **shared + their own** — another user's resources don't exist for them
|
||||||
|
(listings filter, direct lookups 404, so existence isn't leaked).
|
||||||
|
- Creating a resource makes you its owner; owners manage their own resources without
|
||||||
|
admin help (spawn/kill agents, schedules, approvals, sync, memory notes — everything
|
||||||
|
project-nested follows the project's owner).
|
||||||
|
- **Shared resources are admin-managed** and behave exactly like the pre-accounts world:
|
||||||
|
visible to all, editable by admins. Legacy rows all land here on upgrade, so nothing
|
||||||
|
changes until people start owning things.
|
||||||
|
- At **launch**, an agent gets only what its project's owner can see: their skills +
|
||||||
|
connectors + the shared set. One user's tools never reach another user's agents, and
|
||||||
|
a private model backend can't be selected for someone else's spawn or schedule.
|
||||||
|
- Deleting a user **reassigns their resources to shared** (never orphans or deletes
|
||||||
|
work); an admin can also reassign a project's owner explicitly (`PATCH /projects/:p`).
|
||||||
|
- Global infrastructure stays admin-only: git servers, the Claude account login,
|
||||||
|
permission overrides, global memory notes, and user management itself.
|
||||||
|
|
||||||
|
### Legacy env tokens
|
||||||
|
|
||||||
|
`AUTH_TOKEN` / `ADMIN_TOKEN` / `SHARED_CONTEXT_WRITE_TOKEN` keep working with their
|
||||||
|
historical semantics (see-everything machine credentials; the admin token passes admin
|
||||||
|
gates). They're the right tool for scripts and CI — and the break-glass if every admin
|
||||||
|
is locked out. Resources they create are shared. The dashboard's sign-in page keeps a
|
||||||
|
"Use an API token" fallback for token-only deployments.
|
||||||
|
|
||||||
## Web management
|
## Web management
|
||||||
|
|
||||||
The dashboard (and the API under it) manages everything — git credentials & hosts,
|
The dashboard (and the API under it) manages everything — git credentials & hosts,
|
||||||
@@ -350,10 +403,19 @@ agent's identity and `DATABASE_URL` injected into its environment. `--role`
|
|||||||
|
|
||||||
## API reference
|
## API reference
|
||||||
|
|
||||||
All routes require `Authorization: Bearer <AUTH_TOKEN>`. `GET /health` is unauthenticated.
|
All routes require `Authorization: Bearer <token>` — a user session token from
|
||||||
|
`POST /auth/login` or a legacy env token. `GET /health`, `GET /auth/status`, and the
|
||||||
|
account bootstrap routes (`setup`/`login`/`forgot`/`reset`) are unauthenticated.
|
||||||
|
|
||||||
| Method & path | Purpose |
|
| Method & path | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| `GET /auth/status` | `{initialized, smtp_configured}` — drives the setup-vs-signin page |
|
||||||
|
| `POST /auth/setup` | Create the first account (becomes the admin) |
|
||||||
|
| `POST /auth/login` · `POST /auth/logout` | Session lifecycle (opaque bearer, hash-stored) |
|
||||||
|
| `GET /auth/me` · `POST /auth/change-password` | Who am I / rotate my password |
|
||||||
|
| `POST /auth/forgot` · `POST /auth/reset` | Email reset link / spend a reset or invite link |
|
||||||
|
| `GET`/`POST /auth/users` · `PATCH`/`DELETE /auth/users/:id` | Admin user management (invite links) |
|
||||||
|
| `POST /auth/users/:id/reset-link` | Admin-minted reset/invite link (the no-SMTP path) |
|
||||||
| `GET /projects` · `POST /projects` | List / register projects |
|
| `GET /projects` · `POST /projects` | List / register projects |
|
||||||
| `GET /projects/:p/agents` · `POST …` | List / register agents (project-scoped) |
|
| `GET /projects/:p/agents` · `POST …` | List / register agents (project-scoped) |
|
||||||
| `GET /projects/:p/agents/:name/checkmark` | The agent's current-state checkmark |
|
| `GET /projects/:p/agents/:name/checkmark` | The agent's current-state checkmark |
|
||||||
|
|||||||
Reference in New Issue
Block a user