carlos / platform Public

plan: claim Plan 11 rev 3 (review r2: tenant-exec boundary check, prefix-covering no-process witness, tenant-child verification, claim-launch artifact cleanup on flag-off)

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 0df82f5bbe81496ab6a6345376687264f41f928b parent 44ac8e1
1 file changed, +2182 −47
  • docs/superpowers/plans/2026-10-09-host-lifecycle-claim-11-region-ha-integration.md +2182 −47

CI passed — run details

diff --git a/docs/superpowers/plans/2026-10-09-host-lifecycle-claim-11-region-ha-integration.md b/docs/superpowers/plans/2026-10-09-host-lifecycle-claim-11-region-ha-integration.md
index fa382dd32..d779b6fd0 100644
--- a/docs/superpowers/plans/2026-10-09-host-lifecycle-claim-11-region-ha-integration.md
+++ b/docs/superpowers/plans/2026-10-09-host-lifecycle-claim-11-region-ha-integration.md
@@ -1,9 +1,11 @@
# Host lifecycle claim, Plan 11: region HA integration
-Implementation plan, 2026-10-09, rev 2. This implements the claim session's accepted answers in amadan discussion #236: R1-R5, the narrowed R8, R10, `relocation-release` and `enrolled_at`. Claim spec rev 6.31 is normative. This plan authorizes no production changes.
+Implementation plan, 2026-10-09, rev 3. This implements the claim session's accepted answers in amadan discussion #236: R1-R5, the narrowed R8, R10, `relocation-release` and `enrolled_at`. Claim spec rev 6.31 is normative. This plan authorizes no production changes.
**Rev 2 — review round 1:** answers both P1 and both P2 findings in Task D's R10 root launch, with executable code and failing-first regressions. The published region HA signatures are unchanged. The root launch protocol capability becomes `isolated-unit-v2`, and internal root/kernel/unit interfaces are extended; agent and root must be upgraded together. The mandatory Task D amendment runs after G. Kinds without a root-side authoritative attempt verifier are refused; production operation launches remain gated on those verifiers.
+**Rev 3 — review round 2:** answers both P1 and both P2 findings in `findings-plan11-r2.md`. Task C/D amendment below adds a root-authenticated reservation handshake at the trampoline's `ForkExec` boundary, a final boot-relative deadline check, directory-prefix no-process coverage, tenant-child observation within the pinned invocation, and stopped-unit cleanup before flag-off legacy starts. The published region HA signatures remain unchanged. Root's internal launch capability becomes `isolated-unit-v3`; root, agent and trampoline must be upgraded together. Replay A–G, D2, then D3R and D3. The Rev 1/2 code and results below are historical stages; the Rev 3 amendment is mandatory.
+
## Interfaces for region HA
```go
@@ -13016,62 +13018,2195 @@ nice -n 10 go test -p 1 -timeout 5m ./internal/hostbox ./internal/instancectl ./
Expected: all amendment and existing launch/retry/drain tests pass. A slow valid start makes one unit start, obtains one grant, makes no stop, and leaves `AttemptsReserved` at one.
-## Region HA integration and rollout
-
-Keep all flags off while landing the code. Registration alone does not activate relocation. The rollout still needs Plan 9's verified identity, complete owners, activation census and floor guard. Region HA additionally needs `CARLOS_REGION_HA`; this implementation shares Group D with `CARLOS_MOVES_PHASE1`, so it requires that group to be complete too. Missing owners, an older root protocol, unsupported role shapes and incomplete stop evidence all refuse.
-
-The claim-enabled protocol in Task D carries a main role in main's isolated run unit, using the nested instance layout and a supported memory tier. Seed, sidecar and other layouts remain refused where the reservation cannot describe them. The existing flag-off `/start`, `/seed`, process backend and unit templates retain their existing path. Staged builds must also be root-owned and not writable by the agent or tenant. Verify the deployed stager satisfies that rule before activation. Do not remove those refusals to make a probe pass. Extend the reservation and root verification first if another shape is required.
-
-Before region HA stage B is armed, install and test root-owned authoritative attempt readers for each enabled operation kind, including the prepared executable/input digest and the exact operation/session/install identity. Until then, the new per-kind preflight refuses those launches before admission or attempt reservation. Keep the existing region HA adapter signatures. Also prove the root-owned input snapshot bind, same-invocation namespace/exec wait, final deadline check and snapshot teardown against the deployed unit template in the disposable environment.
-
-Rebase the stage B adapters onto these interfaces and test these joins:
-
-1. Replace its duplicate `AutomaticLoss`, `LocalDischarge`, loss-ID helper and `Claim.EvidenceRefs` with Task A's canonical definitions. Preserve its fence-consuming transitions, D4 retention and strict decode tests.
-2. Register the complete relocation owner set once. Route the probe through `OperationAttempts`; do not apply the old `Extend` hook or introduce a separate probe lifecycle. Keep the same admitted session through `relocating/probing -> relocating/serving`. The built region branch currently commits directly to kind `serving`; that kind change ends this session and requires a separate serving wake. It must not be treated as an implicit continuation.
-3. Reconcile its prepared-generation and `released` coverage admission with the base's generic admission. This plan permits only the accepted cold probe shape. It does not import region HA's transition producer or grant a new coverage authority. An adapter cannot bypass the gate with an optional policy callback.
-4. For a drain, call `ClaimAssembly.WritersStopped`, register the files publisher under `ClaimProducerFiles`, retain replication and file publication, and use root's observation. Drop the region branch's duplicate descriptor observer and consume the claim-owned `(TID, Device, Inode)` descriptor shape. Confirm with the live Litestream witness that descriptors opened lazily after `Replicate` do not leave the narrowing hint incomplete. An incomplete hint refuses the observation. After release publication, queue the stop-only obligation and request the full root stop. Pass its immutable key and digest to `RelocationReleaseReceipts.Complete`, then perform the region-owned executor transition. Never use a writers-only witness as a full stop receipt.
-5. A release without its exact root receipt remains pending, including if the source terminates before producing it. Plan 11 grants no fence-only discharge for this new obligation. That receipt-only rule is the accepted contract; the region-owned termination transition must account for it before activation. It must not silently drop the obligation or route it through generic chore completion.
-6. Preserve the source's files live set until the authoritative record commits. Moves Plan 5 must replace the stand-in view and prove the push, landing-install and rollback-return park exclusions at each destructive step. The model's passing J1 tests do not implement those production exclusions.
-7. Keep the R6 no-LTX refusal for every positive writer session. Keep volume-based automatic discharge refused for legacy roster entries with unknown enrollment time. Publish no invented timestamp or empty generation.
-
-The release floor must include claim-level `evidence_refs` and its nested `AutomaticLoss` fields, roster `enrolled_at`, the `relocation-release` kind, and the launch journal and protocol additions, including `writers_closed`, `close-writers`, the `spec` preimage, capability-only `kind`, and `isolated-unit-v2`. Publish it only after every enrolled actor and console can strictly decode these fields, the signed census proves the same executable set, and the existing guard transaction has armed the new floor. Do not substitute the branch's commit hash for a release tag or floor. Old binaries must not participate after publication. Rollback must respect the published floor.
-
-Run the region HA two-box model and its joint review after the stage B adapters are rewritten. Then exercise isolated launch, cancellation, root crash recovery, writers-only stop and full stop in a designated disposable systemd/cgroup environment. This authoring session used local mocks and kernel fixtures; it did not create or modify production infrastructure.
-
-## Final verification
-
-After all tasks, run the following in the disposable export. Run commands serially; no background test jobs.
-
-```bash
-nice -n 10 go test -p 1 -timeout 5m ./internal/hostactors ./internal/hostclaim ./internal/hostbox ./internal/hostguard ./internal/stopevidence ./internal/instancectl -count=1
-nice -n 10 go test -p 1 -timeout 5m ./internal/host ./internal/activator -run 'TestPlan11|TestRev2Shape|TestStopWriters|TestWriters|TestVerifyWriters|TestVerifyNoProcess|TestNoProcess|TestRootRole|TestRootClaimProcess|TestClaimLaunch|TestClaimUnitStop|TestClaimStop|TestClaimProducer|TestPlan9D|TestClaimIsolation' -count=1
-nice -n 10 go vet ./internal/hostactors ./internal/hostclaim ./internal/hostbox ./internal/hostguard ./internal/stopevidence ./internal/instancectl ./internal/host ./internal/activator
-nice -n 10 go build ./internal/host ./internal/activator ./internal/instancectl
-GOOS=darwin GOARCH=arm64 nice -n 10 go build ./internal/host ./internal/activator ./internal/instancectl
-GOOS=windows GOARCH=amd64 nice -n 10 go build ./internal/host ./internal/activator ./internal/instancectl
-```
-
-Check `gofmt -l` on every changed Go file. Before a later implementation commit lands, run the repository's full build, vet, test and formatting gate and obtain green amadan CI on that tip. This plan's focused overlay verification does not replace that landing gate.
+## Task C/D amendment (Rev 3): execution boundary, witness coverage and flag-off cleanup
-### Rev 1 authoring results
+Run this amendment after D2. It supersedes D2's assumption that authorization immediately before `systemctl start` reaches tenant execution. Type=simple returns while unit setup continues. `instanceexec.runInit` remains the parent and calls `ForkExec` for the tenant.
-The plan was replayed from a fresh export of the pinned base. All fourteen test and implementation patch blocks applied in order, and the resulting changed Go files matched the tested combined overlay byte for byte. No git index or ref was changed.
+The public region HA declarations, `OperationAttempts`, `ClaimLaunchDigest`, and `RootRoleLauncher` keep their signatures. Internal root units now require `StartReserved`; the old `StartChecked` helper remains for preparation tests and is not a production claim-launch provider. Capability `isolated-unit-v3` refuses an older root that cannot enforce the execution boundary.
-The supporting packages `hostactors`, `hostclaim`, `hostbox`, `hostguard`, `stopevidence` and `instancectl` passed their full package tests. The focused host and activator set passed, including Plan 9 dispatch, writers-only stop, descriptor and no-process observations, root launch, receipt validation and disabled-flag regressions. Vet passed for all eight packages. Builds of host, activator and instancectl passed on Linux and cross-compiled for darwin/arm64 and windows/amd64. Formatting was checked on all changed files.
+Root holds the existing host guard across preparation, the unit start, the trampoline handshake and readiness. It freezes argv/environment and a separate permit containing the reservation ID, input digest, boot ID and an absolute `CLOCK_BOOTTIME` deadline. An operation permit expires no later than its captured `SafeUntil`; serving gets a bounded 30-second start window. Renewal cannot extend an already frozen permit. The runtime drop-in forces `instance-exec -claim-reservation=... run %I` and read-only binds the permit, the private launch socket and the kernel boot ID. The boot-ID bind is required because the existing template's `ProcSubset=pid` hides `/proc/sys`. `%I` preserves the template's unescaped instance identifier.
-Red runs first exposed the missing record, enrollment, dispatch, writer-stop, observation and launch interfaces. Integration regressions then demonstrated mutable policy authority, cached witness aliasing, an omitted isolated cgroup, hidden root observation interfaces, a stale console enrollment time, refusal of a valid session-zero release, recovery settling an unobserved unit, and a probe launch before the executor became the admitted writer. Each corresponding guard was fixed and its focused tests passed. The final plan places every test block before its implementation.
+After namespace and Landlock setup, the trampoline authenticates root on the bound socket. Root authenticates the connecting process as the pinned unit MainPID, checks the same systemd invocation, rereads the exact intended reservation and authoritative claim/attempt, and rechecks local participation, the live lock and deadline. A missing, mismatched, replaced, closed or expired reservation receives no permit. The trampoline compares the response with its root-owned read-only permit, closes the connection, and samples boot identity and `CLOCK_BOOTTIME` immediately before `ForkExec`. The mandatory check comes from root's ExecStart argument; tenant argv/environment cannot opt out. Cancellation or a root restart fails closed. Readiness does not create a second attempt.
-Some early delegated socket runs failed because of sandbox restrictions or long Unix socket paths; the parent reran the focused combined set under its short disk-backed TMPDIR and it passed. The long `TestClaimCompletedProducersBoundStopAndRetry` test exceeded an early delegated timeout; it is outside the final focused set. No production systemd unit, deployment, bucket, roster or floor was changed. Real unit isolation and the joint two-box stage B activation remain rollout checks, not claimed authoring results.
+Root observes the tenant child separately from the supervising MainPID. It reads every main-process thread's `children` file because `runInit` forks from a locked thread, requires one direct child, and checks its parent, process start, cgroup, namespace-bound executable and running executable. It repeats identity checks around observation and pins both parent and child while waiting. An ambiguous child set refuses; it never guesses among adopted children. Grants and journal entries retain the supervising MainPID and its start time. The real-process regression runs the actual `runInit` with `/bin/sleep`, with the test executable remaining its parent.
-### Rev 2 authoring results
+The no-process witness retains the validated inventory directory prefixes independently of the current listing. Root scans every process/thread descriptor table against those prefixes, including nested paths and the kernel's ` (deleted)` names, even when no files remain in the listing. Matches include a separator, so an adjacent directory is not covered. Existing named-file/device/inode checks remain. An absent not-yet-materialized directory still retains its prefix; no coverage or an unreadable scan refuses a receipt. The journal and containment checks still bracket the observation.
-The four R10 behavioral regressions were run against the completed Rev 1 overlay first. They failed with the expected behavioral assertions, including the premature kill of a slow valid start. The amendment's successful root fixtures now contain authoritative attempts. Its additional tests cover both serving and operation tuples, missing verifiers, input and executable substitution, pending/lost/expired participation, loss of the live lock, state changes during unit preparation, namespace/exec delay, timeout, cancellation, PID reuse and invocation replacement, and snapshot binding/cleanup.
+A flag-off controller discovers persisted claim artifacts from disk. Legacy run-unit stop/start proves the unit idle and the cgroup empty before removing `60-claim-launch.conf`, reloading systemd, and deleting the launch snapshot, permit and socket. A failure blocks restart and preserves evidence for retry. The closed socket inode remains until that proof: `ExecStopPost` also creates a namespace and needs the bind source. A live replacement invocation is not stopped by the old handshake's cleanup. No artifact-free legacy launch acquires a claim requirement.
-All seventeen patch blocks replayed from the pinned base with zero fuzz. The resulting Go files matched the tested combined overlay byte for byte, and formatting was clean. The full supporting-package tests passed for `hostactors`, `hostclaim`, `hostbox`, `hostguard`, `stopevidence` and `instancectl`; the expanded focused host/activator set passed, including the amendment, launch, retry/drain and Plan 9 dispatch regressions. The focused amendment/launch set also passed from the fresh replay. Vet passed for all eight packages. Host, activator and instancectl built on Linux and cross-compiled for darwin/arm64 and windows/amd64.
+### Failing-first round 2 regressions
-The default Go cache was read-only in the sandbox, so verification used the explicitly permitted shared cache `/home/paulca/.cache/tmp/claude-1001/gocache`. It was not deleted. An initial broad host run exceeded the Unix socket path limit; the bootstrap now uses a shorter disk-backed temporary directory, and the same tests passed there. These setup errors were not counted as regression evidence. No git index, ref or worktree registration was written.
+Apply these tests to the completed Rev 2 export. They use the old interfaces and compile before any Rev 3 implementation. The expected failures are behavioral: tenant execution after setup consumes the deadline, observation of the trampoline executable, false success for deleted/nested/empty-directory holders, and surviving claim artifacts or stale inputs during the flag-off transition.
-Opus reviewed the root authority and snapshot implementation and found no P1 or P2. Its data-root observation is handled by refusing production roots other than the template's `/data`, with a construction regression. The parent checked the final callback ordering, per-kind preflight refusal and replacement-invocation cleanup. Sonnet supplied the preflight shape changes; the parent integrated and verified them. No live systemd mount or production experiment was run. The real namespace/snapshot and region HA stage B checks remain activation gates.
+<!-- apply: D3R tests -->
+```diff
+--- /dev/null
++++ b/internal/host/claimlaunch_rev3_boundary_test.go
+@@ -0,0 +1,38 @@
++package host
++
++import (
++ "context"
++ "github.com/carlosframework/platform/internal/hostbox"
++ "github.com/carlosframework/platform/internal/instancectl"
++ "testing"
++)
++
++type rev3SetupUnits struct {
++ *launchUnits
++ setup func()
++ tenant int
++}
++
++// Rev 2 calls this path: asynchronous setup happens after its only check.
++func (u *rev3SetupUnits) StartChecked(ctx context.Context, in instancectl.ClaimUnit, l instancectl.Launch, check func() error) (instancectl.ClaimUnitStarted, error) {
++ s, err := u.launchUnits.StartChecked(ctx, in, l, check)
++ if err != nil {
++ return s, err
++ }
++ u.setup()
++ u.tenant++
++ return s, nil
++}
++func TestRev3ExpiryDuringUnitSetup(t *testing.T) {
++ f := newLaunchFixture(t, rev2Operation())
++ u := &rev3SetupUnits{launchUnits: f.units, setup: func() { f.root.Clock.(*rev2Clock).boot = 1000 }}
++ f.root.Units = u
++ _, err := f.root.Serve(t.Context(), f.peer, f.req)
++ if err == nil || u.tenant != 0 {
++ t.Fatalf("expired during namespace setup: grant error=%v tenant executions=%d", err, u.tenant)
++ }
++ r, e := f.journal.UnitReservation(t.Context(), f.target, f.req.Reservation.ID)
++ if e != nil || r.State != hostbox.UnitSettled {
++ t.Fatalf("failed launch not settled: %+v %v", r, e)
++ }
++}
+--- /dev/null
++++ b/internal/host/claimlaunch_rev3_child_linux_test.go
+@@ -0,0 +1,78 @@
++//go:build linux
++
++package host
++
++import (
++ "os"
++ "os/exec"
++ "path/filepath"
++ "strconv"
++ "strings"
++ "syscall"
++ "testing"
++ "time"
++
++ "github.com/carlosframework/platform/internal/instancectl"
++)
++
++func TestPlan11Rev3ObservesRealInitTenant(t *testing.T) {
++ raw, err := os.ReadFile("/proc/self/cgroup")
++ if err != nil || !strings.HasPrefix(string(raw), "0::/") {
++ t.Skip("no unified cgroup membership")
++ }
++ rel := strings.TrimSpace(strings.TrimPrefix(strings.SplitN(string(raw), "\n", 2)[0], "0::/"))
++ if _, err := os.Stat(filepath.Join("/sys/fs/cgroup", rel)); err != nil {
++ t.Skip("cgroup tree not visible:", err)
++ }
++ binary := filepath.Join(t.TempDir(), "instanceexec.test")
++ build := exec.Command("go", "test", "-c", "-o", binary, "../instanceexec")
++ if out, err := build.CombinedOutput(); err != nil {
++ t.Fatalf("build init helper: %v\n%s", err, out)
++ }
++ cmd := exec.Command(binary, "-test.run=^TestPlan11Rev3InitObserveHelper$")
++ cmd.Env = append(os.Environ(), "CARLOS_REV3_OBSERVE_HELPER=1")
++ if err := cmd.Start(); err != nil {
++ t.Fatal(err)
++ }
++ t.Cleanup(func() { _ = cmd.Process.Signal(syscall.SIGTERM); _ = cmd.Wait() })
++ deadline := time.Now().Add(5 * time.Second)
++ for {
++ tasks, err := filepath.Glob(filepath.Join("/proc", strconv.Itoa(cmd.Process.Pid), "task", "*", "children"))
++ if err != nil {
++ t.Fatal(err)
++ }
++ found := false
++ for _, path := range tasks {
++ raw, _ := os.ReadFile(path)
++ for _, child := range strings.Fields(string(raw)) {
++ if st, err := os.Stat(filepath.Join("/proc", child, "exe")); err == nil {
++ want, err := os.Stat("/bin/sleep")
++ if err == nil && os.SameFile(st, want) {
++ found = true
++ }
++ }
++ }
++ }
++ if found {
++ break
++ }
++ if time.Now().After(deadline) {
++ t.Fatal("real init did not fork tenant")
++ }
++ time.Sleep(time.Millisecond)
++ }
++ k := linuxClaimLaunchKernel{CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc", BoundApp: "bin/sleep"}
++ started := instancectl.ClaimUnitStarted{Unit: "u", InvocationID: "i", ControlGroup: rel, MainPID: cmd.Process.Pid}
++ got, err := k.Observe(started)
++ if err != nil {
++ t.Fatal(err)
++ }
++ want := plan11LaunchFileID(t, "/bin/sleep")
++ if got.Running != want || got.Executable != want {
++ t.Fatalf("observed trampoline instead of tenant: got=%+v tenant=%+v", got, want)
++ }
++ mainStart, err := claimLaunchProcessStart("/proc", cmd.Process.Pid)
++ if err != nil || got.ProcessStart != mainStart {
++ t.Fatalf("main PID lifetime lost: got=%+v main=%s err=%v", got, mainStart, err)
++ }
++}
+--- /dev/null
++++ b/internal/host/claimstop_noprocess_rev3_linux_test.go
+@@ -0,0 +1,129 @@
++//go:build linux
++
++package host
++
++import (
++ "errors"
++ "os"
++ "path/filepath"
++ "strconv"
++ "testing"
++)
++
++func rev3HoldTarget(t *testing.T, fx *writersKernelFixture, pid, fd int, target string) {
++ t.Helper()
++ dir := filepath.Join(fx.proc, strconv.Itoa(pid), "task", strconv.Itoa(pid), "fd")
++ if err := os.MkdirAll(dir, 0o700); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.Symlink(target, filepath.Join(dir, strconv.Itoa(fd))); err != nil {
++ t.Fatal(err)
++ }
++}
++
++func TestRev3NoProcessCoversDeletedDatabaseAndWAL(t *testing.T) {
++ for _, suffix := range []string{"", "-wal"} {
++ t.Run(suffix, func(t *testing.T) {
++ fx := newWritersKernelFixture(t)
++ fx.dropAgentHolds(t)
++ target := fx.db + suffix
++ for _, path := range []string{fx.db, fx.db + "-wal"} {
++ if err := os.Remove(path); err != nil {
++ t.Fatal(err)
++ }
++ }
++ rev3HoldTarget(t, fx, 31, 8, target+" (deleted)")
++ if _, err := fx.noProcess(t, false); err == nil {
++ t.Fatalf("deleted holder passed: %v", err)
++ }
++ })
++ }
++}
++
++func TestRev3NoProcessCoversNestedInstallationFiles(t *testing.T) {
++ fx := newWritersKernelFixture(t)
++ fx.dropAgentHolds(t)
++ nested := filepath.Join(filepath.Dir(fx.db), "sqlite", "scratch.db")
++ if err := os.MkdirAll(filepath.Dir(nested), 0o700); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.WriteFile(nested, nil, 0o600); err != nil {
++ t.Fatal(err)
++ }
++ rev3HoldTarget(t, fx, 32, 9, nested)
++ if _, err := fx.noProcess(t, false); !errors.Is(err, ErrClaimStopIdentity) {
++ t.Fatalf("nested holder passed: %v", err)
++ }
++}
++
++func TestRev3NoProcessScansWhenInstallationDirectoryIsEmpty(t *testing.T) {
++ fx := newWritersKernelFixture(t)
++ fx.dropAgentHolds(t)
++ if err := os.Remove(fx.db); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.Remove(fx.db + "-wal"); err != nil {
++ t.Fatal(err)
++ }
++ target := filepath.Join(filepath.Dir(fx.db), "gone.db")
++ rev3HoldTarget(t, fx, 33, 10, target+" (deleted)")
++ if _, err := fx.noProcess(t, false); !errors.Is(err, ErrClaimStopIdentity) {
++ t.Fatalf("empty directory suppressed descriptor scan: %v", err)
++ }
++}
++
++func TestRev3NoProcessPrefixHasPathBoundary(t *testing.T) {
++ fx := newWritersKernelFixture(t)
++ fx.dropAgentHolds(t)
++ sibling := filepath.Dir(fx.db) + "-old"
++ if err := os.Mkdir(sibling, 0o700); err != nil {
++ t.Fatal(err)
++ }
++ target := filepath.Join(sibling, "data.db")
++ if err := os.WriteFile(target, nil, 0o600); err != nil {
++ t.Fatal(err)
++ }
++ rev3HoldTarget(t, fx, 34, 11, target)
++ if _, err := fx.noProcess(t, false); err != nil {
++ t.Fatalf("prefix sibling matched: %v", err)
++ }
++}
++
++func TestRev3NoProcessDescriptorScanErrorsFailClosed(t *testing.T) {
++ fx := newWritersKernelFixture(t)
++ fx.dropAgentHolds(t)
++ dir := filepath.Join(fx.proc, "35", "task", "35", "fd")
++ if err := os.MkdirAll(dir, 0o700); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.WriteFile(filepath.Join(dir, "12"), nil, 0o600); err != nil {
++ t.Fatal(err)
++ }
++ if _, err := fx.noProcess(t, false); err == nil {
++ t.Fatal("unreadable descriptor entry passed")
++ }
++}
++
++func TestRev3NoProcessCoversUnlinkedNestedFile(t *testing.T) {
++ fx := newWritersKernelFixture(t)
++ fx.dropAgentHolds(t)
++ for _, p := range []string{fx.db, fx.db + "-wal"} {
++ if err := os.Remove(p); err != nil {
++ t.Fatal(err)
++ }
++ }
++ nested := filepath.Join(filepath.Dir(fx.db), "sqlite", "gone.db")
++ if err := os.MkdirAll(filepath.Dir(nested), 0700); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.WriteFile(nested, nil, 0600); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.Remove(nested); err != nil {
++ t.Fatal(err)
++ }
++ rev3HoldTarget(t, fx, 36, 13, nested+" (deleted)")
++ if _, err := fx.noProcess(t, false); !errors.Is(err, ErrClaimStopIdentity) {
++ t.Fatalf("unlinked nested holder passed: %v", err)
++ }
++}
+--- /dev/null
++++ b/internal/instancectl/legacy_claim_cleanup_rev3_test.go
+@@ -0,0 +1,270 @@
++package instancectl
++
++import (
++ "context"
++ "encoding/json"
++ "errors"
++ "net/http"
++ "net/http/httptest"
++ "os"
++ "path/filepath"
++ "strconv"
++ "strings"
++ "testing"
++
++ "github.com/carlosframework/platform/internal/cgroup"
++ "github.com/carlosframework/platform/internal/provision"
++)
++
++func legacyTier() string { return strconv.FormatInt(cgroup.TierDefault.MemoryMax, 10) }
++
++func legacyClaimFixture(t *testing.T) (*Server, *unitCtl, string, string, string) {
++ t.Helper()
++ u, ctl, in, _ := claimUnitFixture(t)
++ u.UnitRoot, u.DataRoot = t.TempDir(), t.TempDir()
++ if err := os.Chmod(u.UnitRoot, 0700); err != nil {
++ t.Fatal(err)
++ }
++ if err := u.freezeLaunch(t.Context(), in.Host, Launch{Schema: LaunchSchema, Host: in.Host, Argv: []string{"/opt/carlos/app"}}); err != nil {
++ t.Fatal(err)
++ }
++ source, dropin, err := u.launchPaths(in.Host)
++ if err != nil {
++ t.Fatal(err)
++ }
++ ctl.calls = nil
++ s := &Server{
++ Systemctl: ctl.run,
++ CgroupRoot: u.CgroupRoot,
++ ExeRoot: u.ExeRoot,
++ DropInDir: u.UnitRoot,
++ DataRoot: u.DataRoot,
++ cgroupWaitBudget: u.WaitBudget,
++ }
++ return s, ctl, in.Host, filepath.Join(source, "launch.json"), filepath.Join(dropin, "60-claim-launch.conf")
++}
++
++func legacyRequest(t *testing.T, s *Server, path string) *httptest.ResponseRecorder {
++ t.Helper()
++ rec := httptest.NewRecorder()
++ s.Handler().ServeHTTP(rec, httptest.NewRequest(http.MethodPost, path, nil))
++ return rec
++}
++
++func TestRev3ClaimOnFlagOffLegacyStart(t *testing.T) {
++ u, ctl, in, _ := claimUnitFixture(t)
++ u.UnitRoot, u.DataRoot = t.TempDir(), t.TempDir()
++ if err := os.Chmod(u.UnitRoot, 0700); err != nil {
++ t.Fatal(err)
++ }
++ old := Launch{Schema: LaunchSchema, Host: in.Host, Argv: []string{"/old/app", "old-argument"}, Env: []string{"VERSION=old"}}
++ if err := u.freezeLaunch(t.Context(), in.Host, old); err != nil {
++ t.Fatal(err)
++ }
++ source, dropinDir, err := u.launchPaths(in.Host)
++ if err != nil {
++ t.Fatal(err)
++ }
++ snapshot := filepath.Join(source, "launch.json")
++ dropin := filepath.Join(dropinDir, "60-claim-launch.conf")
++
++ // The claim-on server has an active invocation when the feature flag is
++ // turned off and instance-control is reconstructed without ClaimStop.
++ ctl.state = "active"
++ populate(t, ctl.events, true)
++ newLaunch := Launch{Schema: LaunchSchema, Host: in.Host, Argv: []string{"/new/app", "new-argument"}, Env: []string{"VERSION=new"}}
++ raw, err := json.Marshal(newLaunch)
++ if err != nil {
++ t.Fatal(err)
++ }
++ instanceDir := provision.InstanceDir(u.DataRoot, in.Host)
++ if err := os.MkdirAll(instanceDir, 0755); err != nil {
++ t.Fatal(err)
++ }
++ mutable := filepath.Join(instanceDir, LaunchFileName(RoleRun))
++ if err := os.WriteFile(mutable, raw, 0644); err != nil {
++ t.Fatal(err)
++ }
++
++ var provedStopped bool
++ var consumed Launch
++ systemctl := func(ctx context.Context, args ...string) (string, error) {
++ if args[0] == "start" {
++ launchPath := mutable
++ if _, err := os.Stat(dropin); err == nil {
++ launchPath = snapshot
++ }
++ body, err := os.ReadFile(launchPath)
++ if err != nil {
++ return "", err
++ }
++ if err := json.Unmarshal(body, &consumed); err != nil {
++ return "", err
++ }
++ }
++ out, err := ctl.run(ctx, args...)
++ if args[0] == "show" && strings.Contains(args[2], "ActiveState") {
++ for _, path := range []string{snapshot, dropin} {
++ if _, statErr := os.Stat(path); statErr != nil {
++ t.Fatalf("artifact removed before stopped-state proof: %s: %v", path, statErr)
++ }
++ }
++ provedStopped = strings.Contains(out, "ActiveState=inactive")
++ }
++ return out, err
++ }
++ s := &Server{
++ ClaimStop: nil,
++ Systemctl: systemctl,
++ CgroupRoot: u.CgroupRoot,
++ ExeRoot: u.ExeRoot,
++ DropInDir: u.UnitRoot,
++ DataRoot: u.DataRoot,
++ }
++ if rec := legacyRequest(t, s, "/stop?host="+in.Host+"&role=run"); rec.Code != http.StatusNoContent {
++ t.Fatalf("stop=%d: %s", rec.Code, rec.Body.String())
++ }
++ if !provedStopped {
++ t.Fatal("legacy stop did not prove the claim invocation inactive")
++ }
++ for _, path := range []string{snapshot, dropin} {
++ if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
++ t.Fatalf("old claim artifact survived proved stop: %s: %v", path, err)
++ }
++ }
++ if rec := legacyRequest(t, s, "/start?host="+in.Host+"&role=run&memory_max="+legacyTier()); rec.Code != http.StatusOK {
++ t.Fatalf("start=%d: %s", rec.Code, rec.Body.String())
++ }
++ if strings.Join(consumed.Argv, " ") != strings.Join(newLaunch.Argv, " ") || strings.Join(consumed.Env, " ") != strings.Join(newLaunch.Env, " ") {
++ t.Fatalf("legacy start consumed %+v, want new mutable launch %+v", consumed, newLaunch)
++ }
++}
++
++func TestRev3LegacyStartClearsStoppedClaimLaunchBeforeStarting(t *testing.T) {
++ s, ctl, host, snapshot, dropin := legacyClaimFixture(t)
++ rec := legacyRequest(t, s, "/start?host="+host+"&role=run&memory_max="+legacyTier())
++ if rec.Code != http.StatusOK {
++ t.Fatalf("start=%d: %s", rec.Code, rec.Body.String())
++ }
++ for _, path := range []string{snapshot, dropin} {
++ if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
++ t.Fatalf("claim artifact survived legacy start: %s: %v", path, err)
++ }
++ }
++ calls := ctl.joined()
++ if proof, reload, start := strings.Index(calls, "show -p ActiveState"), strings.Index(calls, "daemon-reload"), strings.Index(calls, "start "); proof < 0 || reload < proof || start < reload {
++ t.Fatalf("cleanup did not precede start:\n%s", calls)
++ }
++}
++
++func TestRev3LegacyClaimCleanupFailsClosed(t *testing.T) {
++ for _, orphan := range []string{"permit.json", "exec.sock"} {
++ t.Run("orphaned "+orphan, func(t *testing.T) {
++ s, ctl, host, snapshot, dropin := legacyClaimFixture(t)
++ source := filepath.Dir(snapshot)
++ for _, path := range []string{snapshot, dropin} {
++ if err := os.Remove(path); err != nil {
++ t.Fatal(err)
++ }
++ }
++ orphanPath := filepath.Join(source, orphan)
++ if err := os.WriteFile(orphanPath, []byte("claim boundary"), 0600); err != nil {
++ t.Fatal(err)
++ }
++ ctl.state = "active"
++ populate(t, ctl.events, true)
++ rec := legacyRequest(t, s, "/start?host="+host+"&role=run&memory_max="+legacyTier())
++ if rec.Code != http.StatusBadGateway || strings.Contains(ctl.joined(), "start ") {
++ t.Fatalf("start=%d calls:\n%s", rec.Code, ctl.joined())
++ }
++ if !strings.Contains(ctl.joined(), "show -p ActiveState") {
++ t.Fatalf("orphan did not force stop proof:\n%s", ctl.joined())
++ }
++ if _, err := os.Stat(orphanPath); err != nil {
++ t.Fatalf("live orphan removed: %v", err)
++ }
++ })
++ }
++
++ t.Run("live unit", func(t *testing.T) {
++ s, ctl, host, snapshot, dropin := legacyClaimFixture(t)
++ ctl.state = "active"
++ populate(t, ctl.events, true)
++ rec := legacyRequest(t, s, "/start?host="+host+"&role=run&memory_max="+legacyTier())
++ if rec.Code != http.StatusBadGateway || strings.Contains(ctl.joined(), "start ") {
++ t.Fatalf("start=%d calls:\n%s", rec.Code, ctl.joined())
++ }
++ for _, path := range []string{snapshot, dropin} {
++ if _, err := os.Stat(path); err != nil {
++ t.Fatalf("live artifact removed: %s: %v", path, err)
++ }
++ }
++ })
++
++ t.Run("proof error after stop", func(t *testing.T) {
++ s, ctl, host, snapshot, dropin := legacyClaimFixture(t)
++ s.Systemctl = func(ctx context.Context, args ...string) (string, error) {
++ if args[0] == "show" {
++ return "", errors.New("bus gone")
++ }
++ return ctl.run(ctx, args...)
++ }
++ rec := legacyRequest(t, s, "/stop?host="+host+"&role=run")
++ if rec.Code != http.StatusBadGateway {
++ t.Fatalf("stop=%d: %s", rec.Code, rec.Body.String())
++ }
++ for _, path := range []string{snapshot, dropin} {
++ if _, err := os.Stat(path); err != nil {
++ t.Fatalf("artifact removed without proof: %s: %v", path, err)
++ }
++ }
++ })
++
++ t.Run("cleanup error", func(t *testing.T) {
++ s, ctl, host, _, dropin := legacyClaimFixture(t)
++ if err := os.Remove(dropin); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.Mkdir(dropin, 0700); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.WriteFile(filepath.Join(dropin, "busy"), nil, 0600); err != nil {
++ t.Fatal(err)
++ }
++ rec := legacyRequest(t, s, "/start?host="+host+"&role=run&memory_max="+legacyTier())
++ if rec.Code != http.StatusBadGateway || strings.Contains(ctl.joined(), "start ") {
++ t.Fatalf("start=%d calls:\n%s", rec.Code, ctl.joined())
++ }
++ })
++
++ t.Run("reload error", func(t *testing.T) {
++ s, ctl, host, snapshot, dropin := legacyClaimFixture(t)
++ s.Systemctl = func(ctx context.Context, args ...string) (string, error) {
++ if args[0] == "daemon-reload" {
++ return "", errors.New("reload failed")
++ }
++ return ctl.run(ctx, args...)
++ }
++ rec := legacyRequest(t, s, "/start?host="+host+"&role=run&memory_max="+legacyTier())
++ if rec.Code != http.StatusBadGateway || strings.Contains(ctl.joined(), "start ") {
++ t.Fatalf("start=%d calls:\n%s", rec.Code, ctl.joined())
++ }
++ for _, path := range []string{snapshot, dropin} {
++ if _, err := os.Stat(path); err != nil {
++ t.Fatalf("artifact removed after reload failure: %s: %v", path, err)
++ }
++ }
++ })
++}
++
++func TestRev3LegacyStartWithoutClaimArtifactsKeepsOldPath(t *testing.T) {
++ ctl := &fakeCtl{answers: map[string]string{"show -p InvocationID": "new\n"}}
++ s := &Server{Systemctl: ctl.run}
++ rec := legacyRequest(t, s, "/start?host=shop.example&role=run&memory_max="+legacyTier())
++ if rec.Code != http.StatusOK {
++ t.Fatalf("start=%d: %s", rec.Code, rec.Body.String())
++ }
++ if calls := ctl.joined(); strings.Contains(calls, "ActiveState") || strings.Contains(calls, "daemon-reload") {
++ t.Fatalf("artifact-free start gained cleanup calls:\n%s", calls)
++ }
++}
+--- /dev/null
++++ b/internal/instanceexec/claimlaunch_rev3_init_test.go
+@@ -0,0 +1,17 @@
++//go:build linux
++
++package instanceexec
++
++import (
++ "os"
++ "testing"
++)
++
++func TestPlan11Rev3InitObserveHelper(t *testing.T) {
++ if os.Getenv("CARLOS_REV3_OBSERVE_HELPER") != "1" {
++ return
++ }
++ if err := runInit("/bin/sleep", []string{"sleep", "30"}, os.Environ(), func() error { return nil }); err != nil {
++ t.Fatal(err)
++ }
++}
+```
+
+```bash
+python3 "$CLAIM11_SCRATCH/apply.py" "$CLAIM11_PLAN" D3R tests
+nice -n 10 go test -p 1 -timeout 5m ./internal/host ./internal/instancectl -run 'TestRev3|TestPlan11Rev3ObservesRealInitTenant' -count=1
+```
+
+Expected: the round 2 assertions fail. The real-init test must run on a Linux host with a visible unified cgroup tree; a skip is not execution-boundary evidence.
+
+### Boundary and identity tests
+
+After recording that red run, add the boundary protocol tests and adapt the existing successful fixtures to the parent/child arrangement. These tests exercise the real Unix socket, peer PID, reservation response, setup-time expiry, changed invocation, local post-response expiry before the actual `ForkExec`, immutable permit, boot mismatch and binding lifetime. The first run has missing Rev 3 types/methods until the implementation is applied.
+
+<!-- apply: D3 tests -->
+```diff
+--- a/internal/host/claimlaunch_kernel_linux_test.go
++++ b/internal/host/claimlaunch_kernel_linux_test.go
+@@ -6,6 +6,7 @@
+ "crypto/sha256"
+ "encoding/hex"
+ "os"
++ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+@@ -96,17 +97,19 @@
+ if _, err := os.Stat(filepath.Join("/sys/fs/cgroup", rel)); err != nil {
+ t.Skip("cgroup tree not visible:", err)
+ }
+- bound := filepath.Join(t.TempDir(), "bound-app")
+- if err := os.WriteFile(bound, []byte("app"), 0o555); err != nil {
++ bound := "/bin/sleep"
++ child := exec.Command(bound, "30")
++ if err := child.Start(); err != nil {
+ t.Fatal(err)
+ }
++ t.Cleanup(func() { _ = child.Process.Kill(); _ = child.Wait() })
+ k := linuxClaimLaunchKernel{CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc", BoundApp: strings.TrimPrefix(bound, "/")}
+ started := instancectl.ClaimUnitStarted{Unit: "u", InvocationID: "i", ControlGroup: rel, MainPID: os.Getpid()}
+ got, err := k.Observe(started)
+ if err != nil {
+ t.Fatal(err)
+ }
+- if got.ProcessStart != procSelfStart(t) || got.Group != plan11LaunchFileID(t, filepath.Join("/sys/fs/cgroup", rel)) || got.Executable != plan11LaunchFileID(t, bound) {
++ if got.ProcessStart != procSelfStart(t) || got.Group != plan11LaunchFileID(t, filepath.Join("/sys/fs/cgroup", rel)) || got.Executable != plan11LaunchFileID(t, bound) || got.Running != got.Executable || got.TenantPID != child.Process.Pid || got.TenantStart == "" {
+ t.Fatalf("observed=%+v", got)
+ }
+ started.ControlGroup = "carlos.slice/elsewhere.service"
+--- a/internal/host/claimlaunch_rev3_boundary_test.go
++++ b/internal/host/claimlaunch_rev3_boundary_test.go
+@@ -36,3 +36,17 @@
+ t.Fatalf("failed launch not settled: %+v %v", r, e)
+ }
+ }
++
++func (u *rev3SetupUnits) StartReserved(ctx context.Context, in instancectl.ClaimUnit, l instancectl.Launch, p instancectl.ClaimExecPermit, check func() error) (instancectl.ClaimUnitStarted, error) {
++ s, err := u.launchUnits.StartChecked(ctx, in, l, check)
++ if err != nil {
++ return s, err
++ }
++ u.setup()
++ if err = check(); err != nil {
++ _ = u.Stop(ctx, in.Host)
++ return instancectl.ClaimUnitStarted{}, err
++ }
++ u.tenant++
++ return s, nil
++}
+--- a/internal/host/claimlaunch_rev3_child_linux_test.go
++++ b/internal/host/claimlaunch_rev3_child_linux_test.go
+@@ -3,6 +3,7 @@
+ package host
+
+ import (
++ "errors"
+ "os"
+ "os/exec"
+ "path/filepath"
+@@ -12,6 +13,7 @@
+ "testing"
+ "time"
+
++ "github.com/carlosframework/platform/internal/hostbox"
+ "github.com/carlosframework/platform/internal/instancectl"
+ )
+
+@@ -76,3 +78,113 @@
+ t.Fatalf("main PID lifetime lost: got=%+v main=%s err=%v", got, mainStart, err)
+ }
+ }
++
++func rev3ChildFixture(t *testing.T) (linuxClaimLaunchKernel, instancectl.ClaimUnitStarted, func(string, string)) {
++ t.Helper()
++ root := t.TempDir()
++ write := func(path, value string) {
++ t.Helper()
++ path = filepath.Join(root, path)
++ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.WriteFile(path, []byte(value), 0644); err != nil {
++ t.Fatal(err)
++ }
++ }
++ stat := func(pid, ppid, start string) string {
++ return pid + " (app) S " + ppid + strings.Repeat(" 0", 17) + " " + start
++ }
++ write("proc/100/stat", stat("100", "1", "main-start"))
++ write("proc/200/stat", stat("200", "100", "child-start"))
++ write("proc/100/cgroup", "0::/unit\n")
++ write("proc/200/cgroup", "0::/unit\n")
++ write("proc/100/task/100/children", "")
++ write("proc/100/task/101/children", "200")
++ write("proc/100/root/opt/carlos/app", "tenant")
++ if err := os.MkdirAll(filepath.Join(root, "proc/200/root/opt/carlos"), 0755); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.Link(filepath.Join(root, "proc/100/root/opt/carlos/app"), filepath.Join(root, "proc/200/root/opt/carlos/app")); err != nil {
++ t.Fatal(err)
++ }
++ if err := os.Link(filepath.Join(root, "proc/100/root/opt/carlos/app"), filepath.Join(root, "proc/200/exe")); err != nil {
++ t.Fatal(err)
++ }
++ write("proc/100/exe", "trampoline")
++ if err := os.MkdirAll(filepath.Join(root, "cgroup/unit"), 0755); err != nil {
++ t.Fatal(err)
++ }
++ return linuxClaimLaunchKernel{ProcRoot: filepath.Join(root, "proc"), CgroupRoot: filepath.Join(root, "cgroup")}, instancectl.ClaimUnitStarted{MainPID: 100, ControlGroup: "unit"}, write
++}
++
++func TestPlan11Rev3ChildScope(t *testing.T) {
++ for _, name := range []string{"valid", "other parent", "other cgroup", "ambiguous", "no tenant", "dead tenant", "other namespace bind"} {
++ t.Run(name, func(t *testing.T) {
++ k, s, write := rev3ChildFixture(t)
++ switch name {
++ case "other parent":
++ write("proc/200/stat", "200 (app) S 99"+strings.Repeat(" 0", 17)+" child-start")
++ case "other cgroup":
++ write("proc/200/cgroup", "0::/other\n")
++ case "ambiguous":
++ write("proc/100/task/100/children", "300")
++ case "no tenant":
++ write("proc/100/task/101/children", "")
++ case "dead tenant":
++ write("proc/200/stat", "200 (app) Z 100"+strings.Repeat(" 0", 17)+" child-start")
++ case "other namespace bind":
++ if err := os.Remove(filepath.Join(k.ProcRoot, "200/root/opt/carlos/app")); err != nil {
++ t.Fatal(err)
++ }
++ write("proc/200/root/opt/carlos/app", "other")
++ }
++ got, err := k.Observe(s)
++ if name == "valid" {
++ if err != nil || got.TenantPID != 200 || got.TenantStart != "child-start" || got.ProcessStart != "main-start" || got.Running != got.Executable {
++ t.Fatalf("observation=%+v err=%v", got, err)
++ }
++ } else if name == "no tenant" {
++ if !errors.Is(err, errClaimLaunchPending) || got.ProcessStart != "main-start" || got.TenantPID != 0 {
++ t.Fatalf("pending=%+v err=%v", got, err)
++ }
++ } else if err == nil {
++ t.Fatalf("unsafe child accepted: %+v", got)
++ }
++ })
++ }
++}
++
++func TestPlan11Rev3PinsTenantReadiness(t *testing.T) {
++ for _, name := range []string{"stable", "changed pid", "reused pid", "disappeared"} {
++ t.Run(name, func(t *testing.T) {
++ f := newLaunchFixture(t, servingOp())
++ calls := 0
++ f.kernel.observe = func(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) {
++ calls++
++ got := f.kernel.observed
++ got.TenantPID, got.TenantStart = 4243, "child-start"
++ if calls == 1 {
++ return got, errClaimLaunchPending
++ }
++ switch name {
++ case "changed pid":
++ got.TenantPID++
++ case "reused pid":
++ got.TenantStart = "other-start"
++ case "disappeared":
++ got.TenantPID, got.TenantStart = 0, ""
++ }
++ return got, nil
++ }
++ got, err := f.root.Serve(t.Context(), f.peer, f.req)
++ if name == "stable" {
++ if err != nil || got.Grant == nil || got.Grant.PID != 4242 || got.Grant.ProcessStart != "ps-4242" {
++ t.Fatalf("main supervision identity changed: %+v %v", got, err)
++ }
++ } else if !errors.Is(err, hostbox.ErrChanged) {
++ t.Fatalf("replacement child granted: %+v %v", got, err)
++ }
++ })
++ }
++}
+--- a/internal/host/claimlaunch_root_test.go
++++ b/internal/host/claimlaunch_root_test.go
+@@ -155,7 +155,7 @@
+ actor := &hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 7, Region: "eu", Eligible: true}
+ exe := hostbox.LaunchFile{Device: 5, Inode: 6}
+ units := &launchUnits{started: instancectl.ClaimUnitStarted{Unit: instancectl.UnitName(instancectl.RoleRun, host), InvocationID: "unit-inv-1", ControlGroup: instancectl.ClaimUnitControlGroup(host), MainPID: 4242}}
+- kernel := &launchKernel{path: "/opt/carlos/versions/v1/app", want: exe, observed: ClaimLaunchObserved{ProcessStart: "ps-4242", Group: hostbox.LaunchFile{Device: 3, Inode: 4}, Executable: exe}}
++ kernel := &launchKernel{path: "/opt/carlos/versions/v1/app", want: exe, observed: ClaimLaunchObserved{ProcessStart: "ps-4242", TenantPID: 4243, TenantStart: "ps-4243", Group: hostbox.LaunchFile{Device: 3, Inode: 4}, Executable: exe}}
+ peer := StopPeer{UID: 1001, PID: 42, ProcessStart: "start-42", BootID: "boot-1", ControllerID: "controller-1"}
+ root := &ClaimLaunchRoot{Guard: guard, Journal: journal, Units: units, Kernel: kernel, Actor: func(context.Context) (hostbox.Actor, error) { return *actor, nil }}
+ reservation := hostbox.LaunchReservation{ID: "attempt-1:main", Role: "main", InvocationID: "inv-1", Inputs: "inputs-1", Target: target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(host)}, Executable: exe}
+@@ -495,3 +495,10 @@
+ }
+ return k.Verify(path, want)
+ }
++
++func (u *launchUnits) StartReserved(ctx context.Context, in instancectl.ClaimUnit, l instancectl.Launch, p instancectl.ClaimExecPermit, check func() error) (instancectl.ClaimUnitStarted, error) {
++ if !p.Valid() {
++ return instancectl.ClaimUnitStarted{}, instancectl.ErrClaimUnit
++ }
++ return u.StartChecked(ctx, in, l, check)
++}
+--- /dev/null
++++ b/internal/instancectl/claimexec_rev3_linux_test.go
+@@ -0,0 +1,102 @@
++//go:build linux
++
++package instancectl
++
++import (
++ "context"
++ "encoding/json"
++ "net"
++ "os"
++ "path/filepath"
++ "strings"
++ "testing"
++)
++
++func TestRev3ReservedUnitChecksAtTrampolineBoundary(t *testing.T) {
++ for _, name := range []string{"valid", "expired during setup", "wrong peer", "wrong reservation", "replacement invocation"} {
++ t.Run(name, func(t *testing.T) {
++ u, ctl, in, _ := claimUnitFixture(t)
++ dir, err := os.MkdirTemp("", "rg")
++ if err != nil {
++ t.Fatal(err)
++ }
++ defer os.RemoveAll(dir)
++ u.ExeRoot = filepath.Join(dir, "exe")
++ u.UnitRoot = filepath.Join(dir, "unit")
++ u.DataRoot = filepath.Join(dir, "data")
++ p := ClaimExecPermit{Reservation: "a:main", Inputs: "hash", BootID: "boot", SafeUntil: 200}
++ setup := false
++ checks := 0
++ received := make(chan error, 1)
++ original := u.Systemctl
++ u.Systemctl = func(ctx context.Context, args ...string) (string, error) {
++ out, err := original(ctx, args...)
++ if args[0] != "start" || err != nil {
++ return out, err
++ }
++ setup = true
++ ctl.mu.Lock()
++ ctl.pid = os.Getpid()
++ if name == "wrong peer" {
++ ctl.pid++
++ }
++ ctl.mu.Unlock()
++ source, dropin, _ := u.launchPaths(in.Host)
++ body, e := os.ReadFile(filepath.Join(dropin, "60-claim-launch.conf"))
++ if e != nil || !strings.Contains(string(body), "BindReadOnlyPaths=/proc/sys/kernel/random/boot_id:"+ClaimExecBootID) || !strings.Contains(string(body), "instance-exec -claim-reservation="+ClaimExecSnapshot+" run %I\n") {
++ t.Errorf("missing trusted trampoline binds/argument: %s %v", body, e)
++ }
++ go func() {
++ c, e := net.Dial("unix", filepath.Join(source, "exec.sock"))
++ if e != nil {
++ received <- e
++ return
++ }
++ defer c.Close()
++ q := p
++ if name == "wrong reservation" {
++ q.Reservation = "other"
++ }
++ if e = json.NewEncoder(c).Encode(q); e == nil {
++ var got ClaimExecPermit
++ e = json.NewDecoder(c).Decode(&got)
++ }
++ received <- e
++ }()
++ return out, nil
++ }
++ l := Launch{Schema: LaunchSchema, Host: in.Host, Argv: []string{LiveDir + "/" + in.Host}}
++ _, err = u.StartReserved(t.Context(), in, l, p, func() error {
++ checks++
++ if setup && name == "expired during setup" {
++ return ErrClaimUnit
++ }
++ if setup && name == "replacement invocation" {
++ ctl.mu.Lock()
++ ctl.invocation = "replacement"
++ ctl.mu.Unlock()
++ }
++ return nil
++ })
++ clientErr := <-received
++ if name == "valid" {
++ if err != nil || clientErr != nil || checks != 2 {
++ t.Fatalf("start=%v gate=%v checks=%d", err, clientErr, checks)
++ }
++ source, _, _ := u.launchPaths(in.Host)
++ if _, err := os.Lstat(filepath.Join(source, "exec.sock")); err != nil {
++ t.Fatalf("ExecStopPost bind source removed early: %v", err)
++ }
++ _ = u.Stop(t.Context(), in.Host)
++ if _, err := os.Lstat(filepath.Join(source, "exec.sock")); !os.IsNotExist(err) {
++ t.Fatalf("stopped gate source retained: %v", err)
++ }
++ } else if err == nil || clientErr == nil {
++ t.Fatalf("unsafe start=%v gate=%v", err, clientErr)
++ }
++ if !strings.Contains(ctl.joined(), "start ") {
++ t.Fatal("did not reach asynchronous setup")
++ }
++ })
++ }
++}
+--- /dev/null
++++ b/internal/instanceexec/claimexec_rev3_linux_test.go
+@@ -0,0 +1,137 @@
++//go:build linux
++
++package instanceexec
++
++import (
++ "encoding/json"
++ "net"
++ "os"
++ "os/exec"
++ "path/filepath"
++ "testing"
++
++ "github.com/carlosframework/platform/internal/instancectl"
++)
++
++func TestRev3ExecBoundaryHelper(t *testing.T) {
++ if os.Getenv("REV3_EXEC_HELPER") != "1" {
++ return
++ }
++ args := os.Args
++ dir := args[len(args)-1]
++ // Simulate setup consuming the lease. This uses runInit's production fork
++ // path and a real socket; only the host clock and namespace scope are faked.
++ clock := int64(100)
++ err := runInitChecked("/bin/sh", []string{"sh", "-c", "echo tenant > \"$1\"", "sh", filepath.Join(dir, "tenant")}, os.Environ(), func() error { clock = 200; return nil }, func() error {
++ return checkClaimExec(filepath.Join(dir, "permit"), filepath.Join(dir, "gate"), uint32(os.Geteuid()), func() (string, int64, error) { return "boot", clock, nil })
++ })
++ if err == nil {
++ t.Fatal("expired execution accepted")
++ }
++}
++func TestRev3TrampolineExpiresBeforeForkExec(t *testing.T) {
++ dir, err := os.MkdirTemp("", "rx")
++ if err != nil {
++ t.Fatal(err)
++ }
++ defer os.RemoveAll(dir)
++ p := instancectl.ClaimExecPermit{Reservation: "attempt:main", Inputs: "digest", BootID: "boot", SafeUntil: 200}
++ b, _ := json.Marshal(p)
++ if err = os.WriteFile(filepath.Join(dir, "permit"), b, 0444); err != nil {
++ t.Fatal(err)
++ }
++ ln, err := net.Listen("unix", filepath.Join(dir, "gate"))
++ if err != nil {
++ t.Fatal(err)
++ }
++ defer ln.Close()
++ done := make(chan error, 1)
++ go func() {
++ c, e := ln.Accept()
++ if e != nil {
++ done <- e
++ return
++ }
++ defer c.Close()
++ var q instancectl.ClaimExecPermit
++ e = json.NewDecoder(c).Decode(&q)
++ if e == nil {
++ e = json.NewEncoder(c).Encode(p)
++ }
++ done <- e
++ }()
++ cmd := exec.Command(os.Args[0], "-test.run=^TestRev3ExecBoundaryHelper$", "--", dir)
++ cmd.Env = append(os.Environ(), "REV3_EXEC_HELPER=1")
++ cmd.Dir = dir
++ if out, err := cmd.CombinedOutput(); err != nil {
++ t.Fatalf("helper: %v %s", err, out)
++ }
++ if err = <-done; err != nil {
++ t.Fatal(err)
++ }
++ if _, err = os.Stat(filepath.Join(dir, "tenant")); !os.IsNotExist(err) {
++ t.Fatalf("tenant executed after expiry: %v", err)
++ }
++}
++func TestRev3BoundaryTrustAndPermit(t *testing.T) {
++ for _, name := range []string{"valid", "expired", "wrong boot", "wrong reservation", "writable", "missing", "symlink"} {
++ t.Run(name, func(t *testing.T) {
++ dir, err := os.MkdirTemp("", "rp")
++ if err != nil {
++ t.Fatal(err)
++ }
++ defer os.RemoveAll(dir)
++ p := instancectl.ClaimExecPermit{Reservation: "attempt:main", Inputs: "digest", BootID: "boot", SafeUntil: 200}
++ path := filepath.Join(dir, "permit")
++ b, _ := json.Marshal(p)
++ if err = os.WriteFile(path, b, 0444); err != nil {
++ t.Fatal(err)
++ }
++ switch name {
++ case "writable":
++ os.Chmod(path, 0644)
++ case "missing":
++ os.Remove(path)
++ case "symlink":
++ os.Rename(path, path+".real")
++ os.Symlink(path+".real", path)
++ }
++ ln, err := net.Listen("unix", filepath.Join(dir, "gate"))
++ if err != nil {
++ t.Fatal(err)
++ }
++ done := make(chan struct{})
++ go func() {
++ defer close(done)
++ c, e := ln.Accept()
++ if e != nil {
++ return
++ }
++ defer c.Close()
++ var q instancectl.ClaimExecPermit
++ if json.NewDecoder(c).Decode(&q) != nil {
++ return
++ }
++ if name == "wrong reservation" {
++ p.Reservation = "other"
++ }
++ _ = json.NewEncoder(c).Encode(p)
++ }()
++ clock := func() (string, int64, error) {
++ if name == "expired" {
++ return "boot", 200, nil
++ }
++ if name == "wrong boot" {
++ return "other", 100, nil
++ }
++ return "boot", 100, nil
++ }
++ err = checkClaimExec(path, filepath.Join(dir, "gate"), uint32(os.Geteuid()), clock)
++ ln.Close()
++ <-done
++ if (err == nil) != (name == "valid") {
++ t.Fatalf("%s: %v", name, err)
++ }
++ })
++ }
++}
+```
+
+```bash
+python3 "$CLAIM11_SCRATCH/apply.py" "$CLAIM11_PLAN" D3 tests
+nice -n 10 go test -p 1 -timeout 5m ./internal/host ./internal/instancectl ./internal/instanceexec -run 'TestRev3|TestPlan11Rev3' -count=1
+```
+
+### Implementation
+
+<!-- apply: D3 implementation -->
+```diff
+--- a/internal/host/claimlaunch_authority.go
++++ b/internal/host/claimlaunch_authority.go
+@@ -150,7 +150,8 @@
+ }
+ ctx, cancel := context.WithTimeout(ctx, budget)
+ defer cancel()
+- var pinned string
++ var pinned, tenantStart string
++ var tenantPID int
+ for {
+ if err := ctx.Err(); err != nil {
+ return ClaimLaunchObserved{}, err
+@@ -163,10 +164,16 @@
+ return ClaimLaunchObserved{}, hostbox.ErrChanged
+ }
+ pinned = got.ProcessStart
++ if (got.TenantPID == 0) != (got.TenantStart == "") || (tenantPID != 0 && (tenantPID != got.TenantPID || tenantStart != got.TenantStart)) {
++ return ClaimLaunchObserved{}, hostbox.ErrChanged
++ }
++ if got.TenantPID != 0 {
++ tenantPID, tenantStart = got.TenantPID, got.TenantStart
++ }
+ if err != nil && !errors.Is(err, errClaimLaunchPending) {
+ return ClaimLaunchObserved{}, err
+ }
+- if err == nil && got.Executable == want && got.Running == want {
++ if err == nil && got.TenantPID > 0 && got.TenantStart != "" && got.Executable == want && got.Running == want {
+ if err := h.Units.Same(ctx, s); err != nil {
+ return ClaimLaunchObserved{}, err
+ }
+--- /dev/null
++++ b/internal/host/claimlaunch_execpermit.go
+@@ -0,0 +1,41 @@
++package host
++
++import (
++ "context"
++ "github.com/carlosframework/platform/internal/hostbox"
++ "github.com/carlosframework/platform/internal/hostclaim"
++ execlease "github.com/carlosframework/platform/internal/hostexec"
++ "github.com/carlosframework/platform/internal/instancectl"
++ "time"
++)
++
++func (h *ClaimLaunchRoot) execPermit(ctx context.Context, q hostbox.RoleLaunchRequest) (instancectl.ClaimExecPermit, error) {
++ r := q.Reservation
++ o, err := h.Guard.Claims.Read(ctx, r.Target.Host)
++ if err != nil {
++ return instancectl.ClaimExecPermit{}, err
++ }
++ if o.Absent || o.Claim == nil {
++ return instancectl.ClaimExecPermit{}, hostbox.ErrChanged
++ }
++ _, boot, err := h.Clock.Now()
++ if err != nil {
++ return instancectl.ClaimExecPermit{}, err
++ }
++ // Serving also gets a finite start window. An operation's lease may only
++ // shorten it; a later renewal cannot extend this reservation's permit.
++ until := boot + int64(30*time.Second)
++ if o.Claim.Op.Kind != hostclaim.KindServing {
++ state, err := execlease.InspectLocal(h.Guard.DataDir, r.Target.Host)
++ if err != nil {
++ return instancectl.ClaimExecPermit{}, err
++ }
++ if state.SafeUntil < until {
++ until = state.SafeUntil
++ }
++ }
++ if boot >= until {
++ return instancectl.ClaimExecPermit{}, execlease.ErrLost
++ }
++ return instancectl.ClaimExecPermit{Reservation: r.ID, Inputs: r.Inputs, BootID: r.Target.BootID, SafeUntil: until}, nil
++}
+--- a/internal/host/claimlaunch_kernel_linux.go
++++ b/internal/host/claimlaunch_kernel_linux.go
+@@ -75,24 +75,82 @@
+ return nil
+ }
+
+-func claimLaunchProcessStart(procRoot string, pid int) (string, error) {
++func claimLaunchProcessIdentity(procRoot string, pid int) (string, int, error) {
+ raw, err := os.ReadFile(filepath.Join(procRoot, strconv.Itoa(pid), "stat"))
+ if err != nil {
+- return "", err
++ return "", 0, err
+ }
+ i := strings.LastIndexByte(string(raw), ')')
+ if i < 0 {
+- return "", hostbox.ErrChanged
++ return "", 0, hostbox.ErrChanged
+ }
+ fields := strings.Fields(string(raw)[i+1:])
+ if len(fields) < 20 || fields[0] == "Z" || fields[0] == "X" {
+- return "", hostbox.ErrChanged
+- }
+- return fields[19], nil
+-}
+-
+-// Observe reads the unit's main process twice around everything else, so a
+-// reused pid cannot lend its identity to the observation.
++ return "", 0, hostbox.ErrChanged
++ }
++ parent, err := strconv.Atoi(fields[1])
++ if err != nil {
++ return "", 0, hostbox.ErrChanged
++ }
++ return fields[19], parent, nil
++}
++
++func claimLaunchProcessStart(procRoot string, pid int) (string, error) {
++ start, _, err := claimLaunchProcessIdentity(procRoot, pid)
++ return start, err
++}
++
++func (k linuxClaimLaunchKernel) member(pid int, group string) error {
++ raw, err := os.ReadFile(filepath.Join(k.ProcRoot, strconv.Itoa(pid), "cgroup"))
++ if err != nil {
++ return err
++ }
++ for _, line := range strings.Split(string(raw), "\n") {
++ if line == "0::/"+group {
++ return nil
++ }
++ }
++ return hostbox.ErrChanged
++}
++
++func (k linuxClaimLaunchKernel) tenant(main int) (int, error) {
++ tasks := filepath.Join(k.ProcRoot, strconv.Itoa(main), "task")
++ entries, err := os.ReadDir(tasks)
++ if err != nil {
++ return 0, err
++ }
++ children := map[int]bool{}
++ for _, entry := range entries {
++ raw, err := os.ReadFile(filepath.Join(tasks, entry.Name(), "children"))
++ if errors.Is(err, os.ErrNotExist) {
++ continue
++ }
++ if err != nil {
++ return 0, err
++ }
++ for _, word := range strings.Fields(string(raw)) {
++ pid, err := strconv.Atoi(word)
++ if err != nil || pid <= 0 || pid == main {
++ return 0, hostbox.ErrChanged
++ }
++ children[pid] = true
++ }
++ }
++ if len(children) == 0 {
++ return 0, errClaimLaunchPending
++ }
++ if len(children) != 1 {
++ return 0, hostbox.ErrChanged
++ }
++ for pid := range children {
++ return pid, nil
++ }
++ return 0, hostbox.ErrChanged
++}
++
++// Observe keeps the main process as the unit identity and verifies its one
++// direct tenant child. runInit forks on a locked thread, so every task's
++// children file participates; the leader's children file alone is incomplete.
+ func (k linuxClaimLaunchKernel) Observe(s instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error) {
+ if s.MainPID <= 0 || !filepath.IsLocal(s.ControlGroup) || !claimLaunchAbs(k.CgroupRoot) || !claimLaunchAbs(k.ProcRoot) {
+ return ClaimLaunchObserved{}, hostbox.ErrChanged
+@@ -108,34 +166,62 @@
+ if group.Mode&unix.S_IFMT != unix.S_IFDIR {
+ return ClaimLaunchObserved{}, hostbox.ErrChanged
+ }
+- membership, err := os.ReadFile(filepath.Join(k.ProcRoot, strconv.Itoa(s.MainPID), "cgroup"))
+- if err != nil {
+- return ClaimLaunchObserved{}, err
+- }
+- member := false
+- for _, line := range strings.Split(string(membership), "\n") {
+- member = member || line == "0::/"+s.ControlGroup
+- }
+- if !member {
+- return ClaimLaunchObserved{}, hostbox.ErrChanged
++ observed := ClaimLaunchObserved{ProcessStart: start, Group: hostbox.LaunchFile{Device: uint64(group.Dev), Inode: group.Ino}}
++ if err := k.member(s.MainPID, s.ControlGroup); err != nil {
++ return observed, err
++ }
++ child, childErr := k.tenant(s.MainPID)
++ if childErr != nil {
++ again, err := claimLaunchProcessStart(k.ProcRoot, s.MainPID)
++ if err != nil || again != start {
++ return observed, hostbox.ErrChanged
++ }
++ return observed, childErr
++ }
++ childStart, parent, err := claimLaunchProcessIdentity(k.ProcRoot, child)
++ if err != nil || parent != s.MainPID {
++ return observed, hostbox.ErrChanged
++ }
++ observed.TenantPID, observed.TenantStart = child, childStart
++ if err := k.member(child, s.ControlGroup); err != nil {
++ return observed, err
+ }
+ bound := k.BoundApp
+ if bound == "" {
+ bound = "opt/carlos/app"
+ }
+- var app, running unix.Stat_t
+- observed := ClaimLaunchObserved{ProcessStart: start, Group: hostbox.LaunchFile{Device: uint64(group.Dev), Inode: group.Ino}}
+- boundErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(s.MainPID), "root", bound), &app)
+- execErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(s.MainPID), "exe"), &running)
++ var app, mainApp, running unix.Stat_t
++ boundErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(child), "root", bound), &app)
++ mainBoundErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(s.MainPID), "root", bound), &mainApp)
++ execErr := unix.Stat(filepath.Join(k.ProcRoot, strconv.Itoa(child), "exe"), &running)
++ againChild, parent, err := claimLaunchProcessIdentity(k.ProcRoot, child)
++ if err != nil || againChild != childStart || parent != s.MainPID {
++ return observed, hostbox.ErrChanged
++ }
+ again, err := claimLaunchProcessStart(k.ProcRoot, s.MainPID)
+ if err != nil || again != start {
+- return ClaimLaunchObserved{}, hostbox.ErrChanged
+- }
+- if boundErr != nil {
+- if errors.Is(boundErr, os.ErrNotExist) {
++ return observed, hostbox.ErrChanged
++ }
++ if err := k.member(s.MainPID, s.ControlGroup); err != nil {
++ return observed, err
++ }
++ if err := k.member(child, s.ControlGroup); err != nil {
++ return observed, err
++ }
++ againPID, err := k.tenant(s.MainPID)
++ if err != nil || againPID != child {
++ return observed, hostbox.ErrChanged
++ }
++ for _, err := range []error{boundErr, mainBoundErr} {
++ if errors.Is(err, os.ErrNotExist) {
+ return observed, errClaimLaunchPending
+ }
+- return observed, boundErr
++ if err != nil {
++ return observed, err
++ }
++ }
++ if app.Dev != mainApp.Dev || app.Ino != mainApp.Ino {
++ return observed, hostbox.ErrChanged
+ }
+ if execErr != nil {
+ return observed, execErr
+--- a/internal/host/claimlaunch_root.go
++++ b/internal/host/claimlaunch_root.go
+@@ -27,7 +27,7 @@
+ // ClaimLaunchUnits starts, stops and observes the host's isolated run unit.
+ // Production is instancectl.ClaimUnits.
+ type ClaimLaunchUnits interface {
+- StartChecked(context.Context, instancectl.ClaimUnit, instancectl.Launch, func() error) (instancectl.ClaimUnitStarted, error)
++ StartReserved(context.Context, instancectl.ClaimUnit, instancectl.Launch, instancectl.ClaimExecPermit, func() error) (instancectl.ClaimUnitStarted, error)
+ Same(context.Context, instancectl.ClaimUnitStarted) error
+ Stop(context.Context, string) error
+ Gone(context.Context, string, string) error
+@@ -39,14 +39,15 @@
+ // tree with exactly the pinned identity.
+ Verify(string, hostbox.LaunchFile) error
+ VerifyDigest(context.Context, string, hostbox.LaunchFile, string) error
+- // Observe reads the started unit's main process: its start time, the
+- // identity of the cgroup it is a member of, and the build bound at the
+- // unit's /opt/carlos/app.
++ // Observe reads the main process lifetime and cgroup, and its tenant
++ // child's lifetime, namespace-bound build and running executable.
+ Observe(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error)
+ }
+
+ type ClaimLaunchObserved struct {
+ ProcessStart string
++ TenantPID int
++ TenantStart string
+ Group, Executable hostbox.LaunchFile
+ Running hostbox.LaunchFile
+ }
+@@ -217,9 +218,13 @@
+ if err != nil {
+ return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, ""))
+ }
+- started, err := h.Units.StartChecked(ctx, instancectl.ClaimUnit{Host: t.Host, Executable: q.Executable, MemoryMax: q.MemoryMax}, launch, func() error {
+- // Unit preparation can block. Read the claim, actor and local deadline
+- // again at the final systemctl boundary, still under this host guard.
++ permit, err := h.execPermit(ctx, q)
++ if err != nil {
++ return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, ""))
++ }
++ started, err := h.Units.StartReserved(ctx, instancectl.ClaimUnit{Host: t.Host, Executable: q.Executable, MemoryMax: q.MemoryMax}, launch, permit, func() error {
++ // Check before starting the unit and again when its authenticated
++ // trampoline reaches ForkExec, under this same host guard.
+ fresh, err := h.Guard.Claims.Read(ctx, t.Host)
+ if err != nil {
+ return err
+@@ -234,7 +239,24 @@
+ if in.WritersClosed {
+ return hostbox.ErrChanged
+ }
+- return h.authorize(ctx, peer, q, fresh.Claim)
++ rr, err := h.Journal.UnitReservation(ctx, t, r.ID)
++ if err != nil {
++ return err
++ }
++ if rr.State != hostbox.UnitIntended || !sameUnitRequest(rr, r) {
++ return hostbox.ErrChanged
++ }
++ if err := h.authorize(ctx, peer, q, fresh.Claim); err != nil {
++ return err
++ }
++ _, boot, err := h.Clock.Now()
++ if err != nil {
++ return err
++ }
++ if boot >= permit.SafeUntil {
++ return execlease.ErrLost
++ }
++ return nil
+ })
+ if err != nil {
+ return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, ""))
+--- a/internal/host/claimstop_writers_linux.go
++++ b/internal/host/claimstop_writers_linux.go
+@@ -12,6 +12,7 @@
+ "path/filepath"
+ "reflect"
+ "sort"
++ "strconv"
+ "strings"
+ "syscall"
+ "time"
+@@ -152,9 +153,12 @@
+ if err != nil {
+ return StopKernelEvidence{}, err
+ }
+- paths, err := installationFiles(roots.Managed, inv, false)
+- if err != nil {
+- return StopKernelEvidence{}, err
++ paths, prefixes, err := installationCoverage(roots.Managed, inv, false)
++ if err != nil {
++ return StopKernelEvidence{}, err
++ }
++ if len(paths)+len(prefixes) == 0 {
++ return StopKernelEvidence{}, ErrClaimStopInventory
+ }
+ if len(paths) > 0 {
+ held, err := stopevidence.Holders(ctx, roots.Proc, paths, nil)
+@@ -163,6 +167,15 @@
+ }
+ if len(held) != 0 {
+ return StopKernelEvidence{}, fmt.Errorf("%w: pid %d holds an installation file", ErrClaimStopIdentity, held[0].PID)
++ }
++ }
++ if len(prefixes) > 0 {
++ prefixHeld, err := installationPrefixHolders(ctx, roots.Proc, prefixes)
++ if err != nil {
++ return StopKernelEvidence{}, err
++ }
++ if len(prefixHeld) != 0 {
++ return StopKernelEvidence{}, fmt.Errorf("%w: pid %d holds an installation file", ErrClaimStopIdentity, prefixHeld[0])
+ }
+ }
+ if has, err := launches.HasHost(ctx, t.Host); err != nil {
+@@ -186,10 +199,20 @@
+ // checked against the inventory's device and inode first. dataOnly keeps
+ // the "data" root alone (the database's state directory).
+ func installationFiles(managed map[string]string, inv StopInventory, dataOnly bool) ([]string, error) {
++ paths, _, err := installationCoverage(managed, inv, dataOnly)
++ return paths, err
++}
++
++// installationCoverage returns both the currently named files and the
++// inventory directory prefixes. The latter keep the session-zero witness
++// effective for nested and open-but-unlinked files, including an installation
++// directory whose current listing is empty.
++func installationCoverage(managed map[string]string, inv StopInventory, dataOnly bool) ([]string, []string, error) {
+ if !inv.Complete || len(managed) == 0 {
+- return nil, ErrClaimStopInventory
++ return nil, nil, ErrClaimStopInventory
+ }
+ seen := map[string]bool{}
++ prefixSet := map[string]bool{}
+ dirs := 0
+ for _, e := range inv.Entries {
+ if e.Kind != "path" && e.Kind != "live-link" {
+@@ -200,7 +223,7 @@
+ }
+ root := managed[e.ManagedRoot]
+ if !filepath.IsAbs(root) || !filepath.IsLocal(e.RelativePath) {
+- return nil, ErrClaimStopInventory
++ return nil, nil, ErrClaimStopInventory
+ }
+ path := filepath.Join(root, e.RelativePath)
+ if e.Type&syscall.S_IFMT != syscall.S_IFDIR {
+@@ -208,20 +231,21 @@
+ continue
+ }
+ dirs++
++ prefixSet[filepath.Clean(path)] = true
+ info, err := os.Lstat(path)
+ if errors.Is(err, os.ErrNotExist) && e.Materialization != "materialized" {
+ continue
+ }
+ if err != nil {
+- return nil, err
++ return nil, nil, err
+ }
+ st, ok := info.Sys().(*syscall.Stat_t)
+ if !ok || !info.IsDir() || uint64(st.Dev) != e.Device || st.Ino != e.Inode {
+- return nil, fmt.Errorf("%w: %s is not the inventory's directory", ErrClaimStopInventory, path)
++ return nil, nil, fmt.Errorf("%w: %s is not the inventory's directory", ErrClaimStopInventory, path)
+ }
+ entries, err := os.ReadDir(path)
+ if err != nil {
+- return nil, err
++ return nil, nil, err
+ }
+ for _, entry := range entries {
+ if entry.Type().IsRegular() {
+@@ -230,14 +254,114 @@
+ }
+ }
+ if dataOnly && dirs == 0 {
+- return nil, fmt.Errorf("%w: the inventory names no state directory", ErrClaimStopInventory)
++ return nil, nil, fmt.Errorf("%w: the inventory names no state directory", ErrClaimStopInventory)
+ }
+ out := make([]string, 0, len(seen))
+ for path := range seen {
+ out = append(out, path)
+ }
+ sort.Strings(out)
+- return out, nil
++ prefixes := make([]string, 0, len(prefixSet))
++ for prefix := range prefixSet {
++ prefixes = append(prefixes, prefix)
++ }
++ sort.Strings(prefixes)
++ return out, prefixes, nil
++}
++
++// installationPrefixHolders performs a strict /proc descriptor scan and
++// matches absolute descriptor targets beneath an inventory directory. A path
++// separator is part of the match so a sibling such as "instance-old" is not
++// covered by the prefix "instance". Deleted targets retain their useful name
++// in /proc and are matched after removing its kernel suffix.
++func installationPrefixHolders(ctx context.Context, procRoot string, prefixes []string) ([]int, error) {
++ if ctx == nil || !filepath.IsAbs(procRoot) || len(prefixes) == 0 {
++ return nil, ErrClaimStopInventory
++ }
++ matches := func(target string) bool {
++ if !filepath.IsAbs(target) {
++ return false
++ }
++ target = filepath.Clean(strings.TrimSuffix(target, " (deleted)"))
++ for _, prefix := range prefixes {
++ if target == prefix || strings.HasPrefix(target, prefix+string(os.PathSeparator)) {
++ return true
++ }
++ }
++ return false
++ }
++ processes, err := os.ReadDir(procRoot)
++ if err != nil {
++ return nil, err
++ }
++ seen := map[int]bool{}
++ for _, process := range processes {
++ if err := ctx.Err(); err != nil {
++ return nil, err
++ }
++ pid, err := strconv.Atoi(process.Name())
++ if err != nil || pid <= 0 {
++ continue
++ }
++ pidDir := filepath.Join(procRoot, process.Name())
++ tasks, err := readStopProcDir(filepath.Join(pidDir, "task"), pidDir)
++ if err != nil {
++ return nil, fmt.Errorf("pid %d: %w", pid, err)
++ }
++ for _, task := range tasks {
++ if err := ctx.Err(); err != nil {
++ return nil, err
++ }
++ tid, err := strconv.Atoi(task.Name())
++ if err != nil || tid <= 0 {
++ continue
++ }
++ taskDir := filepath.Join(pidDir, "task", task.Name())
++ fds, err := readStopProcDir(filepath.Join(taskDir, "fd"), taskDir)
++ if err != nil {
++ return nil, fmt.Errorf("pid %d tid %d: %w", pid, tid, err)
++ }
++ for _, fd := range fds {
++ if err := ctx.Err(); err != nil {
++ return nil, err
++ }
++ if _, err := strconv.Atoi(fd.Name()); err != nil {
++ continue
++ }
++ target, err := os.Readlink(filepath.Join(taskDir, "fd", fd.Name()))
++ if errors.Is(err, os.ErrNotExist) {
++ continue
++ }
++ if err != nil {
++ return nil, fmt.Errorf("pid %d tid %d fd %s: %w", pid, tid, fd.Name(), err)
++ }
++ if matches(target) {
++ seen[pid] = true
++ }
++ }
++ }
++ }
++ if err := ctx.Err(); err != nil {
++ return nil, err
++ }
++ holders := make([]int, 0, len(seen))
++ for pid := range seen {
++ holders = append(holders, pid)
++ }
++ sort.Ints(holders)
++ return holders, nil
++}
++
++func readStopProcDir(path, owner string) ([]os.DirEntry, error) {
++ entries, err := os.ReadDir(path)
++ if errors.Is(err, os.ErrNotExist) {
++ if _, ownerErr := os.Stat(owner); errors.Is(ownerErr, os.ErrNotExist) {
++ return nil, nil
++ } else if ownerErr != nil {
++ return nil, ownerErr
++ }
++ }
++ return entries, err
+ }
+
+ // hostContainmentEmpty checks the host's containment under the agent's
+--- a/internal/hostbox/rolelaunch.go
++++ b/internal/hostbox/rolelaunch.go
+@@ -21,7 +21,7 @@
+ // The request names which reservation to run; nothing in it is authority.
+ const (
+ RoleLaunchV = 1
+- RoleLaunchProtocol = "isolated-unit-v2"
++ RoleLaunchProtocol = "isolated-unit-v3"
+ RoleLaunchLimit = 32 << 10
+
+ RoleLaunchCapability = "capability"
+--- /dev/null
++++ b/internal/instancectl/claimexec.go
+@@ -0,0 +1,132 @@
++package instancectl
++
++import (
++ "context"
++ "encoding/json"
++ "errors"
++ "io"
++ "net"
++ "os"
++ "path/filepath"
++ "time"
++
++ "github.com/carlosframework/platform/internal/instancefs"
++)
++
++// ClaimExecPermit is root's one-launch reservation. SafeUntil is CLOCK_BOOTTIME
++// nanoseconds on BootID; the trampoline compares it immediately before ForkExec.
++type ClaimExecPermit struct {
++ Reservation, Inputs, BootID string
++ SafeUntil int64
++}
++
++func (p ClaimExecPermit) Valid() bool {
++ return p.Reservation != "" && p.Inputs != "" && p.BootID != "" && p.SafeUntil > 0
++}
++
++const ClaimExecSocket = "/run/carlos/claim-exec.sock"
++const ClaimExecSnapshot = "/run/carlos/claim-exec.json"
++const ClaimExecBootID = "/run/carlos/claim-boot-id"
++
++func (u ClaimUnits) StartReserved(ctx context.Context, in ClaimUnit, l Launch, p ClaimExecPermit, check func() error) (ClaimUnitStarted, error) {
++ if !p.Valid() || check == nil {
++ return ClaimUnitStarted{}, ErrClaimUnit
++ }
++ return u.start(ctx, in, &l, check, &p)
++}
++
++type claimExecGate struct {
++ listener *net.UnixListener
++ path string
++ permit ClaimExecPermit
++}
++
++// Keep the closed socket inode for ExecStopPost namespace setup. clearLaunch
++// removes it only after the unit has stopped and its cgroup is empty.
++func (g *claimExecGate) close() { g.listener.Close() }
++func (u ClaimUnits) prepareExecGate(ctx context.Context, host string, p ClaimExecPermit) (*claimExecGate, error) {
++ source, dropin, err := u.launchPaths(host)
++ if err != nil {
++ return nil, err
++ }
++ raw, err := json.Marshal(p)
++ if err != nil {
++ return nil, err
++ }
++ if err = instancefs.WriteAtomic(source, "permit.json", raw, 0444); err != nil {
++ return nil, err
++ }
++ socket := filepath.Join(source, "exec.sock")
++ // An old socket is removed only after start proved the old unit stopped.
++ if err = os.Remove(socket); err != nil && !errors.Is(err, os.ErrNotExist) {
++ return nil, err
++ }
++ ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: socket, Net: "unix"})
++ if err != nil {
++ return nil, err
++ }
++ ln.SetUnlinkOnClose(false)
++ gate := &claimExecGate{ln, socket, p}
++ fail := func(e error) (*claimExecGate, error) { gate.close(); return nil, e }
++ // Host ancestry remains root-private; inside the namespace peer identity is
++ // checked against the pinned unit MainPID, never against a supplied PID.
++ if err = os.Chmod(socket, 0666); err != nil {
++ return fail(err)
++ }
++ body, err := os.ReadFile(filepath.Join(dropin, "60-claim-launch.conf"))
++ if err != nil {
++ return fail(err)
++ }
++ body = append(body, []byte("BindReadOnlyPaths="+socket+":"+ClaimExecSocket+"\nBindReadOnlyPaths="+filepath.Join(source, "permit.json")+":"+ClaimExecSnapshot+"\nBindReadOnlyPaths=/proc/sys/kernel/random/boot_id:"+ClaimExecBootID+"\nExecStart=\nExecStart="+SystemBinary+" instance-exec -claim-reservation="+ClaimExecSnapshot+" run %I\n")...)
++ if err = instancefs.WriteAtomic(dropin, "60-claim-launch.conf", body, 0644); err != nil {
++ return fail(err)
++ }
++ if _, err = u.Systemctl(ctx, "daemon-reload"); err != nil {
++ return fail(err)
++ }
++ return gate, nil
++}
++
++func (g *claimExecGate) authorize(ctx context.Context, u ClaimUnits, s ClaimUnitStarted, check func() error) error {
++ deadline := time.Now().Add(u.budget())
++ if d, ok := ctx.Deadline(); ok && d.Before(deadline) {
++ deadline = d
++ }
++ if err := g.listener.SetDeadline(deadline); err != nil {
++ return err
++ }
++ stop := context.AfterFunc(ctx, func() { g.listener.Close() })
++ defer stop()
++ conn, err := g.listener.AcceptUnix()
++ if err != nil {
++ return err
++ }
++ defer conn.Close()
++ closeOnCancel := context.AfterFunc(ctx, func() { conn.Close() })
++ defer closeOnCancel()
++ if err = conn.SetDeadline(deadline); err != nil {
++ return err
++ }
++ if err = claimExecPeer(conn, s.MainPID); err != nil {
++ return err
++ }
++ if err = u.Same(ctx, s); err != nil {
++ return err
++ }
++ var asked ClaimExecPermit
++ dec := json.NewDecoder(io.LimitReader(conn, 4096))
++ if err = dec.Decode(&asked); err != nil {
++ return err
++ }
++ if asked != g.permit {
++ return ErrClaimUnit
++ }
++ if err = check(); err != nil {
++ return err
++ }
++ if err = u.Same(ctx, s); err != nil {
++ return err
++ }
++ // One connection, one permit. Closing/restarting root cannot replay it.
++ return json.NewEncoder(conn).Encode(g.permit)
++}
+--- /dev/null
++++ b/internal/instancectl/claimexec_linux.go
+@@ -0,0 +1,16 @@
++//go:build linux
++
++package instancectl
++
++import "net"
++
++func claimExecPeer(c net.Conn, pid int) error {
++ cred, err := peerCred(c)
++ if err != nil {
++ return err
++ }
++ if int(cred.Pid) != pid {
++ return ErrClaimUnit
++ }
++ return nil
++}
+--- /dev/null
++++ b/internal/instancectl/claimexec_other.go
+@@ -0,0 +1,7 @@
++//go:build !linux
++
++package instancectl
++
++import "net"
++
++func claimExecPeer(net.Conn, int) error { return ErrClaimUnit }
+--- a/internal/instancectl/claimlaunch_snapshot.go
++++ b/internal/instancectl/claimlaunch_snapshot.go
+@@ -78,13 +78,13 @@
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+- if err == nil {
+- if _, err = u.Systemctl(ctx, "daemon-reload"); err != nil {
++ if _, err = u.Systemctl(ctx, "daemon-reload"); err != nil {
++ return err
++ }
++ for _, name := range []string{"launch.json", "permit.json", "exec.sock"} {
++ if err := os.Remove(filepath.Join(source, name)); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ }
+- if err := os.Remove(filepath.Join(source, "launch.json")); err != nil && !errors.Is(err, os.ErrNotExist) {
+- return err
+- }
+ return nil
+ }
+--- a/internal/instancectl/claimunit.go
++++ b/internal/instancectl/claimunit.go
+@@ -114,7 +114,7 @@
+ // unit is idle, and stops any invocation it then cannot vouch for: one that
+ // systemd did not newly create, or that runs outside the host's run cgroup.
+ func (u ClaimUnits) Start(ctx context.Context, in ClaimUnit) (ClaimUnitStarted, error) {
+- return u.start(ctx, in, nil, nil)
++ return u.start(ctx, in, nil, nil, nil)
+ }
+
+ // StartChecked runs the root check after all preparation, immediately before
+@@ -124,10 +124,10 @@
+ if check == nil {
+ return ClaimUnitStarted{}, ErrClaimUnit
+ }
+- return u.start(ctx, in, &launch, check)
+-}
+-
+-func (u ClaimUnits) start(ctx context.Context, in ClaimUnit, launch *Launch, check func() error) (ClaimUnitStarted, error) {
++ return u.start(ctx, in, &launch, check, nil)
++}
++
++func (u ClaimUnits) start(ctx context.Context, in ClaimUnit, launch *Launch, check func() error, permit *ClaimExecPermit) (ClaimUnitStarted, error) {
+ tier, ok := claimTier(in.MemoryMax)
+ if u.Systemctl == nil || u.CgroupRoot == "" || u.ExeRoot == "" || !ok || !provision.ValidHostShape(in.Host) ||
+ !filepath.IsAbs(in.Executable) || filepath.Clean(in.Executable) != in.Executable || in.Executable == SystemBinary || strings.HasPrefix(in.Executable, LiveDir+"/") {
+@@ -164,6 +164,14 @@
+ if err := u.freezeLaunch(ctx, in.Host, *launch); err != nil {
+ return ClaimUnitStarted{}, err
+ }
++ }
++ var gate *claimExecGate
++ if permit != nil {
++ gate, err = u.prepareExecGate(ctx, in.Host, *permit)
++ if err != nil {
++ return ClaimUnitStarted{}, errors.Join(err, u.clearLaunch(ctx, in.Host))
++ }
++ defer gate.close()
+ }
+ if check != nil {
+ if err := check(); err != nil {
+@@ -193,7 +201,18 @@
+ if after["ControlGroup"] != "/"+ClaimUnitControlGroup(in.Host) {
+ return refuse(ErrClaimUnit)
+ }
+- return ClaimUnitStarted{Unit: unit, InvocationID: id, ControlGroup: ClaimUnitControlGroup(in.Host), MainPID: pid}, nil
++ started := ClaimUnitStarted{Unit: unit, InvocationID: id, ControlGroup: ClaimUnitControlGroup(in.Host), MainPID: pid}
++ if gate != nil {
++ if err := gate.authorize(ready, u, started, check); err != nil {
++ cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), u.budget())
++ defer cancel()
++ if sameErr := u.Same(cleanup, started); sameErr != nil {
++ return ClaimUnitStarted{}, errors.Join(err, u.Gone(cleanup, in.Host, started.InvocationID))
++ }
++ return refuse(err)
++ }
++ }
++ return started, nil
+ }
+ select {
+ case <-ready.Done():
+--- /dev/null
++++ b/internal/instancectl/legacy_claim_cleanup.go
+@@ -0,0 +1,72 @@
++package instancectl
++
++import (
++ "context"
++ "errors"
++ "fmt"
++ "os"
++ "path/filepath"
++
++ "github.com/carlosframework/platform/internal/instancefs"
++)
++
++// clearLegacyClaimLaunch removes a claim launch snapshot before the legacy
++// controller may reuse the main run unit. Artifact-free hosts keep the old
++// path: they do not acquire a systemd query or cgroup read.
++func (s *Server) clearLegacyClaimLaunch(ctx context.Context, host string) error {
++ if s.DropInDir == "" || s.ExeRoot == "" || s.DataRoot == "" || s.CgroupRoot == "" {
++ return nil
++ }
++ u := ClaimUnits{
++ Systemctl: s.Systemctl,
++ CgroupRoot: s.CgroupRoot,
++ ExeRoot: s.ExeRoot,
++ UnitRoot: s.DropInDir,
++ DataRoot: s.DataRoot,
++ }
++ source, dropin, err := u.launchPaths(host)
++ if err != nil {
++ return err
++ }
++ found := false
++ dropinPath := filepath.Join(dropin, "60-claim-launch.conf")
++ var dropinBody []byte
++ for _, path := range []string{
++ filepath.Join(source, "launch.json"),
++ filepath.Join(source, "permit.json"),
++ filepath.Join(source, "exec.sock"),
++ dropinPath,
++ } {
++ if _, err := os.Lstat(path); err == nil {
++ found = true
++ if path == dropinPath {
++ dropinBody, err = os.ReadFile(path)
++ if err != nil {
++ return fmt.Errorf("read claim launch %s: %w", path, err)
++ }
++ }
++ } else if !errors.Is(err, os.ErrNotExist) {
++ return fmt.Errorf("inspect claim launch %s: %w", path, err)
++ }
++ }
++ if !found {
++ return nil
++ }
++ // Gone proves both facts required before clearLaunch mutates either
++ // artifact: systemd says the unit is idle and its cgroup is empty.
++ if err := u.Gone(ctx, host, ""); err != nil {
++ // clearLaunch removes the drop-in before daemon-reload. Restore it
++ // when that reload fails, so a later attempt cannot mistake a cached
++ // systemd binding for an artifact-free host and start through it.
++ if dropinBody != nil {
++ if _, statErr := os.Lstat(dropinPath); errors.Is(statErr, os.ErrNotExist) {
++ restoreErr := instancefs.WriteAtomic(dropin, filepath.Base(dropinPath), dropinBody, 0644)
++ if restoreErr != nil {
++ err = errors.Join(err, fmt.Errorf("restore claim launch drop-in: %w", restoreErr))
++ }
++ }
++ }
++ return fmt.Errorf("clear claim launch for %s: %w", host, err)
++ }
++ return nil
++}
+--- a/internal/instancectl/server.go
++++ b/internal/instancectl/server.go
+@@ -387,6 +387,12 @@
+ }
+ ctx := r.Context()
+ unit := UnitName(req.role, req.host)
++ if req.role == RoleRun {
++ if err := s.clearLegacyClaimLaunch(ctx, req.host); err != nil {
++ http.Error(w, "start: "+err.Error(), http.StatusBadGateway)
++ return
++ }
++ }
+ if s.refuseStart(w, req.host, unit, func() error { return s.ensureHostExe(req.host) }) {
+ return
+ }
+@@ -537,6 +543,12 @@
+ http.Error(w, "stop: "+strings.TrimSpace(out+" "+err.Error()), http.StatusBadGateway)
+ return
+ }
++ if req.role == RoleRun {
++ if err := s.clearLegacyClaimLaunch(r.Context(), req.host); err != nil {
++ http.Error(w, "stop: "+err.Error(), http.StatusBadGateway)
++ return
++ }
++ }
+ w.WriteHeader(http.StatusNoContent)
+ }
+
+--- /dev/null
++++ b/internal/instanceexec/claimexec_linux.go
+@@ -0,0 +1,104 @@
++//go:build linux
++
++package instanceexec
++
++import (
++ "encoding/json"
++ "errors"
++ "io"
++ "net"
++ "os"
++ "strings"
++ "time"
++
++ "github.com/carlosframework/platform/internal/instancectl"
++ "golang.org/x/sys/unix"
++)
++
++func scopedClaimInit(argv0 string, argv, env []string, snapshot string) error {
++ return runInitChecked(argv0, argv, env, ScopeAbstractUnix, func() error {
++ return checkClaimExec(snapshot, instancectl.ClaimExecSocket, 0, claimBootTime)
++ })
++}
++func claimBootTime() (string, int64, error) {
++ b, err := os.ReadFile(instancectl.ClaimExecBootID)
++ if err != nil {
++ return "", 0, err
++ }
++ var ts unix.Timespec
++ if err = unix.ClockGettime(unix.CLOCK_BOOTTIME, &ts); err != nil {
++ return "", 0, err
++ }
++ return strings.TrimSpace(string(b)), ts.Nano(), nil
++}
++
++// The path is a root-controlled ExecStart argument, never tenant environment
++// or a field of launch.run.json. Its read-only bind cannot be opted out of.
++func checkClaimExec(snapshot, socket string, owner uint32, now func() (string, int64, error)) error {
++ fd, err := unix.Open(snapshot, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
++ if err != nil {
++ return err
++ }
++ f := os.NewFile(uintptr(fd), snapshot)
++ defer f.Close()
++ var st unix.Stat_t
++ if err = unix.Fstat(fd, &st); err != nil {
++ return err
++ }
++ if st.Uid != owner || st.Mode&unix.S_IFMT != unix.S_IFREG || st.Mode&0222 != 0 {
++ return instancectl.ErrClaimUnit
++ }
++ var want instancectl.ClaimExecPermit
++ if err = json.NewDecoder(io.LimitReader(f, 4096)).Decode(&want); err != nil {
++ return err
++ }
++ if !want.Valid() {
++ return instancectl.ErrClaimUnit
++ }
++ conn, err := net.DialTimeout("unix", socket, 5*time.Second)
++ if err != nil {
++ return err
++ }
++ defer conn.Close()
++ if err = conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
++ return err
++ }
++ raw, err := conn.(*net.UnixConn).SyscallConn()
++ if err != nil {
++ return err
++ }
++ var cred *unix.Ucred
++ var peerErr error
++ if err = raw.Control(func(fd uintptr) { cred, peerErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED) }); err != nil {
++ return err
++ }
++ if peerErr != nil {
++ return peerErr
++ }
++ if cred.Uid != owner {
++ return instancectl.ErrClaimUnit
++ }
++ if err = json.NewEncoder(conn).Encode(want); err != nil {
++ return err
++ }
++ var got instancectl.ClaimExecPermit
++ if err = json.NewDecoder(io.LimitReader(conn, 4096)).Decode(&got); err != nil {
++ return err
++ }
++ if got != want {
++ return instancectl.ErrClaimUnit
++ }
++ // Explicit close precedes the final time sample. No IO or namespace setup
++ // follows this sample before runInitChecked calls ForkExec.
++ if err = conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
++ return err
++ }
++ boot, n, err := now()
++ if err != nil {
++ return err
++ }
++ if boot != want.BootID || n >= want.SafeUntil {
++ return instancectl.ErrClaimUnit
++ }
++ return nil
++}
+--- /dev/null
++++ b/internal/instanceexec/claimexec_other.go
+@@ -0,0 +1,7 @@
++//go:build !linux
++
++package instanceexec
++
++import "errors"
++
++func scopedClaimInit(string, []string, []string, string) error { return errors.ErrUnsupported }
+--- a/internal/instanceexec/exec.go
++++ b/internal/instanceexec/exec.go
+@@ -42,6 +42,7 @@
+ func RunExec(args []string) {
+ fs := flag.NewFlagSet("instance-exec", flag.ExitOnError)
+ fs.Usage = cliflag.Usage(fs, "usage: carlos instance-exec <run|seed|sidecar> <instance-id>")
++ claimSnapshot := fs.String("claim-reservation", "", "root launch reservation")
+ _ = fs.Parse(args)
+ if fs.NArg() != 2 {
+ fs.Usage()
+@@ -53,7 +54,14 @@
+ }
+ // The tenant is scoped (#178, see ScopeAbstractUnix) and runs under
+ // the trampoline as its init (#126, see init_linux.go).
+- if err := execLaunch(dataDir(), role, fs.Arg(1), scopedInit); err != nil {
++ launch := scopedInit
++ if *claimSnapshot != "" {
++ if *claimSnapshot != instancectl.ClaimExecSnapshot || role != instancectl.RoleRun || strings.Contains(fs.Arg(1), ":") {
++ log.Fatal("instance-exec: claim boundary refused")
++ }
++ launch = func(argv0 string, argv, env []string) error { return scopedClaimInit(argv0, argv, env, *claimSnapshot) }
++ }
++ if err := execLaunch(dataDir(), role, fs.Arg(1), launch); err != nil {
+ log.Fatalf("instance-exec: %v", err)
+ }
+ }
+--- a/internal/instanceexec/init_linux.go
++++ b/internal/instanceexec/init_linux.go
+@@ -62,6 +62,10 @@
+ }
+
+ func runInit(argv0 string, argv, env []string, scope func() error) error {
++ return runInitChecked(argv0, argv, env, scope, nil)
++}
++
++func runInitChecked(argv0 string, argv, env []string, scope, check func() error) error {
+ // Before anything can start the tenant: a stop that arrives early is
+ // queued and forwarded, not dropped by PID 1's missing defaults.
+ sigs := make(chan os.Signal, 32)
+@@ -83,6 +87,11 @@
+ // stdio pass as they did there: anything not close-on-exec stays
+ // open across fork and exec. Setsid gives the tenant the session of
+ // its own that systemd gave the main process.
++ if check != nil {
++ if err := check(); err != nil {
++ return err
++ }
++ }
+ tenant, err := syscall.ForkExec(argv0, argv, &syscall.ProcAttr{
+ Env: env,
+ Files: []uintptr{0, 1, 2},
+```
+
+```bash
+python3 "$CLAIM11_SCRATCH/apply.py" "$CLAIM11_PLAN" D3 implementation
+nice -n 10 go test -p 1 -timeout 5m ./internal/host ./internal/instancectl ./internal/instanceexec -run 'TestRev3|TestPlan11Rev[23]|TestClaimLaunch|TestVerifyNoProcessAt|TestVerifyWritersAt' -count=1
+```
+
+Expected: all round 2 regressions and the existing launch/witness tests pass. The flag-off transition consumes the new mutable launch file's argv/environment after removing the old frozen values. No production systemd unit or namespace is created by these tests; test the actual bind setup and root UID handshake in the disposable rollout environment before activation.
+
+## Region HA integration and rollout
+
+Keep all flags off while landing the code. Registration alone does not activate relocation. The rollout still needs Plan 9's verified identity, complete owners, activation census and floor guard. Region HA additionally needs `CARLOS_REGION_HA`; this implementation shares Group D with `CARLOS_MOVES_PHASE1`, so it requires that group to be complete too. Missing owners, an older root protocol, unsupported role shapes and incomplete stop evidence all refuse.
+
+The claim-enabled protocol in Task D carries a main role in main's isolated run unit, using the nested instance layout and a supported memory tier. Seed, sidecar and other layouts remain refused where the reservation cannot describe them. The existing flag-off `/start`, `/seed`, process backend and unit templates retain their existing path. Staged builds must also be root-owned and not writable by the agent or tenant. Verify the deployed stager satisfies that rule before activation. Do not remove those refusals to make a probe pass. Extend the reservation and root verification first if another shape is required.
+
+Before region HA stage B is armed, install and test root-owned authoritative attempt readers for each enabled operation kind, including the prepared executable/input digest and the exact operation/session/install identity. Until then, the new per-kind preflight refuses those launches before admission or attempt reservation. Keep the existing region HA adapter signatures. Also prove the root-owned input snapshot bind, same-invocation tenant-child wait, trampoline reservation/deadline check and flag-off snapshot teardown against the deployed unit template in the disposable environment.
+
+Rebase the stage B adapters onto these interfaces and test these joins:
+
+1. Replace its duplicate `AutomaticLoss`, `LocalDischarge`, loss-ID helper and `Claim.EvidenceRefs` with Task A's canonical definitions. Preserve its fence-consuming transitions, D4 retention and strict decode tests.
+2. Register the complete relocation owner set once. Route the probe through `OperationAttempts`; do not apply the old `Extend` hook or introduce a separate probe lifecycle. Keep the same admitted session through `relocating/probing -> relocating/serving`. The built region branch currently commits directly to kind `serving`; that kind change ends this session and requires a separate serving wake. It must not be treated as an implicit continuation.
+3. Reconcile its prepared-generation and `released` coverage admission with the base's generic admission. This plan permits only the accepted cold probe shape. It does not import region HA's transition producer or grant a new coverage authority. An adapter cannot bypass the gate with an optional policy callback.
+4. For a drain, call `ClaimAssembly.WritersStopped`, register the files publisher under `ClaimProducerFiles`, retain replication and file publication, and use root's observation. Drop the region branch's duplicate descriptor observer and consume the claim-owned `(TID, Device, Inode)` descriptor shape. Confirm with the live Litestream witness that descriptors opened lazily after `Replicate` do not leave the narrowing hint incomplete. An incomplete hint refuses the observation. After release publication, queue the stop-only obligation and request the full root stop. Pass its immutable key and digest to `RelocationReleaseReceipts.Complete`, then perform the region-owned executor transition. Never use a writers-only witness as a full stop receipt.
+5. A release without its exact root receipt remains pending, including if the source terminates before producing it. Plan 11 grants no fence-only discharge for this new obligation. That receipt-only rule is the accepted contract; the region-owned termination transition must account for it before activation. It must not silently drop the obligation or route it through generic chore completion.
+6. Preserve the source's files live set until the authoritative record commits. Moves Plan 5 must replace the stand-in view and prove the push, landing-install and rollback-return park exclusions at each destructive step. The model's passing J1 tests do not implement those production exclusions.
+7. Keep the R6 no-LTX refusal for every positive writer session. Keep volume-based automatic discharge refused for legacy roster entries with unknown enrollment time. Publish no invented timestamp or empty generation.
+
+The release floor must include claim-level `evidence_refs` and its nested `AutomaticLoss` fields, roster `enrolled_at`, the `relocation-release` kind, and the launch journal and protocol additions, including `writers_closed`, `close-writers`, the `spec` preimage, capability-only `kind`, and `isolated-unit-v3`, the mandatory trampoline permit, and its internal root/unit handshake. Publish it only after every enrolled actor and console can strictly decode these fields, the signed census proves the same executable set, and the existing guard transaction has armed the new floor. Do not substitute the branch's commit hash for a release tag or floor. Old binaries must not participate after publication. Rollback must respect the published floor.
+
+Run the region HA two-box model and its joint review after the stage B adapters are rewritten. Then exercise isolated launch, cancellation, root crash recovery, writers-only stop and full stop in a designated disposable systemd/cgroup environment. This authoring session used local mocks and kernel fixtures; it did not create or modify production infrastructure.
+
+## Final verification
+
+After all tasks, run the following in the disposable export. Run commands serially; no background test jobs.
+
+```bash
+nice -n 10 go test -p 1 -timeout 5m ./internal/hostactors ./internal/hostclaim ./internal/hostbox ./internal/hostguard ./internal/stopevidence ./internal/instancectl ./internal/instanceexec -count=1
+nice -n 10 go test -p 1 -timeout 5m ./internal/host ./internal/activator -run 'TestRev3|TestPlan11|TestRev2Shape|TestStopWriters|TestWriters|TestVerifyWriters|TestVerifyNoProcess|TestNoProcess|TestRootRole|TestRootClaimProcess|TestClaimLaunch|TestClaimUnitStop|TestClaimStop|TestClaimProducer|TestPlan9D|TestClaimIsolation' -count=1
+nice -n 10 go vet ./internal/hostactors ./internal/hostclaim ./internal/hostbox ./internal/hostguard ./internal/stopevidence ./internal/instancectl ./internal/instanceexec ./internal/host ./internal/activator
+nice -n 10 go build ./internal/host ./internal/activator ./internal/instancectl ./internal/instanceexec
+GOOS=darwin GOARCH=arm64 nice -n 10 go build ./internal/host ./internal/activator ./internal/instancectl ./internal/instanceexec
+GOOS=windows GOARCH=amd64 nice -n 10 go build ./internal/host ./internal/activator ./internal/instancectl ./internal/instanceexec
+```
+
+Check `gofmt -l` on every changed Go file. Before a later implementation commit lands, run the repository's full build, vet, test and formatting gate and obtain green amadan CI on that tip. This plan's focused overlay verification does not replace that landing gate.
+
+### Rev 1 authoring results
+
+The plan was replayed from a fresh export of the pinned base. All fourteen test and implementation patch blocks applied in order, and the resulting changed Go files matched the tested combined overlay byte for byte. No git index or ref was changed.
+
+The supporting packages `hostactors`, `hostclaim`, `hostbox`, `hostguard`, `stopevidence` and `instancectl` passed their full package tests. The focused host and activator set passed, including Plan 9 dispatch, writers-only stop, descriptor and no-process observations, root launch, receipt validation and disabled-flag regressions. Vet passed for all eight packages. Builds of host, activator and instancectl passed on Linux and cross-compiled for darwin/arm64 and windows/amd64. Formatting was checked on all changed files.
+
+Red runs first exposed the missing record, enrollment, dispatch, writer-stop, observation and launch interfaces. Integration regressions then demonstrated mutable policy authority, cached witness aliasing, an omitted isolated cgroup, hidden root observation interfaces, a stale console enrollment time, refusal of a valid session-zero release, recovery settling an unobserved unit, and a probe launch before the executor became the admitted writer. Each corresponding guard was fixed and its focused tests passed. The final plan places every test block before its implementation.
+
+Some early delegated socket runs failed because of sandbox restrictions or long Unix socket paths; the parent reran the focused combined set under its short disk-backed TMPDIR and it passed. The long `TestClaimCompletedProducersBoundStopAndRetry` test exceeded an early delegated timeout; it is outside the final focused set. No production systemd unit, deployment, bucket, roster or floor was changed. Real unit isolation and the joint two-box stage B activation remain rollout checks, not claimed authoring results.
+
+### Rev 2 authoring results
+
+The four R10 behavioral regressions were run against the completed Rev 1 overlay first. They failed with the expected behavioral assertions, including the premature kill of a slow valid start. The amendment's successful root fixtures now contain authoritative attempts. Its additional tests cover both serving and operation tuples, missing verifiers, input and executable substitution, pending/lost/expired participation, loss of the live lock, state changes during unit preparation, namespace/exec delay, timeout, cancellation, PID reuse and invocation replacement, and snapshot binding/cleanup.
+
+All seventeen patch blocks replayed from the pinned base with zero fuzz. The resulting Go files matched the tested combined overlay byte for byte, and formatting was clean. The full supporting-package tests passed for `hostactors`, `hostclaim`, `hostbox`, `hostguard`, `stopevidence` and `instancectl`; the expanded focused host/activator set passed, including the amendment, launch, retry/drain and Plan 9 dispatch regressions. The focused amendment/launch set also passed from the fresh replay. Vet passed for all eight packages. Host, activator and instancectl built on Linux and cross-compiled for darwin/arm64 and windows/amd64.
+
+The default Go cache was read-only in the sandbox, so verification used the explicitly permitted shared cache `/home/paulca/.cache/tmp/claude-1001/gocache`. It was not deleted. An initial broad host run exceeded the Unix socket path limit; the bootstrap now uses a shorter disk-backed temporary directory, and the same tests passed there. These setup errors were not counted as regression evidence. No git index, ref or worktree registration was written.
+
+Opus reviewed the root authority and snapshot implementation and found no P1 or P2. Its data-root observation is handled by refusing production roots other than the template's `/data`, with a construction regression. The parent checked the final callback ordering, per-kind preflight refusal and replacement-invocation cleanup. Sonnet supplied the preflight shape changes; the parent integrated and verified them. No live systemd mount or production experiment was run. The real namespace/snapshot and region HA stage B checks remain activation gates.
+
+### Rev 3 authoring results
+
+The D3R regressions compiled against the completed Rev 2 export and failed on all four round 2 defects. Setup-time expiry returned a grant and counted one tenant execution; the actual `runInit` parent was mistaken for the tenant; deleted DB/WAL, nested, unlinked and empty-directory cases returned false no-process success; and the reconstructed flag-off controller started with claim artifacts still installed. The transition test now writes different argv/environment and verifies that legacy restart consumes those new values after stopped-unit cleanup.
+
+All twenty patch blocks replayed from a fresh export of `ef3c4bff7d612c5bb4584713fef40bc0cb5a43ca` with zero fuzz. Every resulting Go file matched the tested overlay byte for byte; changed-file formatting was clean. The new regressions, Rev 2 authority checks, launch and no-process tests passed in that fresh replay. The combined focused host/activator gate passed. Full supporting-package tests passed for `hostactors`, `hostclaim`, `hostbox`, `hostguard`, `stopevidence`, `instancectl` and `instanceexec`; the last two were rerun after the boundary integration fixes. Vet passed for these packages plus host/activator. Host, activator, instancectl and instanceexec built on Linux and cross-compiled for darwin/arm64 and windows/amd64. A delegated full-host run was interrupted without a verdict; no full-repository gate is claimed.
+
+OpenAI subagents supplied bounded witness, legacy-cleanup and child-observation work. A bounded OpenAI review of the new boundary found the template's hidden boot-ID path, escaped `%i` argument, and early socket unlink before `ExecStopPost`. The amendment binds the boot ID explicitly, uses `%I`, retains the closed socket until stopped-unit proof, and tests the generated bindings and socket lifetime. No Anthropic-model subagent or review was used in this run; the Rev 1/2 review records above are historical.
+
+The default Go cache returned a read-only-filesystem error. Verification used `/home/paulca/.cache/tmp/claude-1001/gocache`, which was retained. This revision changes only this plan in the repository; no git index, refs, worktree registration or production resources were written. Tests used local sockets, kernel fixtures and the actual parent/child process arrangement; they did not create a systemd namespace or test live bind mounts. The disposable systemd rollout check and region HA stage B activation gates remain required.
## Rev 1 self-review