# I0 Single-Operator Authentication / Session Gates — 2026-08-23

Status: source implementation complete and buildbox-green on `fix/i0-single-operator-session` @ `0c542bcf78ee30fb151f98a391915b2d11ba5714`. This branch contains the current pushed dogfood recovery head and the full-chain autonomy repair; live dogfood deployment remains owner-secret-gated.

Purpose: close accepted ADR-0009 without expanding into I9 multi-user/OIDC/RBAC scope.

## Binding target

```text
browser -> apps/web public ingress
  POST /auth/login { password }
  -> control plane verifies argon2id hash
  -> 256-bit opaque token
  -> PostgreSQL stores SHA-256 token hash only
  -> __Host-awp_session; HttpOnly; Secure; SameSite=Lax; Path=/

subsequent owner request
  -> apps/web derives session reference only from cookie
  -> control plane re-resolves session -> Human Principal on every protected request
  -> invalid / expired / idle-expired / revoked session => 401
```

Agent execution callbacks remain on their existing Attempt-scoped completion token path. Startup/restart reconciliation uses explicit System Principal authority. This slice does not add self-registration, password reset, MFA, orgs, tenants, RBAC, OIDC, CLI/MCP tokens, or cross-origin hosting.

## Required gates

- [x] **A1 — Session schema:** durable `sessions` table contains id, unique token hash, principal id, created/last-seen/absolute-expiry/revoked timestamps and user-agent digest. No plaintext token column exists.
- [x] **A2 — Argon2id login:** control plane verifies `AWP_OPERATOR_PASSWORD_HASH` with Argon2id-compatible verifier; bad password fails closed.
- [x] **A3 — Opaque token:** successful login mints 32 random bytes encoded base64url; database receives only SHA-256 hash.
- [x] **A4 — Expiry:** absolute expiry is 30 days; idle expiry is 7 days; absolute expiry never extends; `last_seen_at` refreshes no more than once per 5 minutes.
- [x] **A5 — Revocation/logout:** logout revokes the current row; expired/revoked/unknown sessions resolve to 401; password-rotation support can revoke all sessions for the operator.
- [x] **A6 — Control-plane resolution:** every owner-facing project/account/read/mutation endpoint resolves the session in the control plane and derives its `MutationContext.authority`; no trusted forwarded Principal/capability header exists.
- [x] **A7 — Explicit system callers:** startup dispatch reconciliation and Attempt callback routes no longer manufacture `principal:owner`; they use explicit System Principal authority or their scoped callback credential path.
- [x] **A8 — Gateway cookie:** `apps/web` is the public auth/session ingress; login sets `__Host-awp_session` with HttpOnly, Secure, SameSite=Lax, Path=/ and no Domain; logout clears it.
- [x] **A9 — Gateway session forwarding:** public requests cannot choose the internal session header; `apps/web` derives it only from the cookie and forwards only the opaque session reference to the loopback control plane.
- [x] **A10 — CSRF:** public state-changing requests with cross-site `Origin` or `Sec-Fetch-Site` are rejected; GET never performs a state-changing operation.
- [x] **A11 — No gateway DB:** `apps/web` gains no PostgreSQL dependency/connection; session storage remains control-plane-only.
- [x] **A12 — Public owner UX:** unauthenticated owner pages redirect to `/login`; invalid session is cleared and redirected; authenticated page/API requests preserve the canonical I1 UI/API behavior.
- [x] **A13 — Deployment contract:** dogfood config requires the operator password hash only in the control-plane environment; web service does not receive password hash or DB credentials.
- [x] **A14 — Exact gates green:** focused auth/persistence/web/control-plane tests plus full `pnpm check` pass on an authorized buildbox at the exact pushed head.

## Verification evidence

Exact source candidate: `0c542bcf78ee30fb151f98a391915b2d11ba5714`.

Authorized buildbox: `debian2`, Node 22.

`pnpm check` PASS on the exact candidate:

- recursive build/typecheck: PASS;
- ESLint: PASS;
- Vitest: 49 files passed / 2 opt-in files skipped; 184 tests passed / 5 skipped;
- build: PASS;
- architecture: 6/6 PASS;
- dependency-cruiser: zero violations across 114 modules / 199 dependencies;
- Prettier: PASS.

The two opt-in PostgreSQL suites were then executed separately against disposable PostgreSQL 16 and both passed:

- `tests/golive/02-control-plane-postgres.test.ts`: 4/4 PASS, including authenticated Hono authoring and session continuity across control-plane restart;
- `tests/golive/28-self-project.test.ts`: 1/1 PASS, including self-Project bootstrap and authenticated hierarchy readback.

Built-process integration against disposable PostgreSQL also passed with the actual compiled control plane and web ingress:

```text
AUTH_WEB_INTEGRATION PASS root=303 login=303 projects=200 invalid=303 csrf=403 forged=401 callback_get=405 control=loopback
```

This proves unauthenticated redirect, real Argon2id login, exact secure `__Host-` cookie issuance, authenticated owner access, invalid-session clearing, cross-site mutation rejection, rejection of caller-forged internal session identity, POST-only machine callback ingress, and loopback-only control-plane binding.

Additional focused tests prove 32-byte opaque token generation, SHA-256-only token persistence, 30-day absolute expiry, 7-day idle expiry, five-minute `last_seen_at` throttling, revocation/logout, real Argon2id verification, automatic invalidation when the configured operator credential hash rotates, explicit System authority for Attempt callbacks/startup recovery, and the absence of PostgreSQL/password credentials from the web process.

## Owner-gated live boundary

Source implementation and deployment mechanics are complete. Live Debian3 remains deliberately unchanged because `/home/user/.config/awp-dogfood/operator-password-hash` is absent. `tailscale serve status --json` is still `{}`. The deploy script now refuses to proceed without an owner-controlled Argon2id hash and would otherwise configure HTTPS/Tailscale Serve, loopback web/control-plane listeners, and HTTPS Attempt callback ingress.

Do not invent or copy a password/hash from unrelated workstation, Overdeck, GitHub, or provider credentials. A real owner login and canonical GOLIVE re-proof require the owner-controlled password/hash pair to be provisioned at that boundary. `GOLIVE.md` therefore remains 0/30 until the canonical live journey is re-proven.
