WIP claim Plan 14 track E checkpoint 40 (not for merge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 files changed,
+401
−1
internal/activator/backend_exec.go+9 −0internal/activator/containment.go+6 −0internal/activator/plan14_generation_backend_test.go+146 −0internal/host/plan14_generation_backend_root_linux_test.go+184 −0internal/instancectl/plan14_generation_legacy_test.go+50 −0internal/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 |