plan: claim Plan 14 rev 2 (Astra r1 and moves review: registry-independent proof observation, zdd generation and swap-failure authority, release→root-host→legacy-host lock order, durable descend retry, migration refuses during active moves, root-accepted floor projection with independent refresh, conservative floor reconstruction; integrated against moves' reviewed held-release tip; re-anchored on main 8916558d)
Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+703
−55
docs/superpowers/plans/2026-10-12-host-lifecycle-claim-14-root-trusted-state.md+703 −55
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 156.638s
ok github.com/carlosframework/platform/cmd/console 2.203s
ok github.com/carlosframework/platform/internal/activator 55.698s
ok github.com/carlosframework/platform/internal/agentsock 0.015s
ok github.com/carlosframework/platform/internal/analytics 0.019s
ok github.com/carlosframework/platform/internal/appconfig 0.962s
ok github.com/carlosframework/platform/internal/apperr 0.070s
ok github.com/carlosframework/platform/internal/applog 0.117s
ok github.com/carlosframework/platform/internal/awsinv 0.003s
ok github.com/carlosframework/platform/internal/caps 0.012s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.038s
ok github.com/carlosframework/platform/internal/clamd 0.054s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 52.822s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 1.385s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.011s
ok github.com/carlosframework/platform/internal/dbverify 0.011s
ok github.com/carlosframework/platform/internal/dnsrec 0.011s
ok github.com/carlosframework/platform/internal/docsite 0.010s
ok github.com/carlosframework/platform/internal/domains 1.089s
ok github.com/carlosframework/platform/internal/doorkey 0.010s
ok github.com/carlosframework/platform/internal/economics 0.010s
ok github.com/carlosframework/platform/internal/edge 23.093s
ok github.com/carlosframework/platform/internal/edgesfile 0.010s
ok github.com/carlosframework/platform/internal/fleetapi 0.096s
ok github.com/carlosframework/platform/internal/gate 1.654s
ok github.com/carlosframework/platform/internal/host 6.674s
ok github.com/carlosframework/platform/internal/infraview 0.010s
ok github.com/carlosframework/platform/internal/instancectl 0.414s
ok github.com/carlosframework/platform/internal/instanceexec 0.010s
ok github.com/carlosframework/platform/internal/instancefs 0.010s
ok github.com/carlosframework/platform/internal/lease 5.076s
ok github.com/carlosframework/platform/internal/ledger 0.010s
ok github.com/carlosframework/platform/internal/mail 0.052s
ok github.com/carlosframework/platform/internal/maintenance 0.088s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.483s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.047s
ok github.com/carlosframework/platform/internal/metrics 0.015s
ok github.com/carlosframework/platform/internal/objstore 0.082s
ok github.com/carlosframework/platform/internal/ops 0.099s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 0.844s
ok github.com/carlosframework/platform/internal/publicrelease 0.007s
ok github.com/carlosframework/platform/internal/registry 1.238s
ok github.com/carlosframework/platform/internal/relay 7.218s
ok github.com/carlosframework/platform/internal/release 3.566s
ok github.com/carlosframework/platform/internal/s3compat 0.006s
ok github.com/carlosframework/platform/internal/schedule 1.402s
ok github.com/carlosframework/platform/internal/status 0.026s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.005s
ok github.com/carlosframework/platform/internal/tunnelclient 0.005s
ok github.com/carlosframework/platform/internal/unixdial 0.005s
ok github.com/carlosframework/platform/internal/usage 0.006s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.552s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
go: downloading github.com/ncruces/go-strftime v1.0.0
--- shard 1 of 5 ---
cross-compiling darwin/amd64
go: downloading github.com/ncruces/go-strftime v1.0.0
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
go: downloading github.com/ncruces/go-strftime v1.0.0
--- shard 4 of 5 ---
cross-compiling windows/arm64
go: downloading github.com/ncruces/go-strftime v1.0.0
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
minio: downloaded, digest verified
mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
mc: downloaded, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.024s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-14-root-trusted-state.md b/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-14-root-trusted-state.md| index c30ca89d3..60b5f441d 100644 |
| --- a/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-14-root-trusted-state.md |
| +++ b/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-14-root-trusted-state.md |
| @@ -1,8 +1,13 @@ |
| # Host lifecycle claim, Plan 14: root-trusted state |
| -Implementation plan, 2026-10-12, rev 1, for amadan #280. Based on `39986908150d2e3c90477de4320c0d2941ffaac6`, the available `origin/main`. The claim-spec amendment is rev 6.34. This plan authorizes no production changes, commits, or activation. |
| +Implementation plan, 2026-10-12, rev 2, for amadan #280. Based on `8916558d34f68698f488ee7a0aa7f4a55808c380`, the pinned `origin/main`. The claim-spec amendment is rev 6.34. This plan authorizes no production changes, commits, or activation. |
| -Requirements: `/home/paulca/.cache/tmp/claude-1001/own/after236/plan14-brief.md`. The initiating finding is P3-3 in `opus-task7b-authority.md` beside that brief. This plan and the write report are implementation instructions for the controller. No product copy is proposed. |
| +Requirements: `plan14-brief.md` and `plan14-rev2.md` in `/home/paulca/.cache/tmp/claude-1001/own/after236/`; both binding reviews named in the revision brief apply. Paul approved the claim-only split and the pre-claim D3, release-history and floor tightening. The initiating finding is P3-3 in `opus-task7b-authority.md` beside that brief. This plan and the write report are implementation instructions for the controller. No product copy is proposed. |
| + |
| +## Revision log |
| + |
| +- Rev 1 (97a75f65): original authority census and A/B foundation on 39986908. |
| +- Rev 2 (2026-10-12): re-anchor on 8916558d; answer Astra's five P2s and moves' three P2s/P3. Add ancestor-sync retry code, release-lock concurrency code, conservative floor reconstruction code, migration preflight code and constructor census tests. Expand B3 and C–H into ordered tests-first tasks with concrete fixtures, production consumers and refusal/positive assertions. Replay current moves at 342f84d740b98dfdd1fcec6626fa9aef79b00247, including the real agent gate and reviewed held-release contracts. No commit or production operation is authorized. |
| ## Step 1: Findings from the code |
| @@ -21,29 +26,27 @@ The local attack considered here is replacement, deletion, editing and concurren |
| | `/data/hostguard/<host>.json`: incarnation, active/retired stamps | `hostguard.Guard.Activate`, `RetireInstallation*`, and cold-handover methods. The agent (`carlos`) activates through `hostbox.Reconciler.Install`, `ClaimWakeAssembly`, `movesland`, `movesrecover`, and recovery handover. Root instance-control also changes the same record during cleanup/cold handover. It is a shared-writer file, not root's record. | `hostbox.WorkSource.Select` calls package `Capture`, then `Guard.With` checks installation and fences. Root adoption builds this same WorkSource. Plan 11's `ClaimLaunchRoot.Launch` calls `Guard.With` directly. Stop, cleanup and cold handover read it too. | Fresh claim gives the holder tuple. Root's typed installation inventory establishes the local installation. Root must retain its own retired-stamp decisions; a fresh claim alone cannot prove a retired local installation was reinstalled. | |
| | Same file: `ActiveFence` | Root `ClaimStopCoordinator.FenceAndStop` calls `BeginFence` before drain/stop; root cold handover calls `BeginColdHandover`. All writes go through `recordDir.write`, mode `0660`. The service UID can also modify those bytes directly. | `Guard.With` denies the exact session; `ResumeFence`, `ExistingFence` and `PendingFences` determine recovery; `CompleteFence` uses the active target to authorize completion. | Root's own durable fence intent. The current claim does not encode an in-progress local fence. Kernel observation can prove a stop, but cannot reconstruct an erased intent while the claim still permits the session. | |
| | Same file: `FenceJournal` | Root `CompleteFence` after kernel/drain verification; root recovery calls it too. No ownership check makes those later bytes root-authentic. | `With` preserves completed-session denial; `FenceReceiptFor`, `CompletedColdPredecessor`, `CompletedWriterStops`, cleanup and retirement use receipts. Agent cold admission and artifact retirement also read them. | Exact bytes retained by root, or an already published root-signed receipt verified against the enrolled key, tuple and digest. The local raw receipt is JSON, not itself a signature. Never infer provenance merely from its fields. | |
| -| `/data/healthy/<acct>/<app>.json` | Root adoption's `proofRoot.observeAndRecord` calls `release.RecordHealthy` after `proofObserver.observe`; `recordSettled` writes after a checked named-unit restart. The ordinary agent reads the history; it is not the intended writer. | `ProofGate.admit` bypasses the prover wait on a manifest hash; `bindRecording` permits staged bytes by hash. Root `runAdopt`, `heldStagePass` and moves Task 7b all build a gate over DataDir. | Re-observe the running executable, systemd invocation, binding and readiness as today; retain the result under root-owned ancestors. A valid signed prover proof is the other existing route. Neither a manifest hash alone nor the old local file proves health. | |
| +| `/data/healthy/<acct>/<app>.json` | Root adoption's `proofRoot.observeAndRecord` calls `release.RecordHealthy` after `proofObserver.observe`; `recordSettled` writes after a checked named-unit restart. The ordinary agent reads the history; it is not the intended writer. | `ProofGate.admit` bypasses the prover wait on a manifest hash; `bindRecording` permits staged bytes by hash. Root `runAdopt`, `heldStagePass` and moves Task 7b all build a gate over DataDir. | Re-observe the running executable and systemd invocation using independently resolved ownership, unit binding and readiness (Task B3); retain the result under root-owned ancestors. A valid signed prover proof is the other existing route. Neither a manifest hash alone nor the old local file proves health. | |
| `hostguard/guard.go:116–160`, `record_unix.go`, `fence.go`, `recoveryfence.go`, `coldhandover.go` and `writerstop.go` contain these readers and writers. `hostbox/work.go:57–113` is the selector/recheck path. The current adoption wiring is at `host/agent.go:4198,4322–4348`; the brief's older line numbers have moved. `release/healthy.go`, `proofgate.go:264,345`, `host/proofroot.go` and `proofobserve.go` establish the D3 chain. |
| +The health writer is also vulnerable before persistence: `proofObserver.observe` trusts registry `Socket`, `Addr`, `Health`, account and app. `routeReady` returns nil for a cleared upstream, so a forged row can skip readiness and attribute a healthy hash or minted proof to another app. Moving that result into a root directory would preserve the forgery. Task B3 closes the observation and attribution boundary, including `recordSettled` and cached observations reused by `mintAndLog`. |
| + |
| The hostguard design currently does make agent-writable state authoritative for fences root relies on. This is not fixed by a different filename or by making only `BeginFence` write as root. The record, lock namespace, recovery enumeration, receipt readers and admission checks must be split together. |
| ### Plan 11 and moves Task 7b |
| Plan 11's root launch checks the claim's retry reservation, exact launch-input digest, actor, staged executable and kernel identities. Those checks remain necessary. They do not repair a stripped local fence: `ClaimLaunchRoot` still enters through the shared Guard. It also reads `hostexec.InspectLocal` and `CheckLocal` in `claimlaunch_authority.go`; the service UID owns both the state and its lock namespace. |
| -Read the moves work as these deltas, not as an export of its older whole branch: |
| +Read current moves with `git -C /home/paulca/amadan.net/carlos/platform-moves-p5f show 342f84d740b98dfdd1fcec6626fa9aef79b00247:<path>`. This tip contains 5d06e596 and all reviewed held-release fixes: persisted receipts before success (1676eb69), explicit revert/clean-return refusals (a8c99b22), action/kind/phase binding (50e4d94b), validated quoted logging (09ac47ba), and one request per host/three total (5d06e596). Retain them all. The real agent constructor is `movespass.go`'s `heldReleaseHooksVia` call at line 553 on that tip. It differs from main's `heldReleaseReady` call; B2 changes both this constructor and root's constructor. |
| -- `27b83074c34225e7dff60deafb2fd2787399bbbc` adds `moves.JournalFile` and `DecodeJournal`. |
| -- `cf0b6de5e1a1262b23ccf3b5c63906acf301805a` registers the claim-only held-release route. |
| -- `ed97cbc73c471f3fbff53fc9c9f5e9186ac12fb2` adds the root handler, publication selector, render-journal reader and receipt. |
| - |
| -`movesheldrelease_production.go` copies `a.Guard` into a new WorkSource and constructs `NewProofGate(ProofPolicyPath, dataDir)`. Therefore both P3-3 attacks reach root through the new on-demand route. Root selects the work itself, but selects it from the same compromised local authority. `movesheldrelease_linux.go` safely reads the agent render journal; that makes it a bounded, no-symlink hint, not authenticated render evidence. |
| +`movesheldrelease_production.go` copies `a.Guard` into a WorkSource and constructs a gate over DataDir. Both reported attacks reach this root route. `movesheldrelease_linux.go`'s anchored, bounded render-journal read remains a hint check, with its unsafe-path refusals unchanged. The limiter, receipt write failure, receipt tuple binding and explicit publication refusal table remain intact. |
| Task 7b keeps receipts at **`/var/lib/carlos-claims/held-release`**, under the claim-mode service's `StateDirectory`. Use the same root namespace for claim authority. There is an integration trap: main's `hostbox.PrepareRootJournalDirectory` accepts only `retire` and `migration-*` subdirectories, so both Task 7b's `held-release` and this plan's `authority` need explicit root-owned-directory classifications. Do not broadly permit arbitrary subdirectories. |
| -The three named deltas alone do not compile on the pinned main: `StageHeldVerified` is absent. The narrow prerequisites are `b3d423390c5f733c31c4b06e6df5a72dfdb06f6d` (that function and tests) and `5a46611265d20775a9f18e2979c69bb01959ec1b` (claim-mode read-back). The replay keeps these separate from main and labels the resulting integration check. |
| +The old three-commit replay omitted current safety fixes and the agent's real constructor. Rev 2 integrates the complete current moves delta from its merge base with pinned main in a scratch export, using three-way file merges. It does not replace main files with older branch copies. The only source inventory conflict and its exact resolution are recorded in the replay section. |
| -Task 7b's render hash is not permission to publish another kind/phase. In particular, a missing agent journal cannot be the reason a forbidden revert or clean-return probe is believed unreachable. Keep the permitted operation/version table explicit. A publication receipt must bind action, kind and phase as well as the holder, operation, version and render identity, and replay must still respect a later root fence. Those are integration requirements in Task C, not assumptions that the three historical commits already satisfy. |
| +Task 7b's render hash is not permission to publish another kind/phase. In particular, a missing agent journal cannot be the reason a forbidden revert or clean-return probe is believed unreachable. Keep the permitted operation/version table explicit. A publication receipt must bind action, kind and phase as well as the holder, operation, version and render identity, and replay must still respect a later root fence. These reviewed contracts are already present at the pinned moves tip. Task C adds the root fence check before receipt replay; the integration must retain the existing contracts. |
| ### Wider root-read census |
| @@ -56,6 +59,8 @@ The investigation followed the root entrypoints `runAdopt` (ordinary, stage, res |
| | `.moves/<host>/<op>/journal.json`, render steps | Agent moves worker | Task 7b compares render hashes before publication; held request selects a record-pinned version. **Hint only if no authority row depends on it.** | Task C: explicit policy table, root recheck and root-owned publication receipt. If a row requires completed render as authority, root must reproduce/verify the render and retain its own result; an agent hash is insufficient. | |
| | `healthy/` | Root observation, replaceable parent | D3 proof bypass, described above | Task B: new trusted history, no import of old assertions. | |
| | `refusals/`, `moves-held/refusals/`, `programs/`, `proof-required/` | Root checked restart/proof path; release passes can also write program history; all under service parent | D1 retry suppression, D2 old-program exceptions, D3 proof mint's prior-refusal exclusion, and break-glass diagnostics. Removing a refusal can remove a deny; a fabricated program-history entry can add an exception. **Not all diagnostic.** | Task E: root's D1/D2/mint decisions read its own release ledger. Agent copies remain status. `proof-required` is diagnostic only; preserve it as such. | |
| +| `zdd/generations.json`, `zdd/generations.seq` | Agent rollout generation publication (`release.WriteGenerations`), including pending reservations | Root `release.Pass` uses `GenerationPins`; `sweepStagedVersions` uses `GenerationVersions`. Deleting both means empty in both readers. **Authority over restaging and destructive sweep.** | Task E2: root reservations before spawn, retained inode/version pins, kernel reconciliation and a positive complete-census marker. Unknown completeness refuses restaging and sweep. Legacy rows add pins only. | |
| +| `swap-failures.json` | Agent rollout `FailureBook.Record`, intents, hold/retry/pointer transitions | Current main `updatesPassOption(dataDir(), true)` calls `rollout.OpenFailureBook`; missing means empty and `Retried` can suppress `Blocks`/`HeldDown`. **Failure/retry authority.** | Task E3: root failure ledger; bounded legacy failures add denies, while legacy `Retried`, served and missing state cannot clear them. Root independently verifies retry/pointer/healthy evidence. | |
| | `channel-first-seen.json` (FirstSeenPath), production/hold ratchets | Both release passes | Root bake timing and remembered pipeline/production holds. **Authority; current spec accepts a weaker box-file threat boundary.** | Task E: independent root observations and ratchets, never an agent-supplied past first-seen time. Unknown historical ratchets require migration refusal/reconstruction, not an empty permissive map. | |
| | `restart-marks.json`, its lock | Agent seeds baselines; root advances unit restart marks | Root decides which signed/control-store restart request is unconsumed. Replacement can cause replay or erase work. | Task E: root-owned consumption record; seed from a fresh request read without issuing a restart. Agent baseline is a hint. Never widen an unknown scoped selector. | |
| | `adopt-pass.lock`, moves held-stage locks | Root and agent on shared writable parents | Serialization of root publication, held read-back and staged-version sweep. **Coordination that affects correctness.** | Root-owned stable root-effect lock from Task A, shared among root callers. Preserve bounded agent read-back coordination, but it cannot substitute for root exclusion. Do not unlink a root lock while a waiter may hold its old inode. | |
| @@ -72,7 +77,7 @@ Root proving keys, proof policy, deployment public key, enrollment and signed ho |
| ## Step 2: Chosen change and ordering |
| -Implement Tasks A–I. A and B include exact replayable patches for the storage primitive and D3 path. C–H specify the remaining production integration, migration and regression work by owning function and invariant; they are requirements, not claims that the A/B patch set implements the whole plan. I contains the exact spec amendment. The reference replay proves the included code compiles on the named base; **it is not a whole-branch implementation or an activation witness**. |
| +Implement Tasks A–I. A/B include exact foundation patches. C1, F1 and G1 add exact tested primitives; H1 adds an exact constructor census test. B3, C2–C4, D1–D3, E1–E3, F2–F4, G2 and H2 specify the remaining integration as tests-first code tasks. Their named acceptance tests are required but are not claimed as implemented by the foundation replay. I contains the exact spec amendment. The reference replay proves the included code compiles on the named base; **it is not a whole-branch implementation or an activation witness**. |
| The smallest safe split keeps the agent's installation bookkeeping where it is, retains root decisions separately, and passes typed requests over the existing control socket. Moving the whole hostguard tree would require the agent to write root authority. Giving the agent write access to a root directory would recreate the defect. Trusting UID 0 only on the leaf would leave the rename attack. |
| @@ -83,7 +88,7 @@ Use these namespaces: |
| | `/var/lib/carlos-claims/authority` | root:root 0700 | Root installation/retirement decisions, fence intents and exact receipts, execution participation, migration record and stable locks. Read-only typed projections reach the agent over the existing authenticated control socket. | |
| | `/var/lib/carlos-claims/held-release` | root:root 0700 | Task 7b's exact publication receipts. Shares root host exclusion with authority. | |
| | `/var/lib/carlos-release/healthy/<acct>` | root:root directories 0755, records 0644 | Root-observed healthy hashes. Both root and the agent can read; only root writes. No secrets. | |
| -| `/var/lib/carlos-release/private` | root:root 0700 | Root refusal/program/bake/restart state and root release locks. Agent gets diagnostic projections, never a writable authority copy. | |
| +| `/var/lib/carlos-release/private` | root:root 0700 | Root refusal/program/bake/restart/failure state, generation reservations/pins and root release locks. Agent gets diagnostic projections, never a writable authority copy. | |
| | Existing `/opt/carlos/system/guard` and `/opt/carlos/system/router` | existing root ownership | Signed floor, root update transaction, plus Task F's root-owned moves-floor ratchet and boot selection. | |
| Provision or open each namespace through checked descriptors from `/`. Refuse symlinks, a non-root owner, group/other write permission, a nonregular leaf, multiple links or an overlarge record. Check the opened object, not a prior `Lstat` pathname. Do not repair ownership of an existing object. Root writes a fresh temporary file within its checked directory, fsyncs it, renames within that descriptor and fsyncs the parent. A trusted-parent missing leaf is different from an untrusted-parent failure. Critical ledgers treat missing state as uninitialized, not an empty allow set. |
| @@ -102,7 +107,7 @@ First failure: tests cannot compile before the package exists. The additional mu |
| ```diff |
| --- /dev/null |
| +++ b/internal/rootstate/state_linux_test.go |
| -@@ -0,0 +1,191 @@ |
| +@@ -0,0 +1,243 @@ |
| +package rootstate |
| + |
| +import ( |
| @@ -294,6 +299,58 @@ First failure: tests cannot compile before the package exists. The additional mu |
| + t.Fatalf("%v", err) |
| + } |
| +} |
| ++ |
| ++func TestPlan14DescendRetriesEveryAncestorSync(t *testing.T) { |
| ++ for failAt := 1; failAt <= 3; failAt++ { |
| ++ t.Run(string(rune('0'+failAt)), func(t *testing.T) { |
| ++ base := t.TempDir() |
| ++ if err := os.Chmod(base, 0700); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ parent, err := os.Open(base) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ defer parent.Close() |
| ++ parts := []string{"a", "b", "c"} |
| ++ calls := 0 |
| ++ injected := errors.New("parent fsync") |
| ++ _, err = descendSync(parent, parts, true, false, uint32(os.Geteuid()), func(fd int) error { |
| ++ calls++ |
| ++ if calls == failAt { |
| ++ return injected |
| ++ } |
| ++ return unix.Fsync(fd) |
| ++ }) |
| ++ if !errors.Is(err, injected) { |
| ++ t.Fatalf("first traversal: %v", err) |
| ++ } |
| ++ if _, err := os.Stat(filepath.Join(append([]string{base}, parts[:failAt]...)...)); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ calls = 0 |
| ++ d, err := descendSync(parent, parts, true, false, uint32(os.Geteuid()), func(fd int) error { |
| ++ calls++ |
| ++ return unix.Fsync(fd) |
| ++ }) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ defer d.Close() |
| ++ if calls != len(parts) { |
| ++ t.Fatalf("retry synced %d parents, want %d", calls, len(parts)) |
| ++ } |
| ++ if err := d.Write("record", []byte("durable")); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ // Even an all-existing path must still propagate a sync failure. |
| ++ _, err = descendSync(parent, parts, true, false, uint32(os.Geteuid()), func(int) error { return injected }) |
| ++ if !errors.Is(err, injected) { |
| ++ t.Fatalf("existing traversal: %v", err) |
| ++ } |
| ++ }) |
| ++ } |
| ++} |
| ``` |
| <!-- apply: A implementation --> |
| @@ -340,7 +397,7 @@ First failure: tests cannot compile before the package exists. The additional mu |
| +} |
| --- /dev/null |
| +++ b/internal/rootstate/state_linux.go |
| -@@ -0,0 +1,212 @@ |
| +@@ -0,0 +1,222 @@ |
| +package rootstate |
| + |
| +import ( |
| @@ -389,6 +446,11 @@ First failure: tests cannot compile before the package exists. The additional mu |
| + |
| +// The owner argument is private; tests use a real user-owned descriptor. |
| +func descend(parent *os.File, parts []string, create, public bool, owner uint32) (Store, error) { |
| ++ return descendSync(parent, parts, create, public, owner, unix.Fsync) |
| ++} |
| ++ |
| ++// syncParent is injected per traversal, never through a mutable global. |
| ++func descendSync(parent *os.File, parts []string, create, public bool, owner uint32, syncParent func(int) error) (Store, error) { |
| + fd, err := unix.FcntlInt(parent.Fd(), unix.F_DUPFD_CLOEXEC, 0) |
| + if err != nil { |
| + return nil, err |
| @@ -414,9 +476,6 @@ First failure: tests cannot compile before the package exists. The additional mu |
| + if e = unix.Mkdirat(fd, name, mode); e != nil && !errors.Is(e, unix.EEXIST) { |
| + return nil, e |
| + } |
| -+ if e = unix.Fsync(fd); e != nil { |
| -+ return nil, e |
| -+ } |
| + next, e = unix.Openat(fd, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0) |
| + } |
| + if e != nil { |
| @@ -434,6 +493,14 @@ First failure: tests cannot compile before the package exists. The additional mu |
| + } |
| + return nil, os.ErrPermission |
| + } |
| ++ // A prior mkdir may be visible after its parent sync failed. Sync |
| ++ // existing entries too, so retry cannot report an undurable path. |
| ++ if create { |
| ++ if e = syncParent(fd); e != nil { |
| ++ unix.Close(next) |
| ++ return nil, e |
| ++ } |
| ++ } |
| + unix.Close(fd) |
| + fd = next |
| + } |
| @@ -571,7 +638,7 @@ The root-only chown test is intentionally separate. A skipped chown witness is n |
| Keep `ProofGate.bakeDir` for diagnostic/refusal routing and give it a separate healthy-history directory. Switch **every production** proof-gate constructor: root adoption, legacy root held-stage, the ordinary agent gate and moves pass. Root observation and settle hooks write the new history. The legacy helper remains for existing isolated tests/callers; no production gate is allowed to select it. The root and agent read the same root-owned history, so an honest agent does not wait forever on a proof root already bypassed. |
| -The migration is lazy and idempotent: root creates the checked namespace on its first observation, records only the independently observed running hash, and never copies `/data/healthy`. `recordLive` stays before the adoption pass. An old historical hash not currently running needs an existing signed prover proof until it is independently observed again. A damaged new history grants no health exception; its writer refuses to repair it silently. A valid signed proof can still satisfy D3. No policy means D3 is still off. |
| +Apply B3 before deploying these history writers. Storage provenance does not establish observation provenance. The history migration is lazy and idempotent: root creates the checked namespace on its first observation, records only the independently observed running hash, and never copies `/data/healthy`. `recordLive` stays before the adoption pass. An old historical hash not currently running needs an existing signed prover proof until it is independently observed again. A damaged new history grants no health exception; its writer refuses to repair it silently. A valid signed proof can still satisfy D3. No policy means D3 is still off. |
| Focused command: `go test ./internal/release ./internal/host -run 'TestPlan14|TestHealthy|TestProof|TestRecordSettled' -count=1`. |
| @@ -834,7 +901,7 @@ First failure: `NewRootProofGate` and the trusted decoder are absent. The runtim |
| reason := fmt.Sprintf("staged sha256 %s, manifest sha256 %s", got, v.sha) |
| --- a/internal/host/agent.go |
| +++ b/internal/host/agent.go |
| -@@ -4195,7 +4195,7 @@ |
| +@@ -4297,7 +4297,7 @@ |
| // (D3, proofobserve.go). Root is the process that can hash the unit's |
| // executable. |
| restart := rootRestartRoute(reg, release.OptDir(), |
| @@ -843,21 +910,21 @@ First failure: `NewRootProofGate` and the trusted decoder are absent. The runtim |
| // The restart-request half, and the ONE place it dispatches from — |
| // including for exec-backed routes, whose children belong to the agent: |
| // rootRestartRoute's try-restart arm reaches a carlos-app@ unit, and an |
| -@@ -4324,10 +4324,9 @@ |
| +@@ -4426,10 +4426,9 @@ |
| release.WithDigestRatchet(pointerRatchet(true, release.OptDir(), dataDir(), os.Getenv)), |
| release.WithRollbackRestart(rollbackUnitRestart(reg, systemdUnits{}, release.OptDir(), os.Getenv)), |
| // D3's gate: a fresh gate per root pass, so the policy file is read |
| - // at the start of this pass (BeginPass) like every other. Its bake |
| - // directory is BakeDir's above: the healthy history it reads is the |
| - // one this pass and recordSettled write. |
| -- release.WithProofGate(release.NewProofGate(release.ProofPolicyPath, dataDir()))} |
| +- release.WithProofGate(release.NewProofGate(release.ProofPolicyPath, dataDir())), |
| + // at the start of this pass (BeginPass) like every other. Healthy |
| + // history is root-owned; diagnostic refusals still use BakeDir. |
| -+ release.WithProofGate(release.NewRootProofGate(release.ProofPolicyPath, dataDir()))} |
| - if !*stageOnly { |
| - passOpts = append(passOpts, release.NagExpiredCanaries(os.Stdout)) |
| - } |
| -@@ -4342,6 +4341,7 @@ |
| ++ release.WithProofGate(release.NewRootProofGate(release.ProofPolicyPath, dataDir())), |
| + // Settings spec §4.3: root alone resolves install requests and |
| + // writes /opt/carlos/consumed/<host>, before it stages. |
| + updatesPassOption(dataDir(), true)} |
| +@@ -4447,6 +4446,7 @@ |
| reg: reg, |
| store: store, |
| bakeDir: dataDir(), |
| @@ -934,7 +1001,7 @@ First failure: `NewRootProofGate` and the trusted decoder are absent. The runtim |
| } |
| --- a/internal/host/movespass.go |
| +++ b/internal/host/movespass.go |
| -@@ -431,7 +431,7 @@ |
| +@@ -432,7 +432,7 @@ |
| pub = nil |
| } |
| cfg.ReleaseReady = heldReleaseReady(reg, cfg.OptDir, dataDir, cfg.Box, pub, pointerRatchet(false, cfg.OptDir, dataDir, getenv), |
| @@ -945,12 +1012,56 @@ First failure: `NewRootProofGate` and the trusted decoder are absent. The runtim |
| cfg.ReadyProbe = func(ctx context.Context, host, path string) moves.Check { |
| ``` |
| -### Task B2: The unmerged moves consumer |
| +### Task B2: Current moves constructors |
| -Apply only after the five separately named moves deltas in the replay section. The constructor in that branch is another root consumer; it cannot retain the legacy history by accident. |
| +For the separate moves replay, integrate pristine pinned main with the pinned moves tip first. Apply B excluding both `internal/host/agent.go` and `internal/host/movespass.go`, then apply B2. B2 supplies the integrated adoption/writer wiring, the real agent `heldReleaseHooksVia` gate, and the root held-release gate. It preserves render-journal checks, receipt failure behavior, publication table and limiter. H1 checks every production host constructor after integration. |
| <!-- apply: B2 implementation --> |
| ```diff |
| +--- a/internal/host/agent.go |
| ++++ b/internal/host/agent.go |
| +@@ -4303,7 +4303,7 @@ |
| + // (D3, proofobserve.go). Root is the process that can hash the unit's |
| + // executable. |
| + restart := rootRestartRoute(reg, release.OptDir(), |
| +- recordSettled(systemdUnits{}, procExePath, release.OptDir(), dataDir(), time.Now)) |
| ++ recordSettled(systemdUnits{}, procExePath, release.OptDir(), release.RootHealthyDir, time.Now)) |
| + // The restart-request half, and the ONE place it dispatches from — |
| + // including for exec-backed routes, whose children belong to the agent: |
| + // rootRestartRoute's try-restart arm reaches a carlos-app@ unit, and an |
| +@@ -4432,10 +4432,9 @@ |
| + release.WithDigestRatchet(pointerRatchet(true, release.OptDir(), dataDir(), os.Getenv)), |
| + release.WithRollbackRestart(rollbackUnitRestart(reg, systemdUnits{}, release.OptDir(), os.Getenv)), |
| + // D3's gate: a fresh gate per root pass, so the policy file is read |
| +- // at the start of this pass (BeginPass) like every other. Its bake |
| +- // directory is BakeDir's above: the healthy history it reads is the |
| +- // one this pass and recordSettled write. |
| +- release.WithProofGate(release.NewProofGate(release.ProofPolicyPath, dataDir())), |
| ++ // at the start of this pass (BeginPass) like every other. Healthy |
| ++ // history is root-owned; diagnostic refusals still use BakeDir. |
| ++ release.WithProofGate(release.NewRootProofGate(release.ProofPolicyPath, dataDir())), |
| + // Settings spec §4.3: root alone resolves install requests and |
| + // writes /opt/carlos/consumed/<host>, before it stages. |
| + updatesPassOption(dataDir(), true)} |
| +@@ -4453,6 +4452,7 @@ |
| + reg: reg, |
| + store: store, |
| + bakeDir: dataDir(), |
| ++ healthyDir: release.RootHealthyDir, |
| + policyPath: release.ProofPolicyPath, |
| + readKey: readProvingKey, |
| + hourly: !*stageOnly, |
| +--- a/internal/host/movespass.go |
| ++++ b/internal/host/movespass.go |
| +@@ -551,7 +551,7 @@ |
| + via.Relay = newHeldReleaseRootClient(controlsock.Path) |
| + } |
| + cfg.ReleaseReady, cfg.ReleaseLive = heldReleaseHooksVia(via, writes, reg, cfg.OptDir, dataDir, cfg.Box, pub, pointerRatchet(false, cfg.OptDir, dataDir, getenv), |
| +- release.NewProofGate(release.ProofPolicyPath, dataDir), stageMarkerRequester(dataDir)) |
| ++ release.NewRootProofGate(release.ProofPolicyPath, dataDir), stageMarkerRequester(dataDir)) |
| + if svc != nil { |
| + // S1 asks the instance itself, woken through the activator. |
| + cfg.ReadyProbe = func(ctx context.Context, host, path string) moves.Check { |
| --- a/internal/host/movesheldrelease_production.go |
| +++ b/internal/host/movesheldrelease_production.go |
| @@ -71,7 +71,7 @@ |
| @@ -964,7 +1075,17 @@ Apply only after the five separately named moves deltas in the replay section. T |
| ReceiptDir: heldReleaseReceiptDir, |
| ``` |
| -Task B changes D3 with the claim flag **off as well as on**. The exact behavior change is loss of the old writable-history exception. It does not change the configured require/prove set, signature verification, release authorization, staged hash binding, restart semantics or route selection. Task E completes the separate refusal/mint-history trust boundary; passing Task B alone does not establish all release defences against a hostile agent. |
| +Task B changes D3 with claims off and on: old writable healthy assertions cease to grant exceptions. B3 adds independent observation and attribution; E completes refusal/mint authority. **Do not ship the A/B foundation as a complete D3 fix before B3 and the mint-refusal portion of E.** |
| + |
| +### Task B3: Establish readiness and attribution before writing health |
| + |
| +Files: add `internal/host/proofbinding.go` and `proofbinding_test.go`; change `proofobserve.go`, `proofroot.go`, `agent.go` and their tests. This is a mandatory integration task, not included foundation code. |
| + |
| +1. Tests first: add `TestPlan14ProofObservationRejectsForgedRegistry` in `proofbinding_test.go`. Start from a real executable fixture and active unit fixture whose invocation is unchanged. Keep the authoritative instance record fixed at account `bab`, app `console`, unit `console.service`, TCP upstream `127.0.0.1:4000`, probe `/ready`. Independently mutate registry `Account`, `App`, `Host`, `Unit`, `Socket`, `Addr`, `Addrs` and `Health` (disabled, nil, alternate path). For each mutation invoke the real `observeAndRecord`, `mintAndLog` and settled hook with recording store spies. Assert no healthy write and no `PutProof` for a mismatched row. For cleared upstreams the independently configured failing probe must never become successful through a nil callback. The original row with a failing probe writes nothing; the original row with a successful probe writes to `bab/console` only. An always-refuse resolver must fail the positive case. |
| +2. Add private `proofBinding` containing a canonical `registry.Route`, authoritative account/app, source identity (record key/ETag or root binding digest), and explicit readiness mode. Add `resolveProofBinding(ctx, candidate)` to the production observer dependency. Resolve the host from root's trusted installation/unit binding, then fetch the exact deployment record through the existing provider. Never pick the record's account/app from the candidate alone. A fresh claim additionally supplies holder ownership when claim-enabled. For pre-claim routes, use root-installed binding metadata plus the authoritative deployment record. A legacy manually configured route needs an explicitly provisioned root-owned binding; without it, recording/minting is unavailable and signed prover proofs remain usable. Do not auto-seed this binding from registry contents. Root binding paths and the systemd fragment/drop-in chain need Task A ancestor checks; `systemctl show ExecStart` alone does not authenticate account/app or readiness. |
| +3. Derive TCP/Unix readiness from `Record.AddrsOrDefault`, `HealthOrDefault` and the trusted instance socket derivation/root unit definition. Compare candidate fields with that canonical shape; a mismatch refuses instead of silently accepting a forged row. Named-unit executable observation requires an actual configured readiness test. If a legitimate unit has no upstream, its root-owned binding must explicitly authorize the existing non-network readiness mode; empty registry fields never select that mode. Check the actual unit definition, invocation, PID/start and `/proc/<pid>/exe` against the live binary before and after readiness. Binding ambiguity refuses. |
| +4. Carry the resolved binding through `liveObservation`. `observeAndRecord` uses its account/app, never a later registry row. `mintAndLog` re-resolves binding and invalidates cached observations when record ETag/root-binding digest, account/app, readiness or unit changes, even if the live symlink is unchanged. `mint` accepts the resolved binding rather than a free account argument. The settled callback calls the same resolver and readiness observation with the settle-specific duration, preserving the invocation checks. Add `TestPlan14ProofBindingChangesAfterObservation`: mutate the record between `recordLive` and `mintAndLog`; zero proofs until a new successful observation under the new binding. |
| +5. Add `TestPlan14ProofBindingProvenance` using real writable-parent, wrong-owner, symlink and rename fixtures for the root binding and unit metadata. Root-owned healthy storage must stay empty on each failure. Keep a valid signed proof acceptance witness and existing `TestRecordSettled` hash/invocation witnesses. Run `go test ./internal/host ./internal/release -run 'TestPlan14Proof|TestProof|TestRecordSettled|TestHealthy' -count=1`, then mutation-test removal of the canonical comparison and return of `routeReady(candidate)`; the forged attribution and cleared-upstream cases must fail. |
| ## Task C: Separate root fences from the agent guard |
| @@ -982,20 +1103,152 @@ Change the readers and writers as one task: |
| 4. Persist the root fence and fsync before sending a drain frame, killing a process or acknowledging the fence. Persist exact completed evidence before replying. On ambiguous completion, reread the same root record and reuse the same bytes and original evidence time. A failed directory fsync is an error, including after rename; retry syncs the already-visible state before proceeding. Old completed sessions stay denied when a new incarnation is installed. |
| 5. Cold handover first records the successor barrier in root authority while retaining the predecessor's receipt. Only the existing verified retirement/inventory-rebind completion clears that exact barrier. An ordinary `Activate`, a claim session increment, a registry edit or a recreated agent guard file cannot clear it. Record root retirement before artifact effects; replay never relabels artifacts to a successor. |
| 6. Serve a **read-only** typed root fence view on the existing authenticated journal transport. Extend `JournalRequest/Response` additively and classify the operation as a read. The request identifies host and captured tuple; it supplies no fence records. The view returns root's exact deny/receipt data and migration status. The agent's guard checks the view for every guarded admission and its receipt helpers use that view. Missing old-server support is unavailable, never an empty view. Agent writes cannot erase or complete a root fence. No generic root write-file, chmod, raw-JSON or SQL verb is added. |
| -7. Preserve honest-agent serialization with the existing local hostguard, but never let its replaceable inode serialize two root effects. Define the order as root host lock before legacy host lock for operations needing both. A fence-view read is a descriptor-pinned atomic snapshot and acquires neither lock, so an agent holding the legacy lock cannot deadlock with root. Root's final grant check reads under its root lock. Receipts and views are immutable copies; never hold a root lock while waiting for the agent's drain. On return, reacquire and revalidate the exact target. |
| -8. `ClaimRootWithLaunch`, `NewProductionRootClaimStop`, `ClaimStopHTTP.RecoverPending`, root cleanup and the Task 7b WorkSource all receive the same backend from assembly. For Task 7b, stage remains host-independent signed release staging; publish, rollback and receipt replay are root-guarded. Bind the publication receipt to action/kind/phase. A receipt says an earlier effect completed, not that the session may publish again after it was fenced. Use the same root release lock for root adoption and held publication. |
| +7. Preserve honest-agent serialization with the existing local hostguard, but never let its replaceable inode serialize two root effects. Define the global order as **root release lock → root host lock → legacy host lock** for operations needing them. Acquire the held-publication root release lock before entering `Work.With`, never by replacing the leaf lock callback inside `MaterializeHeldUnder`. The removable held-stage lock remains a separate leaf after these locks. Stage-only work takes release then held-stage. No callback holding a host or held-stage lock may acquire release. A fence-view read is a descriptor-pinned atomic snapshot and acquires neither lock, so an agent holding the legacy lock cannot deadlock with root. Root's final grant check reads under its root lock. Receipts and views are immutable copies; never hold a root lock while waiting for the agent's drain. On return, reacquire and revalidate the exact target. |
| +8. `ClaimRootWithLaunch`, `NewProductionRootClaimStop`, `ClaimStopHTTP.RecoverPending`, root cleanup and the Task 7b WorkSource all receive the same backend from assembly. For Task 7b, stage remains host-independent signed release staging; publish, rollback and receipt replay are root-guarded. Bind the publication receipt to action/kind/phase. A receipt says an earlier effect completed, not that the session may publish again after it was fenced. Use the same stable root release lock for root adoption, held stage/publication and version sweep. Root receipt replay enters this order and rechecks the root fence before returning success. Release the release/host locks before waiting for an agent drain; revalidate on reentry. |
| Task 7b may retain the agent render journal only as a not-yet/readiness hint within an independently authorized publication row. Explicitly refuse reverting and clean-return probe rows unless the controller's binding publication table authorizes them and root independently verifies their required render. For landing/dirty-return rows, re-read the pinned record and claim under the root guard, require the authorized version and existing signed release/D3 checks. Do not describe the matching agent journal as root evidence. Where a root decision needs completed render rather than mere readiness, factor the existing render-input calculation into a read-only function, have root fetch the pinned config/record inputs itself, verify the exact rendered digest, and record that result in its receipt. |
| Required regression files: `internal/hostguard/plan14_root_fences_test.go`, `internal/host/plan14_root_launch_test.go`, and moves' `internal/host/plan14_held_root_test.go`. Use real agent and root fixture directories; exercise the real root handler/selector and an effect counter, not just an error helper. Fence a session, erase/replace/rename its agent record and its legacy lock, then try launch, held publish and adoption. All effects stay at zero, including replay of a previously successful publication receipt. Pending recovery must still enumerate the original root target. Test cold-handover crash before and after each durable boundary, restart, new boot, successor stamp and old receipt replay. A valid successor after verified completion must run, so an always-refuse implementation does not pass. |
| +### C1: Exact release serialization primitive |
| + |
| +Apply the tests first; `WithRelease` is missing. Apply the implementation and run `go test ./internal/rootstate -run 'TestPlan14Concurrent|TestPlan14Descend' -count=1`. The test uses separate descriptor opens and concurrent adoption/publication lock shapes. It is a primitive test; C2 tests the actual handlers and catches an inverted production callback. |
| + |
| +<!-- apply: C tests --> |
| +```diff |
| +--- /dev/null |
| ++++ b/internal/rootstate/release_linux_test.go |
| +@@ -0,0 +1,88 @@ |
| ++package rootstate |
| ++ |
| ++import ( |
| ++ "context" |
| ++ "errors" |
| ++ "os" |
| ++ "path/filepath" |
| ++ "sync/atomic" |
| ++ "testing" |
| ++ "time" |
| ++) |
| ++ |
| ++func TestPlan14ConcurrentAdoptionPublicationOrder(t *testing.T) { |
| ++ path, release := fixture(t) |
| ++ _, host := fixture(t) |
| ++ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
| ++ defer cancel() |
| ++ entered, finish := make(chan struct{}), make(chan struct{}) |
| ++ done := make(chan error, 2) |
| ++ var effects atomic.Int32 |
| ++ go func() { |
| ++ done <- WithRelease(ctx, release, func() error { |
| ++ unlock, err := host.Lock(ctx, "host.lock") |
| ++ if err != nil { |
| ++ return err |
| ++ } |
| ++ defer unlock() |
| ++ close(entered) |
| ++ select { |
| ++ case <-finish: |
| ++ case <-ctx.Done(): |
| ++ return ctx.Err() |
| ++ } |
| ++ effects.Add(1) |
| ++ return nil |
| ++ }) |
| ++ }() |
| ++ select { |
| ++ case <-entered: |
| ++ case <-ctx.Done(): |
| ++ t.Fatal(ctx.Err()) |
| ++ } |
| ++ // A separate open is a distinct flock description, as in two root processes. |
| ++ parent, err := os.Open(filepath.Dir(path)) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ defer parent.Close() |
| ++ other, err := descend(parent, []string{filepath.Base(path)}, false, false, uint32(os.Geteuid())) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ defer other.Close() |
| ++ short, stop := context.WithCancel(ctx) |
| ++ stop() |
| ++ if err := WithRelease(short, other, func() error { effects.Add(100); return nil }); !errors.Is(err, context.Canceled) { |
| ++ t.Fatalf("canceled contender: %v", err) |
| ++ } |
| ++ go func() { |
| ++ done <- WithRelease(ctx, other, func() error { |
| ++ unlock, err := host.Lock(ctx, "host.lock") |
| ++ if err != nil { |
| ++ return err |
| ++ } |
| ++ defer unlock() |
| ++ effects.Add(1) |
| ++ return nil |
| ++ }) |
| ++ }() |
| ++ close(finish) |
| ++ for i := 0; i < 2; i++ { |
| ++ select { |
| ++ case err := <-done: |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ case <-ctx.Done(): |
| ++ t.Fatal("release/host lock deadlock") |
| ++ } |
| ++ } |
| ++ if effects.Load() != 2 { |
| ++ t.Fatalf("effects = %d", effects.Load()) |
| ++ } |
| ++ // The stable lock survives completion; neither caller unlinks it. |
| ++ if _, err := os.Stat(filepath.Join(path, "release.lock")); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++} |
| +``` |
| + |
| +<!-- apply: C implementation --> |
| +```diff |
| +--- /dev/null |
| ++++ b/internal/rootstate/release.go |
| +@@ -0,0 +1,20 @@ |
| ++package rootstate |
| ++ |
| ++import ( |
| ++ "context" |
| ++ "os" |
| ++) |
| ++ |
| ++// WithRelease runs outside Work.With: release -> root host -> legacy host. |
| ++// The caller supplies the checked private release store shared by all hosts. |
| ++func WithRelease(ctx context.Context, store Store, effect func() error) error { |
| ++ if store == nil || effect == nil { |
| ++ return os.ErrPermission |
| ++ } |
| ++ unlock, err := store.Lock(ctx, "release.lock") |
| ++ if err != nil { |
| ++ return err |
| ++ } |
| ++ defer unlock() |
| ++ return effect() |
| ++} |
| +``` |
| + |
| +### C2–C4: Wire and verify the root guard |
| + |
| +- **C2**, `agent.go`, `movesheldrelease_root.go`, `movesheldrelease_production.go`, `release/materializeheld*.go`: pass the checked private release store to `rootstate.WithRelease`. Adoption takes it before selecting root work; no mutation runs after a failed lock. Held publication takes it outside `Work.With`; its existing removable `withHeldStageLock` stays at the leaf. Add `TestPlan14AdoptPublishNoLockInversion` in `internal/host/plan14_held_root_test.go`: hold adoption inside its root-host section, start publication and pause it just before `Work.With`; publication must not have entered a host lock while waiting for release. Release adoption, assert both complete within a bounded context and exactly one effect occurs at a time. Reverse the scheduling, then cancel a waiting publisher and assert zero effects/receipt writes. Run with `-race` on the integration runner. Mutate release acquisition back into the materialize callback; the test must time out/fail. Audit `rootJudgePrev` and system apply separately: never acquire release while holding a legacy-host/held-stage lock; preserve apply → prev → floor → registry → held-stage ordering in callers that do not take release. If a release caller needs prev/floor, take release outside that chain and ensure no reverse caller requests release. |
| +- **C3**, `hostguard` root backend and assembly: write the three regression files listed above before the backend. For each public root reader/writer in C.3, run the same fixture through agent-record delete, parent rename, old locked inode plus replacement lock, and forged completed receipt. Assert original root fence bytes/target remain, recovery lists it, and effect counters remain zero. Missing/corrupt root record is unavailable, not an empty guard. After exact verified cold-handover completion, a successor executes once; replay of predecessor remains denied. The new backend must expose capture without overloading `DataDir`. |
| +- **C4**, journal transport: add typed view operation and a dispatch test for peer mismatch, wrong host/tuple, unknown schema and old server. Each refuses with zero writes. A successful view deep-copies immutable root data; mutation of returned slices cannot affect another read. Add a fence after a successful publication receipt and replay the exact request; refusal precedes replay success. Keep `TestHeldReleaseRootReplayAndCrashRecovery`, `TestHeldReleaseRootRefusesUnjournalledRecoveryProbes`, `TestHeldReleaseRootBoundsConcurrency`, and held-stage removal/waiter tests green. |
| + |
| ## Task D: Stop trusting execution deadlines and absence |
| `InspectLocal` plus `CheckLocal` is not an independent root proof: the same service UID can author `SafeUntil`, choose the asserted process identity and hold a replacement lock. A fresh claim does not prove that a local lease-loss latch was never set. Checking an agent-written remote lease's `Expires` instead would move the same assertion, not fix it. |
| Keep the agent's `.hostexec` bookkeeping, but make root the writer of a separate execution record whenever privileged claim work depends on it. Extend the existing instance-control broker with typed begin/renew/settle requests, authenticated to the exact controller process. The broker performs the acquisition/renewal that it attests using the existing execution-lease implementation and configured store, reads fresh claim/roster authority, samples its own suspend-aware clock, and persists the resulting bounded deadline. It never accepts a requested `SafeUntil`, `Expires`, lock path, PID or cleared-loss bit as evidence. There must be exactly one lease owner for an operation: the agent's claim-mode controller becomes the client of this broker, not a second concurrent renewer. Keep the flag-off controller implementation unchanged. |
| -Bind the root record to box, enrollment, boot, controller PID/start, host, incarnation, operation, holder session and installation, lease identity/ETag and root-calculated safe deadline. Use existing TTL/renewal/margin values (90s/20s/5s); do not add a longer lease. Hold the lifetime lock in the root namespace. Root's broker watches the authenticated process, owns expiry/loss and persists a deny before cleanup. Shutdown, failed renewal, uncertain conditional release and suspend preserve loss until the existing positive cleanup/CC9 transition finishes. A request from a replacement controller never reuses the old permit. Reboot invalidates the old monotonic deadline and keeps unresolved cleanup denied. |
| +Bind the root record to box, enrollment, boot, controller PID/start, host, incarnation, operation, holder session and installation, lease identity/ETag and root-calculated safe deadline. Use existing execution TTL/renewal/margin values (90s/20s/5s); do not add a longer execution lease. Moves staging remains the separate 45-minute lease and signing horizon in `internal/moves/lease.go`. Preserve `AggregateLeases.AcquireHeld` borrowing and `ReplicaParkLeaseSet` data-bucket scopes; a deployment-bucket staging lease must never be folded into those data-bucket scopes. Hold the lifetime lock in the root namespace. Root's broker watches the authenticated process, owns expiry/loss and persists a deny before cleanup. Shutdown, failed renewal, uncertain conditional release and suspend preserve loss until the existing positive cleanup/CC9 transition finishes. A request from a replacement controller never reuses the old permit. Reboot invalidates the old monotonic deadline and keeps unresolved cleanup denied. |
| Wire `productionClaimLocal.Prepare/Check`, Plan 11 `authorize` and its last executable check to this root record. Serving requires a positively settled root operation record/absence under a completed migration generation, not just absence of an agent file. Agent status can still expose its mirror. Stop/retirement recovery must remain available after work admission is closed; do not make completion depend on a new lease grant. |
| @@ -1003,11 +1256,19 @@ For `hostfloor.CheckControllersIdle`, `Disk.Begin`, `systemApplier.beginExecutab |
| Required tests in `plan14_root_participation_test.go`: real forged future deadline with real held `.lock`/`.live` files; erased/renamed state; old inode still locked while a replacement is empty; PID reuse and controller restart; suspend crossing deadline; root crash between remote acquisition and record fsync; renewal failure; cleanup failure; update-begun racing acquisition; serving after correctly settled cleanup. Mutate the production root reader back to `.hostexec` and watch the deadline/absence witnesses fail. Until this task exists, privileged operation work must refuse agent-owned participation files after validating every ancestor; that temporary refusal is an activation block, not the final implementation. |
| +### D1–D3: Broker implementation order and exact acceptance |
| + |
| +These are integration code tasks; no broker implementation is included in the foundation patches. |
| + |
| +1. **D1**, `claimroot*.go`, `claimassembly*.go`, `hostexec`: add typed `executionBegin`, `executionRenew`, `executionSettle` and `executionView` operations through the existing authenticated socket. Request fields are schema, request ID, host, captured incarnation/stamp, holder session, op ID and prior root token for renew/settle. There is no expiry, PID, path or lease-duration field. Derive peer PID/start from transport credentials; reject unknown JSON fields. Responses bind the captured tuple, token, boot and root status; deadlines exposed for status never become agent-set authority. Write `TestPlan14ExecutionBrokerPeerAndShape` first: alter every tuple component and peer start time; zero store acquisitions/writes. Repeat a valid begin with the same request ID and tuple: one remote owner and the same root record; another controller cannot borrow it. |
| +2. **D2**, root execution controller and `productionClaimLocal`: write `TestPlan14ExecutionDeadlineFromRequestStart` with fake suspend-aware clock. Begin at 100s, store response at 130s: local safe deadline is at most 190s, and admission closes by 185s. A response arriving after 185s grants nothing. Reboot, suspend past 185s, renewal loss and a forged agent deadline of one year all deny the final `StartChecked`; a successful renew extends only from its own request start. Inject fsync failure after remote acquisition: no success reply or launch; recovery retains pending/lost and releases or waits out the remote lease before settlement. Preserve request identity across retry. |
| +3. **D3**, `hostfloor.CheckControllersIdle`, `Disk.Begin`, `systemApplier.beginExecutableChange`, `controllersIdle`, `controllersRecovered`: write `TestPlan14ExecutionUpdateExclusion` with barriers immediately before durable update-begun and acquisition. In both schedules either update waits for positive settlement or begin is refused; never both effects. Delete the entire service `.hostexec` tree and replace locked `.live`/`.lock` files during the race; outcome stays the same. A root reservation without kernel completeness refuses update even after reboot. A positive empty root census after completed cleanup permits update. Add tests that a 45-minute moves staging lease is untouched, a borrowed `AcquireHeld` lease is not released by the borrower, and replica parks request exactly their existing data-bucket names. |
| + |
| ## Task E: Root release and restart decisions |
| Complete the adjacent pre-claim trust boundary found in the census. The A/B patch set deliberately does not silently reassign `BakeDir`, which would also move many diagnostic/request paths and lose existing denial state. |
| -Add separate root authority stores to release pass configuration and root restart bookkeeping. Route root `ReadPrograms`/program exception writes, blocking `ReadRefusal`/refusal writes, `proofRoot.mint`'s refusal exclusion, FirstSeen/production/hold ratchets and restart consumption marks through `/var/lib/carlos-release/private`. Preserve the existing wire formats and existing decisions once provenance is established. The agent retains its cache and watches read-only status projections. Existing `WithProofGate`/BakeDir diagnostic output remains compatible. Root's private ledger is never readable or writable through a tenant path. |
| +Add separate root authority stores to release pass configuration and root restart bookkeeping. Route root `ReadPrograms`/program exception writes, blocking `ReadRefusal`/refusal writes, `proofRoot.mint`'s refusal exclusion, FirstSeen/production/hold ratchets and restart consumption marks, generation reservations and swap-failure/retry authority through `/var/lib/carlos-release/private`. Preserve the existing wire formats and existing decisions once provenance is established. The agent retains its cache and watches read-only status projections. Existing `WithProofGate`/BakeDir diagnostic output remains compatible. Root's private ledger is never readable or writable through a tenant path. |
| Migrate by fact, not directory copy: |
| @@ -1021,11 +1282,27 @@ Pointer digest ratchets already have a root copy under opt and an agent copy und |
| This task changes flag-off root behavior only by removing service-file grants and by conservative rebaseline/denial while history is unresolved. It must not silently erase a D1 refusal, enable a program change, shorten bake time, loosen a pipeline hold, broaden a restart, or retry an already consumed request. Add real-file before/after witnesses for each case, including old histories intentionally missing and a failed fsync after visible rename. |
| +### E1–E3: Release ledger and generations, tests first |
| + |
| +These are integration tasks. They preserve `BakeDir` diagnostics and use separate root-store dependencies; a directory substitution alone is insufficient. |
| + |
| +1. **E1**, `internal/release/adopt.go`, `refusal.go`, `programhistory.go`, and `bake.go` (`FirstSeen`), `host/proofroot.go`, restart consumption: add `plan14_root_history_test.go`. For each root `ReadPrograms`, `ReadRefusal`, mint refusal list, FirstSeen/holds and restart mark consumer, create a valid legacy record, perform the denial/consumption, then erase/rename/forge its service file. Assert respectively: no program exception; no retry/mint; no shorter bake/hold; no replay/widened restart. Add valid signed retry, newly observed bake interval, and a fresh exact scoped request as positive cases. Root migration has a versioned completeness record per authority category. Missing completeness refuses grants, and rename-visible/fsync-failed state retries its parent sync before success. |
| +2. **E2**, `internal/release/pins.go`, `adopt.go`, `internal/host/versionsweep.go`, rollout generation publisher/launch assembly: add `plan14_root_generations_test.go`. Root retains generation reservations before a spawn is possible. Each binds host/account/app, incarnation/stamp/session when present, build hash/version, canonical staged path plus inode/device, boot, reservation ID and process/cgroup evidence after launch. A pending reservation stays pinned even with no process. The agent may request or mirror a reservation, but cannot release it by publishing an empty map. Root releases only the exact reservation after verified controller join and kernel executable/mount/descriptor absence under the same release exclusion. Acquire release before root-host before legacy-host; reservation insertion and destructive restage/sweep share release exclusion. A trusted complete census is required before returning an empty pin set. |
| + |
| + Write `TestPlan14GenerationDeletionCannotUnpin` first: reserve version V1 for a pending launch, move the live link/registry to V2, delete both real `zdd/generations.json` and `generations.seq`, and invoke real `release.Pass` restaging and `sweepStagedVersions`. V1's inode/hash survives and no overwrite/unlink occurs. Repeat with a running process whose `/proc/<pid>/exe` names V1, a deleted executable still held open, parent rename, an empty legacy publication, and a missing root completeness record. In all unknown cases skip destructive work. `TestPlan14GenerationRetirementReleasesExactPin` joins/stops the process, verifies no kernel refs and retires only V1's reservation; sweep can then remove V1 after grace while V2 stays. Retain a pending V3 reservation as a counterexample to a kernel-only scan. A crash between reserve fsync and launch leaves a pin; crash during release never drops it without positive reconciliation. Legacy generation names/paths can add bounded conservative pins but never establish completeness. Both root consumers use the same root snapshot; do not change only one reader or permit registry deletion to erase account/app attribution. |
| +3. **E3**, `internal/host/updatesroot.go`, `internal/rollout/failures.go`, retry/pointer handlers and root release store: add `plan14_root_failures_test.go`. Change the `root=true` opener in `updatesPassOption` to root's ledger, preserving the agent opener. Root failure authority includes unresolved intents, `Blocks`, `HeldDown`, book identity and restriction epoch. A missing/invalid root migration marker is unavailable, not a new empty book. Import bounded legacy failure tuples only as denies; discard their `Retried`, served, recovery and clear assertions as authority. An agent failure report may add a deny. Clear only after root fetches and validates the existing authoritative retry grant (exact account/app/build/host/book/epoch), qualifying pointer transition or independently observed healthy service as required by the existing rule. Add an authenticated typed retry path if needed; no generic book replacement RPC. |
| + |
| + `TestPlan14SwapFailureSurvivesLegacyTampering`: fail build A on host H, freeze root evidence, then delete `swap-failures.json`, rename its parent, forge `Retried:[H]`, forge an old epoch and claim the build was served. Run the actual root update-settings pass; A remains blocked and a held-down H stays held. `TestPlan14SwapFailureMigrationUnknown`: legacy files absent/corrupt and root marker absent must block managed/settings routes. `TestPlan14SwapFailureVerifiedRetry`: fresh matching retry clears only H/A at its current epoch; stale replay after a second failure clears nothing. Inject root ledger fsync failure after visible retry rename: no adoption until the same decision is durably recovered. A valid new build and a correctly verified retry must proceed. Preserve existing served-build, hold and retry-epoch behavior once provenance is established. |
| + |
| ## Task F: Root moves floors, update and legacy layout reads |
| Preserve the distinction between the signed claim release floor (`hostfloor`) and the older moves floor. The latter intentionally trusted service files against honest mistakes; this plan tightens the root path rather than pretending that comment means it is already secure. |
| -Root apply reads the existing authoritative moves-protocol object itself through the existing provider and records a nondecreasing root floor under `/opt/carlos/system/guard`. The agent may continue refreshing its `/data/.moves` mirror. Root activation, rollback, `judgePrevWith`, `floor-allows-prev` and both systemd boot selectors must read the root floor. Root must first verify its parent chain, bind the candidate binary identity to its existing signed system release/floor, and retain the root verdict. An absent service mirror cannot choose `carlos.prev` or remove a remembered minimum. Never infer “no floor” from a failed remote read. A legacy box with no root floor marker has **unmigrated** state until root positively resolves the deployment's floor; this may defer an offline update/recovery rather than downgrade unsafely. |
| +Root owns an accepted-floor record and a separate deny/quarantine record under `/opt/carlos/system/guard`. The record binds schema, deployment, accepted protocol/minimum release, generation and provenance. An absent root marker means unmigrated; it never means no floor. Root independently reads the authoritative moves-protocol object through the existing provider on every root adoption floor pass (hourly plus startup) and before apply/rollback. Refresh happens **before** the no-`carlos.prev` early return in `rootJudgePrev`; it does not require a platform update or an agent refresh. A read failure cannot lower the durable floor. |
| + |
| +The agent may keep refreshing its mirror for diagnostics, but `withMovesFields` advertises **only root's durably accepted floor** from a root-owned, public-readable projection. Publish the projection only after authoritative persistence and selector-safe line/JSON ordering. Include deployment/schema/generation and bind the previous-binary verdict to the same generation and binary identity. Missing/corrupt/quarantined projection produces no FloorOK acknowledgement, including when the agent reports `prev_release == "none"`. A legacy mirror ahead of root may never be acknowledged. Do not advertise a floor still pending fsync. A stale projection can only delay admission. Existing edges JSON remains additive; the console needs an explicit accepted/quarantined signal to distinguish a proven no-floor deployment from missing state, with old-server/old-agent omission failing closed when a floor is required. |
| + |
| +Conservative reconstruction: today's remote object and legacy files cannot establish complete historical minima. Read both legacy JSON and line using existing bounded anchored readers; retain the componentwise maximum of usable protocol/release minima as **deny-only** evidence. Ignore legacy fields that claim reset or permission. Missing, malformed, conflicting, regressed or remotely deleted history remains quarantined unless complete root-authorized reconstruction is available. A root-authorized reconstruction must name the deployment/enrollment, complete historical bound (or positively attested never-armed state), evidence source/digests and operator authorization; there is no agent reset operation. It may establish authority, but cannot discard a usable stricter legacy deny without explicit root-authorized resolution of that discrepancy. The normal remote refresh may raise an already established floor; it cannot initialize an unknown history as empty. Retain deny evidence before an error/retry, and never replace it with a weaker current remote answer. |
| Update `carlos-system-update.service`, `carlos-system-recover.service`, the generated/bootstrap plan and their source-shape tests together. Keep the old files readable for older agents, but install the root boot selector before allowing an older service-file check to select a fallback. Retain the currently executing certified recovery binary while migrating; do not replace the only recovery route before its floor copy and selector are durable. `carlos.prev` must have the root-floor-aware reader before it can be selected. The local root authorization to lower/reset a floor stays an explicit operator operation; the agent has no reset RPC. |
| @@ -1033,10 +1310,130 @@ Update `carlos-system-update.service`, `carlos-system-recover.service`, the gene |
| Regression witnesses: erase/rename both legacy floor files and their parent while the root floor remains; attempt older update, rollback and both boot selectors. Repeat with an absent/corrupt/hostile root floor, stale root verdict, unexpected signed target, cached bundle substitution and pending root participation. No privileged executable change occurs. A valid same/newer target still applies and recovers. Exercise the generated units themselves on the disposable systemd runner; Go helper tests alone cannot verify shell branching. |
| +### F1: Exact conservative reconstruction decision |
| + |
| +The helper below accepts an already complete, root-authorized history. It has no file/network side effects and does **not** implement the reconstruction authority or proven-never-armed case. A nil history stays quarantined even if remote and legacy agree. A returned floor alongside an error is a deny bound only, never an accepted projection. Nil output is not a request to erase stored accepted/deny state; preserve both while quarantined. F2 must durably retain that bound and refuse its advertisement. Incomparable legacy minima combine both dimensions; malformed input remains unresolved. |
| + |
| +Apply the tests first (`reconstructRootFloor` is missing), then implementation; run `go test ./internal/host -run TestPlan14FloorReconstruction -count=1`. |
| + |
| +<!-- apply: F tests --> |
| +```diff |
| +--- /dev/null |
| ++++ b/internal/host/rootfloor_reconstruct_test.go |
| +@@ -0,0 +1,39 @@ |
| ++package host |
| ++ |
| ++import ( |
| ++ "github.com/carlosframework/platform/internal/moves" |
| ++ "testing" |
| ++) |
| ++ |
| ++func TestPlan14FloorReconstruction(t *testing.T) { |
| ++ a := moves.Floor{Min: 1, MinRelease: "v1.0.0"} |
| ++ b := moves.Floor{Min: 2, MinRelease: "v2.0.0"} |
| ++ c := moves.Floor{Min: 3, MinRelease: "v3.0.0"} |
| ++ for _, tc := range []struct { |
| ++ name string |
| ++ history, remote *moves.Floor |
| ++ legacy []*moves.Floor |
| ++ want *moves.Floor |
| ++ bad bool |
| ++ }{ |
| ++ {"remote deleted", &b, nil, nil, &b, false}, |
| ++ {"remote regressed", &b, &a, nil, &b, true}, |
| ++ {"legacy stricter", &a, &a, []*moves.Floor{&b}, &b, true}, |
| ++ {"legacy cannot grant", nil, &b, []*moves.Floor{&b}, nil, true}, |
| ++ {"both legacy files missing", nil, &a, nil, nil, true}, |
| ++ {"remote and legacy missing", nil, nil, nil, nil, true}, |
| ++ {"corrupt legacy", &a, &c, []*moves.Floor{{Min: 2, MinRelease: "broken"}}, nil, true}, |
| ++ {"root history survives legacy deletion", &b, &c, nil, &c, false}, |
| ++ {"independent minima", &a, nil, []*moves.Floor{{Min: 3, MinRelease: "v1.0.0"}, &b}, &moves.Floor{Min: 3, MinRelease: "v2.0.0"}, true}, |
| ++ } { |
| ++ t.Run(tc.name, func(t *testing.T) { |
| ++ got, err := reconstructRootFloor(tc.history, tc.remote, tc.legacy) |
| ++ if (err != nil) != tc.bad { |
| ++ t.Fatalf("error = %v", err) |
| ++ } |
| ++ if (got == nil) != (tc.want == nil) || (got != nil && *got != *tc.want) { |
| ++ t.Fatalf("floor=%+v, want %+v", got, tc.want) |
| ++ } |
| ++ }) |
| ++ } |
| ++} |
| +``` |
| + |
| +<!-- apply: F implementation --> |
| +```diff |
| +--- /dev/null |
| ++++ b/internal/host/rootfloor_reconstruct.go |
| +@@ -0,0 +1,53 @@ |
| ++package host |
| ++ |
| ++import ( |
| ++ "errors" |
| ++ "github.com/carlosframework/platform/internal/moves" |
| ++) |
| ++ |
| ++var errRootFloorHistory = errors.New("root floor history unavailable") |
| ++ |
| ++// This is a reconstruction decision, not an authority source. history is a |
| ++// complete root-authorized reconstruction; legacy supplies only extra denies. |
| ++// A nil history cannot be upgraded into authority by today's remote object. |
| ++func reconstructRootFloor(history, remote *moves.Floor, legacy []*moves.Floor) (*moves.Floor, error) { |
| ++ if history == nil { |
| ++ return nil, errRootFloorHistory |
| ++ } |
| ++ out := *history |
| ++ valid := func(f *moves.Floor) bool { return f != nil && f.Min > 0 && validFloorRelease(f.MinRelease) } |
| ++ if !valid(&out) { |
| ++ return nil, errRootFloorHistory |
| ++ } |
| ++ for _, f := range legacy { |
| ++ if !valid(f) { |
| ++ return nil, errRootFloorHistory |
| ++ } |
| ++ if err := floorRegression(&out, f); err == nil { |
| ++ out = *f |
| ++ continue |
| ++ } |
| ++ // Independent maxima: a larger protocol and a larger release can occur |
| ++ // in different files. Never choose just one file by its release string. |
| ++ if f.Min > out.Min { |
| ++ out.Min = f.Min |
| ++ } |
| ++ if ok, _ := moves.ReleaseAtOrAbove(f.MinRelease, out.MinRelease); ok { |
| ++ out.MinRelease = f.MinRelease |
| ++ } |
| ++ } |
| ++ if remote == nil { |
| ++ if out != *history { |
| ++ return &out, errRootFloorHistory |
| ++ } |
| ++ return &out, nil |
| ++ } |
| ++ if !valid(remote) { |
| ++ return &out, errRootFloorHistory |
| ++ } |
| ++ if err := floorRegression(&out, remote); err != nil { |
| ++ return &out, err |
| ++ } |
| ++ next := *remote |
| ++ return &next, nil |
| ++} |
| +``` |
| + |
| +### F2–F4: Persistence, refresh, acknowledgement and boot selectors |
| + |
| +1. **F2**, add `rootmovesfloor.go`/tests; change `movesfloor.go` root consumers. Keep accepted and deny state distinct. Persist raised selector line first, private accepted JSON second, durable accepted projection last. Until all prerequisites succeed, revoke/withhold any new acknowledgement. Failed parent sync after rename is retried before success. `TestPlan14RootFloorCrashOrdering` fails each write/sync in turn and reopens through real descriptors; no selector chooses a binary below either root deny or accepted bound. Remote deletion retains B; remote regression A after B retains B and reports refusal; missing both legacy files with no root history stays quarantined, even if remote A is valid. A bounded invalid legacy file quarantines rather than being repaired from A. A complete root-authorized B reconstruction permits B/newer; a never-armed reconstruction is a separate explicit root decision with the same durable marker discipline. |
| +2. **F3**, `rootJudgePrev`, `withMovesFields`, `edgesfile.go`, `console/boxselect.go`: add `TestPlan14FloorRiseWithoutPlatformUpdate`. Run the actual root refresh and edges/door selection against a fake provider, once with `carlos.prev` absent and once with a verified previous binary. Start accepted A, publish B remotely, let agent mirror refresh first: advertised accepted floor remains A and the door requiring B refuses. Run root adoption floor refresh without changing the platform version: after durable B projection the door accepts only if the current binary supports B and either prev is independently known absent or its B-bound verdict permits it. An older prev receives no B verdict and the door remains closed until prev is safely replaced/disabled under existing rules. Inject fsync failure: no B acknowledgement. In the no-prev case assert the remote GET and durable floor write actually occurred before the early return. Delete/forge the mirror: no acceptance change. Missing/quarantined root projection always refuses. The root authority property is tested through the honest agent's projection and door; this is not a new cryptographic attestation against an agent forging its entire edges advertisement. |
| +3. **F4**, `movesfloorgate.go`, both system units, bootstrap/generator and their tests: remove legacy absence grants together. `TestPlan14RootFloorBootSelectors` runs each generated unit selector on the disposable systemd runner with root B and candidate A, missing legacy files, and stale prev verdict. No exec of A; B/newer executes with a matching current verdict. Root floor missing/corrupt or recovery binary lacking the new reader refuses fallback. Test interruption after each line/JSON/projection write. Preserve current certified recovery until new selector and floor are durable. Layout/kernel incompleteness also refuses downgrade. Keep the apply/prev/floor lock order from C2; refresh does not nest a release lock inside a held-stage lock. |
| + |
| ## Task G: Guarded migration and startup |
| Extend the existing offline claim journal migration and startup recovery. Do not add an online “trust this directory” importer or another service. Use `quiesceLegacyClaimJournals`' exact edge-mask ownership, inactive-edge check and service-UID process exclusion. Preserve pre-existing operator masks. Hold exclusion through root fence migration and recovery. Agent absence asserted by a file is insufficient; check the kernel. There is no automatic chown-and-copy path. |
| +Before creating `prepared`, require a fresh complete authoritative census with **no active move**. Any non-nil `Record.Move` (including preparing, ready, cutting, landing before launch, probing, probed, committed cleanup, rollback, revert, abort or stuck) refuses migration. A lingering claim operation, root move journal or unretired move reservation also refuses. Do not settle phases, consume probe budgets, advance sessions, fence an active move or clear migration denies to repair one. Finish or abort the move through its normal protocol first, then retry. The absent agent journal is not evidence of inactivity. This check runs under the migration/admission exclusion and is repeated immediately before the durable prepare transition. Local exclusion alone cannot stop remote admission: the operational migration scope must quiesce move admission for participating hosts at the authoritative door, retain that exclusion through completion, and root must verify it; if that exclusion cannot be established, refuse migration. Do not assume a list-plus-ETag reread eliminates the admission race. |
| + |
| Under a root-owned migration lock, create a versioned root migration record with deployment/bucket, box/enrollment, schema generation and phase. Record root-observed host/installation inventory, the claim ETags and signed directory revision used, and every old admitted session that must remain denied. Enumerate the union of fresh authoritative claims for this box, root installation/launch/retirement journals and validated managed-root census. The old hostguard directory may supply extra candidates or deny-only suspicions; it may neither shrink the union nor establish emptiness. |
| The phases are `prepared`, `fenced`, `recovered`, `complete`: |
| @@ -1050,7 +1447,101 @@ After completion, the agent activates its own bookkeeping from fresh claims as b |
| Crash-test every durable phase, including rename visible but fsync failed; retry with the same migration identity and ensure it never reduces deny coverage. Recovery enumerates root records even if the entire old DataDir has been replaced. Do not delete legacy files during migration. They can be retained as non-authoritative diagnostics and removed later only under their existing inventory/retirement rules. |
| -Health migration is independently safe to deploy first: no historical legacy hash is imported and current live health can be re-observed. Claim migration requires the approved offline stop/re-admission window. Those are different rollout operations; the D3 patch does not require turning on claims. |
| +After B3 and E mint-refusal provenance are implemented, health migration can deploy independently of claims: no historical legacy hash is imported and current live health can be re-observed. Claim migration requires the approved offline stop/re-admission window. Those are different rollout operations; the D3 patch does not require turning on claims. |
| + |
| +### G1: Exact active-move preflight |
| + |
| +`Record.Move` is cleared by the terminal close CAS; committed is still active cleanup. The helper also refuses orphan/unknown claim operations. It permits the ordinary `KindServing` shape with no operation ID or phase; that serving session is fenced during migration rather than treated as an active move. `complete=true` is supplied only by the root authoritative census under the exclusion above, never by a request or legacy file. This helper alone is not a transactional admission barrier. |
| + |
| +Apply tests first, then implementation. Run `go test ./internal/host -run TestPlan14MigrationRefusesEveryMovePhase -count=1`. |
| + |
| +<!-- apply: G tests --> |
| +```diff |
| +--- /dev/null |
| ++++ b/internal/host/rootmigration_preflight_test.go |
| +@@ -0,0 +1,38 @@ |
| ++package host |
| ++ |
| ++import ( |
| ++ "github.com/carlosframework/platform/internal/hostclaim" |
| ++ "github.com/carlosframework/platform/internal/provision" |
| ++ "testing" |
| ++) |
| ++ |
| ++func TestPlan14MigrationRefusesEveryMovePhase(t *testing.T) { |
| ++ for _, state := range []string{provision.MovePreparing, provision.MoveReady, provision.MoveCutting, provision.MoveAborting, provision.MoveReverting, provision.MoveLanding, provision.MoveCommitted, provision.MoveRollingBack, provision.MoveStuck, "future", ""} { |
| ++ for _, phase := range []string{"", provision.LandChecking, provision.LandProbing, provision.LandProbed} { |
| ++ rec := provision.Record{Move: &provision.MoveState{State: state, LandingPhase: phase}} |
| ++ if err := rootMigrationPreflight(true, []provision.Record{rec}, nil); err == nil { |
| ++ t.Fatalf("accepted %s/%s", state, phase) |
| ++ } |
| ++ } |
| ++ } |
| ++ for _, op := range []hostclaim.Op{{ID: "admitted-before-launch"}, {Kind: "recovering"}, {Phase: "future"}} { |
| ++ if err := rootMigrationPreflight(true, nil, []hostclaim.Claim{{Op: op}}); err == nil { |
| ++ t.Fatalf("accepted orphan operation %+v", op) |
| ++ } |
| ++ } |
| ++ if err := rootMigrationPreflight(false, nil, nil); err == nil { |
| ++ t.Fatal("accepted unknown census") |
| ++ } |
| ++ if err := rootMigrationPreflight(true, []provision.Record{{}}, []hostclaim.Claim{{}}); err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ serving := hostclaim.Claim{Op: hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box", Participants: []string{"box"}}} |
| ++ if err := rootMigrationPreflight(true, []provision.Record{{}}, []hostclaim.Claim{serving}); err != nil { |
| ++ t.Fatalf("idle serving session refused migration: %v", err) |
| ++ } |
| ++ serving.Op.ID = "unfinished" |
| ++ if err := rootMigrationPreflight(true, nil, []hostclaim.Claim{serving}); err == nil { |
| ++ t.Fatal("serving kind hid an unfinished operation") |
| ++ } |
| ++ |
| ++} |
| +``` |
| + |
| +<!-- apply: G implementation --> |
| +```diff |
| +--- /dev/null |
| ++++ b/internal/host/rootmigration_preflight.go |
| +@@ -0,0 +1,30 @@ |
| ++package host |
| ++ |
| ++import ( |
| ++ "errors" |
| ++ "github.com/carlosframework/platform/internal/hostclaim" |
| ++ "github.com/carlosframework/platform/internal/provision" |
| ++) |
| ++ |
| ++var errRootMigrationBusy = errors.New("root migration requires an idle, complete census") |
| ++ |
| ++// Call only on fresh authoritative records while admission is excluded. |
| ++// The caller also checks root journals and kernel reservations before writing. |
| ++func rootMigrationPreflight(complete bool, records []provision.Record, claims []hostclaim.Claim) error { |
| ++ if !complete { |
| ++ return errRootMigrationBusy |
| ++ } |
| ++ for _, rec := range records { |
| ++ // Committed still owns cleanup; MoveComplete clears Move when settled. |
| ++ if rec.Move != nil { |
| ++ return errRootMigrationBusy |
| ++ } |
| ++ } |
| ++ for _, claim := range claims { |
| ++ // Refuse every active operation, including unknown future kinds. |
| ++ if claim.Op.ID != "" || (claim.Op.Kind != "" && claim.Op.Kind != hostclaim.KindServing) || claim.Op.Phase != "" { |
| ++ return errRootMigrationBusy |
| ++ } |
| ++ } |
| ++ return nil |
| ++} |
| +``` |
| + |
| +### G2: Migration effects and crash witnesses |
| + |
| +Add `internal/host/plan14_root_migration_test.go` before wiring G1 into offline migration. Use the real typed journal transport with a fake authoritative store and real root-state fixture, plus counters for drain, kill, probe-admission, session advance, receipt writes and migration writes. For every G1 phase and for recovery admitted-before-prelaunch, landing probing and probed-before-commit, assert refusal **before all counters**, with original deadlines, attempt counts, predecessor evidence and session bytes unchanged. Include a move existing only in a root journal and one admitted while the first census is read: refuse without writing `prepared`. A door exclusion failure refuses too. After a normal move close removes all operations and reservations, the same test proceeds through migration. |
| + |
| +Crash after each `prepared`/deny/`fenced`/receipt/`recovered`/`complete` file rename and fsync. Restart with DataDir deleted; root enumerates its retained work and never grants an old session. A failed fsync leaves phase pending until retry syncs it. Reconstruct only verified exact signed evidence. A serving session without an operation is fenced and requires a new admission; it is not mistaken for an active move. Complete migration plus exact verified predecessor handover admits the new session once. An already completed matching generation is idempotent; enrollment/schema mismatch refuses. A pre-existing operator edge mask remains owned by the operator after both failure and success. |
| ## Task H: Census, production wiring and acceptance |
| @@ -1064,11 +1555,11 @@ Census classifications must accompany implementation diffs: |
| | Read-only root fence/participation view | Read-only projection. No registration, fence clearing, expiry extension, receipt manufacture or root write. Fresh request IDs; no stale replay cache can return an older permissive view. | |
| | `PrepareRootJournalDirectory` | Exactly `authority` and moves' `held-release` are additional checked root-owned subdirectories. Preserve existing `retire` and `migration-*` classification and refusal of other entries. | |
| | `claimDataCensus` | Existing `.hostclaims`, `.hostexec`, `hostguard` remain agent/legacy shared bookkeeping. Update its misleading “verify their own contents” comment. Do not delete those classifications just because root authority moved, and do not add a wildcard for DataDir. | |
| -| Root release/health/lock stores and root moves floor | Shared box state outside host workload roots. Not per-host retireable artifacts. No `/var/lib/**` wildcard and no managed-root expansion solely to exempt these files. | |
| +| Root release/health/lock/generation/failure stores and root moves floor | Shared box state outside host workload roots. Not per-host retireable artifacts. No `/var/lib/**` wildcard and no managed-root expansion solely to exempt these files. | |
| | Existing root journal and registry child | No new SQLite open in root. Reuse the already-reviewed root-owned journal boundary; never open a DataDir DB from root. Registry reads/mutations retain the isolated child. | |
| | Constructors and changed function fingerprints | Update only the specific source inventory rows that the changed expression actually affects, naming their preserved authority role. No whole-file inventory regeneration to hide new sites. | |
| -The A/B/B2 reference patches add no claim mutation site and require no existing census snapshot update; the authoring replay runs the existing census. Tasks C–G necessarily change authority sites and need the above explicit entries with their final concrete function names. Do not claim their census has passed before those changes exist. |
| +The A/B/B2/C1/F1/G1/H1 reference patches add no claim mutation site and require no existing census snapshot update; the authoring replay runs the existing census. Tasks C–G necessarily change authority sites and need the above explicit entries with their final concrete function names. Do not claim their census has passed before those changes exist. |
| Root service filesystem access must be deliberate. Claim-mode instance-control already has `StateDirectory=carlos-claims`; add access only for the public healthy store and required root release directory when held-stage root needs to record a result. `ProtectSystem=strict` must not make successful D3 writes impossible. Prefer bootstrap creation of the public history tree with fixed root ownership and mode; do not let systemd create it at its default private mode and then quietly hide it from the agent. Reject a pre-existing hostile directory. Check both clean boot and upgrade. Claim-off instance-control keeps its existing unit/mux/constructor. Adoption and legacy held-stage continue to work with D3 off. |
| @@ -1083,6 +1574,113 @@ Acceptance is a matrix, not just compilation: |
| Before landing the implementation, run `go build ./...`, `go vet ./...`, `go test ./... -count=1`, `gofmt -l .`, the real UID/systemd acceptance matrix and the repository's cross-platform gate once on the integrated tip. Do not call a sandbox-denied socket or skipped privileged witness a pass. This authoring task makes no commit and runs no production test. |
| +### H1: Exact constructor census test |
| + |
| +Apply this test before B to observe the production `NewProofGate` failures, then repeat after B and after current moves integration/B2. It scans selectors rather than a single old line number, so a change from `heldReleaseReady` to `heldReleaseHooksVia` cannot hide the agent consumer. This source test complements B's behavioral forged-history witnesses; it is not observation provenance evidence. |
| + |
| +<!-- apply: H tests --> |
| +```diff |
| +--- /dev/null |
| ++++ b/internal/host/plan14_constructors_test.go |
| +@@ -0,0 +1,90 @@ |
| ++package host |
| ++ |
| ++import ( |
| ++ "go/ast" |
| ++ "go/parser" |
| ++ "go/token" |
| ++ "os" |
| ++ "strings" |
| ++ "testing" |
| ++) |
| ++ |
| ++func TestPlan14ProductionProofConstructors(t *testing.T) { |
| ++ entries, err := os.ReadDir(".") |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ found := map[string]int{} |
| ++ healthyWriter := 0 |
| ++ settledWriter := 0 |
| ++ for _, entry := range entries { |
| ++ name := entry.Name() |
| ++ if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { |
| ++ continue |
| ++ } |
| ++ file, err := parser.ParseFile(token.NewFileSet(), name, nil, 0) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ ast.Inspect(file, func(n ast.Node) bool { |
| ++ if lit, ok := n.(*ast.CompositeLit); ok { |
| ++ if typ, ok := lit.Type.(*ast.Ident); ok && typ.Name == "proofRoot" { |
| ++ for _, elt := range lit.Elts { |
| ++ kv, ok := elt.(*ast.KeyValueExpr) |
| ++ if !ok { |
| ++ continue |
| ++ } |
| ++ key, ok := kv.Key.(*ast.Ident) |
| ++ if !ok || key.Name != "healthyDir" { |
| ++ continue |
| ++ } |
| ++ if isRootHealthy(kv.Value) { |
| ++ healthyWriter++ |
| ++ } |
| ++ } |
| ++ } |
| ++ } |
| ++ if call, ok := n.(*ast.CallExpr); ok { |
| ++ if fn, ok := call.Fun.(*ast.Ident); ok && fn.Name == "recordSettled" && len(call.Args) == 5 && isRootHealthy(call.Args[3]) { |
| ++ settledWriter++ |
| ++ } |
| ++ } |
| ++ |
| ++ sel, ok := n.(*ast.SelectorExpr) |
| ++ if !ok { |
| ++ return true |
| ++ } |
| ++ pkg, ok := sel.X.(*ast.Ident) |
| ++ if !ok || pkg.Name != "release" { |
| ++ return true |
| ++ } |
| ++ if sel.Sel.Name == "NewProofGate" { |
| ++ t.Errorf("%s retains writable healthy authority", name) |
| ++ } |
| ++ if sel.Sel.Name == "NewRootProofGate" { |
| ++ found[name]++ |
| ++ } |
| ++ return true |
| ++ }) |
| ++ } |
| ++ for _, name := range []string{"agent.go", "proofask.go", "heldstage.go", "movespass.go"} { |
| ++ if found[name] != 1 { |
| ++ t.Errorf("%s: trusted constructors = %d", name, found[name]) |
| ++ } |
| ++ } |
| ++ if healthyWriter != 1 || settledWriter != 1 { |
| ++ t.Fatalf("root healthy writers: observation=%d settle=%d", healthyWriter, settledWriter) |
| ++ } |
| ++ if _, err := os.Stat("movesheldrelease_production.go"); err == nil && found["movesheldrelease_production.go"] != 1 { |
| ++ t.Fatal("moves root constructor not wired") |
| ++ } |
| ++} |
| ++ |
| ++func isRootHealthy(expr ast.Expr) bool { |
| ++ sel, ok := expr.(*ast.SelectorExpr) |
| ++ if !ok || sel.Sel.Name != "RootHealthyDir" { |
| ++ return false |
| ++ } |
| ++ pkg, ok := sel.X.(*ast.Ident) |
| ++ return ok && pkg.Name == "release" |
| ++} |
| +``` |
| + |
| +### H2: Named integration and privileged gates |
| + |
| +Run C2–G2's named tests on the complete implementation and maintain a result table with pass/fail/skip per test and build ID. For census changes, add only exact new root journal operations, `authority` and `held-release` directories, and the actual new effect functions. Classify ZDD reserve/release as local root authority (shared box state, outside retireable workload artifacts) and swap-failure add/clear as local failure/retry authority. The root floor projection is read-only; floor refresh is an authoritative remote read plus local ratchet write. Neither is a claim writer. Do not reclassify agent mirror publishers as root authority. |
| + |
| +The real UID/systemd runner must cover the production `rootstate.Open`, B3 binding provenance, C2 concurrent adoption/publication, D3 update exclusion, E2 pending/running generation deletion, E3 swap-failure tampering, F3 both prev branches, F4 both generated boot selectors and G2 active-move refusal/crash resume. Every denial has the positive counterpart named above. Save the matrix before activation. Preserve `#267` root-registry isolation/rootaws tests and current moves `AcquireHeld`, `ReplicaParkLeaseSet`, held receipt, limiter, anchored render read and removable-lock tests. A foundation build or a helper-only test cannot satisfy these gates. |
| + |
| ## Task I: Amend the claim spec to rev 6.34 |
| The design spec is a separate input, not part of the main export. Its rev 6.33 SHA-256 is `5432be6e12e8fc134eabcf3b19733a1a5935971e81c3ebd2f34f70e333945000`. Copy that exact file to scratch before applying this patch. Keep the design worktree's spec unchanged during authoring. |
| @@ -1116,7 +1714,7 @@ The design spec is a separate input, not part of the main export. Its rev 6.33 S |
| The collector then releases hostguard. The agent closes its matching |
| in-memory admission fence, cancels and joins selected application work, |
| -@@ -2977,6 +2979,92 @@ |
| +@@ -2977,6 +2979,122 @@ |
| - Using a route status as the operation identity: it cannot distinguish old and current holds. |
| - Clearing failure on a heartbeat or cached response: neither proves this admitted process is healthy. |
| - Re-admitting a session for each restart under a live replicator: it violates the session boundary. |
| @@ -1162,7 +1760,9 @@ The design spec is a separate input, not part of the main export. Its rev 6.33 S |
| +controller process and performs the acquisition/renewal it records. The |
| +agent remains the work orchestrator and a client of that broker. Root uses |
| +its own suspend-aware clock and the existing 90-second TTL, 20-second renewal |
| -+and 5-second margin. It accepts no caller-supplied expiry or safe deadline. |
| ++and 5-second margin. Moves' 45-minute staging lease remains separate; |
| ++AggregateLeases.AcquireHeld borrowing and ReplicaParkLeaseSet data-bucket scopes |
| ++are unchanged. It accepts no caller-supplied expiry or safe deadline. |
| +A lost or uncertain lease remains denied until positive cleanup and CC9 |
| +settlement. Agent file deletion or replacement locks cannot prove settlement. |
| +Root update exclusion and recovery use the root participation and launch |
| @@ -1174,20 +1774,48 @@ The design spec is a separate input, not part of the main export. Its rev 6.33 S |
| +writable only by root through root-owned ancestors. An old DataDir healthy |
| +hash is not imported as evidence. The existing running-executable/systemd/ |
| +readiness observation or a valid prover signature supplies the fact anew. |
| ++Account/app attribution, unit binding and readiness configuration are independently |
| ++resolved from authoritative deployment records and root-installed bindings. |
| ++Registry Socket/Addr/Health or ownership fields cannot skip readiness, select |
| ++another app's history, or mint a proof. Settle and reused mint observations |
| ++carry and revalidate that same binding. |
| +An unprovable historical rollback waits for proof. The healthy exception |
| +and staged-byte binding use this same trusted history. |
| + |
| +Root release refusals, program exceptions, bake/production/pipeline ratchets, |
| -+restart-consumption state and moves-floor/boot-selection decisions likewise |
| ++restart-consumption state, swap-failure/retry authority, generation reservations |
| ++and moves-floor/boot-selection decisions likewise |
| +have independent root authority. Agent mirrors may request work, describe |
| +status or add a conservative deny; they cannot remove a remembered deny or |
| +supply an exception. Root never treats a missing service file as permission |
| +to downgrade, retry, skip a hold or replay a consumed restart. Signed release |
| +and artifact checks remain in force. Ordinary instance payloads remain under |
| +DataDir with the existing descriptor, containment and typed-inventory checks. |
| ++Root retains pending/running generation pins independently of both legacy |
| ++ZDD files, reconciles them against kernel evidence, and refuses restaging or |
| ++sweep while completeness is unknown. Missing swap-failure files and legacy |
| ++Retried entries cannot remove root's refusal; clearing needs independently |
| ++verified retry, pointer or healthy evidence under the existing rule. |
| ++ |
| ++The lock order is release, root host, legacy host, then any removable |
| ++held-stage leaf lock. Held publication acquires the stable root release lock |
| ++outside Work.With. Root locks are never unlinked; the agent's removable |
| ++held-stage lock and its waiter inode recheck remain separate. Create-mode |
| ++traversal fsyncs every parent entry, including existing entries on retry. |
| ++ |
| ++Root refreshes the moves floor independently of platform updates, including |
| ++when no carlos.prev exists. The agent advertises only a durably accepted root |
| ++projection. Legacy minima add denies only; remote deletion/regression and |
| ++missing historical files cannot initialize a weaker floor. Unresolved history |
| ++is quarantined until root-authorized reconstruction. Floor line/JSON/verdict |
| ++and projection crash ordering must preserve the stricter bound. |
| + |
| +Migration is offline, guarded and idempotent. Reuse the existing edge-mask |
| -+ownership and kernel process-exclusion protocol. Persist a root migration |
| ++ownership and kernel process-exclusion protocol. Refuse migration while any |
| ++move remains active, including admission-before-launch, probing, probed-before- |
| ++commit and committed cleanup. Establish authoritative admission exclusion; |
| ++recheck the fresh census before prepared. Do no per-phase settlement or budget |
| ++consumption. A completed normal move may retry migration. Persist a root migration |
| +record, census authoritative claims plus root journals and managed artifacts, |
| +then retain denies for old admitted sessions before collecting/verifying stop |
| +evidence. A raw legacy fence journal grants nothing. Import completed evidence |
| @@ -1209,7 +1837,7 @@ The design spec is a separate input, not part of the main export. Its rev 6.33 S |
| ## 7. Lost boxes and execution fencing (J-D6, J-N3) |
| -@@ -3450,16 +3538,18 @@ |
| +@@ -3450,16 +3568,18 @@ |
| An active operation means a non-serving operation with an operation ID. Retained participants in `serving` do not cause lease writes. |
| @@ -1233,7 +1861,7 @@ The design spec is a separate input, not part of the main export. Its rev 6.33 S |
| ``` |
| with: |
| -@@ -3471,7 +3561,10 @@ |
| +@@ -3471,7 +3591,10 @@ |
| state: "pending" | "active" | "lost" |
| ``` |
| @@ -1251,16 +1879,16 @@ The design spec is a separate input, not part of the main export. Its rev 6.33 S |
| Add the following item after the existing offline root journal migration/recovery prerequisite in Plan 9's activation checklist. It is additive to Plans 11–13 and their still-open gates. |
| -> **Root authority outside DataDir (Plan 14 / #280).** Before enabling claim launch, held publication or root claim adoption, install and review the root fence/installation backend, root execution-participation broker and all readers on the box, including recovery and the final executable gate. Verify the full root-owned ancestor chain, stable root locks, fsync/rename recovery and authenticated read-only agent views. Complete the offline, kernel-verified legacy migration under the rollout-owned edge mask; verify its durable completion and retain denies for old sessions. Re-admit through the existing stop/handover flow; never import an agent's allow record. Verify the root release/floor ledgers and both recovery boot selectors. Exercise real hostile service-UID ownership, parent rename, lock replacement and file deletion against launch, held publish/replay, adoption, stop recovery and system update. A skipped privileged witness is a blocker. D3 history moves independently with the claim flag off: rebuild from root observation or signed proofs and record any historical rollback that must wait. Preserve existing #267, C8, launch, moves, region-HA, floor and filesystem gates. Obtain scoped approval for the actual deployment, machine list, maintenance/traffic effects and rollback window; this plan provides no production authorization. |
| +> **Root authority outside DataDir (Plan 14 / #280).** Before enabling claim launch, held publication or root claim adoption, install and review the root fence/installation backend, root execution-participation broker and all readers on the box, including recovery and the final executable gate. Prove release → root-host → legacy-host ordering under concurrent adoption/publication. Verify the full root-owned ancestor chain, stable root locks, fsync/rename recovery and authenticated read-only agent views. Refuse migration while any move is active, and verify authoritative move-admission exclusion. Complete the offline, kernel-verified legacy migration under the rollout-owned edge mask; verify its durable completion and retain denies for old sessions. Re-admit through the existing stop/handover flow; never import an agent's allow record. Verify independent observation/readiness attribution, root swap-failure/retry authority, durable pending/running generation pins, the root release/floor ledgers and both recovery boot selectors. Verify floor refresh and root-only acknowledgement with and without carlos.prev, plus conservative quarantine on remote deletion/regression or missing legacy history. Exercise real hostile service-UID ownership, parent rename, lock replacement and file deletion against launch, held publish/replay, adoption, stop recovery and system update. A skipped privileged witness is a blocker. D3 history moves independently with the claim flag off: rebuild from root observation or signed proofs and record any historical rollback that must wait. Preserve existing #267, C8, launch, moves, region-HA, floor and filesystem gates. Obtain scoped approval for the actual deployment, machine list, maintenance/traffic effects and rollback window; this plan provides no production authorization. |
| ## Replay without git writes |
| -A/B are the replayable foundation code; C–H are the remaining implementation requirements and acceptance criteria. Do not present a passing A/B replay as an implementation of the root guard or lease broker. B2 is the separately labeled moves-integration constructor change; I is the document patch. |
| +A/B, C1, F1, G1 and H1 are the replayable foundation code/tests; the numbered integration tasks remain required. B3 is specifically not implemented by relocating the history store. Do not present a passing A/B replay as an implementation of the root guard or lease broker. B2 is the separately labeled moves-integration constructor change; I is the document patch. |
| ```bash |
| CLAIM14_REPO=/home/paulca/amadan.net/carlos/platform/.claude/worktrees/replica-ownership |
| CLAIM14_PLAN="$CLAIM14_REPO/docs/superpowers/plans/2026-10-12-host-lifecycle-claim-14-root-trusted-state.md" |
| -CLAIM14_BASE=39986908150d2e3c90477de4320c0d2941ffaac6 |
| +CLAIM14_BASE=8916558d34f68698f488ee7a0aa7f4a55808c380 |
| CLAIM14_SCRATCH=$(mktemp -d "$TMPDIR/claim14.XXXXXX") |
| export CLAIM14_REPO CLAIM14_PLAN CLAIM14_BASE CLAIM14_SCRATCH |
| mkdir "$CLAIM14_SCRATCH/tree" |
| @@ -1276,13 +1904,17 @@ Save this helper at `$CLAIM14_SCRATCH/apply.py`. It requires an exact match and |
| ```python |
| import pathlib, re, subprocess, sys |
| -plan, task, phase = sys.argv[1:] |
| +plan, task, phase, *exclude = sys.argv[1:] |
| blocks = [body for name, stage, body in re.findall( |
| r'<!-- apply: (\w+) (tests|implementation) -->\n```diff\n(.*?)\n```', |
| pathlib.Path(plan).read_text(), re.S) if name == task and stage == phase] |
| if not blocks: |
| raise SystemExit('missing task/phase') |
| for body in blocks: |
| + if exclude: |
| + pieces = re.split(r'(?=^--- (?:a/|/dev/null))', body, flags=re.M) |
| + body = ''.join(part for part in pieces if not any( |
| + '+++ b/'+name+'\n' in part+'\n' for name in exclude)).rstrip('\n') |
| p = subprocess.run(['patch', '--batch', '--forward', '--fuzz=0', '-p1'], |
| input=body+'\n', text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| print(p.stdout, end='') |
| @@ -1290,18 +1922,34 @@ for body in blocks: |
| raise SystemExit('patch did not apply exactly') |
| ``` |
| -Apply A tests, observe the missing implementation, apply A implementation and rerun. Do the same for B. Apply I only to the verified spec copy. Then run: |
| +Apply A tests, observe the missing implementation, apply A implementation and rerun. Apply B and H tests before B implementation; the H constructor test fails on old production gates. Repeat tests-before-implementation for C, F and G. Apply I only to the verified spec copy. Then run: |
| ```bash |
| -go test ./internal/rootstate ./internal/release -run TestPlan14 -count=1 |
| +go test ./internal/rootstate ./internal/release ./internal/host -run TestPlan14 -count=1 |
| go test ./internal/release ./internal/host -run 'TestHealthy|TestProof|TestRecordSettled' -count=1 |
| go test ./internal/provision -run Census -count=1 |
| go build ./... |
| go vet ./internal/rootstate ./internal/release ./internal/hostguard ./internal/hostbox ./internal/host |
| -gofmt -l internal/rootstate internal/release/healthy.go internal/release/healthy_root.go internal/release/plan14_healthy_test.go internal/release/proofgate.go internal/host/proofroot.go internal/host/agent.go internal/host/heldstage.go internal/host/proofask.go internal/host/movespass.go |
| +gofmt -l . |
| ``` |
| -For the moves integration check, use another export of the same pinned main and apply A/B first. Apply the exact deltas, in order, for `b3d423390c5f733c31c4b06e6df5a72dfdb06f6d`, `5a46611265d20775a9f18e2979c69bb01959ec1b`, `27b83074c34225e7dff60deafb2fd2787399bbbc`, `cf0b6de5e1a1262b23ccf3b5c63906acf301805a`, and `ed97cbc73c471f3fbff53fc9c9f5e9186ac12fb2`. Obtain each delta with `git diff <commit>^ <commit>`. Reject offsets/fuzz; never replace a whole file with an older branch version. Apply B2, then build and run the held-release/D3 tests. Report any missing prerequisite as such, rather than omitting a failing file. |
| +For current moves integration, export pinned main again. Use `git merge-base` with `342f84d740b98dfdd1fcec6626fa9aef79b00247` (observed ancestor `2ad5412cd50cf552d61dfa41b16b41b6a30fed5c`). Enumerate `git diff --name-only <ancestor> <moves-tip>`, read each version with `git show`, and merge each changed text file with `git merge-file -p <main-file> <ancestor-file> <moves-file>`. New files/deletions apply only if main still equals ancestor; otherwise stop for review. This operates only in scratch and writes no Git metadata. At this pin, 89 paths differ and one conflict occurs in `internal/provision/plan9_b8_census_test.go`: retain main's additional `placeBuildState`, `buildStates.write` and `appRecords` rows; keep the `recoveryInstallSource` row classified `record-writer` with `key:SetRecordMove`, recomputing only its merged function fingerprint. Its role remains guarded recovery record mutation. Any further conflict requires explicit reconciliation, not choosing a whole branch file. |
| + |
| +Apply A/B/C/F/G/H foundation patches to that integration. Exclude both `internal/host/agent.go` and `internal/host/movespass.go` from B implementation, then apply B2 exactly. B2 is the integrated replacement for those two file hunks plus root's held-release constructor. Keep main's `updatesPassOption(dataDir(), true)` and current moves' `heldReleaseHooksVia`; no whole-file substitution is needed. Run H after B2; it checks both readers and observation/settle writers. |
| + |
| +The merged census needs exactly these reviewed entries; retain every other main/moves inventory row. The current moves tip introduced two versioned-store wrappers and a read-only exact-byte adapter without their census rows, and changed the prepared-manifest commit body; this is an integration prerequisite discovered by the rev 2 replay, not a Plan 14 authority-site expansion. |
| + |
| +| File and unit | Classification | Kinds | SHA-256 of merged function | |
| +|---|---|---|---| |
| +| `internal/host/movesinstall.go:recoveryInstallSource` | `record-writer` | `key:SetRecordMove` | `1d97e2177b7a4f3da6cb7db57cacf70d0394824bac32d7d037e7db3fd29b286f` | |
| +| `internal/host/movesinstall.go:installAuthorityStore.PutIfAbsent` | `versioned-transport` | `def:PutIfAbsent,sel:PutIfAbsent` | `dc3db56e278cead930bfd2002caef7141be0b38d77fcae3ef766170ecd0cac04` | |
| +| `internal/host/movesinstall.go:installAuthorityStore.CopyVersion` | `versioned-transport` | `def:CopyVersion,sel:CopyVersion` | `f1934a22f1eebd997bc6389554545096acfad55b4b793fda47f0fc650d8cb6a6` | |
| +| `internal/host/movesfiles.go:exactBytesStore.Put` | `nonrecord` | `def:Put` | `7f875cc9a0678d9b0357bfe1246dc998e4e4b3da0043eea47bc71bc0b096557d` | |
| +| `internal/hostfiles/prepared.go:Prepared.Commit` | `nonrecord` | `sel:Put` | `d78f15e321e6ad24e19b08ac470251b6c0166bb97c67c84d130b579b3a7dec0c` | |
| + |
| +The wrappers recheck the captured installation before forwarding versioned-store writes used by prepared-file builds; they construct no record key. The recovery function still writes only through guarded `SetRecordMove`. `exactBytesStore.Put` always refuses without effects. `Prepared.Commit` remains a create-only generation-manifest writer; its added return value carries the exact stored version, not a record write. Verify these five source bodies against their hashes rather than regenerating the whole census. |
| + |
| +Run `go build ./...` and `go test ./internal/host ./internal/release ./internal/hostbox ./internal/instancectl ./internal/provision -run 'TestPlan14|TestHeldReleaseRoot|TestHeldReleaseRequestShape|TestHeldReleaseRouteIsMovesOnly|TestHeldReleaseReady|TestStageHeldVerified|TestHeldStageLock|TestClaimHeldReleaseRouteExistsOnlyInClaimMode|TestAggregate|TestPlan12LandingResidueParkUnderItsOwnLease|TestPlan12LeaseSetSemantics|Census' -count=1`. Report socket restrictions separately; the root socket test is not covered by a helper pass. Keep the reviewed receipt-persistence failure, action/kind/phase binding, explicit revert/clean-return refusals and limiter tests in this selection. A constructor-only replay does not verify C–G production integration. |
| Inspect the report for actual commands, results and limits. After collecting results, leave the scratch working directory, close every process started here, and remove only the exact scratch path created above. Never delete the shared GOCACHE, the pre-existing brief/review directory or a glob of other sessions' scratch. |
| @@ -1309,8 +1957,8 @@ Inspect the report for actual commands, results and limits. After collecting res |
| The review must establish that absence is checked from root-owned state; a trustworthy-looking leaf under `/data` does not suffice. Both root publication and receipt replay must see root's fence. The agent still needs a working read-only view for its own guard and cold-receipt readers, and the cold transition must synchronize the root installation rather than accidentally granting a new stamp before retirement. |
| -Migration deliberately costs a cold re-admission for existing claim sessions and may cost a prover wait for old healthy hashes. There is no safe transparent migration of an unauthenticated positive assertion. Deployment approval must cover that interruption. The controller should assign Tasks C–G as one authority integration before activation; it can land the independently tightening D3 change earlier after ordinary branch review. |
| +Migration deliberately costs a cold re-admission for existing claim sessions and may cost a prover wait for old healthy hashes. There is no safe transparent migration of an unauthenticated positive assertion. Deployment approval must cover that interruption. The controller should assign Tasks C–G as one authority integration before activation; it can land the independently tightening D3 change after B3 and E mint-refusal provenance are complete and reviewed. |
| -The wider census exposes previously accepted service-file trust in moves floors and bake/history state. This brief asks for all root decisions, so the plan includes their tightening. If Paul wants those older threat models retained, that is an explicit scope reduction and leaves the corresponding rows unresolved; it cannot be described as completing the all-root property. No such exception is assumed here. |
| +Paul has approved both the claim-only authority split and the pre-claim D3, release-history and floor scope. There is no open product ruling or optional scope reduction here. The outstanding work is the explicitly named production integration and acceptance, not permission to retain writable authority. |
| The code patches, comments, protocol operation names, this plan, report and amendment are internal implementation material. Existing user-visible refusal wording is reused; implementation owners must inventory/review any new product diagnostic wording before shipping it. No Claude, Anthropic model or subagent was used to author this plan. |