title: Phase 7c-b close — external hatch consumer polls rocky projection
date: 2026-06-29
status: Accepted
phase: 7c-b (external hatch.devarno.cloud/agents consumer)
predecessor: docs/decisions/2026-06-29-phase-7c-a-close.md
spec: docs/specs/2026-06-29-agent-registration-v1.md
plan: docs/plans/2026-06-29-phase-7c-b.md
external_repo: devarno-cloud/hatch
external_pr: 13
external_merge: 08896dc0f877827c5e6d21ee50f945b1dc78704c
Phase 7c-b close
Shipped
devarno-cloud/hatchPR #13 (merge commit08896dc):/agentsrenders the rocky projection alongside its existing airlock user-agent table. Fills the AGENT/TYPE/OWNER/APPROVAL/CAPABILITIES/RATE column shell drafted for this exact projection at the initialhatch.devarno.cloud/agentsscaffold (predecessor decision2026-06-29-agentic-layer-projection.md).src/lib/rocky-registry.ts— thin fetch client.fetchRockyAgents()calls${ROCKY_CONSOLE_URL}/api/relay/agent(defaulthttps://rocky.erid.tech) withx-rocky-user-role: ${ROCKY_SERVICE_ROLE}(defaultobserver). 5sAbortControllertimeout. Non-200 / throw / abort return{ projection: [], error }so a rocky outage never faults/agents.formatRate({tier_floor, seats_required})→"<tier_floor>+ · N seat(s)"per spec §Field semantics.src/app/agents/page.tsx:Promise.all([loadAgents(), fetchRockyAgents()])— the two sections load in parallel and fail independently. The Rocky section adds a telemetry meta line under the agent name (<agent_id> · N invocations · M inflight) when the projection has any activity. Revoked rows render arevokedbadge in the Approval column..env.example: addsROCKY_CONSOLE_URL+ROCKY_SERVICE_ROLEwith spec link.
- 13 new specs; hatch suite 62/62; typecheck +
next lintclean.
Locked decisions
1. No re-fold on the consumer
Hatch trusts the fold that rocky-hq/console already computes. Adding a second fold client-side would duplicate spec surface and diverge on edge cases (out-of-order arrival, dedupe keys). The rocky projection is authoritative; hatch is a view of a view.
Alternatives rejected:
- Hatch computes its own projection from
events— worth it only if hatch adds fields rocky doesn't (it doesn't). Rejected. - Wire the raw event stream through hatch — the projection is what the operator needs; the raw events are debug tooling.
2. x-rocky-user-role: observer stub for cross-service auth
Rocky's GET is gated on the same three-role model (observer+) it uses for browser callers. Hatch is a trusted operator-tier peer under the same erid.tech umbrella, so a static role header is the minimal-surface bridge until airlock ships a signed cross-service token. That upgrade lands with 7c-c.
Alternatives rejected:
- mTLS between hatch and rocky — heavier than warranted; both are Vercel deployments today, not a service mesh.
- Copy the airlock session cookie forward — leaks a user-scoped credential across services with no upside.
3. Parallel independent loads for airlock + rocky
Promise.all([loadAgents(), fetchRockyAgents()]). Either can fail; the other renders. Independent warning cards. The existing user-agent path is unchanged.
4. 5s timeout, best-effort read
Consumer-side timeouts belong on the poller, not on rocky. 5s is tight enough that a wedged endpoint never blocks an SSR render. Rocky's audit is an in-process JSON read; if 5s is short in practice we bump — but making the timeout longer at this altitude just hides slowness.
5. Rocky projection renders below, not replacing, airlock agents
Two distinct populations: airlock's AdminAgent (user-issued API-key agents — the airlock's own "who has an approved API key" list) versus rocky's registry projection (operator-facing agent.* producer view from council/sniffer/stratt/ralph/relay/driver). Same page because both answer "what agents exist"; separated because they answer it at different altitudes. When the two populations converge (e.g. when airlock 7c-c gates agent.approve on both), this split can revisit.
6. No workspace filter UI
Rocky supports ?workspace=<slug> on GET; hatch renders all workspaces flat in this slice. Hatch does not have a workspace switcher — it lists everything the operator owns. A filter UI can land when hatch grows tenant partitioning, not before.
Deferred
- 7c-c — Polar RATE enforcement at invocation time (producer-side, in rocky) +
airlock.devarno.cloudagent.approveverb. The RATE column is currently a display of declared values; enforcement is producer-side by spec §Field semantics. - Airlock-signed service token — replaces the
observerrole stub. Lands with 7c-c. - Streaming subscription (SSE) — polling on SSR is enough at 5000-entry audit ceiling; move to stream when the audit graduates to a journal (Phase 8).
- Deep-links from the projection into rocky.erid.tech — the spec locks
owner.subsystem(SS-NN) as the routing key. Not wired yet.
Acceptance verified
npm run typecheckclean.npm run lintclean (next lint).npm test62/62 (+13 new specs).- Test coverage: URL construction from env, trailing-slash trim, default role header, role override, 200 path, non-200 error surface, network throw, abort/timeout, missing projection field, non-array projection,
formatRatesingular / plural / zero. - Env-var wiring documented in
.env.example.
Phase 7 status after this slice
| Slice | State | Notes |
|---|---|---|
| 7a | closed | contract published, contracts v0.3.0 |
| 7b | closed | all 6 producers + RALPH natural-terminal observer |
| 7c-a | closed | rocky-side audit + GET projection endpoint |
| 7c-b | closed (this) | external hatch consumer wired |
| 7c-c | not-started | Polar RATE enforcement + Airlock agent.approve (external repos) |
Rocky-hq producer side + external hatch consumer side both complete. Remaining Phase 7 work is entirely in polar/airlock territory.
Sources
docs/specs/2026-06-29-agent-registration-v1.md— wire format + Field semantics.docs/decisions/2026-06-29-agentic-layer-projection.md— original decision that drafted the hatch column shell.docs/decisions/2026-06-29-phase-7c-a-close.md— immediate predecessor; producer-side.docs/plans/2026-06-29-phase-7c-b.md— this slice's plan.devarno-cloud/hatchPR #13 (08896dc) — the external code change.