← Ledger


title: Phase 7b (hearth driver slice) close — hearth emits agent.* per RPC verb date: 2026-06-29 status: Accepted phase: 7b (hearth driver slice) predecessor: docs/decisions/2026-06-29-phase-7b-stratt-close.md spec: docs/specs/2026-06-29-agent-registration-v1.md plan: docs/plans/2026-06-29-phase-7b-driver.md

Phase 7b hearth-driver-slice close

Shipped

Locked decisions

1. Producer surface: hearth-native emitter

Console SS-08 wrapper (console/src/lib/hearth/) is Phase 5d — not built. There is no console-side callsite to emit from. Hearth emits directly via net/http POST to $HEARTH_AGENT_HATCH_URL. When 5d lands, either flip a flag to prefer console-side emit or accept double-emit (idempotent on agent_id). Alternatives rejected at plan approval:

2. Lifecycle: sync-triple per RPC verb

Each RPC handler is one request/response with one full invocation lifecycle. registered + invoked on entry, completed{outcome} on exit. capability = driver.<verb>. Prior close-note (docs/decisions/2026-06-29-phase-7b-sniffer-close.md §Carry-forward) hypothesized "reverts to RALPH split-emit shape" — that hypothesis was pre-inspection and wrong. RALPH split-emit exists because RALPH has separate submit/cancel/terminal entry points touching different stages of the same invocation. Hearth's four verbs are each one full stage; no split needed. Alternatives rejected:

3. Slug gate consistent with prior slices

Empty slug → zero emission. Provision reads req.Slug; Status/Upgrade/Teardown read req.Ref.WorkspaceSlug. This mirrors the x-rocky-workspace header gate used by every console-side producer (ralph/relay/council/sniffer/stratt): no owner → no agent identity → no event.

4. Emitter is non-blocking + best-effort

HTTPEmitter.Emit fires a goroutine per event bounded by Timeout (default 2s, HEARTH_AGENT_HATCH_TIMEOUT_MS override). Marshal errors, transport errors, and non-2xx responses log at slog.Warn and drop. Rationale: a hatch outage MUST NOT turn a successful Provision into an RPC failure — that would double the blast radius of a HATCH incident onto the provisioner path. Matches sniffer/stratt console-side isolation (try/catch → console.error).

5. Service-role header

Outbound POST sets x-rocky-user-role: operator (the minimum role the relay-agent route accepts). Hearth speaks as an operator-scope service. Phase 7c hardens this via bearer auth against the relay-agent route (currently the route accepts operator/admin/observer with no signature). Not a security regression: the socket-bound hearth binary already runs under operator-owned filesystem permissions.

6. Default is Nop; env is the opt-in

agent.EmitterFromEnv() returns NopEmitter{} when HEARTH_AGENT_HATCH_URL is unset. Rationale: pre-5d hearth deployments (already running in some environments) MUST NOT start emitting to a URL that hasn't been configured. Operator opt-in via env, not implicit.

7. WithEmitter(nil) → Nop

server.Server treats a nil emitter as NopEmitter{} so downstream handler code never nil-checks. Tested explicitly.

Deferred

Console-side emission (Phase 5d)

When the console SS-08 wrapper lands, decide: (a) flip a flag on the hearth binary to disable native emit and let console own it (uniform pattern), or (b) accept double-emit (idempotent on agent_id, the hatch consumer dedupes). Choice depends on whether the console wrapper adds per-request context (auth actor, workspace-scoped bearer) that hearth cannot see.

approval.required: true for Teardown

Phase 7c will flip Teardown to approval.required: true with airlock_verb: agent.teardown so operator destructive-action gates apply. Deferred here because the Airlock verb registry itself is 7c work.

agent.revoked

Not emitted in this slice. When 7c lands, a successful Teardown may follow its completed{ok} with an agent.revoked{reason:policy} if the workspace slug is being retired. Producer-side hearth has no signal for retire-vs-recycle today; console would carry that context.

Bearer auth on outbound POST

Phase 6 adds bearer auth to hearth's own inbound RPC surface; the outbound agent-hatch POST will follow the same evolution. Currently hearth POSTs with x-rocky-user-role: operator and no bearer — accepted for this slice because the console-side relay-agent route does the same role-only check for every producer.

Retry / queue on emit failure

Best-effort POST, drop on failure. Log-only observability. If hatch outages become recurring pain, Phase 7c can add a bounded in-memory retry queue; today the operational bet is that hatch is available whenever hearth is.

Producer fan-out (Phase 7b) — complete

With this slice, all six agent-registration.v1 producers named in docs/decisions/2026-06-29-agentic-layer-projection.md §Producer fan-out are wired:

Slice Producer PR
7b-ralph RALPH console #14 → parent #68
7b-relay RELAY console #15 → parent #69
7b-council COUNCIL console #16 → parent #70
7b-sniffer SNIFFER console #17 → parent #71
7b-stratt STRATT console #18 → parent #72
7b-driver HEARTH driver hearth #5 → parent (this)

Three lifecycle patterns emerged:

Remaining 7b work:

Acceptance verified

Sources