carlos / platform Public

plan: claim Plans 5 and 6 rev 1, answers their independent reviews

Co-Authored-By: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codex agent, operated by paul@keymail.dev pushed by claude agent, operated by paul@keymail.dev 6b6df8b7be2649522e4ed1cb11cb34519e1ac924 parent 1eacbf8
2 files changed, +208 −27
  • docs/superpowers/plans/2026-10-05-host-lifecycle-claim-5-console-ops.md +78 −12
  • docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md +130 −15

CI cancelled — superseded by a newer push — run details

diff --git a/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-5-console-ops.md b/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-5-console-ops.md
index c5fbdd3da..37d166ff6 100644
--- a/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-5-console-ops.md
+++ b/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-5-console-ops.md
@@ -1,5 +1,7 @@
# Host Lifecycle Claim, Plan 5: console operations
+Rev 1 answers the independent review.
+
> **For agentic workers:** implement this plan task by task, with a failing test before each behavior change. Do not enable `CARLOS_HOST_CLAIM`. Resolve the decisions below before implementing the dependent authority paths; an interface without its production provider is not completed integration.
**Goal:** make instance and sidecar creation, every record retirement, app purge and placement follow the host lifecycle claim. Add the console's recovery pass. Keep all current behavior when the startup flag is unset.
@@ -12,7 +14,7 @@
**Source baseline:** `origin/main` at `04cd2d78`, the committed claim library `47426cc1`, Plan 7's committed `feat/hostclaim-door` at `87c8ef92`, and the published Plan 4 rev 2 code. The planning checkout is older than main. `edge-replica-placements` at `b5b0a53a` was also inspected. There is no `feat/hostclaim-park` commit at authoring time; do not substitute an uncommitted sibling directory for the branch dependency.
-**Scope of the code below:** complete, compiling files for the transitions, record operations, recovery traversal, parking adapter, retirement dispatch and console pass whose behavior the spec defines. The admission coordinator, full purge coordinator, placement completion and production authority providers depend on D1–D6. Their algorithms, call sites and required tests are specified below, but no fabricated wire format or permissive implementation is supplied for those unresolved contracts. Those tasks remain open. This plan is not evidence that the full console integration can be enabled.
+**Scope of the code below:** complete, compiling files for the transitions, record operations, recovery traversal, parking adapter, retirement dispatch and console pass whose behavior the spec defines. The admission coordinator, full purge coordinator, placement completion and production authority providers depend on D1–D7. Their algorithms, call sites and required tests are specified below, but no fabricated wire format or permissive implementation is supplied for those unresolved contracts. Those tasks remain open. This plan is not evidence that the full console integration can be enabled.
## Global constraints
@@ -36,8 +38,9 @@ Resolve these in the owning design before completing the affected implementation
4. **D4 — purge's durable host set, fence ownership and final cleanup.** The journal key and ordered phases are specified, but the persisted host/record/fence list, app identity/generation, per-host progress and cleanup checkpoint schema are not. A crash after draining a registration must not lose the identity of the holderless fence it created. Define behavior when a drained registration's host is currently held by another app or operation: CC21 authorizes fences only from free/absent, so it cannot overwrite that holder. Define replay when a host frees before app cleanup, the exact order of `done`, fence release, admission removal/reopening and journal removal, and retry after the app tombstone was removed. `release.PurgeApp` currently sweeps `purge.json` and `admission.json` with the app prefix; it must not be used unchanged. The journal must remain authoritative until its own completion removes it.
5. **D5 — placement's operation completion and app-wide serialization.** §5.4 defines the start CAS and target intent, but not stop/transfer/install phases, rollback, final executor/stamp, or how one app-level placement change excludes concurrent creates across several host claims. Current placement writes app SQLite state, then routes, then records. Claiming hosts one at a time after the app write is too late. Define a durable app-wide operation/census and its replay owner, including route-only apps and empty apps, plus interaction with `Record.Replicas` and moves. Do not implement `placing → serving` by assigning the target region's first box or issuing an unexplained stamp. The start transition below deliberately has no guessed completion transition.
6. **D6 — create/rollback effects and purge teardown replay.** Private storage is provisioned before `CreateRecord` today; first-route attach writes a separate domain claim before its record; sidecar deletion later removes its wake object; purge tears down store/email estate and console rows. Define durable recovery for those effects under the operation, including ambiguous create writes, a rollback while still `creating`, same-key successor protection for auxiliary objects, and concurrent app restore/rename/move/trash/teardown. A complete create intent must reconstruct every pre-serving effect it permits. App purge needs backend authority on the console; auto-purge boxes lack it and must durably hand work to the same coordinator, without broadening IAM. Define observable pending/accepted outcomes so a successful delete response never claims that parking is finished. Reuse existing copy where accurate; review any new user-facing strings during implementation.
+7. **D7 — provisioning-record writer exclusion and retirement ETag refresh.** An instance PATCH, placement update or other same-incarnation record writer can replace the record after `BeginRetire` saves its ETag. The spec requires an exact conditional delete and a fresh check after a mismatch, but it does not say how a deleting or purging claim excludes those writers, what durable proof permits the saved intent ETag to advance, or whether a conflicting update cancels and restarts retirement. Define one write guard for every record writer and the conditional protocol for refreshing the intent. The console claim-recovery pass owns replay while the claim is `deleting`; the purge coordinator under the same purge ID owns replay while it is `purging`. Until the protocol is approved, both owners keep the operation and record intact and return an operator-visible pending refusal on an ETag mismatch. They never adopt or delete the newer ETag merely because the incarnation still matches.
-D1–D6 are concrete missing contracts. Ordinary implementation choices below—two-minute recovery cadence, helper/package names and in-memory batching—are not additional spec gates.
+D1–D7 are concrete missing contracts. Ordinary implementation choices below—two-minute recovery cadence, helper/package names and in-memory batching—are not additional spec gates.
## Every retirement call site
@@ -462,6 +465,44 @@ func TestRecoveryFreshReadsAndRequiredHooks(t *testing.T) {
}
}
+func TestAppsPreserveLegacyPrefixesAndFindClaimlessPurge(t *testing.T) {
+ f := newFixture(t)
+ for key, body := range map[string][]byte{
+ release.LegacyChannelKey("my-app", "stable"): []byte("legacy channel"),
+ release.LegacyChannelKey("myapp", "stable"): []byte("legacy channel"),
+ release.LegacyManifestKey("myapp", "v1"): []byte("legacy manifest"),
+ release.LegacyArtifactKey("myapp", "v1", "app"): []byte("legacy artifact"),
+ } {
+ if _, err := f.store.Put(f.ctx, key, body, ""); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if _, _, err := ReservePurge(f.ctx, f.store, "bab", "doomed", "purge"); err != nil {
+ t.Fatal(err)
+ }
+ hosts, err := ListClaims(f.ctx, f.store)
+ if err != nil || len(hosts) != 0 {
+ t.Fatalf("claims = %v, %v; want none", hosts, err)
+ }
+ if err := f.c.SweepAllRecords(f.ctx); err != nil {
+ t.Fatalf("record sweep with legacy prefixes: %v", err)
+ }
+ var replayed []App
+ err = SweepAllPurges(f.ctx, f.store, func(_ context.Context, app App, p Purge) error {
+ if p.ID != "purge" {
+ t.Fatalf("purge ID = %q", p.ID)
+ }
+ replayed = append(replayed, app)
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(replayed) != 1 || replayed[0] != (App{Account: "bab", Name: "doomed"}) {
+ t.Fatalf("replayed = %+v", replayed)
+ }
+}
+
func TestOrphanConditionalDeleteCannotEraseReplacement(t *testing.T) {
f := newFixture(t)
r := f.record(t, "shop.bab.example.com")
@@ -619,7 +660,7 @@ func TestClaimRecoveryAndRetirementAreInert(t *testing.T) {
- [ ] **Step 3: Run the new tests before implementation.**
```bash
-go test ./internal/hostops ./internal/provision ./internal/console -run 'AttemptUses|RecoverCreate|CreateRecovery|RetireRecordFlagOff|RetireWaits|AdoptionIs|PurgeJournal|PurgeRetires|PrepareRejects|RecoveryFresh|OrphanConditional|PlacementStart|FenceRelease|ClaimRecoveryAnd' -count=1
+go test ./internal/hostops ./internal/provision ./internal/console -run 'AttemptUses|RecoverCreate|CreateRecovery|RetireRecordFlagOff|RetireWaits|AdoptionIs|PurgeJournal|PurgeRetires|PrepareRejects|RecoveryFresh|AppsPreserve|OrphanConditional|PlacementStart|FenceRelease|ClaimRecoveryAnd' -count=1
```
Expected: compilation fails on the missing `Controller`, `RetireRecord`, `Recovery` and server helper names. Do not alter the assertions to make an existing unguarded path pass. After the code tasks below, the same command must pass. Add the real door/coordinator tests in Tasks 5–9 before their respective integrations; these primitive tests do not stand in for them.
@@ -1408,7 +1449,21 @@ func Apps(ctx context.Context, s release.Store) ([]App, error) {
var out []App
for _, prefix := range accounts {
account := strings.TrimSuffix(strings.TrimPrefix(prefix, "apps/"), "/")
- if _, err := PurgeKey(account, "valid"); err != nil || prefix != "apps/"+account+"/" {
+ if prefix != "apps/"+account+"/" {
+ return nil, ErrIdentity
+ }
+ // The retained legacy layout is apps/<app>/..., so its first segment is
+ // an app name, not necessarily an account. Skip names that are valid apps
+ // but invalid accounts before treating this as a qualified account root.
+ // A letters-only legacy name can pass both validators, so preserve its
+ // children as candidates and let the exact record and purge parsers
+ // validate them. The path alone cannot tell that case from a real account
+ // containing apps with those legal names; never infer deletion authority
+ // over retained legacy content from this prefix shape.
+ if !release.ValidAccount(account) {
+ if release.ValidApp(account) && prefix == release.LegacyAppPrefix(account) {
+ continue
+ }
return nil, ErrIdentity
}
apps, err := s.ListDirs(ctx, prefix)
@@ -1792,7 +1847,7 @@ GOOS=darwin GOARCH=amd64 go build ./...
GOOS=windows GOARCH=amd64 go build ./...
```
-Expected: the new tests pass and all builds exit 0. This verifies real Go interfaces and the specified primitives. It does not certify D1–D6 providers or the later handler integrations, which are not yet implemented.
+Expected: the new tests pass and all builds exit 0. This verifies real Go interfaces and the specified primitives. It does not certify the D1–D6 providers, settle D7 or implement the later handler integrations.
### Task 5: Complete app admission and create integration after D1/D2/D6
@@ -1829,7 +1884,7 @@ Expected before implementation: new ordering/lease witnesses fail. Expected afte
### Task 6: Migrate every retirement caller and finish the pull-door boundary
-**Depends on:** Task 5's production provider, D2/D3 inventory and participant evidence, D6 rollback/pending outcomes. The shared `RetireRecord` dispatcher is already complete; these caller changes remain gated on those contracts.
+**Depends on:** Task 5's production provider, D2/D3 inventory and participant evidence, D6 rollback/pending outcomes, and D7 record-writer exclusion/ETag refresh. The shared `RetireRecord` dispatcher is already complete; these caller changes remain gated on those contracts.
- [ ] **Step 1: Add every table-row regression before changing its caller.** Run the existing API/UI fixtures through their actual routes, with a `DirStore` that records writes and can stop before/after `Put`/`DeleteIfMatch`. Assert the sequence, response, operation ID, exact ETag, preserved domain claim, and post-response claim/record state—not merely that a mock retirement method was invoked. For the enabled variants seed serving claims or exercise approved legacy adoption. The six direct callers plus two purge callers must each have a named test from the table.
@@ -1856,7 +1911,7 @@ These are complete replacement call expressions within their existing `if err :=
- [ ] **Step 3: Bind both rollback paths to their own incarnation.** The create coordinator returns the assigned incarnation even for an ambiguous record PUT. Pass it as `RetireRef.Incarnation` in the sibling-apex rollback. Extend `rollbackAttach` and both its callers (`domainattach.go:226,236`) to carry that captured identity. A duplicate known to belong to another attempt keeps `dropRecord=false`; an ambiguous own write must never rediscover “whatever is there now” and retire it. D6 must supply the creating-state cancel/retire recovery intent; `Controller.RetireRecord` intentionally returns pending for `creating`, rather than inventing a serving transition as a shortcut. Do not erase that outstanding obligation when the handler returns.
-- [ ] **Step 4: Complete delete recovery through the existing transitions.** `BeginRetire` binds the expected incarnation and record ETag, calls `BeginDelete`, and keeps executor/stamp. `Retire` waits for the first-phase barrier, deletes only the intent's ETag and rereads authoritative absence before `ToParking`. A newer same-incarnation record ETag is not silently adopted into the old delete intent. Treat it as pending/operator-visible until the owning protocol defines a safe refresh; never delete the new ETag just because a 412 happened. Existing-claim/missing-holder-record is not automatic legacy adoption: §10 reserves old-console damage for operator resolution.
+- [ ] **Step 4: Complete delete recovery through the existing transitions.** `BeginRetire` binds the expected incarnation and record ETag, calls `BeginDelete`, and keeps executor/stamp. `Retire` waits for the first-phase barrier, deletes only the intent's ETag and rereads authoritative absence before `ToParking`. D7 must first exclude every same-incarnation record writer and define the conditional ETag-refresh protocol. Until then, a newer same-incarnation record ETag remains pending and operator-visible: the console claim-recovery pass owns that `deleting` retry, while the purge coordinator owns the matching `purging` retry. Neither owner silently adopts or deletes the new ETag merely because a 412 happened. Existing-claim/missing-holder-record is not automatic legacy adoption: §10 reserves old-console damage for operator resolution.
The regional/local owners finish chores. The console's pass calls `FreeAfterPark` only after none remain. Emit a serial bump after a new deleting/parking/free transition through the existing `s.bumpSerial` path; it accelerates observation but is not a substitute for the durable claim. Do not create another serial protocol.
@@ -1973,7 +2028,7 @@ The method itself makes the flag-off call a no-op, consistent with existing pass
| Required delivery | Evidence |
|---|---|
-| D1–D6 | Decisions recorded; complete reviewed coordinator/provider code; no production placeholder callbacks |
+| D1–D7 | Decisions recorded; complete reviewed coordinator/provider code and record-writer exclusion/ETag-refresh protocol; no production placeholder callbacks |
| Flag-off | Existing console/provision/domain/purge tests unchanged and passing; no new claim or admission calls |
| Creates | Instance, sidecar, first-route and replica-producing paths all use the one admission protocol |
| Retirement | Every exact call-site row has its named behavior test, including raw purge sweeps |
@@ -2000,12 +2055,23 @@ GOOS=linux GOARCH=arm64 go build ./...
Expected: tests pass, build/vet/cross-build exit 0, `gofmt -l .` prints nothing. Tests that invoke the CLI use `carlosTestEnv` and a scratch `cmd.Dir`; no operator credentials, current project configuration or production traffic. Run the joint model's stated 30k+ seed gate and mutation witnesses for admission fencing, record-before-claim orphan checks, purge teardown ordering and stale placement writes; record the exact model checkout/command when the implementation is integrated. A model pass is additional evidence, not replacement for the real handler/coordinator tests.
-- [ ] **Step 5: Obtain the authority/production-path review required by AGENTS.md.** Use the explicitly named Opus reviewer through the repository's available agent tooling at implementation time; do not label an unavailable review as passed. Review all cross-object races above, D1–D6 resolutions, every old delete door, and the full final branch. This authoring task performs no production action, staging, commit, push or merge.
+- [ ] **Step 5: Obtain the authority/production-path review required by AGENTS.md.** Use the explicitly named Opus reviewer through the repository's available agent tooling at implementation time; do not label an unavailable review as passed. Review all cross-object races above, D1–D7 resolutions, every old delete door, and the full final branch. This authoring task performs no production action, staging, commit, push or merge.
+
+- [ ] **Step 6: Land only the completed, reviewed inert implementation through amadan.** Push the branch to the remote that points to amadan, describe its final behavior and evidence, squash to one change commit, wait for green CI on that exact tip, and use `amadan branch merge carlos/platform feat/hostclaim-console -expect <tip> -message "..."`. Check main's CI after landing. Never merge on GitHub or locally to main. If D1–D7 are still open, describe any primitive-only landing as partial and keep the integration tasks open; do not mark Plan 5 complete or enable the flag.
+
-- [ ] **Step 6: Land only the completed, reviewed inert implementation through amadan.** Push the branch to the remote that points to amadan, describe its final behavior and evidence, squash to one change commit, wait for green CI on that exact tip, and use `amadan branch merge carlos/platform feat/hostclaim-console -expect <tip> -message "..."`. Check main's CI after landing. Never merge on GitHub or locally to main. If D1–D6 are still open, describe any primitive-only landing as partial and keep the integration tasks open; do not mark Plan 5 complete or enable the flag.
+## Self-review
+
+Rev 1 resolves both independent-review findings:
+
+| Finding | Change and regression |
+|---|---|
+| Retained legacy prefixes aborted `Apps` | Invalid-account legacy app roots are recognized before account validation. Valid-account-looking legacy roots remain candidates because the prefix alone is ambiguous; exact record and purge parsers retain fail-closed authority. `TestAppsPreserveLegacyPrefixesAndFindClaimlessPurge` mixes both legacy name shapes with channel and release contents, runs both sweeps with no claims, and replays the qualified claimless purge. |
+| Same-incarnation record updates could strand retirement | D7 now requires one writer-exclusion and conditional ETag-refresh protocol, assigns `deleting` replay to console claim recovery and `purging` replay to the purge coordinator, and preserves the current operator-visible pending refusal until that protocol is approved. Task 6 depends on D7 and forbids adopting or deleting the newer ETag. |
+Rev 1 verification extracted the seven complete `internal/hostops` blocks and the provisioning dispatcher from this Markdown into a uniquely named on-disk source export based on `feat/hostclaim-park` at `4b9b1d77`; no implementation file was created in this repository. The extracted blocks were `gofmt` clean. The focused new regression and retirement test passed, the complete `internal/hostops` suite passed with `-race`, and `go vet` passed for that package. Its test binary compiled for Linux/amd64, Darwin/amd64, Windows/amd64 and Linux/arm64; only the native Linux binary was executed. Restoring the pre-revision `Apps` validation made the new regression fail at the record sweep with `hostops: identity mismatch`, then restoring the revised block made it pass. The default Go cache failed with a read-only-filesystem error, so these checks used the existing shared `/home/paulca/.cache/tmp/gocache`. This focused revision did not rerun the original whole-repository overlay gate or any live test.
-## Authoring verification
+## Original authoring verification (pre-rev 1)
Checked on 2026-10-05 in a disposable on-disk source export, outside the planning repository. The source was Plan 7 at `87c8ef92`, including main `04cd2d78` and the claim library. Because Plan 4 was not committed, the export received its rev 2 **exact** `ParkScope`/holderless-sweep edit and its complete `remote.go`/`sweep.go` blocks. It then received the eleven complete Go files above and the additive console Config edit. No repository source file was changed.
@@ -2017,4 +2083,4 @@ Results:
- The default `/home/paulca/.cache/go-build` was attempted and rejected as read-only. Verification used the **existing shared** `/home/paulca/.cache/tmp/gocache`, not a cache under `/tmp` or a new per-task cache.
- During authoring, the door branch advanced to `3d143b23` with ranged version reads and the Plan 7b package merge. The source export's door files were checked against pinned `87c8ef92`; the evidence above is for that explicit baseline. The branch commands intentionally merge the actual committed dependency at implementation time; retain its range-read/generation changes and rerun the gates then.
-The disposable export and authoring scratch files were removed after verification. Only this plan was written in the planning checkout; nothing was staged or committed. There was no live infrastructure test, and no Opus review was claimed. The full handler/coordinator test matrix, joint-model gate and authority review remain implementation work. In particular, successful compilation does **not** complete D1–D6 or make the inert feature ready to enable.
+The disposable export and authoring scratch files were removed after verification. Only this plan was written in the planning checkout; nothing was staged or committed. There was no live infrastructure test, and no Opus review was claimed. The full handler/coordinator test matrix, joint-model gate and authority review remain implementation work. In particular, successful compilation does **not** complete D1–D7 or make the inert feature ready to enable.
diff --git a/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md b/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md
index ba78eef88..56dd3c815 100644
--- a/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md
+++ b/docs/superpowers/plans/2026-10-05-host-lifecycle-claim-6-box-wiring.md
@@ -1,5 +1,7 @@
# Host lifecycle claim, Plan 6: box wiring
+Rev 1 answers the independent review.
+
> **For agentic workers:** implement task by task, with the failing test first. Keep `CARLOS_HOST_CLAIM` unset. Missing authority providers refuse enabled work; they never select the legacy path. The production integrations marked with a decision dependency remain unfinished until that contract is settled.
**Goal:** wire claim admission and installation retirement into reconcile, admit writer sessions before wake, run regional at-rest sweeps, and adopt legacy hosts through the shared claim transition. Guard every host-specific writer, including separate root commands.
@@ -34,7 +36,7 @@ These numbers belong to this plan, not to the similarly named decisions in Plan
1. **D1 — privileged stop collection and durable evidence.** The agent runs as `carlos`; it cannot inspect every other UID's `/proc/<pid>/fd`. `FenceAndStop` returns process-local evidence, not a root-verifiable durable attestation. Choose the root collector entry point, authenticated agent-to-root request and fence binding, trusted artifact inventory, cgroup identity and boot/controller/op/stamp binding, durable evidence schema, freshness/replay rules, and acknowledgement transport. A root subprocess cannot reconstruct the closed in-memory admission fence merely from a hostname. Coordinate with proposed §7.3 D2/D6 and Plan 8. Until resolved, no stop acknowledgement, local retirement, destructive regional chore or adoption discovery acknowledgement is authorized by the agent alone. Cleanup-only termination may still proceed and report incomplete evidence.
2. **D2 — complete deployment identity, executor and local inventory.** Share Plan 5's complete inventory and unique executor provider with box adoption. Several boxes can occupy one region; fleets, pools, replicas, static/foreign routes and route-only legacy hosts are not an unambiguous “region's box”. Define this actor's enrollment, authoritative effective region, actual legacy writer, host enumeration for orphan residue in regional buckets, and proof that an untouched box has no artifacts. No first-heartbeat/console-box choice. `park.Local` handles paths beneath `DataDir`, but live links under `OptDir`, root unit/env files, registry/timer rows and current inventories have no complete common retirement transaction. Define their stamp ownership, durable journal and retry/ack ordering, including local-record loss and a successor at the same path. Do not put an entire shared registry DB into one host's inventory or treat missing inventory as empty. Until resolved, refuse admission/retirement for those unsupported shapes; no incomplete success acknowledgement.
3. **D3 — moves-owned held inputs, marker publication and live view.** `RenderHeld`/`MaterializeHeld` are specified names, not implemented APIs in the inspected main. Plan 7b selects versions and parses a marker; it neither verifies complete restored bytes nor implements a sweep `park.View`, authorized prepared-journal input, marker CAS publisher or held-materialization service. Moves must provide exact operation/phase-bound config/release identities, prepared pin/open-once authority, snapshot/anchor position-zero behavior, and marker publication/retry authority. Bind the sweep view to the exact claim ETag and region, with a total generation classifier for versions already deleted during a retry. Until supplied, refuse probes, prepared restores, publication and destructive sweeps; never parse guessed journal fields or treat `Select` success as a publication receipt.
-4. **D4 — clean-close proof, including amadan #234.** `internal/lease/lease.go:Replicator.Close` drops the initial `SyncAndWait` error. A separate successful sync also cannot exclude the reported Close race or prove all compactions, retention deletes and timed-out originals finished successfully. Define request accounting, close/join order, immutable certificate contents/maximum verification and the replica-lease boundary through certificate publication. The #234 discussion could not be read in this network-restricted session; its detailed race must be read before this provider is implemented. Until then, emit no certificates, refuse warm admissions, and use cold admission only where all other cold evidence is available. Unknown outcomes remain unknown; fixing one returned error is insufficient.
+4. **D4 — clean-close request evidence and publication ordering, including amadan #234.** Settled by §6.4a: the certificate is `{session, box, max_txid, closed_at}`, written create-only with `If-None-Match` at `<location>/<generation>/closed/<session>.json`; it is never deleted or reused. Warm admission requires the certificate for the observed `holder.session` and equality between its `max_txid` and that generation's remote maximum TXID. These fields, key and comparison are implementation requirements, not open design choices. Still open: request accounting that proves every upload, compaction, snapshot, retention delete and timed-out original completed successfully; the close/join protocol that excludes the #234 race; and replica-lease ownership/order through certificate publication. `internal/lease/lease.go:Replicator.Close` drops the initial `SyncAndWait` error, and a separate successful sync does not establish that evidence. The #234 discussion could not be read in this network-restricted session; its detailed race must be read before this provider is implemented. Until then, emit no certificates, refuse warm admissions, and use cold admission only where all other cold evidence is available. Unknown outcomes remain unknown; fixing one returned error is insufficient.
5. **D5 — regional retirement authority and lease aggregation.** Supply Plan 4's real `Authorize`, `View` and renewing aggregate `Leases` providers, regional worker assignment, durable original evidence, complete host discovery and deployment-wide enforced maximum request duration. Include readable instance and moves staging scopes, not only the replica lock. Define safe retirement evidence for failed admissions, live-session superseded generations, free-host legacy residue, and ownership changes during a run. Until resolved, workers may report missing providers but cannot bind scope, queue destructive work or acknowledge it. No `time.Now()` evidence substitute and no maximum-age guess.
6. **D6 — cross-process execution denial and returning-box cleanup.** §7.3 D6 is explicitly **PROPOSED**, not an approved contract. Plan 8 owns the lifetime lock, durable pending/active/lost record, boot-clock deadline, loss latch, first-phase acknowledgement and quarantine. Plan 6 needs the approved nonblocking-under-flock reader in every root/background writer, plus startup deny before reattachment, timers or config work. Renewal and acknowledgement network waits must not hold hostguard. Agree cleanup that can proceed during a control outage without ordinary `Guard.With`, and retirement after free/successor claims (Plan 4's current local API requires the old holder). Until then active-operation work and unknown startup/quarantine state refuse; serving's no-execution-lease rule does not bypass quarantine or C4.
7. **D7 — strict containment for co-spawns and root unit starts.** Main's `spawnInCgroup` uses best-effort `Manager.Start`; parent and co-spawned sidecar share a populated group. `PrepareStrict` requires an empty group and its `Start` permits one attempt. Nested `startUnit`, `seedUnit`, `startSidecarUnit`, observer reattachment and separate root restarts are different launch routes. Define containment and cleanup identities for those routes and for an auxiliary child joining a running host, without weakening the empty-group API. Until resolved, refuse these enabled spawn shapes explicitly; do not silently omit a requested co-spawned sidecar or let systemd reattach/start around the admission path. Flat one-child strict starts can use Task 5 as written.
@@ -679,7 +681,7 @@ func (r Regional) Sweep(ctx context.Context, host, legacyIncarnation string, evi
### Task 5: Add strict spawn and timer-control adapters
-**Files:** create `internal/activator/claimspawn_test.go`, `claimspawn.go`, `internal/schedule/claimtimers_test.go`, `claimtimers.go`.
+**Files:** create `internal/activator/claimspawn_test.go`, `claimspawn_kernel_linux_test.go`, `claimspawn.go`, `internal/schedule/claimtimers_test.go`, `claimtimers.go`.
- [ ] **Step 1: Add the tests and run them before implementation.**
@@ -720,9 +722,9 @@ func TestClaimSpawnNeverFallsBack(t *testing.T) {
calls, builds := 0, 0
build := func() *exec.Cmd { builds++; return exec.Command("must-not-execute") }
for _, deny := range []bool{true, false} {
- cmd, err := spawnClaimed(context.Background(), true, nil, "shop.example", cgroup.TierDefault, claimSpawnWork{deny, &calls}, hostwrite.Identity{}, build)
- if err == nil || cmd != nil {
- t.Fatal(cmd, err)
+ cmd, prepared, err := spawnClaimed(context.Background(), true, nil, "shop.example", cgroup.TierDefault, claimSpawnWork{deny, &calls}, hostwrite.Identity{}, build)
+ if err == nil || cmd != nil || prepared != nil {
+ t.Fatal(cmd, prepared, err)
}
if deny && !errors.Is(err, hostwrite.ErrUnavailable) {
t.Fatal(err)
@@ -734,6 +736,95 @@ func TestClaimSpawnNeverFallsBack(t *testing.T) {
}
```
+**`internal/activator/claimspawn_kernel_linux_test.go`**
+
+```go
+//go:build linux
+
+package activator
+
+import (
+ "context"
+ "errors"
+ "os"
+ "os/exec"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/cgroup"
+ "github.com/carlosframework/platform/internal/hostwrite"
+)
+
+func TestClaimSpawnKernelChild(t *testing.T) {
+ if os.Getenv("CARLOS_CLAIM_SPAWN_CHILD") != "1" {
+ t.Skip("synthetic contained child only")
+ }
+ time.Sleep(time.Minute)
+}
+
+func TestClaimSpawnCleanupRespawn(t *testing.T) {
+ if os.Getenv("CARLOS_CLAIM_SPAWN_KERNEL") != "1" {
+ t.Skip("requires a disposable delegated user service")
+ }
+ manager := cgroup.Setup(t.Logf)
+ if manager == nil {
+ t.Fatal("missing delegated cgroup")
+ }
+ const host = "claimspawn.example"
+ calls := 0
+ dir := t.TempDir()
+ build := func() *exec.Cmd {
+ cmd := exec.Command(os.Args[0], "-test.run=^TestClaimSpawnKernelChild$", "-test.count=1")
+ cmd.Env = []string{"CARLOS_CLAIM_SPAWN_CHILD=1"}
+ cmd.Dir = dir
+ return cmd
+ }
+ for attempt := range 2 {
+ cmd, prepared, err := spawnClaimed(t.Context(), true, manager, host, cgroup.TierDefault, claimSpawnWork{false, &calls}, hostwrite.Identity{}, build)
+ if err != nil || cmd == nil || prepared == nil {
+ t.Fatalf("spawn %d: cmd=%v prepared=%v err=%v", attempt, cmd, prepared, err)
+ }
+ // The unit's KillMode covers fatal test exits. Normal cleanup below
+ // must release the original reservation without removing the group.
+ if err := prepared.Close(); !errors.Is(err, cgroup.ErrStrictRefused) {
+ t.Fatalf("populated close: %v", err)
+ }
+ cleanup, err := manager.OpenCleanup(host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second)
+ err = cleanup.KillAndEmpty(ctx)
+ cancel()
+ if err != nil {
+ t.Fatal(err)
+ }
+ var exitErr *exec.ExitError
+ if err := cmd.Wait(); err != nil && !errors.As(err, &exitErr) {
+ t.Fatal(err)
+ }
+ if cmd.ProcessState == nil {
+ t.Fatal("child was not reaped")
+ }
+ if err := cleanup.Close(); err != nil {
+ t.Fatal(err)
+ }
+ if other, err := manager.PrepareStrict(host, cgroup.TierDefault); other != nil || !errors.Is(err, cgroup.ErrStrictState) {
+ t.Fatalf("cleanup close released original reservation: %v", err)
+ }
+ if populated, err := prepared.Populated(); err != nil || populated {
+ t.Fatalf("pinned group not proven empty: %v %v", populated, err)
+ }
+ if err := prepared.Close(); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if calls != 2 {
+ t.Fatal(calls)
+ }
+}
+```
+
**`internal/schedule/claimtimers_test.go`**
```go
@@ -784,24 +875,26 @@ import (
"github.com/carlosframework/platform/internal/hostwrite"
)
-func spawnClaimed(ctx context.Context, enabled bool, manager *cgroup.Manager, host string, limit cgroup.Limit, selected hostwrite.Work, identity hostwrite.Identity, build func() *exec.Cmd) (*exec.Cmd, error) {
+func spawnClaimed(ctx context.Context, enabled bool, manager *cgroup.Manager, host string, limit cgroup.Limit, selected hostwrite.Work, identity hostwrite.Identity, build func() *exec.Cmd) (*exec.Cmd, *cgroup.Prepared, error) {
if !enabled {
- return manager.Start(host, limit, build)
+ cmd, err := manager.Start(host, limit, build)
+ return cmd, nil, err
}
if selected == nil || build == nil {
- return nil, hostwrite.ErrUnavailable
+ return nil, nil, hostwrite.ErrUnavailable
}
var cmd *exec.Cmd
+ var prepared *cgroup.Prepared
err := selected.With(ctx, identity, func() error {
- prepared, err := manager.PrepareStrict(host, limit)
+ var err error
+ prepared, err = manager.PrepareStrict(host, limit)
if err != nil {
return err
}
- defer prepared.Close()
cmd = build()
return prepared.Start(cmd)
})
- return cmd, err
+ return cmd, prepared, err
}
```
@@ -873,7 +966,21 @@ func (s SelectedTimers) CountTimers(host, kind string) (int, error) {
var _ TimerStore = SelectedTimers{}
```
-- [ ] **Step 3: Run the focused tests and cross-compile both packages.** The strict helper returns the command even after a construction/start error, so the caller retains any process handle for cleanup. `Prepared.Close` closes descriptors; it is not stop evidence. Production callers still own waiting, cleanup and `OpenCleanup` reconstruction.
+- [ ] **Step 3: Run the focused tests and cross-compile both packages.** `spawnClaimed` returns both the command and the original `*cgroup.Prepared`, including after a failed start. Save both in the process lifecycle owner before handling the error. Retain that same prepared handle until cleanup has joined `Cmd.Wait` (if a process started) and proved the pinned cgroup empty. Then call `prepared.Close`, check its error and clear the saved handle only on success. A kill, wait, population-read or close failure retains the handle for bounded cleanup retries and prevents replacement spawn. Failed starts with no process still require an empty observation and checked close.
+
+`OpenCleanup` may kill/drain and collect evidence, but its `Close` releases only its own descriptors. It cannot release the original prepared handle's reservation. Do not remove the cgroup path through `CleanupHost` before closing the prepared handle, and never defer that close in the spawn helper. Cleanup and waiting run outside hostguard; retain the admission fence until the lifecycle owner has completed them. Closing a reservation is not stop evidence. Task 8 still owns complete stop proof and retirement acknowledgement.
+
+Run the real-cgroup regression below only in a disposable delegated user service. An ordinary test run skips it; a skipped test is not a successful spawn/cleanup proof. The test uses synthetic test-binary children with an isolated environment and scratch working directory, never the CARLOS CLI or operator credentials.
+
+```bash
+go test ./internal/activator ./internal/schedule -run 'TestClaimSpawn|TestSelectedTimer' -count=1
+spawn_scratch=$(mktemp -d "${TMPDIR:-/home/paulca/.cache/tmp}/plan6-spawn.XXXXXX")
+go test -c -o "$spawn_scratch/claimspawn.test" ./internal/activator
+systemd-run --user --wait --pipe --collect -p Delegate=yes -p KillMode=control-group --setenv=CARLOS_CLAIM_SPAWN_KERNEL=1 --setenv=TMPDIR="$spawn_scratch" "$spawn_scratch/claimspawn.test" -test.run='^TestClaimSpawnCleanupRespawn$' -test.count=1
+rm -r "$spawn_scratch"
+```
+
+Expected: the focused tests pass; the delegated regression passes after two actual contained starts. Before the fix, retaining the old eager close and discarding its handle leaves the second preparation refused. Without delegation or strict-spawn support, the explicitly requested kernel run fails and remains a verification gap; never fall back to uncontained spawning.
`SelectedTimers` covers only the control socket's five-method `TimerStore` interface. Capture its work at process admission; construct a per-request facade with the request context and that saved work. Never give an old control socket a fresh installation ticket per request. Runner/converger mutations need their own selections in Task 6.
@@ -1025,7 +1132,7 @@ Within the enabled coordinator:
4. Activate the resulting installing stamp with checked fresh authority. Register the new installation's complete inventory, retiring predecessor artifacts when the operation requires it. Select new work for this new session; do not change a stale caller's ticket in place.
5. Under the new selection, use an **error-returning** purge of local DB, WAL, SHM and both metadata directories. The current `PurgeLocalDBArtifacts` returns no error and ignores every removal failure, so it is forbidden here. Preserve it on the legacy path; implement the checked, pinned-directory variant as part of D2's resolved filesystem contract. Failed purge means no restore/start. Never decide “fresh” from the mere absence of a local file.
6. Restore the selected exact object versions into the approved local location. Verify full LTX checksums/ranges, snapshot identity, final TXID/checksum, SQLite integrity and local file/directory durability. `genselect` reads only a root header and is not this verification. The existing `lease.Restore` uses a mutable current-key Litestream client; it cannot consume a selected plan unchanged. Add the exact-version restore adapter and test it against the actual pinned Litestream version. Replace an object after selection and prove the adapter still reads the selected version or refuses, never current bytes.
-7. If this is the separately proved initial empty bootstrap, start with no LTX at TXID 0; never write a zero-TXID LTX. Run Seed only for that authorized bootstrap. Render held config and materialize held release before every lifecycle probe, including retries. Preserve normal identity/secret ownership rules and the guarded admin-token write. Then use `spawnClaimed` for each supported direct child; refuse D7's unsupported spawn shapes before any partial startup.
+7. If this is the separately proved initial empty bootstrap, start with no LTX at TXID 0; never write a zero-TXID LTX. Run Seed only for that authorized bootstrap. Render held config and materialize held release before every lifecycle probe, including retries. Preserve normal identity/secret ownership rules and the guarded admin-token write. Then use `spawnClaimed` for each supported direct child, retaining its command and prepared handle through Task 5 cleanup; refuse D7's unsupported spawn shapes before any partial startup.
8. Wait for readiness under the operation's policy, then open replication into the immutable `{location, generation, session}` chosen at admission. Construct the replica client once with `GenerationPrefix`; never reread `holder.generation` to retarget a live writer. Validate every level-0, compaction, snapshot and retention key against that prefix. No outside-prefix request reaches storage. Parent- or sibling-generation keys, traversal and marker keys must be refused by the client.
9. Once the moves-owned marker is durable for that generation, verify it and call `Writer.Published`. This CAS walks local predecessors and queues deduplicated retirement chores; no chore before marker publication. Foreign-region predecessors remain uncovered until their own publication or `CoverRegionPredecessors` inside the region's whole-location park CAS. Retrying publication revalidates session, operation, marker and region.
10. On clean shutdown, stop app writes, close/join replication and all its request producers, and prove every request completed successfully before a create-only `closed/<session>.json` certificate. Include compaction, snapshots, retention and timed-out originals. Keep the replica lease boundary required by D4. A failed/unknown close emits no certificate. Warm admission compares the current session's certificate maximum with the certified remote maximum, restores from remote anyway, and advances session in one CAS. An earlier session's certificate cannot authorize continuation.
@@ -1099,9 +1206,9 @@ Expected: all pass; `gofmt -l .` is empty. Use the existing shared on-disk cache
Push the implementation branch to amadan, describe final changes and actual evidence, squash to one change commit, wait for CI on the exact tip, then use `amadan branch merge carlos/platform feat/hostclaim-box -expect <tip> -message "..."` under the repository's reviewed-branch policy. Read main's CI afterward. These are later implementation/landing steps, not actions performed while writing this file. Keep activation, the floor and production rollout in Plan 8.
-## Authoring verification
+## Original authoring verification
-On 2026-10-05, the 13 complete Go file blocks above were checked in a disposable on-disk source export outside this repository. The export used `origin/main` at `562f3dfc`, the real `hostclaim`/`hostguard`/`stopevidence`/`cgroup` packages from `origin/feat/hostclaim-boxguard` at `8c40aca6`, and `genselect` from `origin/feat/hostclaim-genselect` at `574d9469`. Since Plan 4 was not committed, its published rev 3 production blocks and exact additive claim-schema patch supplied `park` and the guard retirement extension. This validates the published interfaces; it is not evidence that a future merge of the unfinished park branch is already green.
+Before rev 1, on 2026-10-05, the original 13 complete Go file blocks were checked in a disposable on-disk source export outside this repository. The export used `origin/main` at `562f3dfc`, the real `hostclaim`/`hostguard`/`stopevidence`/`cgroup` packages from `origin/feat/hostclaim-boxguard` at `8c40aca6`, and `genselect` from `origin/feat/hostclaim-genselect` at `574d9469`. Since Plan 4 was not committed, its published rev 3 production blocks and exact additive claim-schema patch supplied `park` and the guard retirement extension. This validates the published interfaces; it is not evidence that a future merge of the unfinished park branch is already green.
Verification results:
@@ -1114,3 +1221,11 @@ Verification results:
- Orchestra and the read-only amadan #234 lookup were blocked by sandbox networking. Explicitly named Sonnet source-audit and Opus authority-review attempts each timed out after 45 seconds without a result. **No Opus review verdict was obtained.** The implementation/landing review remains required.
The production integrations in Tasks 6–10 are specified, not executed by this authoring check. The repository-wide behavioral test gate remains an implementation requirement; the checks above do not claim it passed for code that has not been implemented. No production resource, flag, service, branch, index or commit was changed. The disposable source/scratch files were removed after verification; only this requested Markdown file remains changed.
+
+## Self-review
+
+Rev 1 checked both independent findings against Plan 3's actual `Prepared`/`Cleanup` implementations and §6.4a. Task 5 now transfers the prepared handle to the process lifecycle owner on successful and failed starts. Its cleanup contract preserves the reservation on every incomplete cleanup and checks `Close` only after waiting and proving emptiness. The new Linux regression performs spawn → `OpenCleanup` kill/drain → wait → original-handle close → second spawn, and explicitly verifies that closing the cleanup handle alone leaves the reservation held.
+
+D4 now records the certificate fields, immutable per-session key, create-only write and exact remote maximum comparison as settled requirements. Only request completion accounting, the Close race and lease/publication ordering remain open there; certificate publication and warm admission continue to refuse until those providers exist.
+
+Rev 1 verification used a fresh disposable export of `origin/main` at `562f3dfc`, with the real `internal/cgroup` package from `8c40aca6` and a Go overlay of the exact revised spawn files plus the existing `hostwrite`/timer adapters. Focused tests for those adapters passed with `-race`; `go vet` passed for `internal/hostwrite`, `internal/activator` and `internal/schedule`. The same packages built for Linux/amd64, Darwin/amd64, Windows/amd64 and Linux/arm64. All seven overlaid blocks matched `gofmt`. The new Linux spawn/cleanup/second-spawn test compiled but skipped without its explicit delegated-service opt-in. A subsequent `systemd-run --user` attempt was refused by the sandbox at the user-bus connection, so real-cgroup execution remains unverified here. The default cache was tried and refused as read-only; checks used the pre-existing shared disk cache `/home/paulca/.cache/tmp/gocache`. No production integration or whole-repository behavioral gate is claimed by this revision check.