← Ledger


title: Phase 5c close — Hearth LocalDocker driver + JSON-over-HTTP RPC date: 2026-06-29 status: Accepted phase: 5c predecessor: docs/decisions/2026-05-25-phase-5b-close.md spec: docs/specs/2026-05-04-rocky-phase-5.md §6 + §7 plan: docs/plans/2026-06-20-hearth-localdocker-rpc-phase-5c.md

Phase 5c close

Shipped

Carry-forwards

1. Docker SDK pinned at v28.5.2

The plan specified github.com/docker/docker@v27.3.1. cmd/hearth/main.go (Task 6) was the first real consumer of client.NewClientWithOpts — unit tests previously used a mocked dockerAPI. v27.3.1's client package imports a sockets.DialPipe symbol that no longer exists in current go-connections, breaking the cmd-binary build. v28.5.2 resolves cleanly. v28 also keeps types.ContainerJSON and types.Container as deprecated aliases for container.InspectResponse and container.Summary; the dockerAPI interface uses the non-aliased forms to satisfy staticcheck SA1019. Future hearth SDK bumps should land in their own PR with a CI run, since v27/v28 transitive deps differ materially.

2. DeploymentRef.Created reads from container inspect, not d.now()

The first cut of Task 2 sourced Created from a driver clock (d.now()) on every Provision call, including the idempotent re-Provision path. Re-invocations returned a fresh timestamp, breaking the spec's idempotence guarantee that (slug, tier, driver) returns "the existing ref." Fix at c7e2f35 extracts cairnetCreated(ctx, summaries) which reads the cairnet container's inspect.Created; both fresh and idempotent paths use it, so ref == ref across repeated Provision(slug, sameTier) calls. Falls back to d.now() if the cairnet container is absent (mid-Provision race) or if inspect/parse fails. The integration test was tightened to assert full struct equality. The takeaway: DeploymentRef.Created is "deployment birth," not "ref construction time"; future drivers (Kustomize in Phase 6a, DevarnoCloud in Phase 6b) should keep this invariant.

3. LabelTier is a fourth canonical label

The plan §Global Constraints listed three canonical labels (managed-by, workspace, role). To enforce tier-mismatch rejection on Provision against an existing workspace, LabelTier = rocky-hq.io/tier was added in Task 2 and lives in internal/driver/localdocker/labels.go alongside the other three. It is applied to network + every container + every volume (writing it on the network is benign — tier-mismatch check reads containers — but it keeps label coverage symmetric for future label-filter sweeps). Future drivers should preserve this label-set so cross-driver Status views remain meaningful.

4. Provision and Teardown are split into helpers for gocyclo

The plan's literal Provision body crossed golangci-lint's default gocyclo ceiling (19 against 15). Task 8.5 (lint cleanup) extracted createNetwork / createWorkspaceContainers / imageFor / pullImage / rollbackWorkspace. Same shape applied to TeardownteardownContainers / teardownNetworks / teardownVolumes. Contract semantics unchanged (rollback semantics preserved, ctx honoring preserved, idempotence path preserved). Future driver implementations should aim for similar cohesion — keep the public method orchestrating, push SDK calls into helpers.

5. Wire-format admin-throw vs SSE-skip parser pattern remains intact

Server boundary parsers reuse parseProvisioningProfile / parseDeploymentRef from @rocky-hq/contracts/go/hearth (Phase 5b §5 carry-forward). The HEARTH throw-on-invalid pattern is the right shape for admin RPC: bad input surfaces as a 4xx (invalid_request code in the error envelope). KILN-extensibility is preserved — the server's JSON decoder does NOT call DisallowUnknownFields, so additive contract bumps don't break older callers. Verified by the unknown-field passthrough test in internal/server/server_test.go.

6. No-test-only-mutation-of-internal/ exception: WorkspaceFilterForTests

Integration cleanup needs label-filtered sweeps over containers/networks/volumes for a workspace slug. The internal workspaceFilter(slug) helper is unexported. Task 7 exported WorkspaceFilterForTests(slug) as a thin wrapper (it is the only test-only export in the localdocker package). Future packages may follow the same convention if cleanup helpers require label-aware lookups; the _ForTests suffix is the convention to keep it grep-able and out of production code.

Phase 5c acceptance verified at merge

Out of scope (deferred per plan §Out-of-scope)

Sources