carlos / platform Public

WIP claim Plan 14 track E checkpoint 40 (not for merge)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev c8b028447d9e0ad8d3898ee9338788d7b92d86f4 parent fd1b746
6 files changed, +401 −1
  • internal/activator/backend_exec.go +9 −0
  • internal/activator/containment.go +6 −0
  • internal/activator/plan14_generation_backend_test.go +146 −0
  • internal/host/plan14_generation_backend_root_linux_test.go +184 −0
  • internal/instancectl/plan14_generation_legacy_test.go +50 −0
  • internal/provision/testdata/plan14-root-reads-e.tsv +6 −1

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	352.239s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.077s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.189s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.709s
ok  	github.com/carlosframework/platform/internal/activator	234.594s
ok  	github.com/carlosframework/platform/internal/agentsock	0.015s
ok  	github.com/carlosframework/platform/internal/alerting	0.003s
ok  	github.com/carlosframework/platform/internal/analytics	0.021s
ok  	github.com/carlosframework/platform/internal/appconfig	2.341s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.132s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.990s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.008s
ok  	github.com/carlosframework/platform/internal/caps	0.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.709s
ok  	github.com/carlosframework/platform/internal/changelog	0.297s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	364.924s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.591s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.156s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.062s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.123s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.016s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.155s
ok  	github.com/carlosframework/platform/internal/host	134.904s
ok  	github.com/carlosframework/platform/internal/hostactors	0.378s
ok  	github.com/carlosframework/platform/internal/hostbox	2.279s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.509s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.013s
ok  	github.com/carlosframework/platform/internal/hostexec	4.982s
ok  	github.com/carlosframework/platform/internal/hostfence	0.039s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.471s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.013s
ok  	github.com/carlosframework/platform/internal/hostguard	1.026s
ok  	github.com/carlosframework/platform/internal/hostops	1.468s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.013s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.013s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.472s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.155s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.014s
ok  	github.com/carlosframework/platform/internal/infraview	0.012s
ok  	github.com/carlosframework/platform/internal/instancectl	1.823s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.721s
ok  	github.com/carlosframework/platform/internal/instancefs	0.018s
ok  	github.com/carlosframework/platform/internal/instancekey	0.014s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.018s
ok  	github.com/carlosframework/platform/internal/layout	1.455s
ok  	github.com/carlosframework/platform/internal/lease	9.460s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.018s
ok  	github.com/carlosframework/platform/internal/ledger	0.024s
ok  	github.com/carlosframework/platform/internal/mail	0.168s
ok  	github.com/carlosframework/platform/internal/maintenance	0.077s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.508s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.090s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.085s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.756s
ok  	github.com/carlosframework/platform/internal/objstore	0.171s
ok  	github.com/carlosframework/platform/internal/ops	0.172s
ok  	github.com/carlosframework/platform/internal/park	0.208s
ok  	github.com/carlosframework/platform/internal/privatefile	0.006s
ok  	github.com/carlosframework/platform/internal/provision	12.098s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.462s
ok  	github.com/carlosframework/platform/internal/relay	7.238s
ok  	github.com/carlosframework/platform/internal/release	21.626s
ok  	github.com/carlosframework/platform/internal/rollout	3.659s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.007s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.005s
ok  	github.com/carlosframework/platform/internal/schedule	2.298s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.293s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.070s
ok  	github.com/carlosframework/platform/internal/status	0.038s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.205s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.006s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.569s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.025s
=== RUN   TestT4StorageProbeMinio
2026/10/14 21:14:29 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE81D8A254C146, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.022s
minio contract: PASS
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go
index 798479ab6..e048db570 100644
--- a/internal/activator/backend_exec.go
+++ b/internal/activator/backend_exec.go
@@ -438,6 +438,9 @@ func (b *ExecBackend) spawnInCgroup(rt registry.Route, build func() *exec.Cmd) (
// R-TIER); zero is b.tier(rt), the live link's, which is every caller but
// a staged spawn.
func (b *ExecBackend) spawnInCgroupGated(rt registry.Route, tier cgroup.Limit, build func() *exec.Cmd, before func() error) (*exec.Cmd, func(), error) {
+ if b.Cfg.HostWrites.Enabled {
+ return nil, nil, ErrClaimIsolationUnavailable
+ }
if tier == (cgroup.Limit{}) {
tier = b.tier(rt)
}
@@ -1431,6 +1434,9 @@ func (c *capBuffer) String() string {
// seeds that file the same way. tier is that build's own (ruling R-TIER);
// zero is the live tier.
func (b *ExecBackend) seedUnit(ctx context.Context, rt registry.Route, exe *instancectl.StagedExe, tier cgroup.Limit) error {
+ if b.Cfg.HostWrites.Enabled {
+ return ErrClaimIsolationUnavailable
+ }
host := rt.Host
if err := b.nestedGate(rt); err != nil {
return err
@@ -2986,6 +2992,9 @@ func (b *ExecBackend) startSidecarUnit(ctx context.Context, rt registry.Route) (
// generation's captured build (zero-downtime spec §7.5), which root
// validates before it lays out the mount, and tier that generation's own.
func (b *ExecBackend) startSidecarUnitFrom(ctx context.Context, rt registry.Route, exe *instancectl.StagedExe, tier cgroup.Limit) (Proc, error) {
+ if b.Cfg.HostWrites.Enabled {
+ return nil, ErrClaimIsolationUnavailable
+ }
if err := b.nestedGate(rt); err != nil {
return nil, err
}
diff --git a/internal/activator/containment.go b/internal/activator/containment.go
index 9e907366d..b1f2bd89a 100644
--- a/internal/activator/containment.go
+++ b/internal/activator/containment.go
@@ -132,6 +132,9 @@ func (b *ExecBackend) spawnInCgroupAt(rt registry.Route, child string, ownTier b
// nothing, is cleaned up as a failed start is, and comes back as before's
// own error. Nil is the spawn exactly as it was.
func (b *ExecBackend) spawnInCgroupAtTier(rt registry.Route, child string, ownTier bool, tier cgroup.Limit, build func() *exec.Cmd, before func() error) (*exec.Cmd, func(), error) {
+ if b.Cfg.HostWrites.Enabled {
+ return nil, nil, ErrClaimIsolationUnavailable
+ }
if child == "" {
return b.spawnInCgroupGated(rt, tier, build, before)
}
@@ -223,6 +226,9 @@ func (b *ExecBackend) unitGen(n uint64) (uint64, error) {
// refuses the start: dropping exe would bind the live link, a build
// nobody recorded.
func (b *ExecBackend) startRole(ctx context.Context, rt registry.Route, role instancectl.Role, gen uint64, exe *instancectl.StagedExe, tier cgroup.Limit) (string, error) {
+ if b.Cfg.HostWrites.Enabled {
+ return "", ErrClaimIsolationUnavailable
+ }
if gen == 0 {
if exe == nil {
return b.Control.Start(ctx, rt.Host, role, b.tier(rt).MemoryMax)
diff --git a/internal/activator/plan14_generation_backend_test.go b/internal/activator/plan14_generation_backend_test.go
new file mode 100644
index 000000000..f609bf736
--- /dev/null
+++ b/internal/activator/plan14_generation_backend_test.go
@@ -0,0 +1,146 @@
+package activator
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os/exec"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/cgroup"
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+func TestPlan14GenerationBackendDirectBoundary(t *testing.T) {
+ for _, claimed := range []bool{false, true} {
+ for _, strict := range []string{"unset", "ordinary", "strict"} {
+ for _, child := range []string{"", "g7", "role-seed", "role-sidecar"} {
+ t.Run(fmt.Sprintf("claim=%t/%s/%s", claimed, strict, child), func(t *testing.T) {
+ b := testExecBackend(t)
+ b.Cfg.HostWrites.Enabled = claimed
+ prepared, built, gates := 0, 0, 0
+ prepareErr := errors.New("strict preparation reached")
+ if strict != "unset" {
+ b.Strict = func(string) bool { return strict == "strict" }
+ b.prepareStrict = func(string, cgroup.Limit) (strictProc, error) {
+ prepared++
+ return nil, prepareErr
+ }
+ }
+ build := func() *exec.Cmd { built++; return exec.Command("true") }
+ before := func() error { gates++; return nil }
+ var cmd *exec.Cmd
+ var settle func()
+ var err error
+ if child == "" {
+ cmd, settle, err = b.spawnInCgroupGated(registry.Route{Host: "direct.example"}, cgroup.Limit{}, build, before)
+ } else {
+ cmd, settle, err = b.spawnInCgroupAtTier(registry.Route{Host: "direct.example"}, child, true, cgroup.Limit{}, build, before)
+ }
+ if cmd != nil {
+ if wait := cmd.Wait(); wait != nil {
+ t.Fatal(wait)
+ }
+ }
+ if settle != nil {
+ settle()
+ }
+ if claimed {
+ if !errors.Is(err, ErrClaimIsolationUnavailable) || cmd != nil || prepared != 0 || built != 0 || gates != 0 {
+ t.Fatalf("unreserved direct launch: cmd=%v prepared=%d built=%d gates=%d err=%v", cmd, prepared, built, gates, err)
+ }
+ } else if strict == "strict" {
+ if !errors.Is(err, ErrStrictRequired) || built != 0 || gates != 0 {
+ t.Fatalf("legacy strict boundary: %v", err)
+ }
+ } else if err != nil || cmd == nil || built != 1 || gates != 1 {
+ t.Fatalf("legacy spawn: cmd=%v built=%d gates=%d err=%v", cmd, built, gates, err)
+ }
+ })
+ }
+ }
+ }
+}
+
+type generationBackendControl struct{ identityControl }
+
+func (c generationBackendControl) StartGen(_ context.Context, host string, n uint64, max int64) (string, error) {
+ c.rec(fmt.Sprintf("startgen %s %d %d", host, n, max))
+ return c.inv, nil
+}
+
+func TestPlan14GenerationBackendControlBoundary(t *testing.T) {
+ for _, claimed := range []bool{false, true} {
+ for _, gen := range []uint64{0, 7} {
+ for _, pinned := range []bool{false, true} {
+ t.Run(fmt.Sprintf("claim=%t/gen=%d/pinned=%t", claimed, gen, pinned), func(t *testing.T) {
+ ctl := &fakeControl{inv: "inv1"}
+ b := testExecBackend(t)
+ b.Cfg.HostWrites.Enabled = claimed
+ b.Control = generationBackendControl{identityControl{ctl}}
+ var exe *instancectl.StagedExe
+ if pinned {
+ exe = &instancectl.StagedExe{Path: "/unused/staged", Version: "v1", Digest: strings.Repeat("a", 64)}
+ }
+ inv, err := b.startRole(t.Context(), registry.Route{Host: "direct.example"}, instancectl.RoleRun, gen, exe, cgroup.Limit{})
+ if claimed {
+ if !errors.Is(err, ErrClaimIsolationUnavailable) || inv != "" || ctl.joined() != "" {
+ t.Fatalf("unreserved control launch: inv=%q calls=%q err=%v", inv, ctl.joined(), err)
+ }
+ } else if err != nil || inv != "inv1" || ctl.joined() == "" {
+ t.Fatalf("legacy control launch: inv=%q calls=%q err=%v", inv, ctl.joined(), err)
+ }
+ })
+ }
+ }
+ }
+}
+
+func TestPlan14GenerationBackendAuxiliaryBoundary(t *testing.T) {
+ for _, claimed := range []bool{false, true} {
+ for _, role := range []string{"seed", "sidecar"} {
+ for _, pinned := range []bool{false, true} {
+ t.Run(fmt.Sprintf("claim=%t/%s/pinned=%t", claimed, role, pinned), func(t *testing.T) {
+ ctl := &fakeControl{inv: "inv1", seed: instancectl.SeedResponse{InvocationID: "s1", Result: "success", Status: "0", Code: "exited"}}
+ unit := &fakeUnit{}
+ b, rt := nestedBackend(t, ctl, unit)
+ b.Cfg.HostWrites.Enabled = claimed
+ b.Control = identityControl{ctl}
+ rt.Seed = `["seed"]`
+ stageVersion(t, b, rt.Host, "v1", "exit 0\n")
+ id, err := b.LiveIdentity(t.Context(), rt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var exe *instancectl.StagedExe
+ if pinned {
+ exe = stagedExeOf(id)
+ }
+ var p Proc
+ if role == "seed" {
+ err = b.seedUnit(t.Context(), rt, exe, cgroup.Limit{})
+ } else {
+ p, err = b.startSidecarUnitFrom(t.Context(), rt, exe, cgroup.Limit{})
+ }
+ calls := ctl.joined()
+ if p != nil {
+ unit.set("inactive", "inv1", false, false)
+ waitFor(t, "sidecar exit", p.Exited)
+ if stop := p.Stop(t.Context()); stop != nil {
+ t.Fatal(stop)
+ }
+ }
+ if claimed {
+ if !errors.Is(err, ErrClaimIsolationUnavailable) || p != nil || calls != "" {
+ t.Fatalf("unreserved auxiliary launch: proc=%v calls=%q err=%v", p, calls, err)
+ }
+ } else if err != nil || calls == "" {
+ t.Fatalf("legacy auxiliary launch: calls=%q err=%v", calls, err)
+ }
+ })
+ }
+ }
+ }
+}
diff --git a/internal/host/plan14_generation_backend_root_linux_test.go b/internal/host/plan14_generation_backend_root_linux_test.go
new file mode 100644
index 000000000..e1e2873a3
--- /dev/null
+++ b/internal/host/plan14_generation_backend_root_linux_test.go
@@ -0,0 +1,184 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestPlan14GenerationBackendInstalledRootRunner(t *testing.T) {
+ dir := generationRootRunnerDir(t)
+ pid1, err := os.ReadFile("/proc/1/comm")
+ if err != nil || strings.TrimSpace(string(pid1)) != "systemd" {
+ t.Fatal("designated runner requires systemd as PID 1")
+ }
+ for _, name := range []string{"data", "run", "opt/live", "opt/versions/v1"} {
+ if err := os.MkdirAll(filepath.Join(dir, name), 0755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ for _, name := range []string{"data", "run"} {
+ if err := os.Chown(filepath.Join(dir, name), 65534, 65534); err != nil {
+ t.Fatal(err)
+ }
+ }
+ probe := filepath.Join(dir, "opt/write-probe")
+ if err := os.WriteFile(probe, nil, 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chown(probe, 65534, 65534); err != nil {
+ t.Fatal(err)
+ }
+ binary, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ raw, err := os.ReadFile(binary)
+ if err != nil {
+ t.Fatal(err)
+ }
+ installed := filepath.Join(dir, "runner.test")
+ if err := os.WriteFile(installed, raw, 0755); err != nil {
+ t.Fatal(err)
+ }
+ artifact := filepath.Join(dir, "opt/versions/v1/app")
+ script := fmt.Sprintf("#!/bin/sh\nprintf 'executed' > '%s'\n", filepath.Join(dir, "data/executed"))
+ if err := os.WriteFile(artifact, []byte(script), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(artifact, filepath.Join(dir, "opt/live/worker.example")); err != nil {
+ t.Fatal(err)
+ }
+ raw, err = unitsFS.ReadFile("units/carlos-edge.service")
+ if err != nil {
+ t.Fatal(err)
+ }
+ ctl := func(ctx context.Context, args ...string) (string, error) {
+ ctx, cancel := context.WithTimeout(ctx, 45*time.Second)
+ defer cancel()
+ out, err := exec.CommandContext(ctx, "systemctl", args...).CombinedOutput()
+ return strings.TrimSpace(string(out)), err
+ }
+ for _, mode := range []string{"off", "on"} {
+ t.Run(mode, func(t *testing.T) {
+ unit := filepath.Base(dir) + "-backend-" + mode + ".service"
+ var lines []string
+ for _, line := range strings.Split(string(raw), "\n") {
+ key, _, _ := strings.Cut(line, "=")
+ switch key {
+ case "Requires", "After", "Wants", "ExecStart", "ExecReload", "EnvironmentFile", "User", "Group", "Restart", "Slice":
+ continue
+ case "ReadWritePaths":
+ line = "ReadWritePaths=" + filepath.Join(dir, "data") + " " + filepath.Join(dir, "run")
+ }
+ lines = append(lines, line)
+ }
+ // Test plumbing replaces identity, dependencies and entrypoint only.
+ // The generated agent's hardening and delegation remain in force.
+ content := strings.Join(lines, "\n") + fmt.Sprintf("\n[Service]\nUser=65534\nGroup=65534\nType=oneshot\nRestart=no\nTimeoutStartSec=45s\nExecStart=%s -test.run=^TestPlan14GenerationBackendUnitPayload$ -test.v -test.timeout=30s\nEnvironment=PLAN14_E40_UNIT=%s PLAN14_E40_MODE=%s\nIPAddressDeny=any\n", installed, dir, mode)
+ path := filepath.Join("/run/systemd/system", unit)
+ file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() {
+ if out, err := ctl(context.Background(), "stop", unit); err != nil {
+ t.Errorf("stop: %v %s", err, out)
+ }
+ _, _ = ctl(context.Background(), "reset-failed", unit)
+ if err := os.Remove(path); err != nil {
+ t.Error(err)
+ }
+ if out, err := ctl(context.Background(), "daemon-reload"); err != nil {
+ t.Errorf("reload: %v %s", err, out)
+ }
+ })
+ _, writeErr := file.WriteString(content)
+ if err := errors.Join(writeErr, file.Close()); err != nil {
+ t.Fatal(err)
+ }
+ for _, args := range [][]string{{"daemon-reload"}, {"start", unit}} {
+ if out, err := ctl(t.Context(), args...); err != nil {
+ logs, _ := exec.CommandContext(t.Context(), "journalctl", "-u", unit, "--no-pager", "-n", "60").CombinedOutput()
+ t.Fatalf("systemctl %v: %v %s\n%s", args, err, out, logs)
+ }
+ }
+ for prop, want := range map[string]string{"Result": "success", "ExecMainStatus": "0"} {
+ if got, err := ctl(t.Context(), "show", unit, "--property="+prop, "--value"); err != nil || got != want {
+ t.Fatalf("%s: %q %v", prop, got, err)
+ }
+ }
+ })
+ }
+}
+
+func TestPlan14GenerationBackendUnitPayload(t *testing.T) {
+ dir, mode := os.Getenv("PLAN14_E40_UNIT"), os.Getenv("PLAN14_E40_MODE")
+ if dir == "" || (mode != "on" && mode != "off") || os.Geteuid() != 65534 {
+ t.Fatal("invoke through TestPlan14GenerationBackendInstalledRootRunner")
+ }
+ opt := filepath.Join(dir, "opt")
+ if err := os.WriteFile(filepath.Join(opt, "write-probe"), nil, 0600); !errors.Is(err, syscall.EROFS) {
+ t.Fatalf("installed agent version namespace is writable: %v", err)
+ }
+ b := &activator.ExecBackend{OptDir: opt, DataDir: filepath.Join(dir, "data")}
+ b.Cfg.HostWrites.Enabled = mode == "on"
+ rt := registry.Route{Host: "worker.example", Kind: "worker", Args: `["run"]`}
+ id, err := release.ResolveLive(opt, rt.Host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ marker := filepath.Join(dir, "data/executed")
+ for _, pinned := range []bool{false, true} {
+ if err := os.Remove(marker); err != nil && !errors.Is(err, os.ErrNotExist) {
+ t.Fatal(err)
+ }
+ var onto *release.StagedIdentity
+ if pinned {
+ onto = &id
+ }
+ p, _, err := b.StartWorkerStaged(t.Context(), rt, nil, onto)
+ if p != nil {
+ t.Cleanup(func() {
+ if err := p.Stop(context.Background()); err != nil {
+ t.Error(err)
+ }
+ })
+ deadline := time.Now().Add(5 * time.Second)
+ for !p.Exited() && time.Now().Before(deadline) {
+ time.Sleep(10 * time.Millisecond)
+ }
+ if !p.Exited() {
+ t.Fatal("fixture worker did not exit")
+ }
+ }
+ if mode == "on" {
+ if !errors.Is(err, activator.ErrClaimIsolationUnavailable) || p != nil {
+ t.Fatalf("claimed raw worker: %v %v", p, err)
+ }
+ if _, err := os.Stat(marker); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("claimed worker executed: %v", err)
+ }
+ } else {
+ if err != nil || p == nil {
+ t.Fatalf("legacy raw worker: %v %v", p, err)
+ }
+ if raw, err := os.ReadFile(marker); err != nil || string(raw) != "executed" {
+ t.Fatalf("legacy execution marker: %q %v", raw, err)
+ }
+ }
+ }
+}
diff --git a/internal/instancectl/plan14_generation_legacy_test.go b/internal/instancectl/plan14_generation_legacy_test.go
new file mode 100644
index 000000000..b9a41155f
--- /dev/null
+++ b/internal/instancectl/plan14_generation_legacy_test.go
@@ -0,0 +1,50 @@
+package instancectl
+
+import (
+ "context"
+ "net/http"
+ "strings"
+ "testing"
+)
+
+func TestPlan14GenerationLegacyLaunchTransport(t *testing.T) {
+ ctl := &fakeCtl{}
+ _, socket := serveOnSocket(t, ctl, func(s *Server) {
+ s.ClaimStop = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
+ t.Error("legacy request reached claim authority")
+ })
+ })
+ c := NewClient(socket)
+ x := StagedExe{Path: "/opt/carlos/versions/v1/app", Version: "v1", Digest: strings.Repeat("a", 64)}
+ for name, run := range map[string]func(context.Context) error{
+ "run-live": func(ctx context.Context) error { _, e := c.Start(ctx, "legacy.example", RoleRun, 536870912); return e },
+ "run-staged": func(ctx context.Context) error {
+ _, e := c.StartExe(ctx, "legacy.example", RoleRun, 536870912, x)
+ return e
+ },
+ "generation-live": func(ctx context.Context) error { _, e := c.StartGen(ctx, "legacy.example", 7, 536870912); return e },
+ "generation-staged": func(ctx context.Context) error {
+ _, e := c.StartGenExe(ctx, "legacy.example", 7, 536870912, x)
+ return e
+ },
+ "seed-live": func(ctx context.Context) error { _, e := c.Seed(ctx, "legacy.example", 536870912); return e },
+ "seed-staged": func(ctx context.Context) error { _, e := c.SeedExe(ctx, "legacy.example", 536870912, x); return e },
+ "sidecar-live": func(ctx context.Context) error {
+ _, e := c.Start(ctx, "legacy.example", RoleSidecar, 536870912)
+ return e
+ },
+ "sidecar-staged": func(ctx context.Context) error {
+ _, e := c.StartSidecarExe(ctx, "legacy.example", 536870912, x)
+ return e
+ },
+ } {
+ t.Run(name, func(t *testing.T) {
+ if err := run(t.Context()); err == nil || !strings.Contains(err.Error(), ": 503:") {
+ t.Fatalf("legacy launch did not meet root refusal: %v", err)
+ }
+ })
+ }
+ if got := ctl.joined(); got != "" {
+ t.Fatalf("legacy launch reached systemctl: %s", got)
+ }
+}
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index e82a3f436..509af3c84 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -346,4 +346,9 @@ internal/hostbox/retirement_timers.go retirementWithTimers authority C,E2 E38: r
internal/activator/activator.go Service.respawnWorkerLocked authority E2 E39: close the unsupported claimed worker path before backend entry, replacement or supervised stop. Claims-off paths remain unchanged. This is refusal coverage, not positive root-reserved worker launch support. Fixtures: TestPlan14GenerationWorkerStartBoundary, TestPlan14GenerationWorkerRealBackendRefusal, TestPlan14GenerationWorkerReplacementBoundary, TestPlan14GenerationWorkerSupervisionBoundary. Pre-flight 29 and E39 worker launch audit. whole:Route@11815,whole:Route@13321,whole:Route@360,whole:Route@4164,whole:Route@6157,whole:Route@6338 096ad340799cd2a3754f85cc87149cb9ad97175027329e480f456aa9cd785f48
internal/activator/workerbuild.go Service.startWorker authority E2 E39: close the unsupported claimed worker path before backend entry, replacement or supervised stop. Claims-off paths remain unchanged. This is refusal coverage, not positive root-reserved worker launch support. Fixtures: TestPlan14GenerationWorkerStartBoundary, TestPlan14GenerationWorkerRealBackendRefusal, TestPlan14GenerationWorkerReplacementBoundary, TestPlan14GenerationWorkerSupervisionBoundary. Pre-flight 29 and E39 worker launch audit. registry:Host@1201,registry:Host@1379,registry:Host@2314,whole:Route@2420,whole:Route@931 be27c7d049a6009a8a22798569e457f5126f14d5e7c56aafb5b1bd260717f6f5
internal/activator/workerswap.go Service.ReplaceWorker authority E2 E39: close the unsupported claimed worker path before backend entry, replacement or supervised stop. Claims-off paths remain unchanged. This is refusal coverage, not positive root-reserved worker launch support. Fixtures: TestPlan14GenerationWorkerStartBoundary, TestPlan14GenerationWorkerRealBackendRefusal, TestPlan14GenerationWorkerReplacementBoundary, TestPlan14GenerationWorkerSupervisionBoundary. Pre-flight 29 and E39 worker launch audit. whole:Route@2163 1a4042b1d03f2d4202b150375a0a7965630ab7cfc5a29b1dea378110d8d95cd7
-@coverage internal/activator ed90925168c6971781112ebbc2f8516047b3bb2d230a372432cdac4b1e57c2cc
+@coverage internal/activator 64907bbda63aa712c8f875ef3daab9e5a097ca00090f482237950d0d977d2a31
+internal/activator/backend_exec.go ExecBackend.seedUnit authority E2 E40: claim-enabled raw backend refuses unsupported direct, placed and legacy-control launches before spawn. Claims-off body unchanged. TestPlan14GenerationBackendDirectBoundary, TestPlan14GenerationBackendControlBoundary, TestPlan14GenerationBackendAuxiliaryBoundary; each guard has an independent behavioral mutation. Agent-side refusal is not root reservation or containment evidence. file:Stat@1716,registry:Host@207,whole:Route@1069,whole:Route@1195,whole:Route@239,whole:Route@3274,whole:Route@3404,whole:Route@3939,whole:Route@604,whole:Route@685,whole:Route@751 0c5d68e0c522a52e21bfb74141ff309a3d115ecc5c31181b45ee0d076ef391d8
+internal/activator/backend_exec.go ExecBackend.spawnInCgroupGated authority E2 E40: claim-enabled raw backend refuses unsupported direct, placed and legacy-control launches before spawn. Claims-off body unchanged. TestPlan14GenerationBackendDirectBoundary, TestPlan14GenerationBackendControlBoundary, TestPlan14GenerationBackendAuxiliaryBoundary; each guard has an independent behavioral mutation. Agent-side refusal is not root reservation or containment evidence. registry:Host@1033,registry:Host@1096,registry:Host@1302,registry:Host@1401,registry:Host@1457,registry:Host@1530,registry:Host@346,registry:Host@428,registry:Host@450,registry:Host@684,registry:Host@763,registry:Host@831,registry:Host@990,whole:Route@282 10206208862c6f55a8312e1da34a34207719ca71a234eb08852adef7efde0b7a
+internal/activator/backend_exec.go ExecBackend.startSidecarUnitFrom authority E2 E40: claim-enabled raw backend refuses unsupported direct, placed and legacy-control launches before spawn. Claims-off body unchanged. TestPlan14GenerationBackendDirectBoundary, TestPlan14GenerationBackendControlBoundary, TestPlan14GenerationBackendAuxiliaryBoundary; each guard has an independent behavioral mutation. Agent-side refusal is not root reservation or containment evidence. registry:Host@1073,registry:Host@1540,registry:Host@1615,registry:Host@1761,registry:Host@1874,registry:Host@1929,registry:Host@992,whole:Route@1495,whole:Route@1656,whole:Route@1964,whole:Route@247,whole:Route@752,whole:Route@838,whole:Route@896 02104c3d98041c64e8b31a1a732baf302f81b848bb223b690f9bb2195a35041a
+internal/activator/containment.go ExecBackend.spawnInCgroupAtTier authority E2 E40: claim-enabled raw backend refuses unsupported direct, placed and legacy-control launches before spawn. Claims-off body unchanged. TestPlan14GenerationBackendDirectBoundary, TestPlan14GenerationBackendControlBoundary, TestPlan14GenerationBackendAuxiliaryBoundary; each guard has an independent behavioral mutation. Agent-side refusal is not root reservation or containment evidence. registry:Host@1098,registry:Host@543,registry:Host@625,registry:Host@756,registry:Host@812,registry:Host@911,whole:Route@312,whole:Route@441 99fa73504a1418a92bb3cfb2d89a34b2de4346ec7635ce5bee9044343d7b2eca
+internal/activator/containment.go ExecBackend.startRole authority E2 E40: claim-enabled raw backend refuses unsupported direct, placed and legacy-control launches before spawn. Claims-off body unchanged. TestPlan14GenerationBackendDirectBoundary, TestPlan14GenerationBackendControlBoundary, TestPlan14GenerationBackendAuxiliaryBoundary; each guard has an independent behavioral mutation. Agent-side refusal is not root reservation or containment evidence. registry:Host@312,registry:Host@459,registry:Host@573,registry:Host@835,registry:Host@899,whole:Route@334,whole:Route@539,whole:Route@724 cd68c5f7705f207659124dc3302d9977987a70cfbdec5a92ffe24262caf2256f