← Ledger


title: Phase 7c-c-a close — entitlement + approval seams; RALPH gate wired date: 2026-06-29 status: Accepted phase: 7c-c-a (rocky-side producer gating for external Polar + Airlock) predecessor: docs/decisions/2026-06-29-phase-7c-b-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7c-c-a.md

Phase 7c-c-a close

Shipped

Locked decisions

1. Two seams, two backing sources, env-driven stubs

Entitlement (Polar) and approval (Airlock) are distinct producer gates with different external sources. Modeling them as one combined "canInvoke" check would collapse two dimensions the spec keeps separate. The RATE column and the APPROVAL column are two operator concerns on the hatch projection — the producer gate mirrors that split.

Env stubs (not "TODO" no-ops): the stub is a real gate. Setting ROCKY_POLAR_TIER_WS=solo and declaring a tier_floor: fleet registration DENIES today. That's the shape of the real thing; only the source-of-truth changes when 7c-c-b/c land.

2. Unknown Polar tier strings floor to solo (conservative)

ROCKY_POLAR_TIER_WS=godmode → treated as solo, not "assume the highest". A misconfigured env var must never silently grant a higher entitlement. Explicitly tested. Same principle for negative / non-numeric seat counts (fall to the default 1, never wrap or grant unlimited).

3. Deny short-circuits before HATCH emit

Both denial paths return the error response BEFORE emitHatch(ralph.run.submitted), BEFORE submitRun, and BEFORE emitAgentHatch(agent.registered|invoked). A denied invocation is a non-event on both projections — HATCH ledger and the agent projection agree that nothing happened. This preserves the projection invariant that emitted events reflect actual work.

Alternative rejected: emit agent.registered on first hit anyway (regardless of deny) so hatch shows a pending row. Overreaches on producer semantics — hatch already renders unregistered agents as OWNER: unknown per spec §Distinction; there's no need to force a registration for a rejected first attempt.

4. Order: approval before entitlement

403 (auth-shaped) precedes 402 (billing-shaped). If both gates would deny, the operator sees the auth reason first — matches the way the airlock model already surfaces auth failures ahead of quota failures elsewhere in the codebase.

5. Gate at RALPH only in this slice

Fan-out to relay/council/sniffer/stratt/driver is deferred. Two reasons:

RALPH is the highest-cost producer (spawns ralph serve runs), so gating it first is the highest-leverage placement.

6. Env-key mapping: uppercase, hyphens → underscores

Predictable transform for both workspace slugs and agent IDs. ws-aROCKY_POLAR_TIER_WS_A. ws-a-council-sageROCKY_AGENT_APPROVED_WS_A_COUNCIL_SAGE. Tested explicitly.

Deferred

Acceptance verified

Phase 7 status after this slice

Slice State Notes
7a closed contract published
7b closed 6 producers + RALPH terminal observer
7c-a closed rocky audit + GET projection
7c-b closed external hatch consumer
7c-c-a closed (this) rocky-side entitlement + approval seams; RALPH gated
7c-c-a-fanout not-started pattern replicate to relay/council/sniffer/stratt/driver
7c-c-b not-started (external) Polar SDK replaces entitlement env stub
7c-c-c not-started (external, airlock) agent.approve verb + signed token

Sources