2026-05-10 — Per-user auth via Airlock cross-apex handoff (TASKSET 4)
status: published
mr: MR-7
Context
Section titled “Context”The petrova.host dashboard previously had no user-level authentication. All act mutations were attributed to the anonymous actor "human:dashboard", making it impossible to audit which operator submitted a given act PR. The Vercel Password Protection that had been gating the surface was a deployment-level secret, not an identity.
TASKSET 4 introduces per-operator identity as the fourth step in the governance-completeness plan, unlocking multi-operator use and threaded actor attribution on every act PR.
Decision
Section titled “Decision”Use Airlock’s cross-apex JWT handoff (flow F9, GET /api/auth/handoff?return=<callback>) rather than GitHub OAuth or a standalone OIDC client registration.
Why F9 and not standard OIDC (authorization_code)?
F9 requires only a one-time row insertion in the handoff_consumers table (via Hatch) rather than provisioning an OAuth client secret, storing it in Vercel, and implementing PKCE. The handoff JWT has a 60-second TTL so there is no refresh-token lifecycle to manage. The protocol is already battle-tested by stratt.dev and other cross-apex consumers in the fleet.
Why not GitHub OAuth (original spec)?
Airlock is the canonical identity provider for the devarno ecosystem. GitHub OAuth would introduce a second IdP for the same operator population, fragment audit logs across two providers, and require separate credential rotation. Centralising on Airlock is the right architectural choice.
Session model:
After the handoff callback verifies the EdDSA JWT against Airlock’s JWKS, a __petrova_session HS256 cookie (7-day TTL, signed with SESSION_SECRET) is issued. Middleware validates this cookie on every request to /console/** and /api/act, setting Astro.locals.user. No server-side session store is needed.
Role gate:
Only Airlock users with role === "admin" are admitted. Any other authenticated user is redirected to /denied. This matches hatch’s model and ensures the control-plane dashboard is not accessible to non-operator airlock accounts (e.g. SMO1 end users who share the same Airlock instance).
Actor attribution:
/api/act reads locals.user.email and injects actor: "human:<email>" into every RPC params object. Act PRs created via the dashboard now carry a per-user actor string visible on /console/acts. Unauthenticated calls to /api/act return 401 (belt-and-suspenders — middleware blocks them first).
Alternatives considered
Section titled “Alternatives considered”- GitHub OAuth — rejected (two IdPs, separate credential lifecycle, no audit log convergence).
- Airlock standard OIDC (authorization_code + PKCE) — valid but heavier; requires OAuth client provisioning and refresh-token handling. No incremental benefit over F9 for an operator-only surface.
- API key per operator — stateless but cannot leverage Airlock’s existing session UX or TOTP/passkey 2FA.
Consequences
Section titled “Consequences”- positive: Act PRs carry
actor: "human:<email>"— full attribution inpetrova.acts.recentand on GitHub PR metadata. - positive: No new credential type. Operators reuse their existing Airlock admin session (including 2FA if enrolled).
- positive: Logout clears only the
__petrova_sessioncookie; the Airlock session itself remains active for other devarno surfaces. - constraint:
https://petrova.hostmust be registered in thehandoff_consumerstable before the first production deploy. Seedocs/runbooks/petrova-host-airlock-setup.md. - constraint: Preview deployments cannot use the live handoff flow.
AUTH_DISABLED=truemust be set in Vercel Preview environment for preview workflows to function. - neutral:
SESSION_SECRETrotation logs out all active sessions simultaneously. Document and schedule rotations.
New env vars
Section titled “New env vars”| Var | Default | Required |
|---|---|---|
SESSION_SECRET | — | yes (≥32 chars) |
AIRLOCK_URL | https://airlock.devarno.cloud | no |
PETROVA_HOST_ORIGIN | https://petrova.host | recommended explicit |
References
Section titled “References”dashboard/src/middleware.ts(new)dashboard/src/pages/api/auth/callback.ts(new)dashboard/src/pages/api/auth/logout.ts(new)dashboard/src/pages/api/act.ts(modified — actor injection + 401 guard)dashboard/src/layouts/Shell.astro(modified — user chip + logout)docs/runbooks/petrova-host-airlock-setup.md(new)- Airlock F9 flow:
airlock/src/routes/handoff.ts - Related:
atlas/findings/2026-04-18-stratt-dev-cross-apex-diagnose.md