plan: claim Plan 6b rev 4, review round 3 (isolated launches carry the reservation; handover authorizes the recorded predecessor)
Co-Authored-By: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+638
−139
docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md+638 −139
CI passed — run details
diff --git a/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md b/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md| index d939c5e04..c14925356 100644 |
| --- a/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md |
| +++ b/docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md |
| @@ -1,11 +1,13 @@ |
| # Host lifecycle claim, Plan 6b: production box assembly |
| -Implementation plan, 2026-10-07, rev 3. Build on `feat/hostclaim-box-assembly`, cut from `origin/main` at **2b2cc879a669b850bf8ec410b13d20673709477d**. Plan 6 landed as `d649d8d4`; this base includes `84c80eba`, region HA plan 1b and the subsequent main changes. This document supplies the next implementation steps. Authoring it stages nothing, commits nothing and creates no implementation branch. |
| +Implementation plan, 2026-10-07, rev 4. Build on `feat/hostclaim-box-assembly`, cut from `origin/main` at **1da67e63e481c3a7b1768c4309c42c20dea3c254**. Plan 6 landed as `d649d8d4`; this base includes `84c80eba`, region HA plan 1b and the subsequent main changes, including temporary static sites at `f0606ae1`. This document supplies the next implementation steps. Authoring it stages nothing, commits nothing and creates no implementation branch. |
| Rev 2 answers all four round-1 findings: P2-1 moves inbound, config-gate and co-spawn refusals before admission effects (Task 4); P2-2 retains the persisted attempt's boot/controller tuple and routes restart recovery through root's dead-controller proof (Task 7); P2-3 journals recoverable pre-launch state before restore or materialization (Tasks 3, 5 and 7); P2-4 rebases every block onto current main and retains `boxid.Verifier.Enroll` and main's region HA readers (Tasks 1 and 6). Task 2 supplies regression tests; the authoring verification records the executed failures and gates. |
| Rev 3 answers the three round-2 findings. P2-1 preserves main's flag-off adoption ordering and fatal registry diagnostics, including unit arming before a failed registry open (Task 7). P2-2 retires the persisted launch tuple through recorded-session recovery and the authenticated root endpoint, preserving original receipt bytes across claim bookkeeping changes (Task 7). P2-3 validates the executable before creating a role child, commits an exact-parent intention before mkdir, and lets root settle only an absent or empty intended child after drain or controller-death proof (Tasks 3, 5 and 7). Task 2 adds failing-first regressions. These restore three invariants: flag-off effects retain main's ordering; cleanup authority follows the durable launch owner; and every created role child is recoverable before it can receive a process. |
| +Rev 4 answers round 3. P1 removes the agent-side tenant execution fallback and refuses every current enabled launch shape before admission (Tasks 5 and 7). Main's nested exec path uses root-owned isolated systemd units, and that protocol cannot yet carry a durable per-role reservation. Flat, foreign, systemd, seed, standalone-sidecar and co-spawn paths also refuse. This revision therefore supports no enabled tenant launch; adding a reservation-bearing root protocol is an explicit activation gate. The invariant is that claim guarding never weakens main's tenant isolation: tenant code cannot execute in the agent's mount namespace or obtain writable cgroup or journal authority. P2 authenticates the current requester separately from the exact completed predecessor receipt, retaining its recorded boot, controller and directory revision (Task 7). The invariant is that cleanup authority follows the durable predecessor, while authentication follows the live requester. The final replay refreshes the console patches onto `1da67e63`, preserving the temporary-sites additions that landed during verification. Task 2 adds refusal regressions, restart/reboot handover through the next wake's admission and restore, and an opt-in kernel isolation witness. |
| + |
| > For agentic workers: apply the tasks in order, tests before implementation. Keep `CARLOS_HOST_CLAIM` unset. Do not execute branch creation or deployment commands in the design worktree. The code blocks below are complete new files or exact patches against the pinned main base. Use the extractor below to apply patches. Missing authority refuses the dependent operation before an effect. |
| The work covers review findings 1 through 4 in `.superpowers/sdd/2026-10-05-host-lifecycle-claim-6-box-wiring/review-610.md` from the `box` worktree: startup composition, durable wake retries, reconcile admission and retirement, and root evidence and role launches. It uses claim **rev 6.30**, especially section 1.1 and the ruled P6-D1 through P6-D8 in section 6.6. Plan 6 rev 4 remains implementation history; its landed code is already on main. Do not replay it. |
| @@ -20,6 +22,7 @@ The flag uses main's trimmed-nonempty test. `0` enables it. With the flag unset, |
| | Plan 8b, `2026-10-06-host-lifecycle-claim-8b-floor-roster.md` | `hostactors.Directory`, sole signed `control/host-actors.json` roster, verified enrollment/cloud tuple, `hostfloor.Disk` and executing-image gate | Refuse enabled startup and root mutations. `ClaimDependencyError` names the missing provider. Neither environment values nor heartbeat data substitute for this verification. | |
| | Moves index, `origin/design-moves-any-region`, `2026-10-06-moves-phase1-0-index.md` | C1 admitted-start/operation attempt adapter; C2 selected held timers; C4 regional park worker; `MoveInputs`, `InstallManifest`, `RenderHeld`, `MaterializeHeld`, marker publication and combined live view | Refuse affected held/prepared/probe work and destructive sweeps. No arbitrary journal strings, locally invented prepared wire shape, or generation-only live view. | |
| | Plan 5 / ownership backfill | `hostbox.Reconciler.Adopt` and ownership-acquisition validation through the #232 coordinator | Existing unexplained installations remain unadopted. The blank-installation census below does not assign existing artifacts to a new stamp. | |
| +| Main isolated exec (#126), this plan's activation gate | Reservation-bearing extension of the root instance-control protocol and final isolated launcher | No enabled tenant launch is supported by these blocks. Main's nested units cannot consume the reservation yet. Do not register an agent-side launcher as a substitute. | |
| | Region HA | `relocating`, released coverage, once-selected executor and two-box witnesses | Keep relocation unsupported. Moves Phase 1 remains restricted to one eligible box per region. | |
| `ClaimLaunchInputs` below is only an in-process projection of the owner's validated inputs. It is not a persisted contract or a replacement for moves' types. The adapter must retain and check the original observation, holder, stamp, session, operation, phase, executor and region. It selects recovery inputs from `recovery.inputs`, not obsolete forward `move.inputs`. Origin-qualified files manifests are established before app execution and remain independent of the database baseline. |
| @@ -34,7 +37,7 @@ The production binding seam is explicit: the production entrypoints pass their a |
| - Reserve the serving attempt before admitting a writer; bind the resulting session before app execution. An ambiguous admission, failed start or crash does not refund the reservation. Exact invocation health or a conditional authenticated reset clears the episode. Probe budgets belong to the operation and cannot clear serving failure. |
| - Capture work before reading its inputs. Recheck the captured tuple immediately before each effect. A changed hostname occupant cannot be recaptured into old work. |
| - Persist the host fence and launch reservation before draining or execution. Validate the executable before creating a role child; persist its exact-parent intention before mkdir and its child identity before Start. Stop evidence proves producer termination and kernel observations; it does not prove completion of already accepted remote requests. |
| -- Every supported role launches in its own empty child of the recorded parent. Unsupported systemd, seed, standalone-sidecar or co-spawned shapes refuse the complete enabled launch until their reservation-bearing launch path exists. Legacy root verbs cannot supply a bypass. |
| +- Claim guarding never weakens main's tenant isolation. Every future enabled role launch must pass its durable reservation through main's root-owned isolated launcher, with tenant code unable to write the delegated cgroup tree, claim journals or launch journals. These blocks refuse all current shapes, including nested exec, because none has that protocol. Flat, foreign, systemd, seed, standalone-sidecar and co-spawn paths refuse before admission. Legacy root verbs cannot supply a bypass. |
| - Guard before registry transaction. Do not hold SQL transactions, activator mutexes or hostguard across drain, renewal, evidence publication or phase acknowledgement. |
| - Archive exact route and timer contents durably before deletion. Match incarnation, stamp, ownership, row identity and timer registration identity. Recovery preserves a successor at the same key. |
| - Every regional request is bounded to 120 seconds, including SDK retries. Aggregate leases renew and cancel requests when any constituent is lost. Verification is strictly later than the original evidence time plus 17 minutes. Retries retain the original evidence and scope. |
| @@ -52,7 +55,7 @@ git merge-base --is-ancestor 84c80eba origin/main |
| git switch -c feat/hostclaim-box-assembly origin/main |
| ``` |
| -Expected: a clean implementation checkout and the new assembly branch from the latest fetched main. These blocks were verified at `2b2cc879a669b850bf8ec410b13d20673709477d`. If main has moved, compare it with that base and refresh the patches before applying them. Preserve main's `boxid.Verifier.Enroll`, `termfence`, `systemparticipation.go`, `boxrecord`, `edgesclaims` and `boxvolumes`; do not replay region HA plan 1b or reintroduce its superseded readers. Preserve the existing flag-off and hostbox fixes, including main's nested-default layout and startup layout recovery. The enabled claim path remains limited to its explicitly supported launch shapes. |
| +Expected: a clean implementation checkout and the new assembly branch from the latest fetched main. These blocks were verified at `1da67e63e481c3a7b1768c4309c42c20dea3c254`. If main has moved, compare it with that base and refresh the patches before applying them. Preserve main's `boxid.Verifier.Enroll`, `termfence`, `systemparticipation.go`, `boxrecord`, `edgesclaims` and `boxvolumes`; do not replay region HA plan 1b or reintroduce its superseded readers. Preserve the existing flag-off and hostbox fixes, including main's nested-default layout and startup layout recovery. Keep the main isolated-exec design and its console layout door (`carlos system layout`) intact. These blocks support no enabled launch shape until the root protocol carries and validates the reservation. |
| Read these exact local sources before changing code: |
| @@ -106,7 +109,7 @@ for kind, name, body in blocks: |
| ```bash |
| python3 "$TMPDIR/claim6b-apply.py" docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md tests |
| -nice -n 10 go test -p 1 ./internal/cgroup ./internal/hostguard ./internal/registry ./internal/hostbox ./internal/activator ./internal/host ./internal/provision ./internal/console ./internal/instancectl ./internal/schedule -run '^Test(Claim|ServingRetries|Renewing|AggregateContext|Identity|AdapterSelectionLayout|Launch|Prelaunch|PreLaunch|Recover|EmptyCensus|RegistryRows|ColdHandover|HostWakeReset|RootRetirement|BeginFence|Cleanup|Fence|StartAdmitted|SameAdmission|SetupAt|RunAdoptDisabledClaims)' -count=1 |
| +nice -n 10 go test -p 1 ./internal/cgroup ./internal/hostguard ./internal/registry ./internal/hostbox ./internal/activator ./internal/host ./internal/provision ./internal/console ./internal/instancectl ./internal/instanceexec ./internal/schedule -run '^Test(Claim|ServingRetries|Renewing|AggregateContext|Identity|AdapterSelectionLayout|Launch|Prelaunch|PreLaunch|Recover|EmptyCensus|RegistryRows|ColdHandover|HostWakeReset|RootRetirement|BeginFence|Cleanup|Fence|StartAdmitted|SameAdmission|SetupAt|RunAdoptDisabledClaims)' -count=1 |
| ``` |
| Expected red: missing assembly, retry, journal, registry-ownership, delivery-admission and handover APIs. These tests must be present before the implementations below. No passing test that used an unavailable cgroup delegation counts as a kernel witness. |
| @@ -4497,7 +4500,7 @@ func TestLaunchExecutableValidationPrecedesChild(t *testing.T) { |
| return RoleProducer{}, nil |
| }, Executable: func(context.Context, *exec.Cmd) (*os.File, error) { checked = true; return nil, invalid }} |
| attempt := hostclaim.WakeRetryAttempt{ID: "a", InvocationID: "inv", LaunchInputs: "inputs", Session: 1, Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "ctrl"} |
| - _, child, _, _, err := l.Start(ctx, "main", attempt, func() *exec.Cmd { return exec.Command("unused") }, nil) |
| + _, child, _, err := l.prepareRoleReservation(ctx, "main", attempt, func() *exec.Cmd { return exec.Command("unused") }) |
| if !errors.Is(err, invalid) || !checked || child != nil { |
| t.Fatalf("checked=%v child=%v err=%v", checked, child, err) |
| } |
| @@ -5067,6 +5070,532 @@ func TestClaimRetirementRefusesMissingOrAmbiguousLaunchJournal(t *testing.T) { |
| } |
| ``` |
| +#### New file: `internal/host/claimhandover_recovery_test.go` |
| + |
| +```go |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "crypto" |
| + "crypto/rand" |
| + "crypto/rsa" |
| + "crypto/sha256" |
| + "crypto/x509" |
| + "encoding/base64" |
| + "encoding/json" |
| + "errors" |
| + "fmt" |
| + "net" |
| + "net/http" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/boxid" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +type handoverRecoveryRoster struct{ actor hostbox.Actor } |
| + |
| +func (r handoverRecoveryRoster) Actor(context.Context, string) (hostbox.Actor, error) { |
| + return r.actor, nil |
| +} |
| + |
| +type handoverRecoveryEvidence []byte |
| + |
| +func (e handoverRecoveryEvidence) Load(context.Context, string) ([]byte, error) { |
| + return e, nil |
| +} |
| + |
| +func handoverRecoveryIdentity(t *testing.T, actor hostbox.Actor) hostbox.IdentityProvider { |
| + t.Helper() |
| + key, err := rsa.GenerateKey(rand.Reader, 2048) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + actor.Bucket, actor.Namespace, actor.Eligible = "data", "platform", true |
| + actor.Cloud = hostbox.CloudIdentity{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"} |
| + doc, err := json.Marshal(map[string]string{"accountId": actor.Cloud.Account, "region": actor.Cloud.Region, "instanceId": actor.Cloud.InstanceID}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + sum := sha256.Sum256(doc) |
| + signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, sum[:]) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, err := json.Marshal(boxid.Record{V: 1, Box: actor.Box, IdentityDocument: base64.StdEncoding.EncodeToString(doc), IdentitySignature: base64.StdEncoding.EncodeToString(signature)}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return hostbox.IdentityProvider{Account: actor.Cloud.Account, Box: actor.Box, Region: actor.Region, AWSRegion: actor.Cloud.Region, Bucket: actor.Bucket, Namespace: actor.Namespace, Roster: handoverRecoveryRoster{actor}, Evidence: handoverRecoveryEvidence(raw), Certs: map[string]*x509.Certificate{actor.Cloud.Region: {PublicKey: &key.PublicKey, PublicKeyAlgorithm: x509.RSA}}} |
| +} |
| + |
| +func postRecoveryHandover(t *testing.T, socket string, request coldHandoverRequest) int { |
| + t.Helper() |
| + body, err := json.Marshal(request) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { |
| + return (&net.Dialer{}).DialContext(ctx, "unix", socket) |
| + }} |
| + defer transport.CloseIdleConnections() |
| + req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, "http://instance-control/claim/handover", bytes.NewReader(body)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + response, err := (&http.Client{Transport: transport}).Do(req) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer response.Body.Close() |
| + return response.StatusCode |
| +} |
| + |
| +// The kernel and lease are fixtures. Recovery, persisted receipts, the Unix |
| +// root endpoint, cold handover, writer admission and next restore are real. |
| +func TestClaimColdHandoverAfterControllerRecoveryReachesNextWake(t *testing.T) { |
| + for _, boot := range []string{"boot-1", "boot-2"} { |
| + t.Run(boot, func(t *testing.T) { |
| + ctx := t.Context() |
| + f := newRecoveryFixture(t) |
| + // Production reads claims and publishes evidence in the same bucket. |
| + f.evidence = f.claims |
| + former := f.controller("boot-1", "controller-1", 4101, "start-1") |
| + admitted := former.admit(t, f) |
| + f.revision = 8 |
| + restarted := f.controller(boot, "controller-2", 4202, "start-2") |
| + route := registry.Route{Host: recoveryHost, Kind: "instance", ClaimOwnership: registry.ClaimOwnership{ClaimIncarnation: admitted.Claim.Holder.Incarnation, ClaimStamp: admitted.Claim.Holder.Stamp, ClaimOwner: "old-installation", ClaimRowID: "old-route"}} |
| + f.kernel.alive = true |
| + if err := restarted.runtime.Prerequisites(ctx, route, f.read(), recoveryPolicy); !errors.Is(err, ErrClaimStopControllerAlive) { |
| + t.Fatalf("live predecessor accepted: %v", err) |
| + } |
| + f.kernel.alive = false |
| + if err := restarted.runtime.Prerequisites(ctx, route, f.read(), recoveryPolicy); !errors.Is(err, hostbox.ErrChanged) { |
| + t.Fatalf("restart recovery: %v", err) |
| + } |
| + if retry := f.retry(); retry.State != "open" || retry.AttemptsReserved != 1 || retry.Attempt != nil { |
| + t.Fatalf("recovery refunded or stranded attempt: %+v", retry) |
| + } |
| + prior := f.read() |
| + receipt, err := f.guard.CompletedColdPredecessor(ctx, prior.Host, hostguard.Captured{Incarnation: prior.Claim.Holder.Incarnation, Stamp: prior.Claim.Holder.Stamp}, prior.Claim.Holder.Session, prior.Claim.Op.ID, prior.Claim.Op.Phase) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if receipt.Target.ControllerID != "controller-1" || receipt.Target.BootID != "boot-1" || receipt.Target.DirectoryRevision != "7" { |
| + t.Fatalf("recovered tuple was rewritten: %+v", receipt.Target) |
| + } |
| + base := t.TempDir() |
| + inventory, err := hostbox.OpenStopInventoryDB(filepath.Join(base, "inventory.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { inventory.Close() }) |
| + reg, err := registry.Open(filepath.Join(base, "registry.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { reg.Close() }) |
| + if err = reg.Upsert(route); err != nil { |
| + t.Fatal(err) |
| + } |
| + owner := route.ClaimOwnership |
| + in := hostbox.StopInventory{Complete: true, Host: route.Host, Incarnation: owner.ClaimIncarnation, InstallationStamp: owner.ClaimStamp, Box: "box-1", Enrollment: "enrollment-1", DirectoryRevision: "7", Entries: []hostbox.StopInventoryEntry{{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: route.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}}} |
| + if err = inventory.WriteStopInventory(ctx, in); err != nil { |
| + t.Fatal(err) |
| + } |
| + root := ClaimRootHTTP{Launches: f.journal, Stop: ClaimStopHTTP{Coordinator: f.root(recoveryStopDrain{}), Authenticate: func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil }}, Roots: map[string]string{"data": f.guard.DataDir}, Reconcile: hostbox.ReconcileAdapter{Box: "box-1", Enrollment: "enrollment-1", Registry: reg, Inventory: inventory, Retirement: hostbox.TypedRetirement{JournalDir: filepath.Join(base, "retire")}}} |
| + socket := startRetirementRecoveryTransport(t, root) |
| + unauthenticated := root |
| + unauthenticated.Stop.Authenticate = func(context.Context, net.Conn) (StopPeer, error) { return StopPeer{}, errors.New("unknown requester") } |
| + refusedSocket := startRetirementRecoveryTransport(t, unauthenticated) |
| + hook := coldAdmissionHook(f.guard, handoverRecoveryIdentity(t, restarted.local.Actor), socket) |
| + restarted.wake.AfterAdmission = func(ctx context.Context, before, after hostclaim.Observed) error { |
| + h := after.Claim.Holder |
| + request := coldHandoverRequest{Prior: receipt.Target, Next: hostbox.ColdSuccessor{Host: after.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Generation: h.Generation, Box: "box-1", Enrollment: "enrollment-1", DirectoryRevision: "8", ETag: after.ETag, Op: after.Claim.Op.ID, Phase: after.Claim.Op.Phase}} |
| + if status := postRecoveryHandover(t, refusedSocket, request); status != http.StatusForbidden { |
| + t.Fatalf("receipt authenticated an unknown requester: %d", status) |
| + } |
| + for _, field := range []string{"controller", "boot", "revision", "box", "enrollment"} { |
| + forged := request |
| + switch field { |
| + case "controller": |
| + forged.Prior.ControllerID = "unknown-controller" |
| + case "boot": |
| + forged.Prior.BootID = "unknown-boot" |
| + case "revision": |
| + forged.Prior.DirectoryRevision = "8" |
| + case "box": |
| + forged.Prior.Box = "other-box" |
| + case "enrollment": |
| + forged.Prior.Enrollment = "other-enrollment" |
| + } |
| + if status := postRecoveryHandover(t, socket, forged); status != http.StatusBadRequest { |
| + t.Fatalf("forged %s accepted: %d", field, status) |
| + } |
| + } |
| + if row, err := reg.Get(route.Host); err != nil || row == nil || row.ClaimRowID != route.ClaimRowID { |
| + t.Fatalf("refusal changed predecessor row: %+v %v", row, err) |
| + } |
| + if status := postRecoveryHandover(t, socket, request); status != http.StatusNoContent { |
| + t.Fatalf("exact completed predecessor receipt refused: HTTP %d", status) |
| + } |
| + return hook(ctx, before, after) |
| + } |
| + // The fixture supplies the lease; the real Admit invokes root's cold |
| + // handover before activation, prelaunch persistence and retry binding. |
| + f.now = f.retry().NotBefore |
| + next := restarted.admit(t, f) |
| + if next.Claim.Holder.Session <= admitted.Claim.Holder.Session { |
| + t.Fatal("next wake reused predecessor session") |
| + } |
| + if retry := f.retry(); retry.AttemptsReserved != 2 || retry.Attempt == nil || retry.Attempt.ControllerID != "controller-2" || retry.Attempt.BootID != boot { |
| + t.Fatalf("next attempt not charged to current requester: %+v", retry) |
| + } |
| + currentRow, err := reg.Get(route.Host) |
| + if err != nil || currentRow == nil || currentRow.ClaimStamp != next.Claim.Holder.Stamp || currentRow.ClaimRowID == route.ClaimRowID { |
| + t.Fatalf("next wake ownership: %+v %v", currentRow, err) |
| + } |
| + prepared, err := restarted.runtime.Prepare(ctx, *currentRow, next, recoveryPolicy) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = prepared.Restore(ctx); err != nil { |
| + t.Fatalf("next wake restore: %v", err) |
| + } |
| + marker := filepath.Join(f.guard.DataDir, "state", "data.db.gen") |
| + if body, err := os.ReadFile(marker); err != nil || strings.TrimSpace(string(body)) != next.Claim.Holder.Generation { |
| + t.Fatalf("next wake generation: %q %v", body, err) |
| + } |
| + replayed, err := f.guard.FenceReceiptFor(ctx, receipt.Target) |
| + if err != nil || !bytes.Equal(replayed.Bytes, receipt.Bytes) || !replayed.EvidenceAt.Equal(receipt.EvidenceAt) || replayed.Target != receipt.Target { |
| + t.Fatalf("handover rewrote predecessor evidence: %+v %v", replayed, err) |
| + } |
| + launch, err := f.journal.Read(ctx, recoveryTuple(next, restarted.local)) |
| + if err != nil || launch.ControllerPID != 4202 || launch.ProcessStart != "start-2" || launch.Target.DirectoryRevision != fmt.Sprint(f.revision) { |
| + t.Fatalf("next wake launch tuple: %+v %v", launch, err) |
| + } |
| + }) |
| + } |
| +} |
| +``` |
| + |
| +#### New file: `internal/instanceexec/claim_isolation_linux_test.go` |
| + |
| +```go |
| +//go:build linux |
| + |
| +package instanceexec |
| + |
| +import ( |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "syscall" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/instancefs" |
| + "github.com/carlosframework/platform/internal/provision" |
| +) |
| + |
| +// This is an opt-in kernel witness, not a mock or a go-test-on-the-host check. |
| +// The external sandbox wrapper must build this package's test binary and run |
| +// only this test through main's isolated tenant launcher as the tenant payload. |
| +// It must first verify that the edge's delegated cgroup controls exist and are |
| +// writable by carlos in the host namespace, and that all four journal paths |
| +// below are existing regular files in that namespace. Use disposable sandbox |
| +// fixtures, never create fixtures in a production journal directory. Set |
| +// CARLOS_CLAIM_ISOLATION_HOST to the sandbox host, then set both switches below |
| +// only after those host-side checks pass. The assertion cannot establish the |
| +// existence of files deliberately hidden from its own namespace. |
| +// |
| +// A missing opt-in skips. Missing host fixture attestation or incorrect mount |
| +// facts fail. Local CI skips do not prove isolation and cannot enable rollout. |
| +// Opens have no O_CREATE, O_TRUNC or write calls, even if isolation is broken. |
| +func TestClaimTenantCannotWriteContainmentOrJournals(t *testing.T) { |
| + if os.Getenv("CARLOS_CLAIM_ISOLATION_KERNEL") != "1" { |
| + t.Skip("requires sandbox fixtures and main's isolated tenant launcher") |
| + } |
| + if os.Getenv("CARLOS_CLAIM_ISOLATION_HOST_FIXTURES_VERIFIED") != "1" { |
| + t.Fatal("external wrapper must verify host-side journal and delegated cgroup fixtures") |
| + } |
| + host := os.Getenv("CARLOS_CLAIM_ISOLATION_HOST") |
| + if !provision.ValidHostShape(host) { |
| + t.Fatal("sandbox host fixture is missing or invalid") |
| + } |
| + if os.Geteuid() == 0 || os.Getpid() != 1 { |
| + t.Fatal("witness must run as the non-root tenant payload in its private PID namespace") |
| + } |
| + mounts, err := os.ReadFile("/proc/self/mountinfo") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for mountpoint, filesystem := range map[string]string{"/data": "tmpfs", "/sys/fs/cgroup": "cgroup2"} { |
| + readOnly, err := instancefs.MountOptionOf(mountpoint, "ro") |
| + if err != nil || !readOnly { |
| + t.Fatalf("%s must be a read-only mount: %v, %v", mountpoint, readOnly, err) |
| + } |
| + found := false |
| + for _, line := range strings.Split(string(mounts), "\n") { |
| + left, right, ok := strings.Cut(line, " - ") |
| + fields, fs := strings.Fields(left), strings.Fields(right) |
| + if ok && len(fields) >= 6 && fields[4] == mountpoint && len(fs) >= 1 { |
| + found = fs[0] == filesystem |
| + } |
| + } |
| + if !found { |
| + t.Fatalf("%s must have filesystem %s in the tenant namespace", mountpoint, filesystem) |
| + } |
| + } |
| + // The tenant's /data is a fresh read-only tmpfs exposing only instances. |
| + // This is the hiding mechanism; ENOENT alone would be a vacuous assertion. |
| + entries, err := os.ReadDir("/data") |
| + if err != nil || len(entries) != 1 || entries[0].Name() != "instances" || !entries[0].IsDir() { |
| + t.Fatalf("unexpected tenant data namespace: %+v, %v", entries, err) |
| + } |
| + for _, name := range []string{"cgroup.procs", "cgroup.threads", "cgroup.subtree_control"} { |
| + path := filepath.Join("/sys/fs/cgroup/carlos.slice/carlos-edge.service", name) |
| + info, err := os.Stat(path) |
| + if err != nil || !info.Mode().IsRegular() { |
| + t.Fatalf("delegated control fixture %s must exist: %v", path, err) |
| + } |
| + stat, ok := info.Sys().(*syscall.Stat_t) |
| + if !ok || int(stat.Uid) != os.Geteuid() || info.Mode().Perm()&0200 == 0 { |
| + t.Fatalf("%s must be delegated to the tenant UID before read-only mount enforcement", path) |
| + } |
| + f, err := os.OpenFile(path, os.O_WRONLY, 0) |
| + if err == nil { |
| + f.Close() |
| + t.Fatalf("tenant can open %s for write", path) |
| + } |
| + if !errors.Is(err, syscall.EROFS) { |
| + t.Fatalf("%s refusal must be EROFS, not missing fixture or DAC: %v", path, err) |
| + } |
| + } |
| + // Opening cgroup.procs and cgroup.threads for write is necessary to move |
| + // a process or thread. Their refusal proves migration is unavailable |
| + // without attempting to move this process or alter another cgroup. |
| + for _, path := range []string{ |
| + "/data/.hostclaims/launches.db", |
| + "/data/.hostclaims/inventory.db", |
| + "/data/.hostguard/claim-isolation-fixture.json", |
| + filepath.Join("/data/hostguard", host+".json"), |
| + } { |
| + f, err := os.OpenFile(path, os.O_WRONLY, 0) |
| + if err == nil { |
| + f.Close() |
| + t.Fatalf("tenant can open journal %s for write", path) |
| + } |
| + if !errors.Is(err, syscall.ENOENT) { |
| + t.Fatalf("journal %s must be hidden by the verified tenant data mount: %v", path, err) |
| + } |
| + } |
| +} |
| +``` |
| + |
| +#### New file: `internal/activator/claim_isolation_test.go` |
| + |
| +```go |
| +package activator |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/cgroup" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| +) |
| + |
| +func TestClaimRoleLaunchRefusesWithoutIsolatedRoot(t *testing.T) { |
| + for _, role := range []string{"main", "seed", "sidecar", "unknown"} { |
| + t.Run(role, func(t *testing.T) { |
| + journal, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "launch.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer journal.Close() |
| + target := hostguard.FenceTarget{Host: "tenant.test", Incarnation: "inc", Stamp: 1, Session: 1, Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "ctrl"} |
| + work, err := (hostwrite.Config{}).Select(t.Context(), target.Host, "spawn") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + launch := RoleLaunch{Parent: &cgroup.Prepared{}, ParentPath: "parent", Work: work, Target: target, Journal: journal, Executable: func(context.Context, *exec.Cmd) (*os.File, error) { |
| + t.Fatal("unsupported launch inspected executable") |
| + return nil, nil |
| + }, Initialize: func(context.Context) error { |
| + t.Fatal("unsupported launch initialized state") |
| + return nil |
| + }, RegisterProducer: func(context.Context, string) (RoleProducer, error) { |
| + t.Fatal("unsupported launch registered a producer") |
| + return RoleProducer{}, nil |
| + }} |
| + cmd, child, settled, producer, err := launch.Start(t.Context(), role, hostclaim.WakeRetryAttempt{ID: "a", InvocationID: "inv", LaunchInputs: "inputs", Session: 1, Box: "box", Enrollment: "enroll", BootID: "boot", ControllerID: "ctrl"}, func() *exec.Cmd { |
| + t.Fatal("agent built tenant command without isolated root support") |
| + return nil |
| + }, func() error { t.Fatal("unsupported launch reached before-start effect"); return nil }) |
| + if !errors.Is(err, hostbox.ErrUnsupported) || cmd != nil || child != nil || settled != nil || producer.Context != nil || producer.Finish != nil { |
| + t.Fatalf("launch returned execution capability: cmd=%v child=%v err=%v", cmd, child, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestClaimSpawnDisabledKeepsLegacyBuild(t *testing.T) { |
| + calls, builds := 0, 0 |
| + dir := t.TempDir() |
| + cmd, prepared, err := spawnClaimedBefore(t.Context(), false, nil, "tenant.test", cgroup.TierDefault, claimSpawnWork{true, &calls}, hostwrite.Identity{}, func() *exec.Cmd { |
| + builds++ |
| + cmd := exec.Command(filepath.Join(dir, "absent-executable")) |
| + cmd.Dir, cmd.Env = dir, []string{} |
| + return cmd |
| + }, func() error { t.Fatal("disabled helper invoked claim callback"); return nil }) |
| + if builds != 1 || calls != 0 || cmd == nil || prepared != nil || !errors.Is(err, os.ErrNotExist) { |
| + t.Fatalf("legacy dispatch changed: builds=%d guards=%d cmd=%v prepared=%v err=%v", builds, calls, cmd, prepared, err) |
| + } |
| +} |
| +``` |
| + |
| +#### New file: `internal/host/claim_isolation_test.go` |
| + |
| +```go |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "reflect" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/activator" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +type isolationInputs struct { |
| + recoveryInputs |
| + roles []string |
| +} |
| + |
| +func (in isolationInputs) Select(ctx context.Context, rt registry.Route, o hostclaim.Observed, policy activator.StartPolicy) (ClaimLaunchInputs, error) { |
| + selected, err := in.recoveryInputs.Select(ctx, rt, o, policy) |
| + selected.Roles = in.roles |
| + return selected, err |
| +} |
| + |
| +func TestClaimWakeRefusesEveryUnisolatedShapeBeforeAdmission(t *testing.T) { |
| + for _, shape := range []string{"nested", "flat", "foreign", "systemd", "seed", "standalone-sidecar", "co-spawn"} { |
| + t.Run(shape, func(t *testing.T) { |
| + f := newRecoveryFixture(t) |
| + c := f.controller("boot-1", "controller-1", 4101, "41") |
| + // Install the concrete production preflight, not a fixture override. |
| + c.wake.Roles = &ClaimRoleAssembly{Manager: c.wake.Manager, Journal: f.journal} |
| + roles := []string{"main"} |
| + route := registry.Route{Host: recoveryHost, Kind: "instance", Hibernate: true} |
| + switch shape { |
| + case "nested": |
| + route.DBPath = provision.NestedDBPath(f.guard.DataDir, route.Host) |
| + route.Socket = filepath.Join(provision.SocketDir, "instances", provision.InstanceID(route.Host), "app.sock") |
| + case "flat": |
| + route.DBPath = provision.DefaultDBPath(f.guard.DataDir, route.Host) |
| + route.Socket = filepath.Join(provision.SocketDir, route.Host+".sock") |
| + case "foreign": |
| + route.DBPath, route.Socket = "/foreign/data.db", "/foreign/app.sock" |
| + case "systemd": |
| + route.Unit = "tenant.service" |
| + case "seed": |
| + roles = []string{"seed"} |
| + case "standalone-sidecar": |
| + route.Kind = "sidecar" |
| + case "co-spawn": |
| + roles = []string{"main", "sidecar"} |
| + } |
| + c.wake.Inputs = isolationInputs{recoveryInputs{f}, roles} |
| + before := f.read() |
| + guardBefore, err := os.ReadDir(filepath.Join(f.guard.DataDir, "hostguard")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + err = c.runtime.Prerequisites(t.Context(), route, before, recoveryPolicy) |
| + if !errors.Is(err, hostbox.ErrUnsupported) { |
| + t.Fatalf("shape %s: %v", shape, err) |
| + } |
| + if after := f.read(); after.ETag != before.ETag || after.Claim.Holder.WakeRetry != nil { |
| + t.Fatalf("refusal changed claim or spent retry: %+v", after) |
| + } |
| + guardAfter, err := os.ReadDir(filepath.Join(f.guard.DataDir, "hostguard")) |
| + if err != nil || !reflect.DeepEqual(guardBefore, guardAfter) || len(c.wake.pending) != 0 || len(f.requests) != 0 { |
| + t.Fatalf("refusal reached guard/stop/lease: err=%v pending=%d requests=%d", err, len(c.wake.pending), len(f.requests)) |
| + } |
| + if _, err := f.journal.Read(t.Context(), recoveryTuple(before, c.local)); err == nil { |
| + t.Fatal("refusal journaled a launch") |
| + } |
| + }) |
| + } |
| +} |
| +``` |
| + |
| +#### Patch: `internal/activator/claimspawn_test.go` |
| + |
| +```diff |
| +--- a/internal/activator/claimspawn_test.go |
| ++++ b/internal/activator/claimspawn_test.go |
| +@@ -9,0 +10 @@ |
| ++ "github.com/carlosframework/platform/internal/hostbox" |
| +@@ -33 +34 @@ |
| +- if deny && !errors.Is(err, hostwrite.ErrUnavailable) { |
| ++ if !errors.Is(err, hostbox.ErrUnsupported) { |
| +@@ -37 +38 @@ |
| +- if builds != 0 || calls != 2 { |
| ++ if builds != 0 || calls != 0 { |
| +``` |
| + |
| +#### Patch: `internal/activator/claimspawn_kernel_linux_test.go` |
| + |
| +```diff |
| +--- a/internal/activator/claimspawn_kernel_linux_test.go |
| ++++ b/internal/activator/claimspawn_kernel_linux_test.go |
| +@@ -14 +13,0 @@ |
| +- "github.com/carlosframework/platform/internal/hostwrite" |
| +@@ -24 +23 @@ |
| +-func TestClaimSpawnCleanupRespawn(t *testing.T) { |
| ++func TestClaimContainmentCleanupRespawn(t *testing.T) { |
| +@@ -33 +31,0 @@ |
| +- calls := 0 |
| +@@ -42 +40,8 @@ |
| +- cmd, prepared, err := spawnClaimed(t.Context(), true, manager, host, cgroup.TierDefault, claimSpawnWork{false, &calls}, hostwrite.Identity{}, build) |
| ++ // This tests the cgroup primitive with a synthetic test child. It |
| ++ // grants no claim launch authority and is not a tenant-isolation test. |
| ++ prepared, err := manager.PrepareStrict(host, cgroup.TierDefault) |
| ++ if err != nil { |
| ++ t.Fatal(err) |
| ++ } |
| ++ cmd := build() |
| ++ err = prepared.Start(cmd) |
| +@@ -81,3 +85,0 @@ |
| +- if calls != 2 { |
| +- t.Fatal(calls) |
| +- } |
| +``` |
| + |
| ## Task 3: persist containment, inventory and guard recovery |
| Persist reservations before spawn and intentions before materialization. Record the admitted session's boot ID, controller ID, PID, process-start identity and directory revision with its empty parent before restore. Append executable/state file identities and role/producer reservations to that same record when they become available. Store controller PID and kernel process-start identity with each reservation. A complete inventory is an explicit validated record. Recovery never turns missing inventory into proof of emptiness. The guard's durable fence remains authoritative after agent restart, root restart and reboot. Cold handover adds a successor barrier before parking predecessor artifacts; only the exact completed handover can clear it. |
| @@ -8465,7 +8994,11 @@ func (r *ColdRows) UnmarshalJSON(body []byte) error { |
| ## Task 5: reserve serving retries and role launches |
| -Read-only prerequisite failures spend no attempt. Reserve before writer admission, bind the new session before any spawn, and settle every failure through the exact root stop target. A failed admission also releases its held lease; it does not refund retry authority. Recovery uses the persisted attempt, including its boot and controller identity, and refuses while its former controller can still run. Persist the exact pre-launch attempt and controller identity before restore or materialization. Restore failure, materialization failure and a crash between them must remain discoverable by stop and settlement. Prepare the concrete per-role launch only after those steps complete. Pin a verified executable descriptor before child creation. Under the captured guard and pinned parent, verify the child name is absent, durably journal its intended ownership, then create it. Promote that intention to an exact-identity reservation before registering the producer or executing Start. A failed mkdir or preparation leaves an intention, never permission to execute. After a durable fence and drain, or proof of controller death, root settles an absent intended child or records an empty direct child under the same parent. Populated children, unknown descendants, changed parent identities and symlinks refuse. Reboot recovery requires the old parent hierarchy to be absent. Retain every spent attempt; a new attempt uses a new child name. |
| +The agent must never execute tenant code directly. Main routes nested exec through `ExecBackend.startUnit`, the root instance-control service, isolated templates and the `instanceexec` trampoline. Its current start/seed protocol contains no claim reservation. Refuse all enabled shapes with `ErrClaimIsolationUnavailable`, before input selection, retry reservation, writer admission, restore, materialization or cgroup creation. `RoleLaunch.Start` and the inherited `spawnClaimedBefore` helper also refuse independently and return no process or containment handle. The latter retains its exact flag-off branch. There is no direct `child.Start(cmd)` fallback. Keep the flag-off backend selection and layout recovery unchanged. |
| + |
| +The reservation primitive below remains preparatory code for a later root integration. `prepareRoleReservation` validates the executable before child creation and retains the rev 3 durable intention ordering, but has no execution capability and is not called by `Start`. To enable any shape, extend main's authenticated root protocol to carry the exact durable reservation, validate its tuple and executable identity at root, and revalidate it in the final isolated launcher immediately before tenant execution. Preserve main's mount, PID, IPC, syscall, filesystem and cgroup protections. Passing a bare launch command, file path, systemd start result or callback is insufficient. Implement that protocol and its containment witnesses before changing the refusal. |
| + |
| +Read-only prerequisite failures spend no attempt. Reserve before writer admission, bind the new session before any spawn, and settle every failure through the exact root stop target. A failed admission also releases its held lease; it does not refund retry authority. Recovery uses the persisted attempt, including its boot and controller identity, and refuses while its former controller can still run. Persist the exact pre-launch attempt and controller identity before restore or materialization. Restore failure, materialization failure and a crash between them must remain discoverable by stop and settlement. When the root integration is implemented, prepare the concrete per-role launch only after those steps complete. Pin a verified executable descriptor before child creation. Under the captured guard and pinned parent, verify the child name is absent, durably journal its intended ownership, then create it. Promote that intention to an exact-identity reservation before any future root launch is allowed to register the producer or execute tenant code. A failed mkdir or preparation leaves an intention, never permission to execute. After a durable fence and drain, or proof of controller death, root settles an absent intended child or records an empty direct child under the same parent. Populated children, unknown descendants, changed parent identities and symlinks refuse. Reboot recovery requires the old parent hierarchy to be absent. Retain every spent attempt; a new attempt uses a new child name. |
| Mount reset-wake-retry on POST /api/cli/ops/hosts/{host}/resolve with human operator authentication, flag gating, strict bounded JSON, conditional incarnation/episode checks and durable operator-request identity. The Plan 8b host-resolution dispatcher owns the other actions. When the branches meet, put this action in that dispatcher's action switch and retain its declare-lost/readmit routes and checks; do not register a second handler for the same pattern. This reset grants eligibility only. |
| @@ -8570,6 +9103,40 @@ Mount reset-wake-retry on POST /api/cli/ops/hosts/{host}/resolve with human oper |
| +} |
| ``` |
| +#### Patch: `internal/activator/claimspawn.go` |
| + |
| +```diff |
| +--- a/internal/activator/claimspawn.go |
| ++++ b/internal/activator/claimspawn.go |
| +@@ -20,23 +20,3 @@ |
| +- if selected == nil || build == nil { |
| +- return nil, nil, hostwrite.ErrUnavailable |
| +- } |
| +- var cmd *exec.Cmd |
| +- var prepared *cgroup.Prepared |
| +- err := selected.With(ctx, identity, func() error { |
| +- var err error |
| +- prepared, err = manager.PrepareStrict(host, limit) |
| +- if err != nil { |
| +- return err |
| +- } |
| +- if err := ctx.Err(); err != nil { |
| +- return err |
| +- } |
| +- if before != nil { |
| +- if err := before(); err != nil { |
| +- return err |
| +- } |
| +- } |
| +- cmd = build() |
| +- return prepared.Start(cmd) |
| +- }) |
| +- return cmd, prepared, err |
| ++ // Main's isolated root launcher does not yet consume claim reservations. |
| ++ // Keep this old helper closed even if a future caller reintroduces it. |
| ++ return nil, nil, ErrClaimIsolationUnavailable |
| +``` |
| + |
| #### New file: `internal/activator/rolelaunch.go` |
| ```go |
| @@ -8577,6 +9144,7 @@ package activator |
| import ( |
| "context" |
| + "fmt" |
| "os" |
| "os/exec" |
| @@ -8586,6 +9154,10 @@ import ( |
| "github.com/carlosframework/platform/internal/hostwrite" |
| ) |
| +// ErrClaimIsolationUnavailable blocks every current launch shape until the |
| +// root instance-control protocol can validate a durable role reservation. |
| +var ErrClaimIsolationUnavailable = fmt.Errorf("%w: isolated root launcher cannot carry claim reservations", hostbox.ErrUnsupported) |
| + |
| type RoleProducer struct { |
| Context context.Context |
| Finish func(error) |
| @@ -8616,7 +9188,6 @@ import ( |
| "context" |
| "crypto/sha256" |
| "encoding/hex" |
| - "errors" |
| "os" |
| "os/exec" |
| "path" |
| @@ -8631,9 +9202,19 @@ import ( |
| "golang.org/x/sys/unix" |
| ) |
| -func (l RoleLaunch) Start(ctx context.Context, role string, attempt hostclaim.WakeRetryAttempt, build func() *exec.Cmd, before func() error) (*exec.Cmd, *cgroup.Prepared, func(context.Context) error, RoleProducer, error) { |
| - if l.RegisterProducer == nil || l.Parent == nil || l.ParentPath == "" || l.Work == nil || l.Journal == nil || l.Executable == nil || build == nil || attempt.ID == "" || attempt.InvocationID == "" || attempt.LaunchInputs == "" || attempt.Session != l.Target.Session || attempt.Box != l.Target.Box || attempt.Enrollment != l.Target.Enrollment || attempt.BootID != l.Target.BootID || attempt.ControllerID != l.Target.ControllerID { |
| - return nil, nil, nil, RoleProducer{}, hostwrite.ErrUnavailable |
| +// Start cannot execute tenant code in the agent's namespace. Main's nested |
| +// path uses root-owned isolated units; its protocol has no reservation field. |
| +// Keep every shape refused until that protocol and its final root guard exist. |
| +func (l RoleLaunch) Start(context.Context, string, hostclaim.WakeRetryAttempt, func() *exec.Cmd, func() error) (*exec.Cmd, *cgroup.Prepared, func(context.Context) error, RoleProducer, error) { |
| + return nil, nil, nil, RoleProducer{}, ErrClaimIsolationUnavailable |
| +} |
| + |
| +// prepareRoleReservation preserves the recoverable reservation primitive for |
| +// the future root integration. It grants no execution capability and Start |
| +// never calls it while the isolated root protocol is unavailable. |
| +func (l RoleLaunch) prepareRoleReservation(ctx context.Context, role string, attempt hostclaim.WakeRetryAttempt, build func() *exec.Cmd) (*exec.Cmd, *cgroup.Prepared, hostbox.LaunchReservation, error) { |
| + if l.Parent == nil || l.ParentPath == "" || l.Work == nil || l.Journal == nil || l.Executable == nil || build == nil || attempt.ID == "" || attempt.InvocationID == "" || attempt.LaunchInputs == "" || attempt.Session != l.Target.Session || attempt.Box != l.Target.Box || attempt.Enrollment != l.Target.Enrollment || attempt.BootID != l.Target.BootID || attempt.ControllerID != l.Target.ControllerID { |
| + return nil, nil, hostbox.LaunchReservation{}, hostwrite.ErrUnavailable |
| } |
| var cmd *exec.Cmd |
| var executable *os.File |
| @@ -8644,8 +9225,6 @@ func (l RoleLaunch) Start(ctx context.Context, role string, attempt hostclaim.Wa |
| }() |
| var child *cgroup.Prepared |
| var reservation hostbox.LaunchReservation |
| - var reserved bool |
| - var producer RoleProducer |
| err := l.Work.With(ctx, l.Identity, func() error { |
| if l.Initialize != nil { |
| if err := l.Initialize(ctx); err != nil { |
| @@ -8689,70 +9268,9 @@ func (l RoleLaunch) Start(ctx context.Context, role string, attempt hostclaim.Wa |
| if err = l.Journal.Reserve(ctx, reservation); err != nil { |
| return err |
| } |
| - reserved = true |
| - producer, err = l.RegisterProducer(ctx, reservation.ID) |
| - if err != nil { |
| - return err |
| - } |
| - if producer.Context == nil || producer.Finish == nil { |
| - return hostwrite.ErrUnavailable |
| - } |
| - // ExtraFiles preserves the authenticated executable descriptor across exec. |
| - cmd.Path = "/proc/self/fd/" + strconv.Itoa(3+len(cmd.ExtraFiles)) |
| - cmd.ExtraFiles = append(cmd.ExtraFiles, executable) |
| return nil |
| }) |
| - if err == nil && before != nil { |
| - err = before() |
| - } |
| - if err == nil { |
| - err = l.Work.With(ctx, l.Identity, func() error { |
| - inventory, e := l.Journal.Read(ctx, l.Target) |
| - if e != nil { |
| - return e |
| - } |
| - matched := false |
| - for _, saved := range inventory.Reservations { |
| - expected := reservation |
| - expected.State = "reserved" |
| - if saved == expected { |
| - matched = true |
| - } |
| - } |
| - if !matched { |
| - return hostbox.ErrChanged |
| - } |
| - if e = producer.Context.Err(); e != nil { |
| - return e |
| - } |
| - if e = ctx.Err(); e != nil { |
| - return e |
| - } |
| - if e = child.Start(cmd); e != nil { |
| - return e |
| - } |
| - start, e := roleProcessStart(cmd.Process.Pid) |
| - if e != nil { |
| - return e |
| - } |
| - return l.Journal.Started(ctx, reservation, cmd.Process.Pid, start) |
| - }) |
| - } |
| - |
| - settle := func(ctx context.Context) error { |
| - if !reserved { |
| - return nil |
| - } |
| - if child == nil { |
| - return hostwrite.ErrUnavailable |
| - } |
| - populated, e := child.Populated() |
| - if e != nil || populated { |
| - return errors.Join(e, hostwrite.ErrUnavailable) |
| - } |
| - return l.Journal.Settled(ctx, reservation) |
| - } |
| - return cmd, child, settle, producer, err |
| + return cmd, child, reservation, err |
| } |
| func roleProcessStart(pid int) (string, error) { |
| @@ -8836,7 +9354,7 @@ import ( |
| ) |
| func (l RoleLaunch) Start(context.Context, string, hostclaim.WakeRetryAttempt, func() *exec.Cmd, func() error) (*exec.Cmd, *cgroup.Prepared, func(context.Context) error, RoleProducer, error) { |
| - return nil, nil, nil, RoleProducer{}, hostwrite.ErrUnavailable |
| + return nil, nil, nil, RoleProducer{}, ErrClaimIsolationUnavailable |
| } |
| func (l RoleLaunch) ExactHealth(context.Context, *exec.Cmd, hostclaim.WakeRetryAttempt) error { |
| @@ -9092,7 +9610,7 @@ func (d ServingRetries) Wrap(base *ClaimRuntime) (*ClaimRuntime, error) { |
| ```diff |
| --- a/internal/console/authcli.go |
| +++ b/internal/console/authcli.go |
| -@@ -483,0 +484 @@ |
| +@@ -500,0 +501 @@ |
| + s.cliRoute("POST /api/cli/ops/hosts/{host}/resolve", svcrole.Set{}, s.handleAPIHostResolve) |
| ``` |
| @@ -9206,7 +9724,7 @@ func (s *Server) handleAPIHostResolve(w http.ResponseWriter, r *http.Request) { |
| ```diff |
| --- a/internal/console/server.go |
| +++ b/internal/console/server.go |
| -@@ -459,4 +459,5 @@ |
| +@@ -463,4 +463,5 @@ |
| - HostClaim bool |
| - DBVersions lease.VersionedStore |
| - Origin string // the console's public origin \u2014 the OAuth client_id |
| @@ -10194,10 +10712,20 @@ func (h ClaimRootHTTP) serveColdHandover(w http.ResponseWriter, r *http.Request, |
| var request coldHandoverRequest |
| decoder := json.NewDecoder(io.LimitReader(r.Body, 32<<10)) |
| decoder.DisallowUnknownFields() |
| - if decoder.Decode(&request) != nil || decoder.Decode(new(any)) != io.EOF || request.Prior.BootID != peer.BootID || request.Prior.ControllerID != peer.ControllerID { |
| + if decoder.Decode(&request) != nil || decoder.Decode(new(any)) != io.EOF || request.Prior.Box != h.Reconcile.Box || request.Prior.Enrollment != h.Reconcile.Enrollment { |
| w.WriteHeader(http.StatusBadRequest) |
| return |
| } |
| + if request.Prior.BootID != peer.BootID || request.Prior.ControllerID != peer.ControllerID { |
| + // ServeHTTP authenticates the live requester independently. Recovery |
| + // preserves the predecessor's recorded tuple; only its exact completed |
| + // root receipt grants cleanup authority, never the requester's tuple. |
| + receipt, err := h.Stop.Coordinator.Guard.FenceReceiptFor(r.Context(), request.Prior) |
| + if err != nil || verifyRetirementReceipt(r.Context(), receipt) != nil { |
| + w.WriteHeader(http.StatusBadRequest) |
| + return |
| + } |
| + } |
| if h.Launches == nil { |
| w.WriteHeader(http.StatusServiceUnavailable) |
| return |
| @@ -10823,41 +11351,11 @@ func claimReadyPeer(conn net.Conn, pid int) error { |
| return nil |
| } |
| -func (a *ClaimRoleAssembly) Preflight(ctx context.Context, in ClaimLaunchInputs) error { |
| - if a == nil || a.Manager == nil || a.Manager.Root() == "" || a.Journal == nil || a.Inventory == nil || len(in.Roles) != 1 || in.Roles[0] != "main" { |
| - return hostwrite.ErrUnavailable |
| - } |
| - root := a.Manager.Root() |
| - var stat unix.Statfs_t |
| - if err := unix.Statfs(root, &stat); err != nil { |
| - return err |
| - } |
| - if uint64(stat.Type) != uint64(unix.CGROUP2_SUPER_MAGIC) { |
| - return hostwrite.ErrUnavailable |
| - } |
| - controllers, err := os.ReadFile(filepath.Join(root, "cgroup.subtree_control")) |
| - if err != nil { |
| - return err |
| - } |
| - memory := false |
| - for _, c := range strings.Fields(string(controllers)) { |
| - if c == "memory" { |
| - memory = true |
| - } |
| - } |
| - if !memory { |
| - return hostwrite.ErrUnavailable |
| - } |
| - for _, name := range []string{"cgroup.kill", "cgroup.events", "memory.max", "memory.high", "memory.swap.max", "memory.oom.group"} { |
| - if _, err := os.Lstat(filepath.Join(root, name)); err != nil { |
| - return err |
| - } |
| - } |
| - f, err := verifiedClaimExecutable(ctx, exec.Command(in.Executable), in.Executable, in.ExecutableSHA256) |
| - if err != nil { |
| - return err |
| - } |
| - return f.Close() |
| +// Preflight refuses before retry reservation, admission or state preparation. |
| +// Nested exec uses main's systemd-backed isolated launcher. Neither that door |
| +// nor flat exec can carry a claim reservation yet. |
| +func (a *ClaimRoleAssembly) Preflight(context.Context, ClaimLaunchInputs) error { |
| + return activator.ErrClaimIsolationUnavailable |
| } |
| func (a *ClaimRoleAssembly) PrepareState(ctx context.Context, o hostclaim.Observed, work hostwrite.Work, in ClaimLaunchInputs) error { |
| @@ -10979,7 +11477,7 @@ func (*ClaimRoleAssembly) Launch(context.Context, hostclaim.Observed, hostwrite. |
| func claimReadyPeer(net.Conn, int) error { return hostwrite.ErrUnavailable } |
| func (*ClaimRoleAssembly) Preflight(context.Context, ClaimLaunchInputs) error { |
| - return hostwrite.ErrUnavailable |
| + return activator.ErrClaimIsolationUnavailable |
| } |
| func (*ClaimRoleAssembly) PrepareState(context.Context, hostclaim.Observed, hostwrite.Work, ClaimLaunchInputs) error { |
| @@ -12657,6 +13155,11 @@ func (w *ClaimWakeAssembly) Build(ctx context.Context, source hostbox.WorkSource |
| return w.Stop(ctx, o) |
| } |
| runtime.Prerequisites = func(call context.Context, rt registry.Route, o hostclaim.Observed, p activator.StartPolicy) error { |
| + if w.Roles != nil { |
| + if err := w.Roles.Preflight(call, ClaimLaunchInputs{}); err != nil { |
| + return err |
| + } |
| + } |
| if p.Probe || (o.Claim != nil && o.Claim.Op.Kind != hostclaim.KindServing) { |
| return &ClaimDependencyError{Contracts: []string{"moves C1 operation attempt journal and prepared-open adapter"}} |
| } |
| @@ -12667,11 +13170,6 @@ func (w *ClaimWakeAssembly) Build(ctx context.Context, source hostbox.WorkSource |
| if err != nil { |
| return err |
| } |
| - if w.Roles != nil { |
| - if err := w.Roles.Preflight(call, in); err != nil { |
| - return err |
| - } |
| - } |
| if len(in.Roles) != 1 || in.Roles[0] != "main" || in.Executable == "" || len(in.ExecutableSHA256) != 64 || in.DBPath == "" || in.Identity.Config == "" || in.Identity.Release == "" || in.Admission.Mode != "cold" || (in.Baseline == nil && !in.Empty) { |
| return hostbox.ErrUnsupported |
| } |
| @@ -13688,7 +14186,7 @@ Apply all implementation blocks after the tests-first failure. These tasks share |
| ```bash |
| python3 "$TMPDIR/claim6b-apply.py" docs/superpowers/plans/2026-10-06-host-lifecycle-claim-6b-box-assembly.md implementation |
| gofmt -l . |
| -nice -n 10 go test -p 1 ./internal/cgroup ./internal/hostguard ./internal/registry ./internal/hostbox ./internal/activator ./internal/host ./internal/provision ./internal/console ./internal/instancectl ./internal/schedule -run '^Test(Claim|ServingRetries|Renewing|AggregateContext|Identity|AdapterSelectionLayout|Launch|Prelaunch|PreLaunch|Recover|EmptyCensus|RegistryRows|ColdHandover|HostWakeReset|RootRetirement|BeginFence|Cleanup|Fence|StartAdmitted|SameAdmission|SetupAt|RunAdoptDisabledClaims)' -count=1 |
| +nice -n 10 go test -p 1 ./internal/cgroup ./internal/hostguard ./internal/registry ./internal/hostbox ./internal/activator ./internal/host ./internal/provision ./internal/console ./internal/instancectl ./internal/instanceexec ./internal/schedule -run '^Test(Claim|ServingRetries|Renewing|AggregateContext|Identity|AdapterSelectionLayout|Launch|Prelaunch|PreLaunch|Recover|EmptyCensus|RegistryRows|ColdHandover|HostWakeReset|RootRetirement|BeginFence|Cleanup|Fence|StartAdmitted|SameAdmission|SetupAt|RunAdoptDisabledClaims)' -count=1 |
| nice -n 10 go build -p 1 ./... |
| nice -n 10 go vet -p 1 ./... |
| nice -n 10 go test -p 1 ./... -count=1 |
| @@ -13697,7 +14195,7 @@ GOOS=windows GOARCH=amd64 nice -n 10 go build -p 1 ./... |
| GOOS=linux GOARCH=arm64 nice -n 10 go build -p 1 ./... |
| ``` |
| -Use the default Go build cache. For this authoring session only, the user authorized `GOCACHE=/home/paulca/.cache/tmp/claude-1001/own/gocache-6b3` if the default is read-only; remove that fallback cache when done. Put scratch under `/home/paulca/.cache/tmp/claude-1001/` and remove it after verification. Run Go gates serially with `nice -n 10` and `-p 1`. An exported source tree without Git metadata needs `-buildvcs=false`; that flag changes build stamping, not the code being checked. |
| +Use the default Go build cache. For this authoring session only, the user authorized `GOCACHE=/home/paulca/.cache/tmp/claude-1001/own/gocache-6b4` if the default is read-only; remove that fallback cache when done. Put scratch under `/home/paulca/.cache/tmp/claude-1001/` and remove it after verification. Run Go gates serially with `nice -n 10` and `-p 1`. An exported source tree without Git metadata needs `-buildvcs=false`; that flag changes build stamping, not the code being checked. |
| Review the resulting diff against the parent, including actual callers. The repository requires authority/production review by Opus before implementation lands. Plan authorship and local compilation are not that review. Land the eventual implementation only through the reviewed amadan branch and its green exact-tip gate. This document does not authorize a production rollout or infrastructure change. |
| @@ -13706,33 +14204,36 @@ Review the resulting diff against the parent, including actual callers. The repo |
| Keep CARLOS_HOST_CLAIM unset until the owner adapters and these checks pass in the explicitly approved deployment scope. Current deployments have no authority simply because the constructor is present. |
| 1. Verify flag-off startup, ordinary reconcile, timers and root verbs against the same binary. Compare routes and timer bookkeeping before and after a serial bump. No claim provider opens or worker starts from an unset flag. |
| -2. Run the strict containment tests in a delegated system service with the real required controllers, including io. The authoring machine's user service lacks that controller. Confirm every enabled supported launch uses the recorded session parent and a fresh role child, with no host-level fallback. |
| +2. First implement and review the reservation-bearing root launch protocol; these blocks deliberately support no enabled tenant launch. Run the strict containment tests in an approved sandbox system service with the required controllers, including io. The authoring machine's user service lacks that controller. Build `internal/instanceexec`'s test binary and run `TestClaimTenantCannotWriteContainmentOrJournals` as the payload through main's isolated launcher, with claim mode off for this baseline witness. The wrapper must first verify disposable host-side journal fixtures and same-UID writable delegated cgroup controls, as specified in the test, then set its opt-in environment. Require actual `EROFS` on cgroup controls and hidden journals inside the isolated namespace. Repeat through the reservation-bearing claim launcher once it exists. Skips, permissions-only fixtures and a baseline flag-off pass do not authorize claim enablement. Verify that flat and every unsupported shape still refuse before launch effects. |
| 3. Stop a main process with a forked child holding SQLite, WAL and SHM descriptors. Include renamed and deleted files, a late-created WAL, a producer issuing bounded requests, and a timer HTTP request paused after admission. Root must refuse until every producer and child is joined and the complete descriptor scan is clear. Unreadable proc entries or incomplete inventory must produce no receipt. |
| 4. Kill the agent after role intention commit, after child mkdir but before reservation, after reservation, after pre-launch journal commit, after restore but before materialization, after Start and during readiness. Inject restore and materialization failures. Restart with a new controller identity and a newer roster revision in the same enrollment; root must stop the persisted old tuple, refuse while its old process is alive, and accept an empty producer set only with valid evidence. Retry must stop the exact attempt and retain the spent budget. Test all five failed starts, agent restart, changed release and changed stamp. Only exact backend health or the authenticated conditional operator reset grants eligibility. A service credential and a non-operator cannot reset. |
| 5. Stop root after fencing, after drain, after durable receipt, after an archival fsync and after SQL deletion. Resume each point. Publication retries the original bytes and evidence time. A failed acknowledgement must be replayable even after the route disappeared. A replacement row or path must survive predecessor recovery. |
| 6. Reboot with a pending stop. Prove the old controller cannot exist using the real kernel boot ID. Reconstruct positive evidence from the retained inventory and journal; missing old paths on the same boot or a reused path on the new boot must refuse. A persisted fence must deny returning work before provider refresh. |
| -7. Exercise two consecutive cold admissions. Hold the old session stopped while the successor barrier is durable. Crash at every park/row-restore phase, resume, then create a newer timer registration and replay the completed handover. The registration and current bookkeeping must remain unchanged. No old database bytes may be reused as the new writable database. |
| +7. Restart the controller (also test reboot), recover and complete the old owner's receipt, then exercise two consecutive cold admissions. Hold the old session stopped while the successor barrier is durable. Crash at every park/row-restore phase, resume, then create a newer timer registration and replay the completed handover. The registration and current bookkeeping must remain unchanged. No old database bytes may be reused as the new writable database. |
| 8. Lose each constituent regional lease separately while a remote request is blocked. The request must cancel, every handle must close once, and no later phase may publish success. Test a request accepted remotely just before loss; absence of a response remains uncertain until the original evidence's verification horizon. |
| 9. Deliberately remove each external owner provider. Its enabled operation must refuse by the named contract, without creating a claim, retry attempt, launch, materialization, deletion or substitute local receipt. Test initial session-zero empty ownership through the Plan 5 positive-empty contract before enabling blank-installation cold starts. |
| -Do not substitute passing filesystem fixtures for these kernel witnesses. The supported implementation here launches a single exec main role. Other backend shapes refuse the complete enabled path until their per-role launch reservations and stop evidence are supplied. Returning-box cleanup that starts from a Plan 8 loss/quarantine record still requires that owner's durable cleanup-authority adapter; an existing exact local fence already supports cleanup during control-store outage. |
| +Do not substitute passing filesystem fixtures for these kernel witnesses. No current shape can launch tenant code with claims enabled. This includes nested exec because main launches it through systemd units, whose root protocol does not carry reservations yet. All launch-dependent rollout witnesses remain blocked on that protocol; the handover test uses a fixture lease and never executes tenant code. Returning-box cleanup that starts from a Plan 8 loss/quarantine record still requires that owner's durable cleanup-authority adapter; an existing exact local fence already supports cleanup during control-store outage. |
| ## Self-review |
| -Rev 3 integration check: |
| +Rev 4 integration check: |
| + |
| +- The direct tenant execution fallback is removed. Production preflight, `RoleLaunch.Start` and the inherited claim-spawn helper independently refuse current shapes; missing root protocol support cannot become agent-UID or agent-namespace execution. The root legacy start/seed refusals and flag-off behavior remain intact. Tenant cgroup/journal denial has an explicit kernel test, which is skipped locally and remains a deployment gate. |
| +- Cold handover authenticates the current requester and authorizes the exact completed predecessor receipt separately. Box/enrollment scope, original evidence bytes/time and old directory revision remain bound. Restart and reboot fixtures continue through the next admission and restore without authorizing an app launch. |
| - Cold admission, exact-health clearing and spent retry budgets remain unchanged. Child-intention settlement grants no execution authority; a fenced session still needs the existing cold handover before a later wake. The fixture proves settlement and a fresh reservation, not a privileged process launch. |
| - Captured work, pre-effect rechecks and lock ordering remain intact. The intention is committed while the original work guard and parent handle are held; root resolves it only after drain or dead-controller proof. Neither path holds a SQL transaction during drain or kernel observation. |
| -- Every supported launch still requires a new empty child, an exact reservation and the final guarded Start. Complete tree and descriptor scans run after intention resolution; unknown children remain refusals. Wire changes are additive and old exact reservations remain readable. |
| +- The preparatory reservation helper still requires a new empty child and an exact reservation; no helper can start a tenant process. A future supported root launch must perform the final guard inside main's isolated path. Complete tree and descriptor scans run after intention resolution; unknown children remain refusals. Wire changes are additive and old exact reservations remain readable. |
| - Retirement keeps exact inventory, archived rows, successor protection and durable receipt replay. Root authenticates the current requester separately from the persisted target owner. The fresh stop-authority check does not replace the original receipt's identity or evidence time. |
| - Regional request limits, lease cancellation and the evidence horizon are unchanged. Cleanup still needs durable authority; missing journals and ambiguous owners refuse before effects. |
| -- All four round-1 corrections remain: side-effect-free admission refusals, persisted retry-owner recovery, pre-launch journaling before restore/materialization, and main's enrollment/region-HA interfaces. Their focused regressions are included in the rev 3 verification. |
| +- All four round-1 corrections remain: side-effect-free admission refusals, persisted retry-owner recovery, pre-launch journaling before restore/materialization, and main's enrollment/region-HA interfaces. Their focused regressions are included in the rev 4 verification. All three round-2 corrections also remain: flag-off adoption ordering, retirement through the persisted launch tuple and recoverable role-child intention before mkdir. Executable-order testing now calls the reservation-only primitive because enabled Start must refuse before reaching it. |
| - Findings 1 through 4 are tied to concrete production callers, not only injectable helper tests: entrypoints, wake callbacks, provisioning create/keep/removal, mounted operator reset, root socket dispatch, scheduled delivery and shutdown. |
| - The flag decision is captured once. Missing external bindings are named. A placement code never supplies AWS signing region or enrollment proof. |
| - Writer retry state is durable. Lease cleanup and failure settlement do not refund attempts. Probe authority cannot clear serving failure. |
| -- The role launcher pins the executable bytes selected by the authenticated release manifest, not a checksum supplied by an app. Readiness uses the direct backend socket and exact process identity, then rechecks the journal's attempt/session/input binding. |
| +- The preparatory reservation helper pins the executable bytes selected by the authenticated release manifest, not a checksum supplied by an app. Existing readiness helpers retain direct backend and exact invocation checks, but neither readiness nor these helpers grants launch authority while the root protocol is absent. |
| - Cold ownership handover parks the complete predecessor and preserves its archive. Restored rows receive new ownership while timer registration and scheduling state stay exact. A guard barrier closes the crash interval between remote admission and local completion. |
| - The inventory includes managed directories so late-created WAL/SHM names remain in descriptor scope. Missing inventory and reused identities refuse. Unresolved materialization follows only the explicit typed intention recovery below; it never licenses arbitrary artifact capture. Shared directories and the whole registry database are excluded from host retirement. |
| - Root authenticates kernel peer identity and validates evidence independently. Serving stop and operation evidence cannot be confused. Durable cleanup permits old exact-target completion, never new publication authority or successor deletion. |
| @@ -13742,24 +14243,22 @@ Rev 3 integration check: |
| ### External completion gates |
| -These are implementation dependencies, not undecided design choices: Plan 8's verified controller/deadline/quarantine adapter; Plan 8b's signed roster, floor and running-image adapter; Plan 5's positive empty/backfill ownership authority; and moves' held-input selection, operation attempts, timer replacement, marker publication and complete database/files live view. The default production binding refuses until those typed adapters are installed. Region HA remains a separate supported-placement gate. |
| +The reservation-bearing extension of main's isolated root launch protocol is required before any tenant shape can run with claims enabled. It must preserve main's namespace isolation and bind every supported role to the durable reservation; it is not supplied by the external owner adapters. Other implementation dependencies are: Plan 8's verified controller/deadline/quarantine adapter; Plan 8b's signed roster, floor and running-image adapter; Plan 5's positive empty/backfill ownership authority; and moves' held-input selection, operation attempts, timer replacement, marker publication and complete database/files live view. The default production binding refuses until those typed adapters are installed. Region HA remains a separate supported-placement gate. |
| ### Authoring verification |
| -Every one of the 113 file blocks was replayed with this document's extractor into a fresh on-disk export of `origin/main` at `2b2cc879a669b850bf8ec410b13d20673709477d`. A final read of the remote main confirmed that same tip. The replay matched the authored source byte for byte, and whole-tree `gofmt -l .` produced no output. Only this plan was changed in the design worktree. No branch, index or commit was written, and Plan 5's console-ops document was not touched. |
| +All 120 file blocks were replayed with this document's extractor into a fresh on-disk export of `origin/main` at `1da67e63e481c3a7b1768c4309c42c20dea3c254`. Main advanced from `2b2cc879` during verification. The final replay refreshes `internal/console/authcli.go` and `internal/console/server.go` against the temporary-sites merge, preserving its changes and placing the operator reset route at registration time. The new-file blocks match the replay byte for byte. Whole-tree `gofmt -l .` and the plan's whitespace check were clean. Only this plan changed in the design worktree; no branch, index or commit was written, and Plan 5 was not touched. |
| -Applying the tests before implementation produced the expected missing-API compilation failures. Six behavioral failing-first checks then exercised the actual defects: the old adoption ordering failed before reporting unit arming; the old role-launch ordering rejected containment before checking the executable; omitting the durable child intention failed the post-mkdir recovery test; substituting the current controller tuple failed retirement; requiring the current claim ETag rejected the persisted receipt after bookkeeping; and restoring the old HTTP peer/target equality rejected retirement of the recovered tuple. Each corrected path subsequently passed. |
| +Applying tests before implementation produced the expected missing-API compilation failures. Behavioral checks also failed against the old paths: `RoleLaunch.Start` reached initialization without an isolated root launcher, the inherited claim-spawn helper reached its old guard path, and the handover endpoint returned HTTP 400 for an exact completed predecessor after both controller restart and reboot. The corrected paths passed. The handover fixture advances to the existing retry deadline before the next attempt; it does not bypass or refund the retry policy. |
| -Final authoring gates ran serially with `nice -n 10`, `-p 1` and `-buildvcs=false`: |
| +Final gates ran serially under `nice -n 10`, with `-p 1` and `-buildvcs=false` on every Go command: |
| -- `go build ./...`, `go vet ./...` and `go test ./... -run '^$' -count=1` passed. The last command compiles every test package without running the full suite. |
| +- `go build ./...`, `go vet ./...` and `go test ./... -run '^$' -count=1` passed. The last command compiles every test package without executing the full suite. |
| - Darwin/amd64, Windows/amd64 and Linux/arm64 builds passed. |
| -- Focused host, hostguard, activator, hostbox, registry, provision, instancectl, cgroup and schedule tests passed. These include the round-1 admission refusals, retry restart/revision recovery, pre-launch restore/materialization/crash recovery, intended-state recovery, enrollment and layout checks, plus the rev 3 adoption, role-intention and retirement tests. Console `TestHostWakeReset` cases passed separately. |
| - |
| -The adoption regression uses the real startup path and `ensureSystemUnits` against a temporary unit directory with a systemctl fixture. It verifies installed unit files, enable/start calls and the fatal registry diagnostic in a child test process with isolated credentials and working directory. The retirement regression uses the disk journal, real guard and stop coordinator, then the Unix HTTP retirement endpoint. It requires root's controller-death proof, keeps the old roster revision, replays the original receipt after a bookkeeping CAS, refuses an unknown old target, retires the original row and preserves its successor on replay. |
| +- The focused command in Task 9 passed for cgroup, hostguard, registry, hostbox, activator, host, provision, console, instancectl, instanceexec and schedule. It includes the new launch refusals, flag-off helper behavior and restart/reboot handover, plus the earlier admission refusals, persisted retry and retirement tuples, restore/materialization/crash recovery, role intentions, adoption ordering, enrollment, layout and operator-reset regressions. |
| -The child-recovery fixture persists the intention before the actual mkdir, fails preparation before reservation, reopens the journal as after a crash, rejects a populated child, settles the empty child and reserves a new attempt without overwriting the spent attempt. Additional cases cover missing children, unknown descendants, symlinks, replaced parents and reboot absence of both parent and ancestors. These fixtures verify ordering and durable recovery, not privileged cgroup containment or successful application execution; the delegated kernel and retry-launch witnesses remain in the rollout list. |
| +The handover test uses real durable journals, the guard, cold handover, a Unix HTTP endpoint, signed enrollment verification and the next admission/restore. Its lease, kernel and requester authentication are fixtures; it never executes tenant code. It rejects unknown requesters and forged predecessor controller, boot, revision, box and enrollment fields, preserves the old receipt bytes/time, and charges the next attempt to the current controller. The new isolation kernel test is opt-in and skipped locally. It must run as an actual isolated tenant payload against verified disposable host fixtures before it counts as a kernel witness. The existing synthetic child cleanup test remains a cgroup-primitive test and grants no claim-launch authority. |
| -The invariant review covered the three round-2 fixes and all four round-1 fixes. It also closed two retirement integration gaps exposed during review: the current authenticated controller can submit an old target only with its exact completed root receipt, and the root handler binds its local box/enrollment scope from the verified actor. Receipt replay retains its original evidence time and tuple. No authority blocker remained in the final Codex review. Sonnet and Opus were attempted but unavailable because the Claude session limit was exhausted; GPT-5.6 Sol handled the bounded adoption and retirement changes, and GPT-6 Astra reviewed the authority paths. This does not replace the repository's required Opus review before the eventual implementation lands. |
| +The invariant review covered every integration invariant, all four round-1 corrections and all three round-2 corrections. Opus reviewed the isolation refusals, predecessor authorization, inherited helper closure and the request, scheduled, sidecar, adoption and root-service launch paths; it reported no blockers. That static plan review does not replace review and the exact-tip amadan gate for the eventual implementation. |
| -Full-suite execution and the privileged rollout witnesses remain implementation gates. No live deployment or infrastructure change was performed. The default Go cache was read-only; the explicitly authorized fallback was `/home/paulca/.cache/tmp/claude-1001/own/gocache-6b3`. That cache and all scratch created for rev 3 were removed after verification. |
| +No enabled tenant launch shape is implemented by these blocks. Main's isolated root protocol must carry and validate the durable role reservation before any shape can be enabled. Full-suite execution and the privileged rollout witnesses remain implementation gates. No live deployment or infrastructure change was performed. The default Go cache was read-only, so verification used the brief's authorized `/home/paulca/.cache/tmp/claude-1001/own/gocache-6b4`. That fallback cache and every scratch file created for rev 4 were removed after verification. |