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
rocky-hq/hearthPR #5 (merge commit2705199): hearth's JSON-over-HTTP RPC server wraps each of the fourDriververb handlers (provision/status/upgrade/teardown) with the fullagent.{registered,invoked,completed}lifecycle triple peragent-registration.v1. First non-console 7b producer.- Producer surface: hearth-native.
internal/agent/package holds builders + anet/httpEmitter.NopEmitteris the default;EmitterFromEnv()upgrades toHTTPEmitterwhenHEARTH_AGENT_HATCH_URLis set. Emission is non-blocking (goroutine per Emit,HEARTH_AGENT_HATCH_TIMEOUT_MScap) and best-effort (transport errors + non-2xx log atslog.Warnand drop; RPC response never faults from emit failure). - Agent identity:
agent_id:<workspace>-driver-<driver-name>withdriver-namenormalized to[a-z0-9-](e.g.iris-hq-driver-local-docker,iris-hq-driver-devarno-cloud,iris-hq-driver-kustomize).scope:driver;owner.subsystem:SS-08.capabilities: fixed["driver.provision","driver.status","driver.upgrade","driver.teardown"]; per-invocationcapability = driver.<verb>.invocation_id:uuid.NewString()per request.outcome:nilerr →ok; error →error.duration_ms: wall-clock around the driver call.- Slug source:
req.Slug(Provision) orreq.Ref.WorkspaceSlug(Status/Upgrade/Teardown). Empty slug → zero emission (mirrors console-sidex-rocky-workspaceheader gate).
- Contracts:
github.com/rocky-hq/contracts/gobumped v0.2.0 → v0.3.0 (adds theagent/subpath).
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:
- Builders-only, no emitter — ships the pattern but no callsite. Incomplete slice; blocks 7b close on 5d.
- Defer 7b-driver until 5d — blocks Phase 7 producer fan-out on Phase 5 progress. Unnecessary coupling.
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:
- Split-emit like RALPH — would model Provision as registered/invoked and Teardown as completed, treating provision-lifetime as the invocation. Loses per-verb visibility; conflates provisioner state with invocation state.
- Registered-only-once + invoked+completed per verb — distinguishes registry cadence from invocation cadence. Rejected because the hatch projection dedupes registered idempotent-by-body already; the extra complexity buys nothing.
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:
- Sync-triple per invocation (RELAY, SNIFFER, COUNCIL, hearth driver): each request fires registered+invoked+completed.
capabilityverb per invocation. - Split-emit (RALPH): submit fires registered+invoked; cancel fires completed{cancelled}; natural-terminal observer deferred to 7b-ralph-2.
- Registered-only (STRATT): registry-CRUD producer. Registration on upload; no invoked/completed since there is no invocation surface.
Remaining 7b work:
- 7b-ralph-2: RALPH natural-terminal observer (fires
completed{outcome:ok|error}when a run finishes without operator cancel). Deferred at 7b-ralph close. - 7c: HATCH consumer + Polar RATE column projection + Airlock
agent.approve/agent.teardownverbs.
Acceptance verified
go build ./...clean in hearth.go test ./... -race -count=1green in hearth: 6 driver.provision-triple assertions, 4 emitter/http-transport assertions, error-outcome + empty-slug + WithEmitter(nil) coverage.gofumpt -l .empty;go vet ./...clean;golangci-lint runclean (initial reviveredefines-builtin-idoncopy+maxfixed in follow-up commitc203f58).- CI on hearth PR #5: lint pass, test pass, Integration (Docker) pass.
Sources
docs/specs/2026-06-29-agent-registration-v1.md— wire format.docs/decisions/2026-06-29-agentic-layer-projection.md— predecessor; producer-order ralph→relay→council→sniffer→stratt→hearth driver.docs/decisions/2026-06-29-phase-7b-stratt-close.md— predecessor slice.docs/plans/2026-06-29-phase-7b-driver.md— this slice's plan (approved at PR open).