title: Phase 7c-a close — durable agent.* audit + projection endpoint date: 2026-06-29 status: Accepted phase: 7c-a (rocky-side producer for external hatch consumer) predecessor: docs/decisions/2026-06-29-phase-7b-ralph-2-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7c-a.md
Phase 7c-a close
Shipped
rocky-hq/consolePR #20 (merge commit8299d0c): durable audit + read surface for theagent.*HATCH stream that Phase 7b producers already emit.src/lib/agent/audit.ts— rolling 5000-entry JSON array at${getDataDir()}/agent-audit.json. Mirrorssrc/lib/relay/audit.tsshape. Corrupt-file read → empty (no crash). Data path resolves per-call soROCKY_DATA_DIRoverrides work at test time.src/lib/agent/projection.ts— pure foldAgentHatchEvent[] → AgentProjection[]matching the AGENT/TYPE/OWNER/APPROVAL/CAPABILITIES/RATE column set from the spec §Consumer projection. Registration = last-write-wins onagent_id;invocationsfoldsinvoked/completed{ok,error,cancelled}idempotent oninvocation_id;revoked_tsset onagent.revoked(row retained per spec).src/app/api/relay/agent/route.ts:POSTpersists best-effort — a disk-full / lock-contention MUST NOT cascade to a producer response. Persist errors log to[relay/agent] persist failedand the route still returns202.GETgates onobserver+and supports?workspace=<slug>(matchesregistration.owner.workspace_slugforagent.registered, falls back toagent_idprefix for other event kinds because non-registration events don't carry workspace directly),?since=<iso-ts>(inclusive lower bound),?limit=<n>(default 500, cap 5000). Projection is always computed over the FULL filtered set so counters remain correct when the events window is trimmed.
.gitignorefix: anchor theagent/rule to root (/agent/). The unanchored form was hidingsrc/lib/agent/andsrc/app/api/relay/agent/from git and would have silently swallowed this slice's source files.- Contracts version unchanged (
^0.3.0— theAgentHatchEventunion already ships all consumed shapes via@rocky-hq/contracts/agent).
Locked decisions
1. Persistence = rolling JSON file (mirror of relay-audit)
Chosen at plan approval. The relay audit at src/lib/relay/audit.ts already carries the same shape (single-writer, atomic overwrite, 5000-entry trim), and the operator model is single-console-per-workspace. A real event journal (append-only log, offset tracking, compaction) belongs in Phase 8 when multi-consumer / multi-instance land — introducing it here is scope inflation for a producer surface.
Alternatives rejected:
- SQLite / better-sqlite3 store — adds a persistent lock file, a migration surface, and a new dependency for a stream whose scale is already bounded. Not worth it at 5000 entries.
- Event journal with offset cursors — real value only lands once the hatch consumer streams (not polls). 7c-b polls; when it moves to a stream, revisit.
2. Best-effort persistence: 202 even on write failure
Producer isolation is a hard invariant in Phase 7b — sniffer/stratt/driver close-notes all locked "emit failure is non-blocking; hatch outage never faults the producer". POST here is the receive-side of that same invariant. A disk-full at the audit sink must not cascade into a 500 that then propagates upstream via emitAgentHatch back into the producing route. Log-and-swallow keeps producer contracts intact.
The trade-off is silent-drop under sustained failure. Acceptable at this altitude — the projection endpoint's freshness monitoring is the correct place to detect drops, not per-request 5xx.
3. Read endpoint gates observer+, not admin
Mirrors relay-audit surface. Audit content is operational observability, not privileged data — read-broad, write-narrow is the console pattern.
4. Workspace filter falls back to agent_id prefix for non-registration events
Only agent.registered carries registration.owner.workspace_slug in-payload. agent.{invoked,completed,revoked} carry only agent_id. The spec's agent_id format is <workspace>-<scope>-<local>, so a prefix match on ${workspace_slug}- correctly partitions the tail events. Not spec-mandated; documented in-code and asserted via test.
Alternatives rejected:
- Denormalize workspace_slug into every event — spec-breaking, and the six 7b producers would all need re-emits.
- Fold registration into projection first, then filter by projection — works for projection, doesn't work for the
eventsarray returned alongside.
5. Projection computed over full filtered set, not the trimmed events slice
?limit=<n> trims the returned events for wire-size control, but the projection is a summary — clamping it to the same slice would undercount agents that already have >n invocations. Tested explicitly.
6. Cap: limit clamped at 5000 (= audit ceiling)
No point returning more events than the audit ever holds. Non-numeric / negative / missing limit falls to default 500. Guarded and tested.
Deferred (external repos)
7c-b — hatch.devarno.cloud/agents consumer
Polls GET /api/relay/agent, projects to the AGENT/TYPE/OWNER/APPROVAL/CAPABILITIES/RATE table. Not in rocky-hq. This slice unblocks it by giving it a stable read surface.
7c-c — Polar entitlement + Airlock agent.approve
- Polar RATE column enforcement. The audit stores
registration.rate.{tier_floor,seats_required}verbatim. Runtime entitlement checks at invocation-time live in the airlock/polar path, outside this repo. - Airlock
agent.approveverb. Producer registrations already carryapproval.required; the enforcement layer lives in airlock 7c-c.
agent.revoked producer emission
Row retention on revocation is implemented in the projection (row kept, revoked_ts set) but no rocky-hq producer emits agent.revoked yet. Registry-teardown is an airlock/hatch-owned verb (7c-c). The projection supports it in advance so 7c-c can drive it without a rocky-hq change.
Event journal / stream API
GET /api/relay/agent polls a snapshot. A future streaming surface (SSE or WebSocket) would let 7c-b subscribe instead of poll. Not needed for the initial consumer.
Multi-workspace RBAC on the read surface
Today the read gate is role-based, not tenant-partitioned. Multi-workspace partition lands when multi-workspace itself does.
Acceptance verified
npm run typecheckclean.npm run test:run470/470 (+24 vs 7b-ralph-2 → 12 projection specs + 5 audit specs + 12 route specs; no regressions).- POST agent.registered → audit persisted → GET returns event + projection row with matching
agent_id,owner.workspace_slug, capabilities. - POST agent.invoked → projection row's
invocations.total = 1,invocations.inflight = 1. - POST agent.completed{ok} →
invocations.completed = 1,invocations.inflight = 0. - POST agent.completed{error} →
invocations.errored = 1. - POST agent.completed{cancelled} →
invocations.cancelled = 1. - POST agent.revoked → row retained with
revoked_tsset. - POST with mocked
appendAgentAuditEventthrowing → route returns 202 (best-effort). - GET
?workspace=<slug>partitions correctly for both registration and tail events. - GET
?since=<iso-ts>inclusive lower-bound (fake-timer test locks server-stampedts). - GET
?limit=<n>trims events, leaves projection whole; non-numeric/negative/missing clamp to defaults. - GET auth:
observer/operator/adminall 200; missing role 403. - Audit trim: 5001st append drops the oldest; corrupt file read → empty array;
_resetAgentAuditForTestisolates.
Phase 7 status after this slice
| Slice | Producer | Consumer | Notes |
|---|---|---|---|
| 7a | contracts agent-registration.v1 published |
— | closed 2026-06-29 |
| 7b | ralph / relay / council / sniffer / stratt / driver / ralph-2 | (drop) | closed 2026-06-29 |
| 7c-a | rocky-hq audit + GET projection endpoint | — | closed 2026-06-29 (this) |
| 7c-b | — | hatch.devarno.cloud/agents polls |
external repo |
| 7c-c | — | Polar RATE + Airlock agent.approve |
external repos |
With 7c-a landed, the rocky-hq side of Phase 7 is complete. Remaining Phase 7 work is entirely external.
Sources
docs/specs/2026-06-29-agent-registration-v1.md— wire format + consumer projection.docs/decisions/2026-06-29-agentic-layer-projection.md— projection foundation.docs/decisions/2026-06-29-phase-7b-ralph-2-close.md— immediate predecessor.docs/plans/2026-06-29-phase-7c-a.md— this slice's plan.