3.8 KiB
ADR-002: Password accounts and personal agent keys
Status: implemented, 14 October 2026.
Human users sign in with a username/password through /v1/users/session. Agent hosts
continue to use independent, high-entropy bearer keys. This replaces bearer-key-only
human login as the default UI while retaining the existing operator key for initial
account setup and recovery. Protected owner review remains a separate password/TOTP,
challenge-bound workflow.
Administrators create and disable accounts with viewer, editor or admin roles and
project grants. Editors and administrators issue keys for their own agents at
/v1/me/agent-credentials; viewers retain read-only access. The server derives the
owner from the authenticated account and checks requested grants against current
account grants in the same database transaction as issuance. An agent has its own
principal identity for proposals/runs. Users can only list and revoke their own keys.
Administrators can still inspect and revoke all keys.
Passwords are hashed with the existing Argon2id implementation. Login uses an HttpOnly, SameSite=Strict session cookie, with Secure on HTTPS. Public password login requires HTTPS; localhost is allowed for development. Browser writes require CSRF and the existing same-origin validation. The development proxy preserves the public Host, allowing TLS-terminating deployments to validate the browser's actual Origin. Forwarded scheme handling remains Uvicorn's trusted-proxy responsibility; application code does not trust arbitrary forwarding headers.
Five failed logins for a username within 15 minutes block further attempts until that window expires. A shared 100-failure window bounds random-username attempts. Unknown/disabled usernames use a dummy Argon2 verification and generic failure text. This initial limiter favors a small private service; shared lockouts can be abused for denial of service. Larger deployments should add an edge rate limiter.
Page loads restore existing human browser sessions through GET /v1/session with
an explicit same-origin client header. The endpoint returns expiry, a CSRF token and
account kind; it never returns the session cookie or bearer key. A stable one-way
CSRF token is derived from the private session secret and its digest is saved beside
the original CSRF digest. This additive migration preserves pre-upgrade sessions and
old tabs, while preventing reloads or simultaneous tabs from invalidating each other.
Restoration does not extend the original session lifetime. No browser storage is used.
Changing a password requires the current password, invalidates every browser session for that account, and preserves independent agent keys. Disabling an account revokes its login identity and its agent keys atomically under the canonical authority lock. Activity events include account creation, disable and password changes, without passwords or bearer secrets. Sensitive validation errors omit submitted inputs.
The strongest alternative is external OIDC/SSO. It avoids local password management but adds an external identity dependency beyond the requested minimal private-service workflow. There is no anonymous registration or email recovery in this slice.
Migration is additive: user_accounts, login failure tracking, and a nullable owner
column on credentials. Existing bearer keys and browser sessions continue to work.
Account login identities have an unrecoverable random bearer digest; only password
sign-in creates their sessions. Rolling back the routes/UI preserves existing keys;
new human accounts require the updated identity implementation.
Validation covers login/CSRF/TLS/body limits, password hashing and changes, session invalidation, user ownership, grant escalation, agent/human separation, disabled users, login throttling, preserved existing credentials, UI setup and personal key issuance.