← Ledger


title: Phase 7c-c-c consumer close — airlock agent.approve verb wired in console date: 2026-07-02 status: Accepted phase: 7c-c-c (rocky-side consumer of the external airlock verb) predecessor: docs/decisions/2026-06-29-phase-7c-c-a-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7c-c-a.md external: devarno-cloud/airlock#58 (agent.approve verb producer side)

Phase 7c-c-c consumer close

Shipped

Locked decisions

1. Store fact, not JWT

The airlock JWT is a 60-second handoff receipt. Storing it would give us an expiring blob that says nothing after the first minute. Instead the callback verifies once and writes an approval FACT (agent_id, workspace_slug, capabilities, approved_at, airlock_issuer). Producers hit the local store on every invocation — no per-invocation JWKS fetch, no clock-skew failure mode. Revocation lands in a follow-up slice; today's model is "decision-of-record."

2. Env stub stays as a break-glass fallback

ROCKY_AGENT_APPROVED_<AGENT_ID> still overrides an empty store lookup. Reason: prod incidents where airlock is unreachable but a specific agent must be allowed to run should not require a store hand-edit inside the container. The env fallback:

3. Store is per-workspace, not per-org

The approval store key is (agent_id, workspace_slug), not (agent_id, org_id). Rocky-side identity is the workspace slug — orgs are an airlock concern. If two workspaces in the same org both want the same agent, they each need their own approval. This is deliberate: agent identity in rocky is already namespaced by workspace (<workspace_slug>-<producer>-<worker>).

4. Callback route path /api/agent/approve/callback is stable

Airlock verifies the return URL against the handoff_consumers table (rocky.erid.tech already registered at airlock/src/db/migrations/0011_seed_rocky_erid_handoff_consumer.sql). The /api/agent/approve/callback path is now baked into every producer's buildApprovalRedirect call — treat it as a wire contract.

5. Open-redirect defense: absolute next param → /agents

An attacker who could land the operator on the callback with a malicious next=https://evil.example would use us as an open redirector after a valid approval flow. The callback normalizes next to /agents unless it is a bare-path (^/[^/]) URL. Protocol-relative (//) is treated as absolute.

6. 402 (Polar) denials do not carry airlock_url

The gate keeps the 403 (approval) and 402 (entitlement) shapes separate. Polar denials are not resolvable by an airlock round-trip; the client UI on a 402 must route to a different surface (billing / upgrade flow) when 7c-c-b lands. Conflating them would push the caller through an approval that can't help.

Consequences

Successor