← Ledger


title: Phase 7c-c-a-hearth close — Go-side gate in hearth RPC handlers date: 2026-06-29 status: Accepted phase: 7c-c-a-hearth (Go port of the producer-side gate) predecessor: docs/decisions/2026-06-29-phase-7c-c-a-fanout-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7c-c-a.md external_repo: rocky-hq/hearth external_pr: 6 external_merge: 3cad131ef0c777664ddeeb4c68a6122ede3582c2

Phase 7c-c-a-hearth close

Shipped

Locked decisions

1. Wire-parity with the console side

Same env key format (ROCKY_POLAR_TIER_<SLUG> / ROCKY_POLAR_SEATS_<SLUG> / ROCKY_AGENT_APPROVED_<AGENT_ID>; uppercase + hyphens→underscores). Same conservative unknown-tier floor. Same deny reason strings verbatim. An operator troubleshooting a 402/403 in production must not have to remember which subsystem's language they're reading. Divergence here would be a maintenance leak.

2. Env keys are shared across producers, not scoped per-subsystem

ROCKY_POLAR_TIER_IRIS_HQ=team grants entitlement for every producer whose registration owner is iris-hq, across both console and hearth. This matches the operator mental model: a Polar tier is per-workspace, not per-agent. Alternatives rejected:

3. Deny returns typed error codes, not just HTTP status

Hearth's existing ErrorResp shape carries {code, message, retryable}. Denial writes code: "approval_denied" (403) or "entitlement_denied" (402), matching the vocabulary hatch's projection will render in the "why" column. Consumers can key error handling on the code, not just the status.

4. Teardown verb hardening deferred

The predecessor hearth/CLAUDE.md flagged: Teardown should ship with approval.required: true + airlock_verb: agent.teardown. This slice does NOT land that change. Reason: the env-driven approval stub could power it today, but the operator UX is brittle (each teardown requires setting an env flag with a specific per-agent key). Better to wait for the real airlock signed-token contract in 7c-c-c so approval flows through a proper verb, not a manual env toggle. Explicitly noted as future work in the hearth CLAUDE.md.

5. Outbound x-rocky-user-role: operator header unchanged in this slice

The HTTPEmitter continues to send x-rocky-user-role: operator when POSTing agent.* events to /api/relay/agent. This is the same 7c-b caveat carried forward: an airlock-signed cross-service token replaces the header stub in 7c-c-c. Not a regression — the header stub existed pre-slice and the gate seam is independent of the outbound emit auth.

6. Server tests validate wiring, unit tests validate logic

The 4 internal/server/gate_test.go specs assert: default registration allows end-to-end, empty slug skips gate, malformed JSON short-circuits before gate, conservative-floor rule holds through the wiring. Deny-path assertions on tier / seats / approval live in internal/agent/gate_test.go where the registration can be freely mutated. Splitting this way avoids reaching into BuildRegistration from server tests just to force a deny — the helper is already tested at its own level.

7. go.work gitignored

go.work references ../contracts/go — a path that only exists on developer machines where the contracts submodule sits sibling to hearth. Committing it would break CI (unresolvable path). .gitignore catches it now so a future git add -A doesn't sneak it in.

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 seams + RALPH gated
7c-c-a-fanout closed relay/sniffer/council gated; shared helper
7c-c-a-hearth closed (this) Go-side gate in hearth RPC handlers
7c-c-b not-started (external) Polar SDK swap
7c-c-c not-started (external, airlock) agent.approve + signed token

Rocky-hq producer-side of Phase 7 is now complete across TypeScript and Go. Every emitting producer (RALPH, RELAY, SNIFFER, COUNCIL, HEARTH driver × 4 verbs) is gated. STRATT stays ungated per spec (registration is free). Remaining Phase 7 work is entirely external.

Sources