feat: provision private Hermes assistants for authenticated Hope accounts
This commit is contained in:
1 parent
da65da68b3
commit
2aedea84b5
33 files changed
+3063
-1743
No files matched your search
@@ -0,0 +1,109 @@
|
||||
# Account control plane
|
||||
|
||||
Run `python -m control` with FastAPI and uvicorn installed. Defaults to
|
||||
`127.0.0.1:8092`; configure `CONTROL_HOST` / `CONTROL_PORT` explicitly if needed.
|
||||
Run one control process against a database (lifecycle scheduling is process-local).
|
||||
No Docker logic lives in the account API: production imports
|
||||
`control.provisioner.Provisioner()` (locally aliased HermesProvisioner) lazily
|
||||
on startup.
|
||||
|
||||
## Runtime settings
|
||||
|
||||
- `CONTROL_DB_PATH`: SQLite path, default `/data/control/accounts.sqlite3`.
|
||||
Mount persistent private storage. The DB contains hashes and Hermes keys;
|
||||
protect the directory and its WAL/SHM files, backups, and host access.
|
||||
- `CONTROL_ADMIN_USERNAME`: initial administrator lowercase slug, default `admin`.
|
||||
- `CONTROL_ADMIN_PASSWORD`: **required when DB is empty**, 12–256 characters;
|
||||
no built-in password. Ignored once existing accounts are present. Do not log it.
|
||||
- `CONTROL_SERVICE_SECRET`: >=32-character secret for trusted internal callers.
|
||||
Missing/short secret disables all internal access; send as `X-Control-Secret`.
|
||||
- `CONTROL_ALLOWED_ORIGINS`: exact comma-separated HTTPS browser origins.
|
||||
**Set this behind nginx**; no forwarded host/proto headers are trusted.
|
||||
|
||||
Native login sends `Content-Type: application/json` and no browser Origin.
|
||||
Browser login requires its actual Origin to match. Cookie mutations require a
|
||||
matching Origin; bearer-authenticated requests are exempt. Login is throttled
|
||||
per validated client IP and username (10 failed attempts / 5 minutes; successes
|
||||
release their reservation). Only a direct loopback peer may supply `X-Real-IP`,
|
||||
which nginx must overwrite with `$remote_addr`. Do not expose the control port
|
||||
or preserve untrusted client-supplied identity headers in another proxy.
|
||||
Opaque sessions last 24 hours, are hashed at rest and revoked by logout,
|
||||
password reset or deletion. Cookies are Secure, HttpOnly, SameSite=Strict.
|
||||
|
||||
## Public API
|
||||
|
||||
- `POST /api/auth/login` JSON `{username,password}` →
|
||||
`{token,user,expires_at}` (expiry Unix seconds) plus `hope_session` cookie.
|
||||
- `POST /api/auth/logout` → `{ok:true}`; invalid sessions return 401.
|
||||
- `GET /api/auth/me` → `{user}`.
|
||||
- Administrator-only `GET /api/admin/users` → `{users:[...]}`.
|
||||
- Administrator-only `POST /api/admin/users` JSON
|
||||
`{username,display_name,password,role?:"user"}` → 201 `{user}`.
|
||||
- Administrator-only `DELETE /api/admin/users/{id}` → 202 `{user}` in
|
||||
deleting state. Sessions/rooms revoked synchronously before return; container
|
||||
deletion is background, durable and retriable. Last administrator is protected.
|
||||
- Administrator-only `POST /api/admin/users/{id}/password` JSON `{password}` →
|
||||
`{ok:true}`; revokes all sessions and room grants.
|
||||
- Administrator-only `POST /api/admin/users/{id}/retry` → 202 `{user}`;
|
||||
retries failed provisioning or removal, preserving intended lifecycle.
|
||||
- `GET /health` → `{status:"ok"}` (control HTTP liveness, not Hermes readiness).
|
||||
|
||||
Public user is exactly `{id,username,display_name,role,status,container_name,error}`.
|
||||
IDs are stable UUID hex. Usernames match `[a-z][a-z0-9]*(?:-[a-z0-9]+)*`,
|
||||
1–48 characters. Status is `provisioning`, `ready`, `error` or `deleting`.
|
||||
Deleting failures remain deleting with a sanitized visible error, never become
|
||||
login-eligible. Background reconcile runs on startup and every 30 seconds.
|
||||
|
||||
## Trusted speech boundary
|
||||
|
||||
Every internal route requires `X-Control-Secret`, including room lookups.
|
||||
|
||||
- `POST /internal/resolve-session` JSON `{token}` validates the exact cookie or
|
||||
bearer token; returns `{user,hermes_url,hermes_api_key}` only for ready user.
|
||||
- `GET /internal/users/{id}` returns the same ready-user metadata.
|
||||
- `POST /internal/rooms` JSON `{user_id,room_name,session_token,ttl?:3600}`
|
||||
validates that session belongs to the user and persists a unique server-minted
|
||||
room grant, returning 201 `{room_name,user_id}`. TTL 1–86400 sec, capped at
|
||||
original session expiry.
|
||||
- `GET /internal/rooms/{room_name}` resolves that grant only while unexpired
|
||||
and user still active/ready. Returns the same metadata. Deletion/reset revoke
|
||||
grants immediately; speech agents should poll to stop active conversations.
|
||||
|
||||
401 means absent/invalid auth; 403 forbidden role/Origin; 404 missing/deleted
|
||||
user or invalid room; 503 means not yet ready. Metadata is never in public APIs.
|
||||
Do not proxy internal endpoints publicly. Token service resolves client sessions,
|
||||
mints an opaque room name, persists its grant before issuing LiveKit credentials,
|
||||
and never accepts browser-supplied account IDs, room ownership or Hermes URLs.
|
||||
For colocated FastAPI integration use `Depends(control.current_user)` and
|
||||
`Depends(control.administrator)`; these return public user dicts and require
|
||||
this application's `app.state.accounts`/settings. Separate services use the
|
||||
secret-authenticated internal endpoints instead.
|
||||
|
||||
## Provisioner interface
|
||||
|
||||
`create_app(settings: Settings, provisioner=...)` supports offline injection.
|
||||
The adapter receives a **private trusted dict**, including `id`, `username`,
|
||||
`container_name`, `desired`, and stored `hermes_url` / `hermes_api_key`.
|
||||
|
||||
- `async ensure(user) -> dict`: idempotently create/recover owned container;
|
||||
return `{container_name,hermes_url,hermes_api_key}`. Container name must match
|
||||
`hope-hermes-<username>-<id[:8]>`; metadata is persisted before readiness check.
|
||||
- `async ready(user) -> bool`: owned service health, not existence alone.
|
||||
- `async remove(user) -> None`: verify management/user ownership before any
|
||||
removal; retain private data by default; failure raises and remains retriable.
|
||||
- Optional `async aclose()`: close adapter resources during shutdown.
|
||||
|
||||
Async adapters offload blocking Docker/HTTP calls themselves; synchronous
|
||||
adapters are supported using a worker thread. Account SQLite and scrypt work
|
||||
are offloaded from the ASGI loop. Slow provision/remove run separate tasks;
|
||||
request handlers do not await container startup. Exception text is not logged or
|
||||
returned because provider failures may contain credentials.
|
||||
|
||||
Offline tests:
|
||||
|
||||
```sh
|
||||
python -m pytest tests/unit/test_accounts.py --confcutdir=tests/unit -q
|
||||
```
|
||||
|
||||
`--confcutdir` avoids this repository's deployment-oriented top-level fixtures.
|
||||
Use a temporary DB and fake adapter; no Docker or provider calls are made.
|
||||
Reference in new issue
Block a user