Work package P1 of hq design 57 (ADR 0349 clause 5), issue 667.
What changed
declaration.File gains owner-at-runtime: owned (written back whenever it differs), seed (written once, then the program's) or managed (only its keys or marked block are the mesh's). File.Ownership() gives the word, or the one the old form implies (into -> managed, create-once -> seed, else owned). File.Seeded() is create-once or seed.
The declaration gains filled_files (Declaration.Filled, Declaration.Fills(path)): the paths of the files the provisioner fills.
Validation: an unknown word is refused; owned refuses create-once and into; seed refuses into (seed alone is enough, create-once beside it is allowed); managed requires into (which names the format). The old rule that into excludes sealed, bytes, secrets and create-once is unchanged. A filled path must be non-empty, named once, and be the path of a declared file, and every file at that path must say owner-at-runtime.
Apply: the words map onto today's write paths with no new code path. applyFile already dispatches on into (managed -> applyBlock / applyInto); the create-once check now reads Seeded(), so seed by the word alone is kept once present. declaredResolvConf reads Seeded() too.
link.OwnershipContract = 9, and the health statement now says it, so a controller (P6) sends the new fields only to an engine that reads them. An older engine refuses them under the strict parse.
Why
The provisioner's client (P5) and the narrow write verb (P2, P7) need to know which files the provisioner fills and who owns each one at runtime. Nothing sends these fields yet: the reader comes first.
How it is proven (all failed before, pass now)
internal/apply/ownership_test.go: TestEachOwnershipBehavesAsItsOldForm applies each word beside its old form, through the same drift, and requires the same actions and the same bytes: owned is created corrected, seed is created kept, managed json is updated corrected with the machine's own keys kept, and managed block is updated corrected with the lines outside the region kept. TestASeedByTheWordAloneKeepsWhatGrew.
validate/validate_test.go: the exported validator (the one the controller vendors) accepts a declaration with the new fields and refuses an undeclared filled path.
cmd/mesh-host/units_test.go: updated to expect OwnershipContract (one above BusyContract). This is the only existing test changed, because the contract number rises.
Deliberately left for later packages
The write verb that accepts only filled paths: P2 (issue 668).
The controller sending owner-at-runtime, filled_files and checking OwnershipContract: P6 (issue 672).
The engine no longer writing filled files from the declaration: P7 (issue 673).
Reviewer's checklist
Does what issue 667 and design 57 P1 say, and nothing else. The fields, the mapping onto the existing paths, and validation are here. No write path changed except that Seeded() replaces CreateOnce in two reads.
The Done-when proof is in the tests above. I ran them first and they failed (the fields were refused as unknown, and the package did not compile). Existing tests are unchanged except the contract-number test, explained above.
A declaration without the new fields composes and applies as before. Both fields are omitempty, Seeded() equals CreateOnce when the word is absent, and TestEachOwnershipBehavesAsItsOldForm compares old and new form byte for byte.
No secret in any log, event, fixture or commit. No database change.
Strict parse: the node-engine reader accepts the new keys, and OwnershipContract tells a controller when it may send them. No writer sends them in this change.
Ran gofmt -l . (empty), go vet ./..., go test ./... and sh merge-check.sh (exit 0). merge-check said: NOT RUN AGAINST A BUS, because this toolchain has no nats-server, so the bus tests were skipped.
Work package P1 of hq design 57 (ADR 0349 clause 5), issue 667.
## What changed
- `declaration.File` gains `owner-at-runtime`: `owned` (written back whenever it differs), `seed` (written once, then the program's) or `managed` (only its keys or marked block are the mesh's). `File.Ownership()` gives the word, or the one the old form implies (into -> managed, create-once -> seed, else owned). `File.Seeded()` is create-once or seed.
- The declaration gains `filled_files` (`Declaration.Filled`, `Declaration.Fills(path)`): the paths of the files the provisioner fills.
- Validation: an unknown word is refused; owned refuses create-once and into; seed refuses into (seed alone is enough, create-once beside it is allowed); managed requires into (which names the format). The old rule that into excludes sealed, bytes, secrets and create-once is unchanged. A filled path must be non-empty, named once, and be the path of a declared file, and every file at that path must say owner-at-runtime.
- Apply: the words map onto today's write paths with no new code path. applyFile already dispatches on `into` (managed -> applyBlock / applyInto); the create-once check now reads `Seeded()`, so seed by the word alone is kept once present. `declaredResolvConf` reads `Seeded()` too.
- `link.OwnershipContract = 9`, and the health statement now says it, so a controller (P6) sends the new fields only to an engine that reads them. An older engine refuses them under the strict parse.
## Why
The provisioner's client (P5) and the narrow write verb (P2, P7) need to know which files the provisioner fills and who owns each one at runtime. Nothing sends these fields yet: the reader comes first.
## How it is proven (all failed before, pass now)
- internal/apply/ownership_test.go: `TestEachOwnershipBehavesAsItsOldForm` applies each word beside its old form, through the same drift, and requires the same actions and the same bytes: owned is `created corrected`, seed is `created kept`, managed json is `updated corrected` with the machine's own keys kept, and managed block is `updated corrected` with the lines outside the region kept. `TestASeedByTheWordAloneKeepsWhatGrew`.
- internal/declaration/ownership_test.go: `TestAFileSaysWhoOwnsItAtRuntime`, `TestAFileThatDoesNotSayIsOwnedByItsOldForm`, `TestAnOwnershipThatContradictsTheFileIsRefused`, `TestTheDeclarationNamesTheFilesTheProvisionerFills`, `TestAFilledFileMustBeADeclaredFileThatSaysItsOwner`.
- validate/validate_test.go: the exported validator (the one the controller vendors) accepts a declaration with the new fields and refuses an undeclared filled path.
- cmd/mesh-host/units_test.go: updated to expect OwnershipContract (one above BusyContract). This is the only existing test changed, because the contract number rises.
## Deliberately left for later packages
- The write verb that accepts only filled paths: P2 (issue 668).
- The controller sending `owner-at-runtime`, `filled_files` and checking `OwnershipContract`: P6 (issue 672).
- The engine no longer writing filled files from the declaration: P7 (issue 673).
## Reviewer's checklist
1. [x] Does what issue 667 and design 57 P1 say, and nothing else. The fields, the mapping onto the existing paths, and validation are here. No write path changed except that `Seeded()` replaces `CreateOnce` in two reads.
2. [x] The Done-when proof is in the tests above. I ran them first and they failed (the fields were refused as unknown, and the package did not compile). Existing tests are unchanged except the contract-number test, explained above.
3. [x] A declaration without the new fields composes and applies as before. Both fields are `omitempty`, `Seeded()` equals `CreateOnce` when the word is absent, and `TestEachOwnershipBehavesAsItsOldForm` compares old and new form byte for byte.
4. [x] No secret in any log, event, fixture or commit. No database change.
5. [x] Strict parse: the node-engine reader accepts the new keys, and `OwnershipContract` tells a controller when it may send them. No writer sends them in this change.
6. [x] Ran `gofmt -l .` (empty), `go vet ./...`, `go test ./...` and `sh merge-check.sh` (exit 0). merge-check said: NOT RUN AGAINST A BUS, because this toolchain has no nats-server, so the bus tests were skipped.
7. [x] Words: owned, seed, managed (glossary, issue 653), node-engine, provisioner, controller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mesh/merge-gate head 1cc588ebc060 merged into trunk 1203f96a24c8 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
The provisioner (ADR 0349 §5) needs each file it places to say whether it is
owned, a seed or managed, and the node-engine needs to know which paths it
fills. The words map onto today's forms (drift written back, create-once,
into), so no write changes; the reader accepts the fields and states
OwnershipContract before any controller sends them. Issue 667.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deliverynovox/mesh-host@1cc588ebc060 — delivered since 2026-10-13T23:53:19Z
Delivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
tier 0: mesh-host
ace: receives mesh-host
g14: receives mesh-host
novox: receives mesh-host
shanks: receives mesh-host
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before mesh-host is judged on its first machine
Transitions
2026-10-13 23:48 (new) → proposed (announced): novox/mesh-host#109's head announced
2026-10-13 23:51 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
2026-10-13 23:51 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-13 23:52 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-13 23:52 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
2026-10-13 23:53 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-host@1cc588ebc060` — **delivered** since 2026-10-13T23:53:19Z
**Delivery plan** — builds mesh-host → ace, g14, novox, shanks; no bus step
- tier 0: mesh-host
- ace: receives mesh-host
- g14: receives mesh-host
- novox: receives mesh-host
- shanks: receives mesh-host
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before mesh-host is judged on its first machine
**Transitions**
- 2026-10-13 23:48 (new) → proposed (announced): novox/mesh-host#109's head announced
- 2026-10-13 23:51 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
- 2026-10-13 23:51 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-13 23:52 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-13 23:52 published → delivering (go): its walk started: let go by this owner in its group's order, by a person, or on the controller's own path — not a failed walk that carried a later commit for it, after which it waits for a walk of its own commit
- 2026-10-13 23:53 delivering → delivered (done): its walk is done, carrying its own commit: every machine of its deploy plan passed, or was left as its policy says
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Merge check at 1cc588eb
Judged: head 1cc588ebc060 merged into trunk 1203f96a24c8; read beside it: mesh-catalog ebf7f3773dc7, mesh-controller ca3f43c400ba, mesh-controller-main ca3f43c400ba, mesh-tools 954e1a53eef9
mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
mesh/repo-check: PASS — its merge-check.sh passed
Delivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
tier 0: mesh-host
ace: receives mesh-host
g14: receives mesh-host
novox: receives mesh-host
shanks: receives mesh-host
grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before mesh-host is judged on its first machine
read by no module's build: cmd/mesh-host/units_test.go, internal/apply/ownership_test.go, internal/declaration/ownership_test.go, validate/validate_test.go
Run by the build seat on shanks as build-1791935307449323964 (builds --log build-1791935307449323964).
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T23:45:15Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T23:45:15Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
notes:
- cmd/mesh-host/units_test.go, internal/apply/ownership_test.go, internal/declaration/ownership_test.go and validate/validate_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.017s
ok github.com/novox/mesh-host/cmd/mesh-host 6.364s
ok github.com/novox/mesh-host/examples 1.027s
ok github.com/novox/mesh-host/internal/accounts 2.324s
ok github.com/novox/mesh-host/internal/apply 5.156s
ok github.com/novox/mesh-host/internal/bootstrap 4.434s
ok github.com/novox/mesh-host/internal/bundle 1.036s
ok github.com/novox/mesh-host/internal/declaration 1.104s
ok github.com/novox/mesh-host/internal/firewall 1.867s
ok github.com/novox/mesh-host/internal/identity 1.148s
ok github.com/novox/mesh-host/internal/image 1.046s
ok github.com/novox/mesh-host/internal/inventory 1.080s
ok github.com/novox/mesh-host/internal/link 10.519s
ok github.com/novox/mesh-host/internal/liveness 2.132s
ok github.com/novox/mesh-host/internal/meshcli 1.593s
ok github.com/novox/mesh-host/internal/network 1.079s
ok github.com/novox/mesh-host/internal/outward 1.057s
ok github.com/novox/mesh-host/internal/profile 1.665s
ok github.com/novox/mesh-host/internal/reachable 1.058s
ok github.com/novox/mesh-host/internal/store 1.260s
ok github.com/novox/mesh-host/internal/system 1.061s
ok github.com/novox/mesh-host/internal/tunnel 1.064s
ok github.com/novox/mesh-host/internal/units 1.050s
ok github.com/novox/mesh-host/internal/upgrade 1.183s
ok github.com/novox/mesh-host/internal/witness 4.875s
ok github.com/novox/mesh-host/packaging 1.058s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.049s
**Merge check** at `1cc588eb`
Judged: head `1cc588ebc060` merged into trunk `1203f96a24c8`; read beside it: mesh-catalog `ebf7f3773dc7`, mesh-controller `ca3f43c400ba`, mesh-controller-main `ca3f43c400ba`, mesh-tools `954e1a53eef9`
- `mesh/merge-gate`: **PASS** — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)
- `mesh/repo-check`: **PASS** — its merge-check.sh passed
**Delivery plan** — builds mesh-host → ace, g14, novox, shanks; no bus step
- tier 0: mesh-host
- ace: receives mesh-host
- g14: receives mesh-host
- novox: receives mesh-host
- shanks: receives mesh-host
- grants step: if this changes what the bus's user list says (a new tool, subject or user), novox is sent that list alone, every build there kept, before mesh-host is judged on its first machine
- read by no module's build: cmd/mesh-host/units_test.go, internal/apply/ownership_test.go, internal/declaration/ownership_test.go, validate/validate_test.go
Run by the build seat on shanks as `build-1791935307449323964` (`builds --log build-1791935307449323964`).
```
--- the gate: mesh-host
mesh-host: ok
long-running resources without health: 0
files asking for a setting without saying whether it is trusted: 0
1 manifest(s) checked. Judged against the seats this binary carries; a claim on one of the mesh's own seats is judged fully at registration, and a seat declared by a module not given here reads as unknown. Identities judged on a 6-character machine name, against the bounds of the providers given here
judging 0 module(s) changed against 4 machine(s), as the snapshot of 2026-10-13T23:45:15Z says they run
composing every machine as the mesh is
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
composing every machine with the change
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
the bus's user list leaves out 5 user(s) the mesh has minted no credential for: controller, node.f35, node.idt, node.ygcyr, node.yvbpsm. Each is a user that cannot connect until one is issued
merge gate: PASS — every machine composes with the change as it did without (4 of 4 compose)
judged by controller development build against the facts of 2026-10-13T23:45:15Z
machines:
- a machine (f35): composes
- holds mesh-dns-resolver (idt): composes
- holds git, holds mesh-artifact-store, holds mesh-broker, holds mesh-catalog, holds mesh-delivery, holds mesh-dns-resolver, holds mesh-store, holds mesh-vault, holds npm-package-registry, the control node, the hub (ygcyr): composes
- a machine (yvbpsm): composes
a merge would rebuild 1 module(s) in 1 tier(s): mesh-host
notes:
- cmd/mesh-host/units_test.go, internal/apply/ownership_test.go, internal/declaration/ownership_test.go and validate/validate_test.go read by no module's build, and rebuild nothing (issue 280)
--- its merge-check.sh (go toolchain)
ok github.com/novox/mesh-host/cmd/mesh-bootstrap 1.017s
ok github.com/novox/mesh-host/cmd/mesh-host 6.364s
ok github.com/novox/mesh-host/examples 1.027s
ok github.com/novox/mesh-host/internal/accounts 2.324s
ok github.com/novox/mesh-host/internal/apply 5.156s
ok github.com/novox/mesh-host/internal/bootstrap 4.434s
ok github.com/novox/mesh-host/internal/bundle 1.036s
ok github.com/novox/mesh-host/internal/declaration 1.104s
ok github.com/novox/mesh-host/internal/firewall 1.867s
ok github.com/novox/mesh-host/internal/identity 1.148s
ok github.com/novox/mesh-host/internal/image 1.046s
ok github.com/novox/mesh-host/internal/inventory 1.080s
ok github.com/novox/mesh-host/internal/link 10.519s
ok github.com/novox/mesh-host/internal/liveness 2.132s
ok github.com/novox/mesh-host/internal/meshcli 1.593s
ok github.com/novox/mesh-host/internal/network 1.079s
ok github.com/novox/mesh-host/internal/outward 1.057s
ok github.com/novox/mesh-host/internal/profile 1.665s
ok github.com/novox/mesh-host/internal/reachable 1.058s
ok github.com/novox/mesh-host/internal/store 1.260s
ok github.com/novox/mesh-host/internal/system 1.061s
ok github.com/novox/mesh-host/internal/tunnel 1.064s
ok github.com/novox/mesh-host/internal/units 1.050s
ok github.com/novox/mesh-host/internal/upgrade 1.183s
ok github.com/novox/mesh-host/internal/witness 4.875s
ok github.com/novox/mesh-host/packaging 1.058s
? github.com/novox/mesh-host/rootsearch [no test files]
ok github.com/novox/mesh-host/validate 1.049s
```
Independent review of issue 667 (design 57 package P1, ADR 0349 clause 5). I ran these in my own worktree of the head: gofmt -l . printed nothing; go vet ./... passed; sh merge-check.sh exited 0 and ran the whole suite under the race detector (27 packages ok). I reverted the non-test files (main.go, apply.go, declaration.go, messages.go) to the base branch: the new apply tests and the validate test fail ("a file does not use owner-at-runtime", "unknown field filled_files"), and internal/declaration and cmd/mesh-host do not build. With the change, all of them pass. Nothing outside P1 is touched. A declaration without the new fields parses and applies as before, because Seeded() equals CreateOnce when the word is absent. No secret, fixture credential or database change is involved.
Findings. None of them blocks the merge.
internal/link/messages.go:303 and cmd/mesh-host/units_test.go:90. OwnershipContract = 9 is right, and it is needed now. Every earlier field set shipped its reader, its contract and this same units_test.go change in one commit; bb60e7b (BusyContract, ADR 0335) is an example. An engine that reads the fields must say so, or the controller can never send them. The controller's internal/link/protocol.go copies only the contract numbers it checks (1, 2, 3 and 8). P6 (issue 672) must therefore add OwnershipContract = 9 there. No other open mesh-host pull request claims 9. The test change makes the check stricter: it now requires OwnershipContract to be BusyContract+1.
internal/link/messages.go:298-303. This should be settled in P6 or P7, and it can follow. Contract 9 means the engine readsfilled_files, but it still writes those files from the declaration. P7 (issue 673) is what stops that. If P6 sends a filled list to an engine at 9 that does not have P7, the engine and the provisioner's client would both write the file, which breaks ADR 0349 clause 5 (one writer). P7 should raise its own contract, or P6 should send filled_files only once P7 is on the node. The comment on 9 should not promise more than it does now.
internal/declaration/declaration.go:1651. This should be handled in P2. Fills compares the path string exactly. P2's write verb must look up the path exactly as declared: refuse an unclean path such as /etc//a or /etc/a/../a, rather than letting it be read as some other path.
The words owned, seed and managed are not in hq main's glossary yet. That is F4 (issue 653), not this pull request.
The tests that need a bus did not run in my check: 10 TestNats* tests in internal/link were skipped. This machine's toolchain has no nats-server. The only nats-server in the mesh is the bus module's container, and installing one by hand is not allowed. The only change in internal/link is a constant, so these tests do not exercise the change. mesh/repo-check on the build seat runs them if its toolchain holds nats-server.
Verdict: APPROVE at 1cc588ebc0606413cafed86ba4ef471203855a05
Independent review of issue 667 (design 57 package P1, ADR 0349 clause 5). I ran these in my own worktree of the head: `gofmt -l .` printed nothing; `go vet ./...` passed; `sh merge-check.sh` exited 0 and ran the whole suite under the race detector (27 packages ok). I reverted the non-test files (main.go, apply.go, declaration.go, messages.go) to the base branch: the new apply tests and the validate test fail ("a file does not use owner-at-runtime", "unknown field filled_files"), and internal/declaration and cmd/mesh-host do not build. With the change, all of them pass. Nothing outside P1 is touched. A declaration without the new fields parses and applies as before, because `Seeded()` equals `CreateOnce` when the word is absent. No secret, fixture credential or database change is involved.
Findings. None of them blocks the merge.
1. internal/link/messages.go:303 and cmd/mesh-host/units_test.go:90. OwnershipContract = 9 is right, and it is needed now. Every earlier field set shipped its reader, its contract and this same units_test.go change in one commit; bb60e7b (BusyContract, ADR 0335) is an example. An engine that reads the fields must say so, or the controller can never send them. The controller's internal/link/protocol.go copies only the contract numbers it checks (1, 2, 3 and 8). P6 (issue 672) must therefore add `OwnershipContract = 9` there. No other open mesh-host pull request claims 9. The test change makes the check stricter: it now requires OwnershipContract to be BusyContract+1.
2. internal/link/messages.go:298-303. This should be settled in P6 or P7, and it can follow. Contract 9 means the engine *reads* `filled_files`, but it still writes those files from the declaration. P7 (issue 673) is what stops that. If P6 sends a filled list to an engine at 9 that does not have P7, the engine and the provisioner's client would both write the file, which breaks ADR 0349 clause 5 (one writer). P7 should raise its own contract, or P6 should send `filled_files` only once P7 is on the node. The comment on 9 should not promise more than it does now.
3. internal/declaration/declaration.go:1651. This should be handled in P2. `Fills` compares the path string exactly. P2's write verb must look up the path exactly as declared: refuse an unclean path such as `/etc//a` or `/etc/a/../a`, rather than letting it be read as some other path.
4. The words owned, seed and managed are not in hq main's glossary yet. That is F4 (issue 653), not this pull request.
5. The tests that need a bus did not run in my check: 10 `TestNats*` tests in internal/link were skipped. This machine's toolchain has no nats-server. The only nats-server in the mesh is the bus module's container, and installing one by hand is not allowed. The only change in internal/link is a constant, so these tests do not exercise the change. mesh/repo-check on the build seat runs them if its toolchain holds nats-server.
mesh-admin
merged commit 6b54fed706 into feat/57-config-and-provisioning2026-10-13 23:52:13 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Work package P1 of hq design 57 (ADR 0349 clause 5), issue 667.
What changed
declaration.Filegainsowner-at-runtime:owned(written back whenever it differs),seed(written once, then the program's) ormanaged(only its keys or marked block are the mesh's).File.Ownership()gives the word, or the one the old form implies (into -> managed, create-once -> seed, else owned).File.Seeded()is create-once or seed.filled_files(Declaration.Filled,Declaration.Fills(path)): the paths of the files the provisioner fills.into(managed -> applyBlock / applyInto); the create-once check now readsSeeded(), so seed by the word alone is kept once present.declaredResolvConfreadsSeeded()too.link.OwnershipContract = 9, and the health statement now says it, so a controller (P6) sends the new fields only to an engine that reads them. An older engine refuses them under the strict parse.Why
The provisioner's client (P5) and the narrow write verb (P2, P7) need to know which files the provisioner fills and who owns each one at runtime. Nothing sends these fields yet: the reader comes first.
How it is proven (all failed before, pass now)
TestEachOwnershipBehavesAsItsOldFormapplies each word beside its old form, through the same drift, and requires the same actions and the same bytes: owned iscreated corrected, seed iscreated kept, managed json isupdated correctedwith the machine's own keys kept, and managed block isupdated correctedwith the lines outside the region kept.TestASeedByTheWordAloneKeepsWhatGrew.TestAFileSaysWhoOwnsItAtRuntime,TestAFileThatDoesNotSayIsOwnedByItsOldForm,TestAnOwnershipThatContradictsTheFileIsRefused,TestTheDeclarationNamesTheFilesTheProvisionerFills,TestAFilledFileMustBeADeclaredFileThatSaysItsOwner.Deliberately left for later packages
owner-at-runtime,filled_filesand checkingOwnershipContract: P6 (issue 672).Reviewer's checklist
Seeded()replacesCreateOncein two reads.omitempty,Seeded()equalsCreateOncewhen the word is absent, andTestEachOwnershipBehavesAsItsOldFormcompares old and new form byte for byte.OwnershipContracttells a controller when it may send them. No writer sends them in this change.gofmt -l .(empty),go vet ./...,go test ./...andsh merge-check.sh(exit 0). merge-check said: NOT RUN AGAINST A BUS, because this toolchain has no nats-server, so the bus tests were skipped.Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Delivery
novox/mesh-host@1cc588ebc060— delivered since 2026-10-13T23:53:19ZDelivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.Merge check at
1cc588ebJudged: head
1cc588ebc060merged into trunk1203f96a24c8; read beside it: mesh-catalogebf7f3773dc7, mesh-controllerca3f43c400ba, mesh-controller-mainca3f43c400ba, mesh-tools954e1a53eef9mesh/merge-gate: PASS — every machine composes with the change as it did without (4 of 4 compose) (modules: mesh-host)mesh/repo-check: PASS — its merge-check.sh passedDelivery plan — builds mesh-host → ace, g14, novox, shanks; no bus step
Run by the build seat on shanks as
build-1791935307449323964(builds --log build-1791935307449323964).Verdict: APPROVE at
1cc588ebc0Independent review of issue 667 (design 57 package P1, ADR 0349 clause 5). I ran these in my own worktree of the head:
gofmt -l .printed nothing;go vet ./...passed;sh merge-check.shexited 0 and ran the whole suite under the race detector (27 packages ok). I reverted the non-test files (main.go, apply.go, declaration.go, messages.go) to the base branch: the new apply tests and the validate test fail ("a file does not use owner-at-runtime", "unknown field filled_files"), and internal/declaration and cmd/mesh-host do not build. With the change, all of them pass. Nothing outside P1 is touched. A declaration without the new fields parses and applies as before, becauseSeeded()equalsCreateOncewhen the word is absent. No secret, fixture credential or database change is involved.Findings. None of them blocks the merge.
bb60e7b(BusyContract, ADR 0335) is an example. An engine that reads the fields must say so, or the controller can never send them. The controller's internal/link/protocol.go copies only the contract numbers it checks (1, 2, 3 and 8). P6 (issue 672) must therefore addOwnershipContract = 9there. No other open mesh-host pull request claims 9. The test change makes the check stricter: it now requires OwnershipContract to be BusyContract+1.filled_files, but it still writes those files from the declaration. P7 (issue 673) is what stops that. If P6 sends a filled list to an engine at 9 that does not have P7, the engine and the provisioner's client would both write the file, which breaks ADR 0349 clause 5 (one writer). P7 should raise its own contract, or P6 should sendfilled_filesonly once P7 is on the node. The comment on 9 should not promise more than it does now.Fillscompares the path string exactly. P2's write verb must look up the path exactly as declared: refuse an unclean path such as/etc//aor/etc/a/../a, rather than letting it be read as some other path.TestNats*tests in internal/link were skipped. This machine's toolchain has no nats-server. The only nats-server in the mesh is the bus module's container, and installing one by hand is not allowed. The only change in internal/link is a constant, so these tests do not exercise the change. mesh/repo-check on the build seat runs them if its toolchain holds nats-server.