Merge origin/main into carlos-run before landing (zdd-owed-items)
origin/main at 1c5718f6, one commit on top of 132cf996. One file conflicted: the root-read census test. Main's census ruling owed loads after 275, its seal is checked, and the carlos-run overlay loads last. TestPlan14CarlosRunOverlayPreservesLegacySeal includes owed in its chain. Census overlay census-ruling-carlos-run, mechanical re-pins only: - internal/activator envelope re-pinned, for owed's own changes to doWake, seedChild, seedUnit and settleWakeIdentity. This branch does not carry those units, and their merged bodies are main's. - No row changed. Every other envelope still matches. - Seal re-pinned: 96e521e60dfc0b55e2bc08adbb7a7682a607a30599a8cf66e7c5689d6cf7fe5a. Frozen count: main has 1860 declarations (1957 fingerprints), and the merged tree has 1896 (2016). Of the overlay's 59 frozen rows, 23 restate declarations main already freezes, type Server among them. The other 36 are new, so nothing is counted twice. staged.go: main refactored ResolveLivePath onto ResolveLiveLink in internal/release. Instance control and the cmd path do not call it, and POST /cmd still validates a named executable with refuseStagedExe (instancectl/cmd.go:69), as the ruling's condition requires. Gate: build, vet, gofmt and the darwin, windows and linux/arm64 builds clean; activator, instancectl, instanceexec, host, provision, runs, registry, the root package (with its 26 copy-gate tests) and console pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
15 files changed,
+777
−210
docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md+119 −128internal/activator/activator.go+8 −3internal/activator/backend_exec.go+14 −1internal/activator/beforeexec.go+158 −32internal/activator/beforeexec_test.go+52 −7internal/activator/genbackend.go+52 −9internal/activator/wakeidentity_test.go+24 −12internal/activator/wakerecord_recheck_test.go+168 −0internal/activator/wakerecord_refused_test.go+137 −0internal/host/buildstate_planned_test.go+6 −1internal/provision/plan14_legacy_identity_test.go+4 −4internal/provision/plan14_root_reads_test.go+14 −2internal/provision/testdata/plan14-root-reads-carlos-run.tsv+2 −1internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv+13 −0internal/release/staged.go+6 −10
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 295.420s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.824s
ok github.com/carlosframework/platform/cmd/moveprobe 93.299s
ok github.com/carlosframework/platform/cmd/zddprobe 0.706s
ok github.com/carlosframework/platform/internal/activator 249.204s
ok github.com/carlosframework/platform/internal/agentsock 0.021s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.019s
ok github.com/carlosframework/platform/internal/appconfig 2.320s
ok github.com/carlosframework/platform/internal/apperr 0.072s
ok github.com/carlosframework/platform/internal/applog 0.129s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.840s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.009s
ok github.com/carlosframework/platform/internal/caps 0.013s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.708s
ok github.com/carlosframework/platform/internal/changelog 0.313s
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 372.024s
? 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 6.445s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.002s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.208s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 26.300s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.008s
ok github.com/carlosframework/platform/internal/fleetapi 0.124s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.018s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/host 134.784s
ok github.com/carlosframework/platform/internal/hostactors 0.407s
ok github.com/carlosframework/platform/internal/hostbox 2.284s
ok github.com/carlosframework/platform/internal/hostclaim 0.526s
ok github.com/carlosframework/platform/internal/hostevidence 0.011s
ok github.com/carlosframework/platform/internal/hostexec 4.966s
ok github.com/carlosframework/platform/internal/hostfence 0.051s
ok github.com/carlosframework/platform/internal/hostfiles 10.531s
ok github.com/carlosframework/platform/internal/hostfloor 0.015s
ok github.com/carlosframework/platform/internal/hostguard 1.011s
ok github.com/carlosframework/platform/internal/hostops 1.486s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.011s
ok github.com/carlosframework/platform/internal/hostquarantine 0.011s
ok github.com/carlosframework/platform/internal/hostrelocate 0.453s
ok github.com/carlosframework/platform/internal/hostresolve 0.158s
ok github.com/carlosframework/platform/internal/hostwrite 0.012s
ok github.com/carlosframework/platform/internal/infraview 0.011s
ok github.com/carlosframework/platform/internal/instancectl 4.195s
ok github.com/carlosframework/platform/internal/instanceexec 2.754s
ok github.com/carlosframework/platform/internal/instancefs 0.022s
ok github.com/carlosframework/platform/internal/instancekey 0.012s
ok github.com/carlosframework/platform/internal/instanceupdates 0.021s
ok github.com/carlosframework/platform/internal/layout 1.470s
ok github.com/carlosframework/platform/internal/lease 9.830s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.018s
ok github.com/carlosframework/platform/internal/ledger 0.012s
ok github.com/carlosframework/platform/internal/mail 0.178s
ok github.com/carlosframework/platform/internal/maintenance 0.081s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.537s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.072s
ok github.com/carlosframework/platform/internal/metrics 0.006s
ok github.com/carlosframework/platform/internal/moves 21.116s
ok github.com/carlosframework/platform/internal/moves/archive 0.777s
ok github.com/carlosframework/platform/internal/objstore 0.174s
ok github.com/carlosframework/platform/internal/ops 0.163s
ok github.com/carlosframework/platform/internal/park 0.199s
ok github.com/carlosframework/platform/internal/privatefile 0.007s
ok github.com/carlosframework/platform/internal/provision 13.630s
ok github.com/carlosframework/platform/internal/publicrelease 0.006s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.118s
ok github.com/carlosframework/platform/internal/relay 7.260s
ok github.com/carlosframework/platform/internal/release 19.860s
ok github.com/carlosframework/platform/internal/rollout 3.795s
ok github.com/carlosframework/platform/internal/rolloutwords 0.004s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.008s
ok github.com/carlosframework/platform/internal/rootstate 0.025s
ok github.com/carlosframework/platform/internal/runs 0.579s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.321s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.081s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.996s
ok github.com/carlosframework/platform/internal/status 0.041s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.234s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.005s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.563s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 46071970 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, 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.025s
=== RUN TestT4StorageProbeMinio
2026/10/15 07:28:37 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEA35C09276AB3, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.022s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md| index 48548fc37..398479619 100644 |
| --- a/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md |
| +++ b/docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md |
| @@ -29,21 +29,55 @@ plans 1 to 6, in `platform-design-zero-downtime-deploys`. |
| ## Before Gate E: owed |
| -This is the flag-on precondition. It is copied from amadan discussion #272 |
| -("Zero-downtime deploys: owed before CARLOS_SWAP can go on", opened by |
| -claude~paul@keymail.dev), as read on 2026-10-11, including its replies. |
| -As of 2026-10-12, items this branch closes carry a line saying "closed on |
| -zdd-flag-readiness (refs), pending merge". Those are built and task-reviewed on |
| -the branch, and the whole-branch review's first round (Astra, |
| -`CODEX-FINDINGS-ZDD-READINESS.md`) found gaps in several of them, which its |
| -fix wave closed (the commits cited as "final review round 1"). They are not |
| -merged, not released and not rolled, and only Paul closes them on #272. Everything else is still open, and the open items are |
| -collected under "Still owed" below. **The flag stays off on any box until |
| -every item is closed or Paul waives it in writing.** Gates A to D do not |
| -turn the flag on and can go ahead while this list is open. |
| - |
| -The discussion's opening text says zero-downtime deploys are merged |
| -dormant and `CARLOS_SWAP` must stay unset until everything below is closed. |
| +**Current state, 2026-10-14.** This list is copied from amadan discussion |
| +#272 ("Zero-downtime deploys: owed before CARLOS_SWAP can go on"). Every |
| +item on it that blocked the flag is now closed, or is one of the open items |
| +named below. The subsections after this one are the history of how each |
| +item closed, kept for their commit references. Where they say "pending |
| +merge", read the state here instead. |
| + |
| +- **Merged and released.** Readiness rounds 1 to 3 (`zdd-flag-readiness`, |
| + `zdd-flag-readiness-2` and `zdd-flag-readiness-3`). Rev 19, the generation |
| + socket names, is merged as `ebddcfc` and released in v0.27.2. #292 is |
| + merged as `e15ad6b` and released in v0.27.3. |
| +- **The flag.** Gate F passed on annie-welding, and Gate G armed the other |
| + three flagship boxes. All four flagship boxes are armed as of 2026-10-14. |
| + Dev is being armed on 2026-10-14. |
| +- **Kernel checks: closed.** On 2026-10-14 a root test runner ran them on |
| + Debian 13 arm64, kernel 6.12.96, origin/main `2683f16a`, as `User=carlos` |
| + with `Delegate=yes` and `cpuset cpu io memory pids` in the unit's |
| + `subtree_control`. All passed and none skipped: |
| + - `TestClaimContainmentCleanupRespawn`, all three subtests; |
| + - `TestGenerationStartRefusedBesideADirectParentMember`, with its sibling |
| + and recovery controls; |
| + - the `ensureParent` positive control, which is `internal/cgroup`'s |
| + `TestGenKernelGenerationRefusedBesideAFlatMember` |
| + (`CARLOS_GEN_KERNEL=1`) and `TestGenKernelShimRespawnAfterKill` |
| + (`CARLOS_SHIM_KERNEL=1`). |
| + |
| + The details are on #272. |
| +- **Mixed-version retries.** Dev round 4 passed three cases: a new box with |
| + a new console, a book that had not saved, and an old box with a new |
| + console. A new box with an old console was not run. That case is covered |
| + by construction. An old console can only write an unsigned retry, and |
| + every new box ignores an unsigned retry, so the worst it can do is a retry |
| + that does nothing. |
| +- **Owed items from settings plan 4** (branch `zdd-owed-items`): |
| + - Closed: a cold wake's build-state record is now written at the exec, |
| + after the replica-key gate and the cgroup refusals, so a refused launch |
| + leaves no record. The exec is decided again after the write, and a |
| + nested seed writes its obligation before its record (Astra's review of |
| + the branch, P1 and P2). This was owed before O8 (replica keys) is |
| + switched on. |
| + - Closed: `release.ResolveLivePath` now calls `release.ResolveLiveLink`. |
| + The two had identical bodies. |
| + - MOVED to #250: leftover `consumed/` files refuse the claims census. See |
| + "Still owed (code)". |
| +- **Claims-on swaps** remain a future feature. A box with host claims on |
| + declines the flag with the reason code `host-claims-on`. |
| + |
| +The flag stays off on any box that has not passed its gate, and only Paul |
| +closes items on #272. |
| ### Correctness, from Astra's plan 5 review (`CODEX-FINDINGS-ZDD-IMPL-5*.md`) |
| @@ -140,18 +174,15 @@ dormant and `CARLOS_SWAP` must stay unset until everything below is closed. |
| #### Dev-box flag blockers |
| -Two defects blocked the flag on the dev box. Both are fixed in code on |
| -zdd-flag-readiness, pending merge. Their tests are kernel-gated and skip on |
| -the build machine, so neither fix has been seen working on a real kernel. |
| -The dev-box rerun is owed. Both kernel tests now start through the shim, and |
| -zdd-flag-readiness-3 notes that they cannot run on DOCS-T1 (see "Roll notes |
| -from zdd-flag-readiness-3", dev re-check step 7). |
| +Two defects blocked the flag on the dev box. Both are fixed, merged and |
| +released, and both kernel tests passed on a real kernel on 2026-10-14 (see |
| +"Kernel checks" at the top of this section). |
| -- The legacy-parent guard (2c5adbe8). Code fixed. Its kernel test |
| - `TestGenerationStartRefusedBesideADirectParentMember` needs the rerun. |
| -- The `cgroup.kill` respawn (c76a1d44, 66c92efe). Code fixed: a cgroup that |
| - took `cgroup.kill` is recreated, and KillHost and KillSidecar respawn. |
| - Its kernel test `TestClaimContainmentCleanupRespawn` needs the rerun. |
| +- The legacy-parent guard (2c5adbe8). Its kernel test is |
| + `TestGenerationStartRefusedBesideADirectParentMember`. |
| +- The `cgroup.kill` respawn (c76a1d44, 66c92efe). A cgroup that took |
| + `cgroup.kill` is recreated, and KillHost and KillSidecar respawn. Its |
| + kernel test is `TestClaimContainmentCleanupRespawn`. |
| ### Surfaces (plan 6) |
| @@ -241,109 +272,69 @@ two builds side by side, so it restarts instances the old way". The code |
| `host-claims-on` is in the instance's JSON record (`rollout.unavailable`) and |
| in the agent log line `REFUSING CARLOS_SWAP=overlap (host-claims-on)`. |
| -### Still owed (code), after zdd-flag-readiness |
| - |
| -These stay open after the branch, and each one still blocks the flag |
| -unless Paul waives it. Astra's whole-branch review, round 1 |
| -(`CODEX-FINDINGS-ZDD-READINESS.md`), classes each of them as a flag |
| -blocker, and the fix wave for that round did not close them. |
| - |
| -Readiness round 2 (branch `zdd-flag-readiness-2`, from `650e6152`, pending |
| -merge) closes most of them. A closed item below says "Closed on |
| -zdd-flag-readiness-2 (<commit>), pending merge". That means built and |
| -task-reviewed on the branch: not merged, not released, not rolled, and only |
| -Paul closes it on #272. The branch's whole-branch review (Astra) and the |
| -3000-seed rollout-simulation sweep come after these notes. Still open or |
| -still owed after the branch: claims-on swaps (a future feature); the |
| -`ensureParent` kernel positive control; and the dev mixed-version check (see |
| -"Real-box checks"). Paul approved the spec amendment (rev 18) and the seven |
| -declined-box and old-box strings, unchanged, on 2026-10-13 (the copy-apply |
| -commit on the branch, "copy: readiness round 2's seven strings, approved by |
| -Paul unchanged; rev-18 amendment approved"). The code for the other items |
| -is closed on the branch, pending merge. |
| - |
| -Readiness round 2's dev check (2026-10-13) then found four blockers, and a |
| -fifth defect that was only untidy. All of them are closed in code on |
| -zdd-flag-readiness-3 (`ff4b814a` to `496fb6ed`), pending merge, and they still |
| -block the flag until Paul's dev re-check passes. They are: the edge unit's |
| -`RestrictNamespaces=yes` made every generation placement fail (the exec-shim); |
| -arming did not check placement (`cgroup-placement-failed`); the probe app |
| -rejected `--db` (`cmd/zddprobe`); and `TestStrictKernel` had a defect. The |
| -untidy one is failed generation units lingering in `systemctl --failed`. |
| -Commits and the re-check are in "Roll notes from zdd-flag-readiness-3". |
| - |
| -- Nested live-link race on a declined box: needs an expected identity on |
| - `Control.Start`, root-side (task 9). Closed on zdd-flag-readiness-2 |
| - (`903ed7ca`, `c910d020`), pending merge: a declined box pins the live link |
| - once (its identity and the exec gate's verdict) and nested starts and |
| - seeds exec the pinned build. |
| -- Seeds exec the live link ungated on a declined box (task 9). A failed |
| - seed's descendants are not proven (flag off and on), and `removeSeedDB` |
| - can delete a DB a descendant still writes (task 15). Closed on |
| - zdd-flag-readiness-2 (`903ed7ca` for the gate; `4c5e43b5`, `5e51c008`, |
| - `4771d147` for the proof), pending merge: a seed's processes are proven |
| - gone before its database goes, nested seeds included, and an unproven seed |
| - leaves a durable obligation (see the roll note on `.seed-unproven`). |
| -- A legacy sidecar left stranded beside a retained incumbent has no |
| - recovery path yet (task 9). Closed on zdd-flag-readiness-2 (`c910d020`), |
| - pending merge: a refused sidecar restarts on the instance's own build. |
| -- A clean one-shot's good build is published without checking that the |
| - activator still owns the host and that no fence or lost lease came first, |
| - and a WakeWorker can still start a duplicate wake in a narrow window |
| - (task 12). Closed on zdd-flag-readiness-2 (`deb4c811`), pending merge: a |
| - one-shot is recorded good only after a clean park, and a waiting wake |
| - never starts a second. |
| -- The flag-off normal-stop half of F6: a normal stop still proves only the |
| - leader (task 15). Closed on zdd-flag-readiness-2 (`d6996537`, `ac9f3fef`, |
| - `097ca930`, `79032948`), pending merge: seeds and flag-off stops prove |
| - their processes gone before the lease goes, and a flag-off wake proves its |
| - host cgroup empty before it restores. This is an authorized flag-off |
| - change: one `cgroup.events` read per flag-off stop. |
| -- `ensureParent`'s other failures still run a generation uncapped: no |
| - `+memory`, EBUSY or EACCES on `subtree_control`, the mkdir fallback, and |
| - `applyLimit` on `g<n>`. The kernel positive control (a real cgroupfs |
| - showing the populated-parent refusal and the respawn work) is also owed |
| - (task 14). The code is closed on zdd-flag-readiness-2 (`98593909`, |
| - `8530210e`), pending merge: a generation whose limits cannot be applied |
| - does not start (`cgroup.ErrLimitNotApplied`). The kernel positive control |
| - is still owed (see "Real-box checks"). |
| -- The console instance page shows no hold row or Retry button for a declined |
| - box's hold. This needs copy. Closed on zdd-flag-readiness-2 (`6e6ad000`, |
| - `65d4e135`), pending merge. Paul approved the seven new strings |
| - unchanged on 2026-10-13 (the copy-apply commit; record |
| - `docs/superpowers/plans/2026-10-12-zero-downtime-flag-readiness-2-copy.json`). |
| +### Still owed (code) |
| + |
| +**Current state, 2026-10-14.** Readiness rounds 1 to 3 are merged and |
| +released, so every item this section listed after zdd-flag-readiness is |
| +closed on main. Rev 19 (`ebddcfc`, v0.27.2) and #292 (`e15ad6b`, v0.27.3) |
| +are merged and released too. The kernel checks that were owed here are |
| +closed (see "Kernel checks" at the top of "Before Gate E: owed"). |
| + |
| +Closed, with the commits that closed them: |
| + |
| +- The nested live-link race on a declined box (`903ed7ca`, `c910d020`). A |
| + declined box pins the live link once, and nested starts and seeds exec the |
| + pinned build. |
| +- Seeds that exec'd the live link ungated, and the failed seed's proof |
| + (`903ed7ca`; `4c5e43b5`, `5e51c008`, `4771d147`). A seed's processes are |
| + proven gone before its database goes, and an unproven seed leaves a |
| + durable obligation. |
| +- The stranded legacy sidecar (`c910d020`). A refused sidecar restarts on the |
| + instance's own build. |
| +- The one-shot's good build and WakeWorker's duplicate wake (`deb4c811`). |
| +- The flag-off normal-stop half of F6 (`d6996537`, `ac9f3fef`, `097ca930`, |
| + `79032948`). |
| +- `ensureParent`'s uncapped paths (`98593909`, `8530210e`). A generation |
| + whose limits cannot be applied does not start (`cgroup.ErrLimitNotApplied`). |
| + Its kernel positive control passed on 2026-10-14. |
| +- The declined box's hold row and Retry button (`6e6ad000`, `65d4e135`). Paul |
| + approved the seven strings on 2026-10-13. |
| +- The rev-18 spec amendment for console-signed host retries, approved by |
| + Paul on 2026-10-13. The design branch's spec still has to be raised to rev |
| + 18 there. |
| +- The reopen epoch, now the hard rule (`7c7c6141`, `191c12b9`, `692d1533`, |
| + `a6eb8b35`). The proof and its two assumptions are on `FailureBook` |
| + (`internal/rollout/failures.go`). |
| +- Mixed-version retry sequencing (`6e9e84bd`, `9f27cdfa`, `4d8957c1`, |
| + `18bde560`). Grants ride their own object. Dev round 4 checked the |
| + transitions; the one not run is covered by construction (see "Mixed-version |
| + retries" at the top of "Before Gate E: owed"). |
| +- Readiness round 2's dev blockers, closed by readiness round 3: the |
| + exec-shim for `RestrictNamespaces=yes`, the `cgroup-placement-failed` arming |
| + check, `cmd/zddprobe` and the `TestStrictKernel` fix. |
| +- Task 10's grant consumed while the row is moving or landing (`076bd9c0`). |
| +- From settings plan 4, on branch `zdd-owed-items`: a cold wake's build-state |
| + record is written at the exec, after the replica-key gate and the cgroup |
| + refusals, so a refused launch leaves no record (owed before O8). The exec |
| + is decided again after the write, and a nested seed writes its obligation |
| + first. |
| +- Also from settings plan 4, on the same branch: `release.ResolveLivePath` |
| + calls `release.ResolveLiveLink`. |
| + |
| +Still open: |
| + |
| - Claims-on swaps (the four gaps from `CODEX-FINDINGS-ZDD-IMPL-5-MERGE.md`). |
| - A future feature. Still open and not scheduled. |
| -- Spec section 11 still says a retry is a re-signed pointer. The spec needs a |
| - revision for console-signed host-retry grants. Drafted on |
| - zdd-flag-readiness-2 as |
| - `docs/superpowers/specs/2026-10-12-zero-downtime-deploys-rev18-amendment.md` |
| - (this commit), approved by Paul 2026-10-13 (the copy-apply commit). The |
| - design branch's spec is not edited from this branch: the copy step that |
| - raises it to rev 18 is still to do there. |
| -- The reopen epoch bound (c12c35c3), now the hard rule. Paul's ruling |
| - (2026-10-12): a stale grant must never clear a later failure, including |
| - across a crash. Closed on zdd-flag-readiness-2 (`7c7c6141`, `191c12b9`), |
| - pending merge. The rule: the box keeps one restriction generation per app, |
| - moved by every change to any of the app's restrictions; the book is saved |
| - before a generation is published, a save that fails leaves it unpublished |
| - (epoch 0, so no Retry is offered) and is retried on its own; a reopen |
| - starts past every saved value; and a failure book written by an earlier |
| - agent is given a new identity when adopted, so earlier grants name the old |
| - one. The 2^20 advance on every open is gone. The proof sketch and its two |
| - assumptions are in the doc comment on `FailureBook` |
| - (`internal/rollout/failures.go`). The cost if the ruling is wrong: a box |
| - with a failing disk shows holds with no Retry until a save lands. |
| -- Mixed-version retry sequencing: the order in which boxes and consoles take |
| - the signed, book-bound grants needs writing down and checking on dev before |
| - the flag goes on (see "Retries: supported transitions" below). Closed on |
| - zdd-flag-readiness-2 (`6e9e84bd`, `9f27cdfa`, `4d8957c1`, `18bde560`), |
| - pending merge, by construction: grants ride their own object, so no |
| - ordering of box and console rolls applies one unsafely. The dev-box check |
| - across a mixed-version roll is still owed (see "Real-box checks"). |
| - |
| -Task 10's "a grant consumed while the row is moving or landing" is closed |
| -by 076bd9c0 (final review round 1), pending merge. |
| + A future feature, not scheduled. A box with host claims on declines the |
| + flag. |
| +- **MOVED to #250.** Leftover `consumed/` files refuse the claims census. |
| + Root's request record `/opt/carlos/consumed/<host>` and its fence lock |
| + `.<host>.lock` outlive the installation, and the empty-installation census |
| + refuses both. It also refuses another host's record, which is an |
| + unclassified file. platform-1e owns it now, under Plan 14. Root's |
| + retirement removes the consumed file, its lock and the record atomically, |
| + so a re-install passes the empty-installation census. A runner test covers |
| + install, retire and re-install. It is owed before host claims are switched |
| + on, and does not block the flag. |
| ### Roll notes from zdd-flag-readiness |
diff --git a/internal/activator/activator.go b/internal/activator/activator.go| index 6824ac70a..3336a5ca9 100644 |
| --- a/internal/activator/activator.go |
| +++ b/internal/activator/activator.go |
| @@ -2302,8 +2302,11 @@ func (a *Service) doWake(ctx context.Context, e *actEntry) (*wokenInstance, erro |
| } |
| } |
| if ib != nil { |
| - var idErr error |
| - id, w.pend, idErr = a.settleWakeIdentity(ctx, ib, e, rt, held, heldID) |
| + var ( |
| + idErr error |
| + wctx context.Context |
| + ) |
| + id, w.pend, wctx, idErr = a.settleWakeIdentity(ctx, ib, e, rt, held, heldID) |
| if idErr != nil { |
| w.stopOutcome.leaseReleased = a.giveBack(ctx, host, lease) |
| return w, fmt.Errorf("start %s: %w", host, idErr) |
| @@ -2313,7 +2316,9 @@ func (a *Service) doWake(ctx context.Context, e *actEntry) (*wokenInstance, erro |
| // R-EXEC-NOVERSION-2): today's live-link wake, nothing recorded. |
| ib = nil |
| } else { |
| - ctx = withRecordedBuild(ctx, id.Version) |
| + // The record is pending on wctx, written at the first exec |
| + // (amadan #272). |
| + ctx = wctx |
| } |
| } |
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go| index 8ba19b75d..29273339f 100644 |
| --- a/internal/activator/backend_exec.go |
| +++ b/internal/activator/backend_exec.go |
| @@ -1041,7 +1041,7 @@ func (b *ExecBackend) seedChild(ctx context.Context, rt registry.Route, exe stri |
| // wedged seed would hold the lease until the wake's own 5m budget expired. |
| cmd.WaitDelay = seedDrainGrace |
| return cmd |
| - }, nil) |
| + }, pendingRecordHook(ctx)) // the wake's record, at the exec (amadan #272) |
| spawned := err == nil |
| if spawned { |
| err = cmd.Wait() |
| @@ -1496,6 +1496,19 @@ func (b *ExecBackend) seedUnit(ctx context.Context, rt registry.Route, exe *inst |
| if err := recordSeedObligation(host, dbPath, seedObligation{ownsDB: seedOwnsDB, layout: LayoutNested}); err != nil { |
| return err |
| } |
| + // Then the wake's pending record (amadan #272), the last step before |
| + // root is asked, as the flat seed's comes at its exec after its |
| + // obligation (Astra review of zdd-owed-items, P2): an obligation that |
| + // cannot be written records nothing. A record that fails, or a recheck |
| + // that refuses after it, runs no seed, so the obligation just written |
| + // is discharged and the database, which no seed touched, is kept. |
| + if _, err := runPendingRecord(ctx); err != nil { |
| + cause := fmt.Errorf("seed for %s: %w", host, err) |
| + if derr := dischargeSeedObligation(dbPath, false); derr != nil { |
| + return fmt.Errorf("%w; and %w", cause, derr) |
| + } |
| + return cause |
| + } |
| log.Printf("activator: seeding %s: %s", host, strings.Join(argv, " ")) |
| // The client already bounds a seed call at instancectl.DeadlineSeed. |
diff --git a/internal/activator/beforeexec.go b/internal/activator/beforeexec.go| index 81a7366b5..4b7219665 100644 |
| --- a/internal/activator/beforeexec.go |
| +++ b/internal/activator/beforeexec.go |
| @@ -5,6 +5,7 @@ import ( |
| "errors" |
| "fmt" |
| "log" |
| + "sync" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| @@ -28,10 +29,13 @@ var errBuildStateUnwritten = errors.New("the build state could not be written, s |
| // those carry startWorker's exec-time hook on a worker spawn's context, and |
| // that hook is one of recordBuild's callers. |
| // |
| -// A wake that already recorded this build (withRecordedBuild) is not |
| -// written twice. A build with no name (an executable outside the version |
| -// store) is refused while the hook is set: no build state could name it |
| -// (ruling R-EXEC-NOVERSION). |
| +// A cold wake's own record is pending until its first exec |
| +// (settleWakeIdentity, amadan #272): a call for that build writes nothing |
| +// here, and the exec seam writes it (runPendingRecord), after every refusal |
| +// the backend makes. A call for another build writes now and supersedes |
| +// it, so the exec seam never writes the wake's build over this one. A build |
| +// with no name (an executable outside the version store) is refused while |
| +// the hook is set: no build state could name it (ruling R-EXEC-NOVERSION). |
| // |
| // With host claims off the lease is the hibernate lease, so a context that |
| // names no location gets that one (Config.Bucket, HibernatePrefix), and the |
| @@ -42,7 +46,7 @@ func (a *Service) recordBuild(ctx context.Context, host, build string) error { |
| if a.BeforeExec == nil { |
| return nil |
| } |
| - if build != "" && recordedBuild(ctx) == build { |
| + if p := pendingRecordFrom(ctx); p != nil && p.defers(build) { |
| return nil |
| } |
| if build == "" { |
| @@ -96,8 +100,9 @@ func (a *Service) claimedBuild(host string, s ClaimStart) error { |
| } |
| // settleWakeIdentity is doWake's step 2b (settings spec §4.2, Astra I1 |
| -// #5): the one file this wake runs, its seed included, chosen and recorded |
| -// under the wake's lease after the restore and before anything execs. |
| +// #5): the one file this wake runs, its seed included, chosen under the |
| +// wake's lease after the restore and before anything execs. Its record is |
| +// made pending on the context it returns, and written at the first exec. |
| // |
| // - A planned wake runs its plan's boot build, and a pinned wake (a hold |
| // swap's) its pin. Step 0.8 published both before the lease. |
| @@ -131,9 +136,21 @@ func (a *Service) claimedBuild(host string, s ClaimStart) error { |
| // identity with no file comes back, nothing is published or recorded, |
| // and the caller takes the live-link calls in every layout. |
| // |
| -// Then the record (recordBuild). Any error means nothing was seeded or |
| -// spawned; the caller releases the lease. |
| -func (a *Service) settleWakeIdentity(ctx context.Context, ib identityBackend, e *actEntry, rt registry.Route, held string, heldID *release.StagedIdentity) (release.StagedIdentity, pendingGeneration, error) { |
| +// The record is not written here (amadan #272). The context returned |
| +// carries it pending (withPendingRecord), and the seed's or the spawn's |
| +// exec seam writes it (runPendingRecord): after the backend's own refusals |
| +// (the replica-key gate, and the cgroup's: a killed cgroup kept, a |
| +// populated legacy parent, a limit not applied, a placement that failed), |
| +// immediately before the process execs. A launch refused before its exec |
| +// leaves no record naming a build that never ran, and completes no request |
| +// (settings spec §4.2: the record is what completes a request, at |
| +// execution). A recovery plan's boot (the good build in place of the |
| +// installed one the failure book blocks) is recorded with the planner's |
| +// selection (ruling R-PLANNED-RECOVERY), so the agent's revalidation admits |
| +// it before root's consumed file names the installed build failed. With no |
| +// hook nothing is pending, as nothing is recorded. Any error means nothing |
| +// was seeded or spawned; the caller releases the lease. |
| +func (a *Service) settleWakeIdentity(ctx context.Context, ib identityBackend, e *actEntry, rt registry.Route, held string, heldID *release.StagedIdentity) (release.StagedIdentity, pendingGeneration, context.Context, error) { |
| a.mu.Lock() |
| pin, plan := e.pin, e.plan |
| a.mu.Unlock() |
| @@ -152,24 +169,24 @@ func (a *Service) settleWakeIdentity(ctx context.Context, ib identityBackend, e |
| default: |
| var err error |
| if id, err = ib.LiveIdentity(ctx, rt); err != nil { |
| - return release.StagedIdentity{}, 0, err |
| + return release.StagedIdentity{}, 0, ctx, err |
| } |
| captured = true |
| } |
| if !heldIdentityOK(held, id, plan, rt) { |
| log.Printf("activator: %s not woken: it is held on %s, and this wake would run %q (the live link moved after the check); the next wake checks again", rt.Host, held, id.Version) |
| - return release.StagedIdentity{}, 0, fmt.Errorf("%w: %s would run %q, wants %s", errStaleLink, rt.Host, id.Version, held) |
| + return release.StagedIdentity{}, 0, ctx, fmt.Errorf("%w: %s would run %q, wants %s", errStaleLink, rt.Host, id.Version, held) |
| } |
| if captured { |
| if id.Version == "" { |
| - return release.StagedIdentity{}, 0, a.recordBuild(ctx, rt.Host, "") |
| + return release.StagedIdentity{}, 0, ctx, a.recordBuild(ctx, rt.Host, "") |
| } |
| // A declined box's exec-time gate, asked about the file this wake |
| // will run, before it is published or recorded (askLiveExecGate): |
| // the spawn below runs it by path, past the backend's live-link |
| // check. Its refusal comes back as it is, as the backend's would. |
| if err := a.askLiveExecGate(rt, id.Version); err != nil { |
| - return release.StagedIdentity{}, 0, err |
| + return release.StagedIdentity{}, 0, ctx, err |
| } |
| pend = a.addPendingGeneration(rt.Host, id) |
| if err := a.persistGenerations(); err != nil { |
| @@ -177,35 +194,144 @@ func (a *Service) settleWakeIdentity(ctx context.Context, ib identityBackend, e |
| if e.worker { |
| // A worker's wake reads as its exec-time refusal always has |
| // (startWorker's hook, final review F9). |
| - return id, pend, fmt.Errorf("%w: %v", ErrBuildNotPublished, err) |
| + return id, pend, ctx, fmt.Errorf("%w: %v", ErrBuildNotPublished, err) |
| } |
| - return id, pend, errStagedBuildNotPublished |
| + return id, pend, ctx, errStagedBuildNotPublished |
| } |
| } |
| - // A recovery plan's boot (the good build in place of the installed one |
| - // the failure book blocks) is recorded with the planner's selection |
| - // (ruling R-PLANNED-RECOVERY), so the agent's revalidation admits it |
| - // before root's consumed file names the installed build failed. |
| + if a.BeforeExec == nil { |
| + return id, pend, ctx, nil |
| + } |
| rctx := ctx |
| if plan.planned() { |
| rctx = withRecoverySelection(ctx, plan, rt, id.Version) |
| } |
| - if err := a.recordBuild(rctx, rt.Host, id.Version); err != nil { |
| - return id, pend, err |
| + build := id.Version |
| + write := func() error { return a.recordBuild(rctx, rt.Host, build) } |
| + // The wake's own admission, asked again once the write is done (Astra |
| + // review of zdd-owed-items, P1): the bucket write takes time, and a |
| + // fence raised or the wake's context ended meanwhile must start |
| + // nothing, as doWake's own check before the spawn refuses it. |
| + recheck := func() error { |
| + if ctx.Err() != nil || !a.wakeAllowed(rt.Host) { |
| + return ErrFenced |
| + } |
| + return nil |
| } |
| - return id, pend, nil |
| + return id, pend, withPendingRecord(ctx, build, write, recheck), nil |
| } |
| -type recordedBuildKey struct{} |
| +// pendingRecord is one wake's record of build, written at most once, at |
| +// the first exec that asks for it (runPendingRecord). |
| +// |
| +// The write is a bucket PUT, and the lease can be lost, a fence raised or |
| +// an attempt superseded while it runs. So every exec that consults the |
| +// record asks recheck (the wake's own admission: its context and the |
| +// maintenance fence) after it, and an exec seam that has an exec gate asks |
| +// that gate again once the write has run (runExecGate, launchGateWith). |
| +// Either refusal starts nothing. |
| +// |
| +// A refusal there leaves the record naming a build that did not run. It is |
| +// not undone. An undo would be a second bucket write, made after the lease |
| +// may already be lost, so it could land under another holder's lease; the |
| +// object's CAS would stop it overwriting that holder's record, but writing |
| +// outside the lease is exactly what the build state's single-writer rule |
| +// (ruling R-BS-UPDATE) forbids. The window is the one main had before |
| +// amadan #272, where step 2b wrote the record and doWake then checked the |
| +// fence: the record names a build that passed every refusal of its own and |
| +// was about to run, the next wake records again, and a request it completed |
| +// was the request for that build. |
| +type pendingRecord struct { |
| + build string |
| + write func() error |
| + recheck func() error |
| -// withRecordedBuild marks ctx as carrying a wake that has already recorded |
| -// build (doWake's step 2b records once, before its seed and its spawn). |
| -func withRecordedBuild(ctx context.Context, build string) context.Context { |
| - return context.WithValue(ctx, recordedBuildKey{}, build) |
| + mu sync.Mutex |
| + // done is set once the write ran; err is its answer, which every later |
| + // exec of the wake gets too. |
| + done bool |
| + err error |
| + // superseded is set once a record of another build was written in this |
| + // wake (recordBuild): the pending one would name the wrong build. |
| + superseded bool |
| } |
| -// recordedBuild is the build ctx's wake already recorded, or "". |
| -func recordedBuild(ctx context.Context) string { |
| - s, _ := ctx.Value(recordedBuildKey{}).(string) |
| - return s |
| +type pendingWakeKey struct{} |
| + |
| +// withPendingRecord returns ctx carrying build's pending record, written by |
| +// write and followed by recheck (nil asks nothing) at every exec. |
| +func withPendingRecord(ctx context.Context, build string, write, recheck func() error) context.Context { |
| + return context.WithValue(ctx, pendingWakeKey{}, &pendingRecord{build: build, write: write, recheck: recheck}) |
| +} |
| + |
| +func pendingRecordFrom(ctx context.Context) *pendingRecord { |
| + p, _ := ctx.Value(pendingWakeKey{}).(*pendingRecord) |
| + return p |
| +} |
| + |
| +// defers reports whether a record of build is the pending one's to write |
| +// (recordBuild then writes nothing). A record of another build supersedes |
| +// it, and once it is superseded nothing defers to it. |
| +func (p *pendingRecord) defers(build string) bool { |
| + if build == "" { |
| + return false |
| + } |
| + p.mu.Lock() |
| + defer p.mu.Unlock() |
| + if build != p.build { |
| + p.superseded = true |
| + return false |
| + } |
| + return !p.superseded |
| +} |
| + |
| +// run writes the record once, then asks recheck. wrote reports that this |
| +// call made the write. A superseded record writes and asks nothing. |
| +func (p *pendingRecord) run() (wrote bool, err error) { |
| + p.mu.Lock() |
| + if p.superseded { |
| + p.mu.Unlock() |
| + return false, nil |
| + } |
| + if !p.done { |
| + p.err, p.done, wrote = p.write(), true, true |
| + } |
| + err = p.err |
| + p.mu.Unlock() |
| + if err != nil { |
| + return wrote, err |
| + } |
| + if p.recheck != nil { |
| + if err := p.recheck(); err != nil { |
| + return wrote, err |
| + } |
| + } |
| + return wrote, nil |
| +} |
| + |
| +// runPendingRecord writes ctx's pending wake record, if it carries one that |
| +// is still due. Every exec seam a cold wake reaches asks it immediately |
| +// before its exec: the flat seed's and spawn's cgroup before-start hook, |
| +// a worker's, and, nested, right before root is asked to run the seed or |
| +// the run unit. An error refuses that exec, with nothing run. wrote |
| +// reports that this call made the write, so a seam with an exec gate asks |
| +// it again. |
| +func runPendingRecord(ctx context.Context) (wrote bool, err error) { |
| + if p := pendingRecordFrom(ctx); p != nil { |
| + return p.run() |
| + } |
| + return false, nil |
| +} |
| + |
| +// pendingRecordHook is runPendingRecord as a cgroup before-start hook for |
| +// a spawn that has no other: nil when ctx carries no pending record, so |
| +// that spawn is exactly as it was. |
| +func pendingRecordHook(ctx context.Context) func() error { |
| + if pendingRecordFrom(ctx) == nil { |
| + return nil |
| + } |
| + return func() error { |
| + _, err := runPendingRecord(ctx) |
| + return err |
| + } |
| } |
diff --git a/internal/activator/beforeexec_test.go b/internal/activator/beforeexec_test.go| index 239bf27c9..6c90b47a6 100644 |
| --- a/internal/activator/beforeexec_test.go |
| +++ b/internal/activator/beforeexec_test.go |
| @@ -9,6 +9,8 @@ import ( |
| "sync" |
| "testing" |
| "time" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| ) |
| // execLog is a BeforeExec hook that records host@build, the backend's spawn |
| @@ -102,22 +104,65 @@ func TestTheHookIsToldWhereTheLeaseIs(t *testing.T) { |
| } |
| } |
| -// A wake that already recorded this build (Task 5b) is not written twice; |
| -// another build is. |
| +// A wake's own record (Task 5b) is pending until its first exec (amadan |
| +// #272): a call for that build writes nothing, the exec seam writes it |
| +// once, and another build is written at once. A record of another build |
| +// supersedes the pending one, which the exec seam then never writes. |
| func TestARecordedBuildIsNotWrittenTwice(t *testing.T) { |
| + rt := registry.Route{Host: "one.example.com"} |
| a := &Service{cfg: Config{Bucket: "b"}} |
| var l execLog |
| a.BeforeExec = l.hook |
| - ctx := withRecordedBuild(context.Background(), "v1") |
| - if err := a.recordBuild(ctx, "one.example.com", "v1"); err != nil { |
| + ctx := wakeRecord(a, rt.Host, "v1") |
| + if err := a.recordBuild(ctx, rt.Host, "v1"); err != nil { |
| t.Fatal(err) |
| } |
| - if err := a.recordBuild(ctx, "one.example.com", "v2"); err != nil { |
| + if calls, _, _ := l.seen(); len(calls) != 0 { |
| + t.Fatalf("calls = %v before any exec, want none", calls) |
| + } |
| + for range 2 { |
| + if _, err := runPendingRecord(ctx); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := a.recordBuild(ctx, rt.Host, "v2"); err != nil { |
| t.Fatal(err) |
| } |
| - if calls, _, _ := l.seen(); len(calls) != 1 || calls[0] != "one.example.com@v2" { |
| - t.Fatalf("calls = %v, want only the fallback's v2", calls) |
| + if calls, _, _ := l.seen(); !slices.Equal(calls, []string{"one.example.com@v1", "one.example.com@v2"}) { |
| + t.Fatalf("calls = %v, want v1 once at the exec, then the fallback's v2", calls) |
| } |
| + |
| + var s execLog |
| + a.BeforeExec = s.hook |
| + ctx = wakeRecord(a, rt.Host, "v1") |
| + if err := a.recordBuild(ctx, rt.Host, "v2"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := runPendingRecord(ctx); err != nil { |
| + t.Fatal(err) |
| + } |
| + if calls, _, _ := s.seen(); !slices.Equal(calls, []string{"one.example.com@v2"}) { |
| + t.Fatalf("calls = %v, want only v2: the superseded v1 is never written", calls) |
| + } |
| + |
| + // A write that fails refuses every exec of the wake, and is not retried. |
| + f := execLog{fail: failAll} |
| + a.BeforeExec = f.hook |
| + ctx = wakeRecord(a, rt.Host, "v1") |
| + for range 2 { |
| + if _, err := runPendingRecord(ctx); !errors.Is(err, errBuildStateUnwritten) { |
| + t.Fatalf("err = %v, want errBuildStateUnwritten", err) |
| + } |
| + } |
| + if calls, _, _ := f.seen(); len(calls) != 1 { |
| + t.Fatalf("calls = %v, want one attempt", calls) |
| + } |
| +} |
| + |
| +// wakeRecord is the context settleWakeIdentity returns for a wake of host |
| +// that settled build: its record pending. |
| +func wakeRecord(a *Service, host, build string) context.Context { |
| + return withPendingRecord(context.Background(), build, func() error { return a.recordBuild(context.Background(), host, build) }, nil) |
| } |
| // Ruling R-WAKEWORDS: a member never sees the internal text. |
diff --git a/internal/activator/genbackend.go b/internal/activator/genbackend.go| index 06fcc0b21..4daef066e 100644 |
| --- a/internal/activator/genbackend.go |
| +++ b/internal/activator/genbackend.go |
| @@ -84,11 +84,36 @@ func withExecGate(ctx context.Context, gate func() error) context.Context { |
| return context.WithValue(ctx, execGateKey{}, gate) |
| } |
| -// runExecGate asks ctx's exec gate, if it carries one. A spawn with a gate |
| -// also honours its context there: one that has ended starts nothing. With |
| -// no gate (every live-link spawn, the flag off) it is a no-op, so those |
| +// runExecGate asks ctx's exec gate, if it carries one, then writes ctx's |
| +// pending wake record, if it carries one (runPendingRecord, amadan #272), |
| +// and, once that write has run, asks the gate again (Astra review of |
| +// zdd-owed-items, P1): the lease, a fence or the attempt can change during |
| +// the bucket write, and the exec follows the second answer. A spawn with a |
| +// gate also honours its context there: one that has ended starts nothing. |
| +// With neither (every live-link spawn, the flag off) it is a no-op, so those |
| // spawns are exactly as they were. |
| func runExecGate(ctx context.Context) error { |
| + if err := askExecGate(ctx); err != nil { |
| + return err |
| + } |
| + wrote, err := runPendingRecord(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + if wrote { |
| + return askExecGate(ctx) |
| + } |
| + return nil |
| +} |
| + |
| +// RunExecGate is runExecGate for a Backend implemented outside this |
| +// package: such a backend asks it immediately before its exec, as |
| +// ExecBackend does, so the caller's gate and the wake's pending record |
| +// (amadan #272) are honoured there. |
| +func RunExecGate(ctx context.Context) error { return runExecGate(ctx) } |
| + |
| +// askExecGate is runExecGate's gate alone. |
| +func askExecGate(ctx context.Context) error { |
| gate, ok := ctx.Value(execGateKey{}).(func() error) |
| if !ok || gate == nil { |
| return nil |
| @@ -103,25 +128,43 @@ func runExecGate(ctx context.Context) error { |
| // worker's) before-start hook for the cgroup |
| // manager (final review F1): launchHook (the test seam), then ctx's exec |
| // gate, asked immediately before each start attempt, after the cgroup's |
| -// preparation. Nil when ctx carries no gate and no test seam is set, so a |
| -// live-link spawn hands the manager no hook and starts exactly as it did. |
| +// preparation, then ctx's pending wake record (runPendingRecord), and the |
| +// gate again once that write has run. Nil |
| +// when ctx carries no gate and no pending record and no test seam is set, so |
| +// a live-link spawn hands the manager no hook and starts exactly as it did. |
| func (b *ExecBackend) launchGate(ctx context.Context, host string) func() error { |
| return b.launchGateWith(ctx, host, nil) |
| } |
| // launchGateWith is launchGate with live, a live-link spawn's LiveExecGate |
| // check for the build it resolved (Task 9), asked after ctx's exec gate at |
| -// the same point. A nil live is launchGate exactly, so a spawn on a box with |
| -// no LiveExecGate hands the manager the hook it always did. |
| +// the same point, before the pending record and again after its write. A |
| +// nil live is launchGate |
| +// exactly, so a spawn on a box with no LiveExecGate hands the manager the |
| +// hook it always did. |
| func (b *ExecBackend) launchGateWith(ctx context.Context, host string, live func() error) func() error { |
| - if _, ok := ctx.Value(execGateKey{}).(func() error); !ok && b.launchHook == nil && live == nil { |
| + if _, ok := ctx.Value(execGateKey{}).(func() error); !ok && b.launchHook == nil && live == nil && pendingRecordFrom(ctx) == nil { |
| return nil |
| } |
| return func() error { |
| if b.launchHook != nil { |
| b.launchHook(host) |
| } |
| - if err := runExecGate(ctx); err != nil { |
| + if err := askExecGate(ctx); err != nil { |
| + return err |
| + } |
| + if live != nil { |
| + if err := live(); err != nil { |
| + return err |
| + } |
| + } |
| + wrote, err := runPendingRecord(ctx) |
| + if err != nil || !wrote { |
| + return err |
| + } |
| + // The write took time: the gate and the live check are asked again |
| + // immediately before the exec (Astra review of zdd-owed-items, P1). |
| + if err := askExecGate(ctx); err != nil { |
| return err |
| } |
| if live != nil { |
diff --git a/internal/activator/wakeidentity_test.go b/internal/activator/wakeidentity_test.go| index b0affa772..0b220a689 100644 |
| --- a/internal/activator/wakeidentity_test.go |
| +++ b/internal/activator/wakeidentity_test.go |
| @@ -125,7 +125,12 @@ func (b *fakeBackend) LiveIdentity(_ context.Context, rt registry.Route) (releas |
| return fakeIdentity(b.ident.live), nil |
| } |
| -func (b *fakeBackend) StartGenExe(_ context.Context, rt registry.Route, n uint64, perGen bool, id release.StagedIdentity) (Proc, Gen, error) { |
| +func (b *fakeBackend) StartGenExe(ctx context.Context, rt registry.Route, n uint64, perGen bool, id release.StagedIdentity) (Proc, Gen, error) { |
| + // The exec seam, as the real backend's: the exec gate and the wake's |
| + // pending record (amadan #272), before anything starts. |
| + if err := runExecGate(ctx); err != nil { |
| + return nil, Gen{}, err |
| + } |
| b.mu.Lock() |
| defer b.mu.Unlock() |
| b.starts++ |
| @@ -147,7 +152,10 @@ func (b *fakeBackend) StartGenExe(_ context.Context, rt registry.Route, n uint64 |
| return proc, g, nil |
| } |
| -func (b *fakeBackend) SeedExe(_ context.Context, rt registry.Route, id release.StagedIdentity) error { |
| +func (b *fakeBackend) SeedExe(ctx context.Context, rt registry.Route, id release.StagedIdentity) error { |
| + if _, err := runPendingRecord(ctx); err != nil { |
| + return err |
| + } |
| b.mu.Lock() |
| defer b.mu.Unlock() |
| b.seeds++ |
| @@ -399,8 +407,9 @@ func TestAReplanBeforeTheSeedSeedsAndBootsOneBuild(t *testing.T) { |
| } |
| // D6: a replan that only the exec's own selection gate makes (after step |
| -// 2b recorded the first plan's boot) is recorded again by startPinnedAt |
| -// before it execs. |
| +// 2b settled the first plan's boot) is recorded by startPinnedAt before it |
| +// execs. The first plan's boot never ran, so its pending record is never |
| +// written (amadan #272). |
| func TestAReplanAfterTheRecordRecordsTheNewBuild(t *testing.T) { |
| dir := t.TempDir() |
| stale, next := stagedID(t, dir, "v2"), stagedID(t, dir, "v3") |
| @@ -415,8 +424,8 @@ func TestAReplanAfterTheRecordRecordsTheNewBuild(t *testing.T) { |
| } |
| done() |
| calls, at, _ := l.seen() |
| - if !slices.Equal(calls, []string{"one.example.com@v2", "one.example.com@v3"}) || at[1] != 0 { |
| - t.Fatalf("records %v at spawn counts %v, want v2 at the wake, then v3 before the replanned exec", calls, at) |
| + if !slices.Equal(calls, []string{"one.example.com@v3"}) || at[0] != 0 { |
| + t.Fatalf("records %v at spawn counts %v, want only v3, before the replanned exec", calls, at) |
| } |
| if gens := b.started(); len(gens) != 1 || gens[0].Build != "v3" { |
| t.Fatalf("spawned %+v, want the replanned v3", gens) |
| @@ -612,8 +621,10 @@ func TestARefusingHookStartsNothing(t *testing.T) { |
| if err := sa.WakeWorker(alwaysOnWorkerRoute("bot.x.internal")); !errors.Is(err, errBuildStateUnwritten) { |
| t.Fatalf("WakeWorker = %v, want errBuildStateUnwritten", err) |
| } |
| - if n := len(sb.calls()); n != 0 || sb.workerStarts() != 0 { |
| - t.Fatalf("%d staged worker spawns, %d children after a refused record", n, sb.workerStarts()) |
| + // The record is written at the worker's exec (amadan #272), so the |
| + // spawn is entered and refused there: no child. |
| + if sb.workerStarts() != 0 { |
| + t.Fatalf("%d children after a refused record", sb.workerStarts()) |
| } |
| next := stagedID(t, t.TempDir(), "v3") |
| @@ -925,7 +936,8 @@ func TestARefusedSidecarRecordStartsNoSidecar(t *testing.T) { |
| } |
| // At the wake: a replanned build is recorded again by its sidecar (the |
| - // wake's context names the first plan's), and that record is refused. |
| + // wake's context names the first plan's, which never ran and so is never |
| + // written), and that record is refused. |
| dir := t.TempDir() |
| stale, next := stagedID(t, dir, "v2"), stagedID(t, dir, "v3") |
| pa, pb, _ := planActivator(t, WakePlan{}) |
| @@ -933,15 +945,15 @@ func TestARefusedSidecarRecordStartsNoSidecar(t *testing.T) { |
| pb.sidecarStaged = true |
| pa.Planner = laterReplan(stale, next) |
| var n atomic.Int32 |
| - pl := &execLog{fail: func(string) bool { return n.Add(1) == 3 }} |
| + pl := &execLog{fail: func(string) bool { return n.Add(1) == 2 }} |
| pa.BeforeExec = pl.hook |
| done, err = pa.beginRequest(context.Background(), identityRoute(t, host)) |
| if err != nil { |
| t.Fatalf("the wake failed over its sidecar's record: %v", err) |
| } |
| done() |
| - if calls, _, _ := pl.seen(); !slices.Equal(calls, []string{host + "@v2", host + "@v3", host + "@v3"}) { |
| - t.Fatalf("records %v, want the wake's, the replanned spawn's and the sidecar's", calls) |
| + if calls, _, _ := pl.seen(); !slices.Equal(calls, []string{host + "@v3", host + "@v3"}) { |
| + t.Fatalf("records %v, want the replanned spawn's and the sidecar's", calls) |
| } |
| if sc := pb.sidecarsOn(); len(sc) != 0 || pb.sidecarStartCount() != 0 { |
| t.Fatalf("sidecars %+v (%d live): a sidecar ran unrecorded", sc, pb.sidecarStartCount()) |
diff --git a/internal/activator/wakerecord_recheck_test.go b/internal/activator/wakerecord_recheck_test.go| new file mode 100644 |
| index 000000000..a221e7c25 |
| --- /dev/null |
| +++ b/internal/activator/wakerecord_recheck_test.go |
| @@ -0,0 +1,168 @@ |
| +//go:build linux |
| + |
| +package activator |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "slices" |
| + "sync/atomic" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +// Astra's review of zdd-owed-items. P1: the pending record is a bucket |
| +// write, and the lease, a fence or the attempt can change while it runs, so |
| +// the exec is decided again after it. P2: a nested seed writes its |
| +// obligation before its record, so an obligation that fails records |
| +// nothing. |
| + |
| +// P1, the exec gate: a gate that passes before the write and refuses after |
| +// it (the attempt superseded during the PUT) starts nothing, at both seams |
| +// that carry a gate. |
| +func TestAnExecGateRefusingDuringTheRecordStartsNothing(t *testing.T) { |
| + for _, seam := range []string{"runExecGate", "launchGate"} { |
| + t.Run(seam, func(t *testing.T) { |
| + var superseded atomic.Bool |
| + superseding := errors.New("attempt superseded") |
| + gate := func() error { |
| + if superseded.Load() { |
| + return superseding |
| + } |
| + return nil |
| + } |
| + writes := 0 |
| + write := func() error { writes++; superseded.Store(true); return nil } |
| + ctx := withPendingRecord(withExecGate(context.Background(), gate), "v1", write, nil) |
| + var err error |
| + if seam == "runExecGate" { |
| + err = runExecGate(ctx) |
| + } else { |
| + err = (&ExecBackend{}).launchGate(ctx, "one.example.com")() |
| + } |
| + if !errors.Is(err, superseding) { |
| + t.Fatalf("%s = %v, want the gate's refusal after the record", seam, err) |
| + } |
| + if writes != 1 { |
| + t.Fatalf("%d writes, want 1", writes) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// P1, the fence: a maintenance fence raised while a cold wake's record is |
| +// being written starts nothing, at the seed and at the spawn. |
| +func TestAFenceRaisedDuringTheRecordStartsNothing(t *testing.T) { |
| + for _, seeded := range []bool{false, true} { |
| + name := "spawn" |
| + if seeded { |
| + name = "seed" |
| + } |
| + t.Run(name, func(t *testing.T) { |
| + a, b := identityActivator(t, "v1") |
| + b.restoreFresh = seeded |
| + rt := identityRoute(t, "one.example.com") |
| + if seeded { |
| + rt.Seed = `["seed"]` |
| + } |
| + l := &execLog{} |
| + a.BeforeExec = func(ctx context.Context, host, build string) error { |
| + a.mu.Lock() |
| + if a.fences == nil { |
| + a.fences = map[string]uint64{} |
| + } |
| + a.fences[host] = 7 // a maintenance fence lands mid-write |
| + a.mu.Unlock() |
| + return l.hook(ctx, host, build) |
| + } |
| + _, err := a.beginRequest(context.Background(), rt) |
| + if !errors.Is(err, ErrFenced) { |
| + t.Fatalf("wake = %v, want ErrFenced", err) |
| + } |
| + if b.startCount() != 0 || b.seedCount() != 0 { |
| + t.Fatalf("started %d, seeded %d after a fence raised during the record", b.startCount(), b.seedCount()) |
| + } |
| + if calls, _, _ := l.seen(); !slices.Equal(calls, []string{"one.example.com@v1"}) { |
| + t.Fatalf("records %v, want the one write that ran", calls) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// P2: a nested seed whose obligation cannot be written records nothing and |
| +// asks root for nothing; a nested seed whose record fails discharges the |
| +// obligation it wrote, keeps the database, and asks root for nothing. |
| +func TestANestedSeedWritesItsObligationBeforeItsRecord(t *testing.T) { |
| + seedOnce := func(t *testing.T, plant func(rt registry.Route), fail bool) (*fakeControl, registry.Route, []string, error) { |
| + t.Helper() |
| + ctl := &fakeControl{seed: instancectl.SeedResponse{InvocationID: "s1", Result: "success", Status: "0", Code: "exited"}} |
| + b, rt := nestedBackend(t, ctl, &fakeUnit{}) |
| + b.Control = identityControl{ctl} |
| + rt.Seed = `["seed"]` |
| + stageVersion(t, b, rt.Host, "vA", "exit 0\n") |
| + id, err := b.LiveIdentity(context.Background(), rt) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.MkdirAll(filepath.Dir(rt.DBPath), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| + plant(rt) |
| + var records []string |
| + write := func() error { |
| + records = append(records, id.Version) |
| + if fail { |
| + return errBuildStateUnwritten |
| + } |
| + return nil |
| + } |
| + ctx := withPendingRecord(context.Background(), id.Version, write, nil) |
| + err = b.SeedExe(ctx, rt, id) |
| + return ctl, rt, records, err |
| + } |
| + |
| + t.Run("the obligation fails", func(t *testing.T) { |
| + ctl, _, recs, err := seedOnce(t, func(rt registry.Route) { |
| + // An earlier seed's unanswered obligation refuses this one. |
| + if err := os.WriteFile(rt.DBPath+seedUnprovenSuffix, []byte("{}"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + }, false) |
| + if !errors.Is(err, ErrSeedUnproven) { |
| + t.Fatalf("SeedExe = %v, want the obligation's refusal", err) |
| + } |
| + if len(recs) != 0 { |
| + t.Fatalf("records %v, want none: no seed ran", recs) |
| + } |
| + if ctl.joined() != "" { |
| + t.Fatalf("root was asked:\n%s", ctl.joined()) |
| + } |
| + }) |
| + |
| + t.Run("the record fails", func(t *testing.T) { |
| + ctl, rt, recs, err := seedOnce(t, func(rt registry.Route) { |
| + if err := os.WriteFile(rt.DBPath, []byte("data"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + }, true) |
| + if !errors.Is(err, errBuildStateUnwritten) { |
| + t.Fatalf("SeedExe = %v, want the record's refusal", err) |
| + } |
| + if len(recs) != 1 { |
| + t.Fatalf("records %v, want one attempt", recs) |
| + } |
| + if _, err := os.Stat(rt.DBPath + seedUnprovenSuffix); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("the obligation stayed after a refused record: %v", err) |
| + } |
| + if raw, err := os.ReadFile(rt.DBPath); err != nil || string(raw) != "data" { |
| + t.Fatalf("the database was touched: %q, %v", raw, err) |
| + } |
| + if ctl.joined() != "" { |
| + t.Fatalf("root was asked:\n%s", ctl.joined()) |
| + } |
| + }) |
| +} |
diff --git a/internal/activator/wakerecord_refused_test.go b/internal/activator/wakerecord_refused_test.go| new file mode 100644 |
| index 000000000..224175449 |
| --- /dev/null |
| +++ b/internal/activator/wakerecord_refused_test.go |
| @@ -0,0 +1,137 @@ |
| +//go:build linux |
| + |
| +package activator |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "slices" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/cgroup" |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +// Amadan #272, owed before O8: a cold wake's build-state record names the |
| +// build about to run against the instance's database, and completes an |
| +// install request (settings spec §4.2). It used to be written at step 2b, |
| +// before the backend's own refusals: the replica-key gate and the cgroup's |
| +// (a populated legacy parent, a killed cgroup kept, a limit not applied). |
| +// A launch refused there ran nothing, yet left a record naming its build. |
| +// The record is now written at the exec, so a refused launch leaves none. |
| + |
| +// execIdentityBackend is fakeBackend for the lease and the restore, with a |
| +// real ExecBackend for the identity, the seed and the spawn: the exec seams |
| +// and the refusals under test are the production ones. |
| +type execIdentityBackend struct { |
| + *fakeBackend |
| + eb *ExecBackend |
| +} |
| + |
| +func (b *execIdentityBackend) identityFor(registry.Route) identityBackend { return b.eb } |
| + |
| +// stageScript makes body host's live build v1, as the adopt pass lays it |
| +// out (<opt>/versions/v1/app, linked from <opt>/live/<host>). |
| +func stageScript(t *testing.T, eb *ExecBackend, host, body string) { |
| + t.Helper() |
| + stageLive(t, eb, host, "v1") |
| + if err := os.WriteFile(filepath.Join(eb.OptDir, "versions", "v1", "app"), []byte(body), 0o755); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +// refusedWake runs one cold wake of rt over eb with a recording BeforeExec, |
| +// and returns the wake's error and the records written. |
| +func refusedWake(t *testing.T, eb *ExecBackend, rt registry.Route) (error, []string) { |
| + t.Helper() |
| + fb := &fakeBackend{restoreFresh: true} |
| + reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { reg.Close() }) |
| + a := New(Config{Bucket: "test-bucket", Idle: time.Hour}, &execIdentityBackend{fakeBackend: fb, eb: eb}, reg) |
| + l := &execLog{} |
| + a.BeforeExec = l.hook |
| + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) |
| + defer cancel() |
| + done, err := a.beginRequest(ctx, rt) |
| + if err == nil { |
| + done() |
| + } |
| + calls, _, _ := l.seen() |
| + return err, calls |
| +} |
| + |
| +func TestARefusedLaunchLeavesNoBuildStateRecord(t *testing.T) { |
| + const host = "shop.example.com" |
| + route := func(eb *ExecBackend) registry.Route { |
| + return registry.Route{Host: host, Kind: "instance", Hibernate: true, App: "app", |
| + Socket: noControlSocket(t), DBPath: filepath.Join(eb.DataDir, host+".db")} |
| + } |
| + |
| + t.Run("replica seed refused by the replica-key gate", func(t *testing.T) { |
| + eb := testExecBackend(t) |
| + stageScript(t, eb, host, "#!/bin/sh\nexit 0\n") |
| + rt := route(eb) |
| + rt.Replica, rt.Seed = true, `["seed"]` |
| + err, calls := refusedWake(t, eb, rt) |
| + if !errors.Is(err, ErrReplicaKey) { |
| + t.Fatalf("wake = %v, want the replica-key gate's refusal", err) |
| + } |
| + if len(calls) != 0 { |
| + t.Fatalf("records %v after a refused seed, want none", calls) |
| + } |
| + }) |
| + |
| + t.Run("replica spawn refused by the replica-key gate", func(t *testing.T) { |
| + eb := testExecBackend(t) |
| + stageScript(t, eb, host, "#!/bin/sh\nexit 0\n") |
| + rt := route(eb) |
| + rt.Replica = true |
| + err, calls := refusedWake(t, eb, rt) |
| + if !errors.Is(err, ErrReplicaKey) { |
| + t.Fatalf("wake = %v, want the replica-key gate's refusal", err) |
| + } |
| + if len(calls) != 0 { |
| + t.Fatalf("records %v after a refused spawn, want none", calls) |
| + } |
| + }) |
| + |
| + t.Run("seed refused beside a populated legacy parent", func(t *testing.T) { |
| + eb, parent := delegatedBackend(t, host) |
| + eb.GenContainment = true |
| + if err := os.WriteFile(filepath.Join(parent, "cgroup.procs"), []byte("4242\n"), 0o644); err != nil { |
| + t.Fatal(err) |
| + } |
| + stageScript(t, eb, host, "#!/bin/sh\nexit 0\n") |
| + rt := route(eb) |
| + rt.Seed = `["seed"]` |
| + err, calls := refusedWake(t, eb, rt) |
| + if !errors.Is(err, cgroup.ErrLegacyParentPopulated) { |
| + t.Fatalf("wake = %v, want the cgroup's refusal", err) |
| + } |
| + if len(calls) != 0 { |
| + t.Fatalf("records %v after a refused seed, want none", calls) |
| + } |
| + }) |
| + |
| + // The control: a seed that runs (and fails) did execute its build |
| + // against the database, so it is recorded, once, before it ran. |
| + t.Run("a seed that ran is recorded", func(t *testing.T) { |
| + eb := testExecBackend(t) |
| + stageScript(t, eb, host, "#!/bin/sh\nexit 1\n") |
| + rt := route(eb) |
| + rt.Seed = `["seed"]` |
| + err, calls := refusedWake(t, eb, rt) |
| + if err == nil { |
| + t.Fatal("a failing seed woke the instance") |
| + } |
| + if !slices.Equal(calls, []string{host + "@v1"}) { |
| + t.Fatalf("records %v, want v1 once", calls) |
| + } |
| + }) |
| +} |
diff --git a/internal/host/buildstate_planned_test.go b/internal/host/buildstate_planned_test.go| index 38eec7b52..13a29801c 100644 |
| --- a/internal/host/buildstate_planned_test.go |
| +++ b/internal/host/buildstate_planned_test.go |
| @@ -80,7 +80,12 @@ func (b *plannedBackend) StartWorker(context.Context, registry.Route, map[string |
| return nil, "", errors.New("no worker") |
| } |
| -func (b *plannedBackend) StartStaged(_ context.Context, _ registry.Route, g activator.Gen) (activator.Gen, activator.Proc, error) { |
| +func (b *plannedBackend) StartStaged(ctx context.Context, _ registry.Route, g activator.Gen) (activator.Gen, activator.Proc, error) { |
| + // The exec seam, as ExecBackend's: the caller's gate and the wake's |
| + // pending record (amadan #272), before anything starts. |
| + if err := activator.RunExecGate(ctx); err != nil { |
| + return g, nil, err |
| + } |
| b.mu.Lock() |
| defer b.mu.Unlock() |
| b.started = append(b.started, g.Build) |
diff --git a/internal/provision/plan14_legacy_identity_test.go b/internal/provision/plan14_legacy_identity_test.go| index 3e1653c03..9b5d3d422 100644 |
| --- a/internal/provision/plan14_legacy_identity_test.go |
| +++ b/internal/provision/plan14_legacy_identity_test.go |
| @@ -397,19 +397,19 @@ func TestPlan14Overlay292PreservesLegacySeal(t *testing.T) { |
| // Census ruling carlos-run (Plan 14 controller, 2026-10-14): the carlos-run |
| // branch's overlay (carlos run, #259), loaded last, after census ruling |
| -// 275. It keeps every earlier freeze, adds exactly 59 frozen declarations |
| +// owed. It keeps every earlier freeze, adds exactly 59 frozen declarations |
| // (the 58 it was ruled with, and type Server as census ruling 292 classed |
| // it), and every row it writes names the ruling. The two units that still exist and no longer |
| // read are retired under the ruling's name, in the form zddr3 used. |
| func TestPlan14CarlosRunOverlayPreservesLegacySeal(t *testing.T) { |
| root := censusRoot(t) |
| inv := rootReadInventory{Entries: map[string]classifiedRootRead{}, Coverage: map[string]string{}} |
| - for i, name := range []string{"plan14-root-reads.tsv", "plan14-root-reads-moves.tsv", "plan14-root-reads-merge.tsv", "plan14-root-reads-fix1.tsv", "plan14-root-reads-fix2.tsv", "plan14-root-reads-merge3.tsv", "plan14-root-reads-fix4.tsv", "plan14-root-reads-fix5.tsv", "plan14-root-reads-fix6.tsv", "plan14-root-reads-zddr3.tsv", "plan14-root-reads-hotfix281.tsv", "plan14-root-reads-zddr4.tsv", "plan14-root-reads-reserved285.tsv", "plan14-root-reads-292.tsv", "plan14-root-reads-census-ruling-275.tsv"} { |
| + for i, name := range []string{"plan14-root-reads.tsv", "plan14-root-reads-moves.tsv", "plan14-root-reads-merge.tsv", "plan14-root-reads-fix1.tsv", "plan14-root-reads-fix2.tsv", "plan14-root-reads-merge3.tsv", "plan14-root-reads-fix4.tsv", "plan14-root-reads-fix5.tsv", "plan14-root-reads-fix6.tsv", "plan14-root-reads-zddr3.tsv", "plan14-root-reads-hotfix281.tsv", "plan14-root-reads-zddr4.tsv", "plan14-root-reads-reserved285.tsv", "plan14-root-reads-292.tsv", "plan14-root-reads-census-ruling-275.tsv", "plan14-root-reads-census-ruling-owed.tsv"} { |
| if err := loadRootReadInventory(root+"/internal/provision/testdata/"+name, &inv, i > 0); err != nil { |
| t.Fatal(err) |
| } |
| } |
| - if errs := checkRootReadLegacy(inv, "3b2506dd2ab3986d0df0ceda7f596e5a46ea4d1dfbc17b1d0fd8d5f176e8038d"); len(errs) != 0 { |
| + if errs := checkRootReadLegacy(inv, "79db70c4c396457e563bf792027f98eeed7cef3dc54fff8abedb025e7db1dfbb"); len(errs) != 0 { |
| t.Fatal(errs) |
| } |
| original := make(map[string]bool, len(inv.Legacy)) |
| @@ -434,7 +434,7 @@ func TestPlan14CarlosRunOverlayPreservesLegacySeal(t *testing.T) { |
| // Census ruling carlos-run seal: 54 service-user and CLI rows, the four |
| // instance-control rows that extend the J4 seed pattern, and type |
| // Server re-digested as census ruling 292 classed it. |
| - const seal = "4dddd5fdda4b4a244cb23582458d2e377bd241d67586a180add03f940cfd1cd5" |
| + const seal = "96e521e60dfc0b55e2bc08adbb7a7682a607a30599a8cf66e7c5689d6cf7fe5a" |
| if errs := checkRootReadLegacy(inv, seal); len(errs) != 0 { |
| t.Fatal(errs) |
| } |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index 7a5d2a3ac..b721c8207 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -694,9 +694,21 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| if errs := checkRootReadLegacy(inv, seal); len(errs) != 0 { |
| t.Fatal(errs) |
| } |
| + if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv"), &inv, true); err != nil { |
| + t.Fatal(err) |
| + } |
| + // Census ruling owed (amadan #272, zdd-owed-items, platform-1e): a cold |
| + // wake's build-state record is written at its first exec, after the |
| + // replica-key gate and the cgroup refusals, and the exec is decided |
| + // again after the write. Four changed activator rows add four frozen |
| + // declarations. |
| + seal = "79db70c4c396457e563bf792027f98eeed7cef3dc54fff8abedb025e7db1dfbb" |
| + if errs := checkRootReadLegacy(inv, seal); len(errs) != 0 { |
| + t.Fatal(errs) |
| + } |
| // Census ruling carlos-run (Plan 14 controller, 2026-10-14): the |
| // carlos-run branch's overlay (carlos run, #259), loaded last, after |
| - // census ruling 275. It adds 59 frozen declarations: the 58 it was ruled |
| + // census ruling owed. It adds 59 frozen declarations: the 58 it was ruled |
| // with, and instance control's type Server, which census ruling 292 made |
| // accepted-legacy and which this overlay re-digests as main classed it. |
| // Its four instance-control rows are accepted legacy on two conditions, |
| @@ -705,7 +717,7 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-carlos-run.tsv"), &inv, true); err != nil { |
| t.Fatal(err) |
| } |
| - seal = "4dddd5fdda4b4a244cb23582458d2e377bd241d67586a180add03f940cfd1cd5" |
| + seal = "96e521e60dfc0b55e2bc08adbb7a7682a607a30599a8cf66e7c5689d6cf7fe5a" |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
| if len(errs) > 0 { |
diff --git a/internal/provision/testdata/plan14-root-reads-carlos-run.tsv b/internal/provision/testdata/plan14-root-reads-carlos-run.tsv| index bde9b4818..a08755f6b 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-carlos-run.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-carlos-run.tsv |
| @@ -2,6 +2,7 @@ |
| # The four instance-control rows (handleCmdStatus, ensureExeAs, parseRequest and the cmd unit file) are accepted legacy because they extend the J4 seed pattern to the run role. That ruling is CONDITIONAL on two things: the template-equality test stays (TestCmdTemplateEqualsSeedOutsideTheNamedLines), and the cmd verb stays on refuseStagedExe. If either diverges, those rows need a new ruling. |
| # The root-executed run code the scanner gives no row (handleCmd, cleanCmdExe, ensureCmdExeAt, removeCmdExe, and the changed Handler, handleStop, handleReset and handleSweep) is noted by the ruling and pinned by the internal/instancectl envelope. Earlier overlays are immutable. |
| # Pre-land merge (2026-10-15): eight rows re-digested because main's census rulings 292 and zddr4 changed the same units. Each delta from the ruled body is main's; the one extra line is main's new early return in prepareKeyedAt carried into this branch's six-value signature. Instance control's type Server follows main's ruling 292 (accepted-legacy, J2,J4) as this ruling kept it as main classed it. Loaded after census ruling 275. |
| +# Pre-land merge 2 (2026-10-15): loaded after census ruling owed. Only the internal/activator envelope is re-pinned, for owed's own changes to units this overlay does not carry. |
| @legacy 49d62a0659f47a38f7805ef396254be16a123fc56878e8dd2581ccf90a133bc6 apimode.go apiSession.getJSONHeader legacy accepted-legacy accepted-legacy;rev3=census-ruling-carlos-run;basis=the carlos CLI on a person's machine, as that person (no box unit): body of the frozen apiSession.getJSON, which is now its wrapper; it also returns the response header (the run follow's Retry-After) file:ReadAll@590 e5897a162e1e42a3ffb0904a80e2ef95464c221018ed92444dc384270fe5f49a |
| apimode.go apiSession.getJSONHeader legacy accepted-legacy accepted-legacy;rev3=census-ruling-carlos-run;basis=the carlos CLI on a person's machine, as that person (no box unit): body of the frozen apiSession.getJSON, which is now its wrapper; it also returns the response header (the run follow's Retry-After) file:ReadAll@590 e5897a162e1e42a3ffb0904a80e2ef95464c221018ed92444dc384270fe5f49a |
| @legacy 9baa346c72279d5235e8d97691f2fc7ce677b4ac8a904229335d4405467bda55 cmd/console/main.go serve legacy accepted-legacy accepted-legacy;rev3=census-ruling-carlos-run;basis=the console (cmd/console serve under console.service, user carlos-console): main's body with the run sweep started beside the other claim workers; read sites unchanged data-literal@2918,data-literal@4019 d55fd5881179c0ffef47d5becbd69f281d74ed863ade9c908f0c486f275063cc |
| @@ -128,7 +129,7 @@ run.go writeRunFilePart legacy accepted-legacy accepted-legacy;rev3=census-rulin |
| @removed internal/instancectl/hardening.go ensureExe H3 census-ruling-carlos-run: still declared, and reads nothing now: its reads moved unchanged into ensureExeAs, an accepted-legacy row in this overlay. |
| @coverage . 78e65520d544d1b53bb713774ae78f16868a9c4ab4b86d54b47aaceef321c9bf |
| @coverage cmd/console f77208c587db2166ccc032a824c5be48fe92de8efcf77d84b6b1ecd70e2ac7ed |
| -@coverage internal/activator fe23d0ad0887776f992be96f8560432c48569f552d9dc34293af299875c7b875 |
| +@coverage internal/activator a76568e8a625828be454c76213c062f32f1f2612f5f782c47a8e290cd3e490a8 |
| @coverage internal/console 1d54e6732821ab71c38f21712fcf5f94dc3d57122d579fc4f154a4eeb9805512 |
| @coverage internal/host a435289abcdb9effaf3e51bf957d3ecbc43849bc276ed3778b25df1f2bab93a5 |
| @coverage internal/host/units 42a5787e85e324789b151a1ebb724b69fce12e1dd5d8bb72c96ce282bd122605 |
diff --git a/internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv b/internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv| new file mode 100644 |
| index 000000000..37e2989a7 |
| --- /dev/null |
| +++ b/internal/provision/testdata/plan14-root-reads-census-ruling-owed.tsv |
| @@ -0,0 +1,13 @@ |
| +# Plan 14 census ruling owed (amadan #272, branch zdd-owed-items; approved by platform-1e as census-ruling-owed; loaded after census-ruling-275): the cold wake's build-state record moves from step 2b to the first exec, after the replica-key gate and the cgroup refusals, so a refused launch leaves no record (owed before O8). After the write the exec is decided again (the exec gate, the live check, the wake's context and maintenance fence), and a nested seed writes its obligation before its record (Astra's review, P1 and P2). |
| +# The four changed activator units stay accepted legacy because they stay agent-side, running as carlos: only runAgent builds the activator, under carlos-edge.service with User=carlos. The record is the existing BeforeExec build-state write, moved later, never earlier. No new read and no root decision. If any of these units ever runs as root or reads outside that boundary it stops being legacy and needs the Plan 14 owner's ruling. Earlier overlays are immutable. |
| +@legacy bdd27149b9dcce980ebca238207c286eeefd52353b2daa752516123d71a2c0a7 internal/activator/activator.go Service.doWake legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; step 2b no longer writes the build-state record: the wake carries it pending on its context (settleWakeIdentity's) and the first exec writes it. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. registry:DBPath@1032,registry:DBPath@1048,registry:DBPath@1175,registry:DBPath@1186,registry:DBPath@20401,registry:Host@1014,registry:Host@1166,registry:Host@1277,registry:Host@174,registry:Host@2137,registry:Host@3858,registry:Host@861,registry:Host@936,registry:Replication@20436,registry:Seed@11207,registry:Socket@1061,registry:Socket@1077,registry:Socket@1200,registry:Socket@1211,whole:Route@10058,whole:Route@11308,whole:Route@11379,whole:Route@1231,whole:Route@1237,whole:Route@1303,whole:Route@1311,whole:Route@13763,whole:Route@15621,whole:Route@159,whole:Route@16267,whole:Route@17574,whole:Route@17728,whole:Route@18084,whole:Route@18178,whole:Route@18850,whole:Route@19849,whole:Route@19997,whole:Route@2027,whole:Route@20510,whole:Route@21408,whole:Route@21503,whole:Route@3481,whole:Route@3740,whole:Route@3810,whole:Route@3816,whole:Route@3885,whole:Route@3893,whole:Route@4681,whole:Route@5680,whole:Route@6713,whole:Route@7594,whole:Route@9176,whole:Route@9368,whole:Route@959 f09606c9a22d961f17610105b6b0b126676f62874fe8ef8a5b09649a8b36b73f |
| +internal/activator/activator.go Service.doWake legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; step 2b no longer writes the build-state record: the wake carries it pending on its context (settleWakeIdentity's) and the first exec writes it. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. registry:DBPath@1032,registry:DBPath@1048,registry:DBPath@1175,registry:DBPath@1186,registry:DBPath@20401,registry:Host@1014,registry:Host@1166,registry:Host@1277,registry:Host@174,registry:Host@2137,registry:Host@3858,registry:Host@861,registry:Host@936,registry:Replication@20436,registry:Seed@11207,registry:Socket@1061,registry:Socket@1077,registry:Socket@1200,registry:Socket@1211,whole:Route@10058,whole:Route@11308,whole:Route@11379,whole:Route@1231,whole:Route@1237,whole:Route@1303,whole:Route@1311,whole:Route@13763,whole:Route@15621,whole:Route@159,whole:Route@16267,whole:Route@17574,whole:Route@17728,whole:Route@18084,whole:Route@18178,whole:Route@18850,whole:Route@19849,whole:Route@19997,whole:Route@2027,whole:Route@20510,whole:Route@21408,whole:Route@21503,whole:Route@3481,whole:Route@3740,whole:Route@3810,whole:Route@3816,whole:Route@3885,whole:Route@3893,whole:Route@4681,whole:Route@5680,whole:Route@6713,whole:Route@7594,whole:Route@9176,whole:Route@9368,whole:Route@959 f09606c9a22d961f17610105b6b0b126676f62874fe8ef8a5b09649a8b36b73f |
| +@legacy 9dcf78cbffdc590abbafd3245215de0ffeda0d54f236f23a7e63583ed2a71495 internal/activator/backend_exec.go ExecBackend.seedChild legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; the flat seed's cgroup before-start hook writes the wake's pending record, after the cgroup's refusals and immediately before the exec. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. file:CommandContext@3135,file:Stat@2036,registry:App@4384,registry:Host@120,whole:Route@1409,whole:Route@149,whole:Route@1541,whole:Route@1745,whole:Route@176,whole:Route@3083,whole:Route@4379,whole:Route@672,whole:Route@7143,whole:Route@755,whole:Route@9949 4b5fcf0426aeb5b573d1dd0b0ab6ae09b141eca1ee266fd8bcd9ca642242796b |
| +internal/activator/backend_exec.go ExecBackend.seedChild legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; the flat seed's cgroup before-start hook writes the wake's pending record, after the cgroup's refusals and immediately before the exec. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. file:CommandContext@3135,file:Stat@2036,registry:App@4384,registry:Host@120,whole:Route@1409,whole:Route@149,whole:Route@1541,whole:Route@1745,whole:Route@176,whole:Route@3083,whole:Route@4379,whole:Route@672,whole:Route@7143,whole:Route@755,whole:Route@9949 4b5fcf0426aeb5b573d1dd0b0ab6ae09b141eca1ee266fd8bcd9ca642242796b |
| +@legacy a929d4da62b876ce456ac082b71b888be0dace9a588504839785f0075304aaaf internal/activator/backend_exec.go ExecBackend.seedUnit legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; the nested seed writes its obligation, then the wake's pending record, before root is asked; a record that fails or a recheck that refuses discharges the obligation and keeps the database. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. file:Stat@1644,registry:Host@135,whole:Route@1123,whole:Route@167,whole:Route@3863,whole:Route@3993,whole:Route@4528,whole:Route@532,whole:Route@613,whole:Route@679,whole:Route@997 c4665215b34dfcd1da63912a0dbe210da9fec2136b22178d3490115662df0d72 |
| +internal/activator/backend_exec.go ExecBackend.seedUnit legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; the nested seed writes its obligation, then the wake's pending record, before root is asked; a record that fails or a recheck that refuses discharges the obligation and keeps the database. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. file:Stat@1644,registry:Host@135,whole:Route@1123,whole:Route@167,whole:Route@3863,whole:Route@3993,whole:Route@4528,whole:Route@532,whole:Route@613,whole:Route@679,whole:Route@997 c4665215b34dfcd1da63912a0dbe210da9fec2136b22178d3490115662df0d72 |
| +@legacy f8e748d7e1d462ea6d0c3e973fdf38bab439f767bde22c1dde0fffc5f1581cf5 internal/activator/beforeexec.go Service.settleWakeIdentity legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; the record of the identity it settles is returned pending on the context, with the wake's own admission (its context and the maintenance fence) asked again after the write, instead of being written here. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. registry:Host@1007,registry:Host@1135,registry:Host@1267,registry:Host@1702,registry:Host@1870,registry:Host@2402,registry:Host@2759,whole:Route@1588,whole:Route@2310,whole:Route@722,whole:Route@849 844cca5d8ef92ac53a34d580db0fb51f4dc9b3c01c3959e23f90e90ffe5f0846 |
| +internal/activator/beforeexec.go Service.settleWakeIdentity legacy accepted-legacy accepted-legacy;rev3=census-ruling-owed;basis=Census ruling owed (amadan #272, zdd-owed-items): changed legacy row; the record of the identity it settles is returned pending on the context, with the wake's own admission (its context and the maintenance fence) asked again after the write, instead of being written here. Boundary: agent-side, running as carlos (only runAgent builds the activator, under carlos-edge.service, User=carlos); the record is the existing BeforeExec build-state write, moved later, never earlier; no new read, no root decision, and no registry, /data, release or claim read beyond what the unit read before. registry:Host@1007,registry:Host@1135,registry:Host@1267,registry:Host@1702,registry:Host@1870,registry:Host@2402,registry:Host@2759,whole:Route@1588,whole:Route@2310,whole:Route@722,whole:Route@849 844cca5d8ef92ac53a34d580db0fb51f4dc9b3c01c3959e23f90e90ffe5f0846 |
| +# Envelopes, census ruling owed: each changed root source envelope below is accepted only because the units inside it are the ones listed above. internal/activator also gains the pending-record helpers (pendingRecord, withPendingRecord, runPendingRecord, pendingRecordHook), askExecGate and the exported RunExecGate, none of which reads anything. internal/release changes only ResolveLivePath, which now calls ResolveLiveLink; its body was identical, so nothing reads differently. |
| +@coverage internal/activator 281cf9604cfd728e1aa96196e35c15b9a6c88ca180f36aa2784e94d8c4732677 |
| +@coverage internal/release 0261f9202141ea3d57e3a97071df1b2ac342cde420ce27f23193007a7e6df5a9 |
diff --git a/internal/release/staged.go b/internal/release/staged.go| index 2c9e666b3..8487a0743 100644 |
| --- a/internal/release/staged.go |
| +++ b/internal/release/staged.go |
| @@ -163,17 +163,13 @@ func RetainedStaged(optDir, acct, app, ver string) (*StagedIdentity, *Manifest) |
| return &id, m |
| } |
| -// ResolveLivePath is ResolveLive without the hash: one read of host's live |
| -// link, the file at the end of its chain (linkChain) and the link's version, |
| -// with Digest empty. It is for a start that needs only to exec exactly the |
| -// build it named (the activator's live exec gate, Task 9), where hashing a |
| -// build of up to a hundred megabytes at every wake would buy nothing. |
| +// ResolveLivePath is ResolveLiveLink: one read of host's live link, the file |
| +// at the end of its chain (linkChain) and the link's version, with Digest |
| +// empty. It is for a start that needs only to exec exactly the build it |
| +// named (the activator's live exec gate, Task 9), where hashing a build of |
| +// up to a hundred megabytes at every wake would buy nothing. |
| func ResolveLivePath(optDir, host string) (StagedIdentity, error) { |
| - raw, hops, _, err := linkChain(LiveLink(optDir, host)) |
| - if err != nil { |
| - return StagedIdentity{}, err |
| - } |
| - return StagedIdentity{Version: versionOfTarget(optDir, raw), StagedPath: hops[len(hops)-1]}, nil |
| + return ResolveLiveLink(optDir, host) |
| } |
| // VerifyStaged checks the file at id.StagedPath against id.Digest, the digest |