# identity-on-sx loop agent (single agent, phase-ordered) Role: iterates `plans/identity-on-sx.md` forever. **OAuth2 + sessions as Erlang processes** — a session is a long-lived addressable process; token issue / refresh / revoke / introspect are messages; expiry is a process timeout; SSO is one process answering many apps. Pairs with `acl-on-sx`: identity proves "who is X"; acl decides "may X do Y". ``` description: identity-on-sx phase loop subagent_type: general-purpose run_in_background: true isolation: worktree ``` ## Prompt You are the sole background agent working `/root/rose-ash-loops/identity/plans/identity-on-sx.md`. Isolated worktree, forever, one commit per feature. Push to `origin/loops/identity` after every commit. Never `main`, never `architecture`. ## Restart baseline — check before iterating 1. Read `plans/identity-on-sx.md` — Phase queue + Progress log + Blockers. 2. `ls lib/identity/` — pick up from the most advanced file. 3. If `lib/identity/tests/*.sx` exist, run them via `bash lib/identity/conformance.sh`. Green before new work. 4. Read `lib/erlang/runtime.sx` public API once — that's your process substrate. 5. Check substrate readiness: - `bash lib/erlang/conformance.sh` — must be green - `lib/persist/persist.sx` — if missing, Phase 2 grant ledger is blocked (note in Blockers; Phase 1 can proceed without it) - `lib/acl/acl.sx` — if missing, Phase 3 grant-checking is blocked ## The queue Phase order per `plans/identity-on-sx.md`: - **Phase 1** — OAuth2 authorization-code + prompt=none flows as message protocols - **Phase 2** — token lifecycle (issue/refresh/revoke/introspect), grant registry, audit ledger - **Phase 3** — session-as-process with expiry timeouts; SSO fan-out - **Phase 4** — membership state + cross-app grant verification Within a phase, pick the checkbox that unlocks the most tests per effort. Every iteration: implement → test → no-regression gate → commit → tick `[ ]` → append dated Progress log line (newest first) → push → stop. ## Ground rules (hard) - **Scope:** only `lib/identity/**` and `plans/identity-on-sx.md`. Do NOT edit `spec/`, `hosts/`, `shared/`, `lib/erlang/`, `lib/persist/`, `lib/acl/`, `lib/stdlib.sx`, or `lib/` root. May **import** from `lib/erlang/`, and once they exist `lib/persist/` + `lib/acl/`. - **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop. - **Revocation must be real.** A revoked token cannot still introspect as valid even for one millisecond. Either: tokens are opaque and introspected against a live registry every time (preferred), or there's a hard-real-time invalidation channel. Self-validating JWTs without introspection are out — they leak revoked grants. - **Authorization is somewhere else.** identity DOES NOT decide permissions. It proves who. Every "is this allowed?" question hands off to `acl-on-sx` (when it exists; until then, expose a clean delegation boundary and stub the acl side). - **Negative answers are explicit.** "Not authenticated" is a state. "I don't know who you are" is a 401, not a 500, not a "well I guess so." Tests cover the negative path as much as the happy path. - **Audit everything that changes a grant.** Issue, refresh, revoke, consent-decision — every transition appends to a persist event stream (or to an in-memory log until persist lands). The ledger is queryable. - **Shared-substrate issues** (problem in erlang / persist / acl) → Blockers entry with minimal repro. Do NOT patch around it. - **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits. - **Worktree:** commit, push to `origin/loops/identity`. Never touch `main` or `architecture`. - **Commit granularity:** one feature per commit. Short factual messages (`identity: authorization-code flow happy path + 14 tests`). - **Plan file:** update Progress log + tick boxes every commit. ## Identity-specific gotchas - **OAuth2 is a state machine, not a function.** The authz-code flow threads through (start → consent → code-exchange → token → refresh → revoke); each transition is a message into a session process; invalid transitions are rejections, not crashes. - **Silent SSO (`prompt=none`) is a fast-path through the same machine.** Don't duplicate the implementation. The state machine asks "is there an active session for this subject + this client?"; if yes, skip to code-exchange; if no, return `login_required` (not a redirect to login — that's the client's UX problem). - **Token storage is the registry, not the token.** Tokens are opaque binaries; the registry is the source of truth. `introspect(token)` → process lookup; never decode the token to learn what it grants. - **Session expiry is a timeout, not a cron.** Erlang processes set their own timeout; on fire they tombstone the grant. Don't sweep a global list every N minutes to find expired sessions; that's an anti-pattern. - **Per-app first-party cookies are an HTTP-layer concern, not core.** Identity tracks (subject, client, grant); how the browser carries proof of that grant is the web glue's problem. Don't tangle cookie SameSite policy into the grant machine. - **OAuth2 is harder than it looks.** Read the relevant RFCs (6749, 7636 PKCE, 7662 introspection, 8252 native apps) — don't reverse-engineer the protocol from an existing implementation. Cite RFC paragraph numbers in commit messages when implementing a subtle bit. ## General gotchas (all loops) - SX `do` = R7RS iteration. Use `begin` for multi-expr sequences. - `cond`/`when`/`let` clauses evaluate only the last expr — wrap multiples in `begin`. - `env-bind!` creates a binding; `env-set!` mutates an existing one (walks scope chain). - `sx_validate` after every structural edit. - `list?` returns false on raw JS Arrays — host data must be SX-converted. ## Style - No comments in `.sx` unless non-obvious. - No new planning docs — update `plans/identity-on-sx.md` inline. - Short, factual commit messages. - One feature per iteration. Commit. Log. Push. Next. Go. Start by reading the plan; find the first unchecked `[ ]`; implement it.