Operator access contract
Operator access keeps a stable principal identity separate from passwords,
browser sessions, and agent tokens. Human accounts have role admin or
operator; agent principals retain role agent. Every authenticated principal
can see all projects, while only a currently enabled human administrator can
manage accounts and agent credentials. There is no public enrollment or password
recovery route.
All HTTP responses use the standard data, request_id, and server_time
envelope. Mutations require Idempotency-Key; browser mutations also require the
configured origin and CSRF token. Account and credential events contain no
passwords, password hashes, API tokens, token verifiers, cookies, or CSRF values.
Human accounts and passwords
GET /api/v1/auth/account returns the current human’s own account as
data.operator:
{
"id": "principal-id",
"name": "operator-name",
"kind": "human",
"role": "operator",
"enabled": true,
"revision": 2,
"created_at": "2026-09-10T00:00:00.000Z",
"disabled_at": null
}
GET /api/v1/admin/operators?cursor=... lists at most 200 human accounts with a
bounded next_cursor. GET /api/v1/admin/operators/{id} returns one account.
Both require an administrator.
POST /api/v1/admin/operators creates a human account from:
{"name":"reviewer","role":"operator","password":"a private initial password"}
The password must contain 12 through 1024 bytes. It is hashed with uniquely
salted Argon2id outside the database writer transaction. The transaction then
rechecks the administrator and creates the principal, receipt, and event
atomically. The response contains data.operator and never echoes the password.
A retry must re-enter the original password. The service checks it against the
resulting account’s slow password hash and returns idempotency_secret_mismatch
for a different password. If that account has since changed its password, the
original creation request can no longer be secret-verified; inspect the account
instead. The receipt retains neither the password nor another password-checking
oracle. A successful replay returns freshly read account metadata, so the old
receipt cannot represent later role or enabled-state changes as current.
Account creation is the only mutation whose receipt is bound to the human
administrator principal instead of the individual browser session. This permits
replay after signing in again following an uncertain response. The service still
rechecks the new browser session and current administrator role under the writer
lock. Revoked, disabled, or demoted administrators cannot replay it. All other
mutation fingerprints retain their existing session binding.
POST /api/v1/admin/operators/{id}/access accepts:
{"expected_revision":2,"role":"admin","enabled":true}
The writer-locked transaction rechecks the administrator, target revision, and
last-active-administrator invariant. Changing access revokes all target browser
sessions. Two concurrent changes cannot both remove the final administrators.
Stale input returns revision_conflict; an attempt to remove the sole enabled
administrator returns last_active_admin.
POST /api/v1/auth/password is available only to a human browser session and
accepts:
{
"current_password": "the current private password",
"new_password": "a different private password",
"expected_revision": 2
}
Current-password verification and new-password hashing run outside the writer
transaction. Under the writer lock the service rechecks the live browser session,
unchanged old hash, and account revision, then replaces the hash and revokes all
of the account’s browser sessions, including the caller. The response clears the
browser cookie. If the response is lost, sign in with the new password and inspect
GET /api/v1/auth/account; a revoked old session cannot use a receipt as renewed
authority.
Browser sessions
GET /api/v1/browser-sessions?cursor=... lists the caller’s sessions. An
administrator may add principal_id to inspect another human. Items contain only
id, principal_id, created_at, expires_at, revoked_at, and current.
Cookie tokens and their hashes are never returned.
POST /api/v1/browser-sessions/{id}/revoke with {} revokes one of the caller’s
sessions or, for an administrator, another human’s session. Revoking the current
session clears its cookie. Existing task, attempt, job, and resource records are
unchanged.
Agent token rotation
Existing POST /api/v1/admin/agents enrollment creates a stable agent principal
with an initial named credential. POST /api/v1/admin/credentials/{old_credential_id}/rotate accepts:
{"name":"linux-builder-2026-09","revoke_old":true}
revoke_old defaults to true. Rotation requires an active credential belonging
to an enabled agent principal. It creates a separately named credential for that
same principal and, by default, revokes the old credential and closes its agent
sessions. It never transfers or revives an attempt and never resolves or releases
a job or resource hold.
The first response contains principal_id, principal_name, credential,
replaced_credential_id, replaced_credential_revoked, and the new token.
Only that response exposes the token. A retry returns the same credential identity
without token, adds secret_unavailable: true, and directs the administrator to
rotate the newly created replacement credential with a new idempotency key. The
default rotation then revokes that credential while issuing another token.
After a restore revokes every credential, an administrator can preserve an existing agent identity while issuing fresh authentication with:
POST /api/v1/admin/agents/{principal_id}/credentials
{"name":"after-restore"}
The principal must still be an enabled agent. The first response contains its
unchanged principal_id and principal_name, the new credential record, and
the one-time token. A replay returns the saved credential identity without the
token and sets secret_unavailable: true. The operation is available during
restore reconciliation so a recovered administrator can reconnect clients, but
the global restore pause prevents those clients from claiming or changing work
until reconciliation finishes.
Host-local recovery
Lost-password recovery is exported for the server’s local command and has no HTTP route:
recover_operator_password(state, username, new_password, reason)
The command validates and hashes the new password before acquiring SQLite’s writer lock. Its single transaction enables the named existing human principal, increments its revision, revokes all browser sessions, and records a bounded reason. The principal’s ID and role remain unchanged. The transaction does not modify agent credentials or sessions, attempts, jobs, submissions, or resource holds. The old password and all prior browser sessions remain invalid.
The recovery event identifies initiator_kind as host_operator and sets
authenticated_principal_id to null, because the local command has no
authenticated browser or agent principal. subject_principal_id identifies the
recovered account. The existing non-null events.actor_id foreign key also points
to that account solely as a subject_reference, as recorded by
actor_id_role; it must not be presented as the initiator of the recovery.