Instance Admin Area
The instance admin (a.k.a. super-admin) area lets an operator manage the whole Hephaestus deployment — distinct from a workspace admin, whose powers are scoped to a single workspace.
Who is an instance admin?
- An account with
Account.appRole == APP_ADMIN(ADR 0017 native auth). - The issuer mints the namespaced
app_admingranted authority for such accounts (JwtPrincipalFactory). This is deliberately distinct from the per-workspaceadminrole, which is membership-derived and never appears in the JWT.SecurityUtils.isSuperAdmin()readsapp_adminand auto-elevates an instance admin to workspace-admin level only for workspaces they belong to. - The authority comes only from
appRole—JwtPrincipalFactorystrips any reserved authority (app_admin/admin) that might arrive via a grantableaccount_featurerow, so an/admin/users-granted flag can never escalate to instance admin. - First-admin bootstrap (no DB seed required) is covered separately in the auth-cutover runbook.
The shell
The admin area is a dedicated sidebar context (AppSidebar context === "admin") — its own
"Back to app" header with the workspace switcher suppressed (the GitLab/Grafana "admin area" pattern),
not a reuse of the mentor context. It is reachable from an app_admin-gated "Instance admin"
entry in the always-present sidebar footer, so a freshly bootstrapped admin with zero workspaces
can still reach it. The /admin route tree is guarded in beforeLoad (isAppAdmin), and every
endpoint below is enforced server-side by @PreAuthorize("hasAuthority('app_admin')") — the client
is not a security boundary.
Endpoints
All under /admin, all gated by hasAuthority('app_admin'):
| Endpoint | Purpose |
|---|---|
GET /admin/users (adminListUsers) | Paged account list |
PATCH /admin/users/{id} (adminUpdateUser) | Change an account's app role (last-admin guard; can't self-demote) |
DELETE /admin/users/{id}/sessions (adminRevokeUserSessions) | Force sign-out: revoke all of an account's active sessions. Because an impersonation token carries the target's account id as its subject, this also ends any in-flight impersonation of that account. Audited as JWT_REVOKED. |
POST /auth/impersonate (impersonate) | Begin impersonating an account (mandatory reason; no self / no admin→admin; read-only by default via ImpersonationGuard) |
GET /admin/workspaces (adminListWorkspaces) | Metadata-only overview of every workspace (slug, status, provider, owner login, member count, created-at). Cross-tenant via @WorkspaceAgnostic; no tenant content — reaching content is the audited impersonation path. |
GET /admin/audit (adminListAuthEvents) | Read-only viewer over the append-only auth_event log (logins, impersonation, role changes, deletions). Paged, newest-first, filterable by event type; surfaces the (account_id, acting_account_id) pair so impersonated actions stay attributable. |
GET /admin/config-audit (adminListConfigAuditEvents) | Read-only viewer over config_audit_event — who changed which workspace setting, when, and from what to what. Rows are immutable inside the retention window (DB trigger); ConfigAuditRetentionJob is the only way one leaves. |
/admin/llm/connections* (adminListLlmConnections, adminCreateLlmConnection, adminGetLlmConnection, adminUpdateLlmConnection, adminDeleteLlmConnection, adminProbeLlmConnection, adminProbeLlmConnectionDraft) | The instance LLM connection catalog. Routing identity (base URL, wire API, auth mode) is immutable after create; probe tests a saved or draft connection before anything is enabled. |
/admin/llm/models* (adminListLlmModels, adminCreateLlmModel, adminGetLlmModel, adminUpdateLlmModel, adminDeleteLlmModel, adminUpdateLlmModelPrice, adminUpdateLlmModelSharing) | Models under a connection, their prices (temporal supersede-on-insert into llm_model_price), and who may use them — public, or granted per workspace. |
GET/PUT /admin/llm/settings (adminGetLlmSettings, adminUpdateLlmSettings) | The instance LLM settings singleton: egress host allowlist and allowWorkspaceConnections, the switch that lets workspaces register their own provider connections. |
GET /admin/llm/usage (adminGetLlmUsageReport) | Cross-workspace monthly LLM usage and budget report, split by purse (shared models vs each workspace's own provider). |
PUT /admin/workspaces/{workspaceSlug}/llm/budget (adminUpdateWorkspaceLlmBudget) | Set or clear a workspace's monthly cap on shared-model spend — clearing is PUT with monthlyBudgetUsd: null, not DELETE (there is no DELETE mapping; it returns 405). The workspace's cap on its own provider is a different endpoint under /workspaces/**, set by the workspace's own admin. |
GET /admin/settings / PATCH /admin/settings/silent-mode | Read or change the instance-wide outbound delivery brake. Releasing requires If-Match with the ETag returned by GET, so a stale browser cannot release a newer incident response. |
Instance Silent Mode
Silent Mode is an emergency and disaster-recovery brake, not a workspace rollout stage. It is engaged by default on new installs, when the singleton settings row is missing, and on upgrades whose seeded row was never explicitly changed. Detection, observation persistence, inbound webhook processing, synchronization, and admin access continue, but delivery writes to GitHub, GitLab, and Slack are refused at the provider gateway.
Suppression is prospective: a suppressed review is recorded as SUPPRESSED(INSTANCE_SILENCED) for
audit and preview, but is never queued for replay. Releasing the brake therefore sends nothing by
itself; only a new source event can deliver. A re-review that would have edited an existing comment
records the attempted replacement without superseding the live delivered ledger unit.
OAuth/token lifecycle operations, webhook registration, and operator alerts remain available while Silent Mode is engaged.
Impersonation time-box
begin stamps an absolute ceiling imp_exp (hephaestus.auth.impersonation-max-lifetime, default
1h); the issuer caps each token's exp at min(now + accessTtl, imp_exp), and refresh drops the
act claim (auto-exit) once it passes. Note: there is currently no proactive/silent refresh
caller, so a session already ends at the access-token expiry (accessTtl, ~15m) — shorter than the
ceiling. The imp_exp machinery is therefore future-proofing that becomes the binding limit only once
silent refresh is wired; until then the de-facto impersonation time-box is accessTtl.
Deferred / follow-up
- Step-up re-auth gate for impersonate-begin + role-change. Hephaestus owns no first factor for
GitHub (plain OAuth2, no
prompt=login), so a local fresh-re-auth gate is a deliberate-second-step / audit control, not a true second factor. Deferred to a focused PR. - Elevation tagging (
elevated_via_instance_admin): make an instance admin's cross-workspace access distinguishable in the audit trail. Needs a newauth_eventtype (a CHECK-constraint migration) + a log-volume decision — its own slice. - Banner countdown / proactive refresh: only meaningful once the session-refresh decision is made (see above).
APP_AUDITORread-only tier: cut as YAGNI for a single-operator instance; the enum + authority design makes later reintroduction ~1 day.Instance-provided LLM resources (BYO vs pooled): shipped (#1368). The plan recorded here was to buy per-workspace budgets via self-hosted LiteLLM virtual keys rather than build a metering subsystem; that was reversed. An instance admin now registers connections and models under/admin/llm/*, prices them, and grants them to workspaces; workspaces may add their own connection when instance settings permit it. Usage is metered intollm_usage_eventand capped by two independent monthly budgets — the instance's cap on shared-model spend and the workspace's cap on its own provider — which are never summed. See ADR 0026.