apply: tier 0 consumes a declaration and converges this machine #1

Closed
jschoubben wants to merge 3 commits from apply/tier0-declaration-consumer into main
Owner

Starts stage 2 of the node host — the biggest unproven claim in the work breakdown. The host stops only reporting (profile/inventory) and starts doing its one job (ADR 0037): take an ordered list of typed resources and make the machine match it, from a local file, with no mesh present.

This slice implements the network-free vocabulary ADR 0043 names as first:

  • Parse — JSON, refused whole on an unknown version / type / field, a missing id·type·path, or a duplicate id. An older host can't be handed a newer vocabulary and do half of it.
  • directory and file appliers, each reading back after writing — mode and owner asserted against the machine, content compared byte-for-byte. A value that didn't take is a failed apply, not a success.
  • store — the applied-state record, authoritative while disconnected, written atomically. It's what makes removal possible.
  • Convergence — apply in the stated order (host never reorders, ADR 0037), record each success after it works (ADR 0035), remove what was applied before and is no longer declared (reverse order, so a file goes before its directory).
  • Data-loss guard — the host removes only what it created, never what it adopted; a created directory that now holds data is refused (os.Remove, never RemoveAll), per ADR 0018/0030. created is sticky across re-applies.
  • Addressing — a declaration for another node is refused; a host with no identity yet applies its bundle (the first-node path).

Not yet: sealed secrets, and the types needing the network or a runtime (container, package, network, service, archive, user, action) — they follow, and until then the host refuses them rather than doing part of a declaration.

Testing: go test ./... green; the apply suite covers refuse-whole, idempotency, sticky-created, adopted-never-removed, failed-step-fails-and-records-only-what-landed, addressing, and read-back. Verified on the real binary too — which is how the sticky-created bug was caught (the unit test that applied once masked it; running apply twice then dropping a resource exposed a leaked file).

One field-shape note for reviewers: internal/apply/declaration.go's shapes table is the host's copy of the wire contract whose other half is mesh-control's examples/modules/modules_test.go. Deliberately duplicated (the host shares no code with other tiers, ADR 0041) and checked on both sides.

https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF

Starts stage 2 of the node host — the biggest unproven claim in the work breakdown. The host stops only reporting (`profile`/`inventory`) and starts doing its one job (ADR 0037): take an ordered list of typed resources and make the machine match it, from a local file, with no mesh present. **This slice** implements the network-free vocabulary ADR 0043 names as first: - **Parse** — JSON, refused *whole* on an unknown version / type / field, a missing id·type·path, or a duplicate id. An older host can't be handed a newer vocabulary and do half of it. - **`directory` and `file` appliers**, each reading back after writing — mode and owner asserted against the machine, content compared byte-for-byte. A value that didn't take is a failed apply, not a success. - **`store`** — the applied-state record, authoritative while disconnected, written atomically. It's what makes removal possible. - **Convergence** — apply in the stated order (host never reorders, ADR 0037), record each success *after* it works (ADR 0035), remove what was applied before and is no longer declared (reverse order, so a file goes before its directory). - **Data-loss guard** — the host removes only what it *created*, never what it adopted; a created directory that now holds data is refused (`os.Remove`, never `RemoveAll`), per ADR 0018/0030. `created` is sticky across re-applies. - **Addressing** — a declaration for another node is refused; a host with no identity yet applies its bundle (the first-node path). **Not yet:** sealed secrets, and the types needing the network or a runtime (`container`, `package`, `network`, `service`, `archive`, `user`, `action`) — they follow, and until then the host refuses them rather than doing part of a declaration. **Testing:** `go test ./...` green; the apply suite covers refuse-whole, idempotency, sticky-created, adopted-never-removed, failed-step-fails-and-records-only-what-landed, addressing, and read-back. Verified on the real binary too — which is how the sticky-`created` bug was caught (the unit test that applied once masked it; running `apply` twice then dropping a resource exposed a leaked file). One field-shape note for reviewers: `internal/apply/declaration.go`'s `shapes` table is the host's copy of the wire contract whose other half is mesh-control's `examples/modules/modules_test.go`. Deliberately duplicated (the host shares no code with other tiers, ADR 0041) and checked on both sides. https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 1 commit 2026-09-02 20:35:06 +00:00
Stage 2 begins. The host stops only reporting and starts doing its one
job (ADR 0037): take an ordered list of typed resources and make the
machine match it, from a local file, with no mesh present.

What lands in this slice — the network-free vocabulary ADR 0043 names
first:
- Parse: JSON, refused WHOLE on an unknown version, type, field, a
  missing id/type/path, or a duplicate id. An older host cannot be
  handed a newer vocabulary and do half of it.
- directory and file appliers, each reading back after it writes —
  mode and owner asserted against the machine, content compared byte
  for byte. A value that did not take is a failed apply, not a success.
- store: the applied-state record, authoritative while disconnected,
  written atomically. It is what makes removal possible.
- Convergence: apply in the stated order (the host never reorders),
  record each success AFTER it works (ADR 0035), and remove what was
  applied before and is no longer declared — in reverse order, so a
  file goes before the directory that held it.
- The data-loss guard: the host removes ONLY what it created, never
  what it adopted, and a created directory that now holds data is
  refused (os.Remove, never RemoveAll) rather than deleted (ADR 0018,
  0030). created is sticky across re-applies — caught by running the
  real binary, not just the unit tests: recomputing it from disk made
  a re-applied resource look adopted and leak on the next drop.
- Addressing: a declaration for another node is refused; a host with
  no identity yet applies its bundle (the first-node path).

Not yet: sealed secrets, and the types that need the network or a
runtime (container, package, network, service, archive, user, action)
— they follow, and until then the host refuses them rather than doing
part of a declaration.

CLI: mesh-host apply [--store P] FILE.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 1 commit 2026-09-02 20:35:45 +00:00
Keeps the repo honest about itself — 'applies nothing' was true until
the commit before this one.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben added 1 commit 2026-09-02 20:48:31 +00:00
Extends the applier past the filesystem to the two types the workloads
need: a container and the private network it joins. The workloads are
the bulk of what a cutover re-declares (research 009), so this is what
makes a workload manifest actually appliable.

- container: run/reconcile/remove over the runtime. Up to date means a
  container that is ours (a spec-hash label matches this exact
  declaration) AND running; anything else — a changed spec, a stopped
  container, or a foreign one the old control plane left by that name —
  is recreated into ours. Safe because a container carries no state:
  its data is in bind-mounted directories declared separately, and
  recreating it never touches them. Read-back asks the runtime whether
  it is actually running on the declared spec, because 'started' only
  means the runtime returned.
- network: create if absent, adopt if present, remove only what it
  created.
- The runtime is driven through a Runner, faked in unit tests and
  exercised for real in a smoke test that stands a container up, proves
  idempotency, and tears it down — skipped, never failed, where the
  runtime is absent.

The store's per-resource reference generalises from a path to a ref:
a path for files and directories, a name for containers and networks.

Verified end to end through the binary: a container on a bind mount,
then dropped from the declaration — the container is removed and the
data directory survives, which is the migration property itself.

Still deferred: sealed secrets, and package/service/archive/user/action
— refused whole until built, never half-applied.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
Author
Owner

Closing without merging. This was built on main, which is 39 commits behind initialization — the live development branch. initialization already implements a more complete apply than this PR: node identity and sealing, the link and enrolment, the pinned bundle, and every resource type (container, archive, bytes, package, service, action, user, file, directory), on the real wire format ({"declaration":1,…}). It also gets things this PR got wrong: network is a container field, not a separate resource type; failure is per-resource with actions fail-fast, not a blanket stop.

Nothing here should land — it would fork the codebase into a less-complete parallel. Recorded as a lesson: check the active branch (initialization, not main) before starting, the same way mesh-control develops on initialization.

Closing without merging. This was built on `main`, which is 39 commits behind `initialization` — the live development branch. `initialization` already implements a more complete apply than this PR: node identity and **sealing**, the link and enrolment, the pinned bundle, and every resource type (container, archive, bytes, package, service, action, user, file, directory), on the real wire format (`{"declaration":1,…}`). It also gets things this PR got wrong: `network` is a container field, not a separate resource type; failure is per-resource with actions fail-fast, not a blanket stop. Nothing here should land — it would fork the codebase into a less-complete parallel. Recorded as a lesson: check the active branch (`initialization`, not `main`) before starting, the same way mesh-control develops on `initialization`.
jschoubben closed this pull request 2026-09-02 20:57:17 +00:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-host#1