carlos / platform Public

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

Named-unit replacement deferred until always-running startup works; real console and woodstar services pass under bound-unit containment (reserved restart, persistent state, sibling network refusal, edge restart, exact retirement).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 7523012f2490acfbb7320c4c3c1561c7c36aba39 parent 3dbc875
9 files changed, +312 −15
  • internal/host/claimunit_production.go +6 −0
  • internal/host/claimunit_startup_gate_root_linux_test.go +73 −0
  • internal/host/plan14_console_binary_root_linux_test.go +12 −0
  • internal/host/plan14_console_root_linux_test.go +15 −0
  • internal/host/plan14_installed_launch_root_linux_test.go +74 −13
  • internal/host/plan14_woodstar_binary_root_linux_test.go +10 −0
  • internal/host/plan14_woodstar_root_linux_test.go +76 −0
  • internal/provision/testdata/plan14-root-reads-e.tsv +4 −2
  • scripts/plan14-console-root-runner.sh +42 −0

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	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	287.019s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.710s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.100s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.712s
ok  	github.com/carlosframework/platform/internal/activator	249.619s
ok  	github.com/carlosframework/platform/internal/agentsock	0.017s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.458s
ok  	github.com/carlosframework/platform/internal/apperr	0.074s
ok  	github.com/carlosframework/platform/internal/applog	0.127s
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.522s
?   	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.003s
ok  	github.com/carlosframework/platform/internal/cgroup	0.710s
ok  	github.com/carlosframework/platform/internal/changelog	0.302s
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	388.271s
?   	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	6.552s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.022s
ok  	github.com/carlosframework/platform/internal/dbverify	0.008s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.006s
ok  	github.com/carlosframework/platform/internal/docsite	0.011s
ok  	github.com/carlosframework/platform/internal/domains	2.420s
ok  	github.com/carlosframework/platform/internal/doorkey	0.011s
ok  	github.com/carlosframework/platform/internal/economics	0.007s
ok  	github.com/carlosframework/platform/internal/edge	26.670s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.016s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.139s
ok  	github.com/carlosframework/platform/internal/gate	1.665s
ok  	github.com/carlosframework/platform/internal/genselect	0.027s
ok  	github.com/carlosframework/platform/internal/geo	0.030s
ok  	github.com/carlosframework/platform/internal/geoip	0.045s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.163s
ok  	github.com/carlosframework/platform/internal/host	143.665s
ok  	github.com/carlosframework/platform/internal/hostactors	0.273s
ok  	github.com/carlosframework/platform/internal/hostbox	2.240s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.493s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.012s
ok  	github.com/carlosframework/platform/internal/hostexec	4.911s
ok  	github.com/carlosframework/platform/internal/hostfence	0.035s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.517s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.012s
ok  	github.com/carlosframework/platform/internal/hostguard	0.990s
ok  	github.com/carlosframework/platform/internal/hostops	1.563s
?   	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.012s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.446s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.127s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.012s
ok  	github.com/carlosframework/platform/internal/infraview	0.012s
ok  	github.com/carlosframework/platform/internal/instancectl	5.253s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.736s
ok  	github.com/carlosframework/platform/internal/instancefs	0.017s
ok  	github.com/carlosframework/platform/internal/instancekey	0.014s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.020s
ok  	github.com/carlosframework/platform/internal/layout	1.515s
ok  	github.com/carlosframework/platform/internal/lease	9.803s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.021s
ok  	github.com/carlosframework/platform/internal/ledger	0.009s
ok  	github.com/carlosframework/platform/internal/mail	0.149s
ok  	github.com/carlosframework/platform/internal/maintenance	0.086s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.507s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.062s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	20.993s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.753s
ok  	github.com/carlosframework/platform/internal/objstore	0.172s
ok  	github.com/carlosframework/platform/internal/ops	0.167s
ok  	github.com/carlosframework/platform/internal/park	0.202s
ok  	github.com/carlosframework/platform/internal/privatefile	0.007s
ok  	github.com/carlosframework/platform/internal/provision	17.037s
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	5.467s
ok  	github.com/carlosframework/platform/internal/relay	7.238s
ok  	github.com/carlosframework/platform/internal/release	22.321s
ok  	github.com/carlosframework/platform/internal/rollout	3.751s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.010s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.565s
ok  	github.com/carlosframework/platform/internal/s3compat	0.004s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.514s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.584s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.030s
ok  	github.com/carlosframework/platform/internal/status	0.039s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.192s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
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.004s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	1.540s
=== 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
linux/arm64 carlos binary: 47382690 bytes (ceiling 52000000)
--- 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.021s
=== RUN   TestT4StorageProbeMinio
2026/10/16 00:44:24 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEDBE1CFCE6CAB, 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.020s
minio contract: PASS
diff --git a/internal/host/claimunit_production.go b/internal/host/claimunit_production.go
index 28be4f429..ae5bc833e 100644
--- a/internal/host/claimunit_production.go
+++ b/internal/host/claimunit_production.go
@@ -179,6 +179,12 @@ func bindProductionClaimUnits(ctx context.Context, h *ClaimRootWithLaunch, a *Cl
sort.Strings(hosts)
for _, host := range hosts {
shape := shapes[host]
+ // Named services must keep their operator definition until a reserved
+ // boot/adoption controller can keep them running independently of edge.
+ if shape.Backing == "" {
+ log.Print(hostbox.ErrUnsupported)
+ continue
+ }
if !claimHexDigest(shape.Program) {
log.Print(os.ErrPermission)
continue
diff --git a/internal/host/claimunit_startup_gate_root_linux_test.go b/internal/host/claimunit_startup_gate_root_linux_test.go
new file mode 100644
index 000000000..ff7f9f5b8
--- /dev/null
+++ b/internal/host/claimunit_startup_gate_root_linux_test.go
@@ -0,0 +1,73 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "bytes"
+ "context"
+ "os"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/instancectl"
+ "github.com/carlosframework/platform/internal/rootstate"
+)
+
+func plan14ProductionStartupDefersReplacement(t *testing.T, original *ClaimLaunchRoot, material *claimUnitMaterializer, binding claimUnitBinding, ctl func(context.Context, ...string) (string, error)) {
+ t.Helper()
+ launch := *original
+ before, id, err := material.read(binding.Shape.Unit)
+ if err != nil {
+ t.Fatal(err)
+ }
+ invocation, err := ctl(t.Context(), "show", binding.Shape.Unit, "-p", "InvocationID", "--value")
+ if err != nil || invocation == "" {
+ t.Fatal("missing live predecessor", err)
+ }
+ oldOpen, oldMaterials, oldShapes := openClaimUnitBindings, openClaimUnitMaterializations, productionClaimUnitShapes
+ defer func() {
+ openClaimUnitBindings, openClaimUnitMaterializations, productionClaimUnitShapes = oldOpen, oldMaterials, oldShapes
+ }()
+ openClaimUnitBindings = func() (rootstate.Store, error) { return launch.unitBindings.state, nil }
+ openClaimUnitMaterializations = func() (rootstate.Store, error) { return material.state, nil }
+ productionClaimUnitShapes = func() (map[string]claimUnitShape, error) {
+ return map[string]claimUnitShape{binding.Shape.Host: binding.Shape}, nil
+ }
+ base := launch.Units.(instancectl.ClaimUnits)
+ base.BoundHost, base.BoundUnit = "", ""
+ for _, retained := range []bool{true, false} {
+ if !retained {
+ if err := launch.unitBindings.state.Remove(binding.Shape.Host + ".json"); err != nil {
+ t.Fatal(err)
+ }
+ }
+ launch.Units = base
+ wrapped := ClaimRootWithLaunch{Launch: &launch}
+ wrapped.Root.Stop.Coordinator.Kernel = claimUnitStopKernel{Units: base}
+ assembly := &ClaimAssembly{}
+ if err := bindProductionClaimUnits(t.Context(), &wrapped, assembly); err != nil {
+ t.Fatal("named unit blocked control startup", err)
+ }
+ if binding.Shape.Backing == "unit" {
+ if err := material.verify(binding); err != nil {
+ t.Fatal("named startup gate blocked backing-unit materialization", err)
+ }
+ return
+ }
+ after, afterID, err := material.read(binding.Shape.Unit)
+ if err != nil || !bytes.Equal(before, after) || !id.Same(afterID) {
+ t.Fatal("startup replaced named unit before reserved startup works", err)
+ }
+ afterInvocation, err := ctl(t.Context(), "show", binding.Shape.Unit, "-p", "InvocationID", "--value")
+ if err != nil || afterInvocation != invocation {
+ t.Fatal("startup stopped live predecessor", err)
+ }
+ if _, err := material.load(binding.Shape.Host); !os.IsNotExist(err) {
+ t.Fatal("startup retained named replacement intent", err)
+ }
+ if !retained {
+ if _, err := launch.unitBindings.read(binding.Shape.Host); !os.IsNotExist(err) {
+ t.Fatal("startup established named authority before startup works", err)
+ }
+ }
+ }
+}
diff --git a/internal/host/plan14_console_binary_root_linux_test.go b/internal/host/plan14_console_binary_root_linux_test.go
new file mode 100644
index 000000000..c2a8679eb
--- /dev/null
+++ b/internal/host/plan14_console_binary_root_linux_test.go
@@ -0,0 +1,12 @@
+//go:build linux && rootrunner && realconsole
+
+package host
+
+import _ "embed"
+
+// Built from cmd/console by scripts/plan14-console-root-runner.sh.
+//
+//go:embed plan14-console-runner.bin
+var plan14EmbeddedConsole []byte
+
+func init() { plan14ConsoleExecutable = plan14EmbeddedConsole }
diff --git a/internal/host/plan14_console_root_linux_test.go b/internal/host/plan14_console_root_linux_test.go
index 99396de7a..a8ceb2394 100644
--- a/internal/host/plan14_console_root_linux_test.go
+++ b/internal/host/plan14_console_root_linux_test.go
@@ -3,6 +3,8 @@
package host
import (
+ "crypto/ed25519"
+ "encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -10,6 +12,7 @@ import (
"net/http"
"net/url"
"os"
+ "path/filepath"
"strings"
"sync/atomic"
"testing"
@@ -119,3 +122,15 @@ func plan14ConsoleProgram(t *testing.T, host, endpoint string) (claimUnitProgram
}
}
}
+
+func plan14ConsoleBinaryProgram(t *testing.T, p claimUnitProgram) claimUnitProgram {
+ t.Helper()
+ pub, _, err := ed25519.GenerateKey(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ p.Args = []string{"serve"}
+ p.Env = p.Env[1:]
+ p.Env = append(p.Env, "CONSOLE_ORIGIN=http://tenant", "CARLOS_RELEASE_PUBKEY="+base64.StdEncoding.EncodeToString(pub), "CARLOS_DEPLOYMENT_DIR="+filepath.Join(provision.InstanceDir("/data", p.Host), "deployment"), "AWS_EC2_METADATA_DISABLED=true")
+ return p
+}
diff --git a/internal/host/plan14_installed_launch_root_linux_test.go b/internal/host/plan14_installed_launch_root_linux_test.go
index 2310be902..69ecf2df3 100644
--- a/internal/host/plan14_installed_launch_root_linux_test.go
+++ b/internal/host/plan14_installed_launch_root_linux_test.go
@@ -107,11 +107,39 @@ func TestPlan14BoundNetworkEdgeRootRunner(t *testing.T) {
}
}
-func TestPlan14RealConsoleRootRunner(t *testing.T) {
+func TestPlan14ConsoleLibraryRootRunner(t *testing.T) {
plan14InstalledClaimLaunch(t, "console:materialized:tcp")
}
+var plan14ConsoleExecutable, plan14WoodstarExecutable []byte
+
+func TestPlan14RealConsoleRootRunner(t *testing.T) {
+ if len(plan14ConsoleExecutable) == 0 {
+ t.Fatal("build the real executable with scripts/plan14-console-root-runner.sh")
+ }
+ plan14InstalledClaimLaunch(t, "binary:console:materialized:tcp")
+}
+
+func TestPlan14RealWoodstarRootRunner(t *testing.T) {
+ if len(plan14WoodstarExecutable) == 0 {
+ t.Fatal("set PLAN14_WOODSTAR_SOURCE when running scripts/plan14-console-root-runner.sh")
+ }
+ plan14InstalledClaimLaunch(t, "woodstar:materialized:tcp")
+}
+
+func TestPlan14NamedStartupDefersReplacementRootRunner(t *testing.T) {
+ for _, mode := range []string{"tcp", "socket-program", "backing-program"} {
+ t.Run(mode, func(t *testing.T) { plan14InstalledClaimLaunch(t, "defer:migrated:materialized:"+mode) })
+ }
+}
+
func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
+ woodstar := strings.HasPrefix(boundUnit, "woodstar:")
+ boundUnit = strings.TrimPrefix(boundUnit, "woodstar:")
+ deferReplacement := strings.HasPrefix(boundUnit, "defer:")
+ boundUnit = strings.TrimPrefix(boundUnit, "defer:")
+ consoleBinary := strings.HasPrefix(boundUnit, "binary:")
+ boundUnit = strings.TrimPrefix(boundUnit, "binary:")
realConsole := strings.HasPrefix(boundUnit, "console:")
boundUnit = strings.TrimPrefix(boundUnit, "console:")
isolation := strings.HasPrefix(boundUnit, "isolation:")
@@ -191,9 +219,25 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
if tcpMode {
program.Env = append(program.Env, "PLAN14_TENANT_ADDR="+endpoint)
}
- var checkConsole func(*http.Client)
+ var checkService func(*http.Client)
if realConsole {
- program, checkConsole = plan14ConsoleProgram(t, host, endpoint)
+ program, checkService = plan14ConsoleProgram(t, host, endpoint)
+ if consoleBinary {
+ if len(plan14ConsoleExecutable) == 0 {
+ t.Fatal("real console executable missing")
+ }
+ program = plan14ConsoleBinaryProgram(t, program)
+ }
+ }
+ if woodstar {
+ if len(plan14WoodstarExecutable) == 0 {
+ t.Fatal("real Woodstar executable missing")
+ }
+ program, checkService = plan14WoodstarProgram(t, host, endpoint)
+ }
+ healthURL := "http://tenant/"
+ if woodstar {
+ healthURL += "healthz"
}
programRaw, _ := json.Marshal(program)
programSum := sha256.Sum256(programRaw)
@@ -438,6 +482,10 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
}
}
}
+ if deferReplacement {
+ plan14ProductionStartupDefersReplacement(t, f.root, material, binding, ctl)
+ return
+ }
if err := material.install(t.Context(), binding); err != nil {
debug, _ := ctl(t.Context(), "show", unit, "-p", "ActiveState,InvocationID,ControlGroup,FragmentPath,DropInPaths")
t.Fatalf("migration/materialization: %v; unit: %s", err, debug)
@@ -497,6 +545,13 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
if err != nil {
t.Fatal(err)
}
+ runnerBody := body
+ if consoleBinary {
+ body = plan14ConsoleExecutable
+ }
+ if woodstar {
+ body = plan14WoodstarExecutable
+ }
for _, v := range []string{"v1", "v3"} {
if err := release.StageArtifact("/opt/carlos", rec.Account, v, rec.App, body); err != nil {
t.Fatal(err)
@@ -552,7 +607,7 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
lock := &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "release"), true, false) }}
checkSibling := func() {}
if programMode {
- checkSibling = plan14BoundSibling(t, body, ctl, uid, gid, lock)
+ checkSibling = plan14BoundSibling(t, runnerBody, ctl, uid, gid, lock)
checkSibling()
}
if err := f.req.Validate(); err != nil {
@@ -583,7 +638,7 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
}}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport, Timeout: 3 * time.Second}
- resp, err := client.Get("http://tenant/")
+ resp, err := client.Get(healthURL)
if err != nil {
t.Fatal("installed tenant did not serve", err)
}
@@ -592,10 +647,10 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
if resp.StatusCode != 200 {
t.Fatal(resp.Status)
}
- if realConsole {
- checkConsole(client)
+ if checkService != nil {
+ checkService(client)
}
- if isolation || realConsole {
+ if isolation || realConsole || woodstar {
if tcpMode {
siblingTransport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", filepath.Join(provision.InstanceRunDir("sibling.example"), "app.sock"))
@@ -612,7 +667,7 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
}
}
for _, name := range func() []string {
- if realConsole {
+ if realConsole || woodstar {
return nil
}
return []string{"console", "sibling"}
@@ -649,7 +704,7 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
if err != nil || before == "" || after != before {
t.Fatal("edge restart changed bound invocation", before, after, err)
}
- response, err := client.Get("http://tenant/")
+ response, err := client.Get(healthURL)
if err != nil {
t.Fatal("edge restart stopped bound service", err)
}
@@ -683,6 +738,12 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
t.Fatal(records, err)
}
v1 := records[0]
+ if consoleBinary || woodstar {
+ running, err := os.ReadFile(fmt.Sprintf("/proc/%d/exe", v1.Process.TenantPID))
+ if err != nil || sha256.Sum256(running) != sha256.Sum256(body) {
+ t.Fatal("running service is not the built executable", err)
+ }
+ }
if v1.Process.PID != response.Grant.PID || v1.Process.Containment != 1 || v1.Process.TenantPID == 0 {
t.Fatal("missing production process evidence", v1)
}
@@ -752,7 +813,7 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
if restarted.Grant.UnitInvocationID == response.Grant.UnitInvocationID {
t.Fatal("restart reused invocation")
}
- resp, err := client.Get("http://tenant/")
+ resp, err := client.Get(healthURL)
if err != nil {
t.Fatal("restarted endpoint", err)
}
@@ -761,8 +822,8 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) {
if resp.StatusCode != 200 {
t.Fatal(resp.Status)
}
- if realConsole {
- checkConsole(client)
+ if checkService != nil {
+ checkService(client)
}
stop.Reservation = &next
if _, err := f.root.Serve(t.Context(), f.peer, stop); err != nil {
diff --git a/internal/host/plan14_woodstar_binary_root_linux_test.go b/internal/host/plan14_woodstar_binary_root_linux_test.go
new file mode 100644
index 000000000..cd996873f
--- /dev/null
+++ b/internal/host/plan14_woodstar_binary_root_linux_test.go
@@ -0,0 +1,10 @@
+//go:build linux && rootrunner && realwoodstar
+
+package host
+
+import _ "embed"
+
+//go:embed plan14-woodstar-runner.bin
+var plan14EmbeddedWoodstar []byte
+
+func init() { plan14WoodstarExecutable = plan14EmbeddedWoodstar }
diff --git a/internal/host/plan14_woodstar_root_linux_test.go b/internal/host/plan14_woodstar_root_linux_test.go
new file mode 100644
index 000000000..7131e7279
--- /dev/null
+++ b/internal/host/plan14_woodstar_root_linux_test.go
@@ -0,0 +1,76 @@
+//go:build linux && rootrunner
+
+package host
+
+import (
+ "database/sql"
+ "encoding/json"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/provision"
+)
+
+func plan14WoodstarProgram(t *testing.T, host, endpoint string) (claimUnitProgram, func(*http.Client)) {
+ t.Helper()
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var contacted atomic.Int64
+ srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ contacted.Add(1)
+ io.WriteString(w, `<html><head><title>runner upstream</title></head></html>`)
+ })}
+ go srv.Serve(ln)
+ t.Cleanup(func() { srv.Close() })
+ upstream := "http://" + ln.Addr().String()
+ dbPath := provision.NestedDBPath("/data", host)
+ p := claimUnitProgram{Schema: 1, Host: host, Args: []string{"-addr", endpoint, "-db", dbPath, "-allow-private"}}
+ var savedKey string
+ var savedFetch int64
+ return p, func(c *http.Client) {
+ t.Helper()
+ resp, err := c.Get("http://tenant/unfurl?url=" + url.QueryEscape(upstream))
+ if err != nil {
+ t.Fatal(err)
+ }
+ var card map[string]any
+ err = json.NewDecoder(resp.Body).Decode(&card)
+ resp.Body.Close()
+ if err != nil || resp.StatusCode != http.StatusOK || card["ok"] != false || contacted.Load() != 0 {
+ t.Fatal("Woodstar containment allowed sibling fetch", resp.Status, card, contacted.Load(), err)
+ }
+ db, err := sql.Open("sqlite", dbPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer db.Close()
+ var key string
+ if err := db.QueryRow("SELECT value FROM config WHERE key = 'img_sign_key'").Scan(&key); err != nil || len(key) != 64 {
+ t.Fatal("Woodstar did not persist its signing key", err)
+ }
+ var fetched int64
+ if err := db.QueryRow("SELECT fetched_at FROM cards WHERE url = ? AND ok = 0", upstream).Scan(&fetched); err != nil || fetched <= 0 {
+ t.Fatal("Woodstar did not persist its cache", err, fetched)
+ }
+ if savedKey != "" && (savedKey != key || savedFetch != fetched) {
+ t.Fatal("Woodstar restart lost persistent state")
+ }
+ savedKey, savedFetch = key, fetched
+ control := &http.Client{Timeout: time.Second}
+ resp, err = control.Get(upstream)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+ if contacted.Swap(0) != 1 {
+ t.Fatal("upstream control did not connect")
+ }
+ }
+}
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv
index 573333c68..55af99dd2 100644
--- a/internal/provision/testdata/plan14-root-reads-e.tsv
+++ b/internal/provision/testdata/plan14-root-reads-e.tsv
@@ -486,8 +486,8 @@ internal/host/claimunit_program.go ClaimLaunchRoot.boundLaunchSpec authority E2
internal/host/claimunit_handover.go claimUnitMaterializer.handover authority E2 PROPOSED E49: root; retained retired predecessor binding and exact tombstone inode authorize only same-shape successor installation. The caller independently resolves the current root installation. PROPOSED E51: Root compares persistent predecessor/tombstone identity before successor replacement; owner and shape checks remain exact. dfd500b03a93044c4a8c9f67a8eeee0408e4ff163184197a8eb8fe40ee8ff351
# PROPOSED E49
-internal/host/claimunit_production.go bindProductionClaimUnits authority E2 PROPOSED E49: root; named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E50: Root installs retained-placement stop/retirement callbacks, resumes saved retirement before binding, and isolates each configuration/program/materialization failure. Initial binding reads occur outside host exclusion then local owner is checked inside. Readiness looks up the reserved host, not a startup list. PROPOSED E51: Root supplies the kernel boot reader to predecessor capture and stop; startup retirement replay remains ahead of per-unit binding. PROPOSED E52: Root wires peer-verified flat-socket capture and exact retained socket parking into canonical unit migration; no-census path unchanged. file:Read@1150 ffaf6904dece68748905b0ddba9ee94a12f50050402f6bef48f882a88c7ad934
-@coverage internal/host 5036bd72c90bf1e8416ef729277fee1ac422f205d95c6e6e965ab9508a67bddc
+internal/host/claimunit_production.go bindProductionClaimUnits authority E2 PROPOSED E49: root; named placement/program readback from retained root binding, durable unit intent and inode-conditional retirement, and readiness socket ownership. Service route fields are candidates only; final root launch rechecks independently. No no-census path added. Full always-running adoption/handover remains pending. PROPOSED E50: Root installs retained-placement stop/retirement callbacks, resumes saved retirement before binding, and isolates each configuration/program/materialization failure. Initial binding reads occur outside host exclusion then local owner is checked inside. Readiness looks up the reserved host, not a startup list. PROPOSED E51: Root supplies the kernel boot reader to predecessor capture and stop; startup retirement replay remains ahead of per-unit binding. PROPOSED E52: Root wires peer-verified flat-socket capture and exact retained socket parking into canonical unit migration; no-census path unchanged. PROPOSED E54: root skips named-unit binding/materialization until reserved always-running startup exists; retirement recovery still precedes that gate, and backing=unit retains its existing path. Root witness rejects replacement of live TCP and socket predecessors. file:Read@1150 f8cc464409153f09a65a429fabb17dc52d58df5ae170a2eab41e58f81ea04204
+@coverage internal/host 2c955ee613a768c0643e0dfacb0af22bc91b2463bf3cf9eabfc47fa6470cf71b
# PROPOSED E50: root unit migration and R-E50 safety gates
internal/caps/converge.go Converge authority E2 PROPOSED E50: Root capability producer calls CheckHost before any filesystem or systemd effect. 7231c4e38b0093565412265a540af518b60fa8b0f0263502ce8845b2d7c2967d
@@ -565,3 +565,5 @@ internal/hostbox/inventory_identity_linux.go VerifyInventoryIdentity authority E
internal/hostbox/journalservice.go journalRequestHash authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. 99703d244e91b55863a034eb7b8b7ecae50c1ca092ef9dba9594fdaff088b6e3
internal/hostbox/typedpark_linux.go sameArtifactAt authority E2,J2,J4 PROPOSED E53: Executing identity root (state capture also runs as carlos); boot-local device and mount numbers are checked through retained filesystem, handle, inode, owner and topology evidence; no old-boot recapture grants. Tests: TestPlan14InventoryIdentityBootBoundary, TestRootRebootInventoryIdentityPhase1/2. a0e3b899df85823cc4549d49f6577a7eff1549b7a0e4f1b53f4a23573ee4b948
internal/rootstate/authoritydir_linux.go OpenAuthorityDirectory authority E2,J2,J4 PROPOSED E53: Executing identity root; generation pins retain checked root ancestry and persistent filesystem/handle/inode/owner/topology evidence across boot; legacy device-only pins refuse a different boot. Tests: TestPlan14GenerationLegacyPinBootBoundary, TestRootRebootGenerationPinIdentityPhase1/2. file:Open@167,file:Openat@524 52a957fff0ec22967acc9627456cf2f5289a0563ffa94bf92f4ad59961073d7a
+@coverage scripts a620f2df4560d164dae133f136a84f61457cd4e4ac65f0e04bb27c1d9324fd21
+scripts/plan14-console-root-runner.sh external diagnostic E2 PROPOSED E54: builder identity; packages locally built console and optional archived Woodstar executable into rootrunner-only tests, invokes the separately authorized dev runner and removes its own artifacts. Not installed or called by production. file:external@0 d4b0da2675717679695cbe051b4395047cf0f0e31999cec825351f951a721878
diff --git a/scripts/plan14-console-root-runner.sh b/scripts/plan14-console-root-runner.sh
new file mode 100644
index 000000000..80ac60234
--- /dev/null
+++ b/scripts/plan14-console-root-runner.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+set -euo pipefail
+test "$#" -ge 1
+runner=$(realpath "$1")
+shift
+cd "$(dirname "$0")/.."
+worktree=$(pwd)
+artifact=internal/host/plan14-console-runner.bin
+woodstar_artifact=internal/host/plan14-woodstar-runner.bin
+test ! -e "$artifact"
+test ! -e "$woodstar_artifact"
+scratch=$(mktemp -d)
+owned_artifacts=()
+trap 'rm -f -- "${owned_artifacts[@]}"; rm -rf -- "$scratch"' EXIT
+stage_artifact() {
+ python3 - "$1" "$2" <<'PYTHON'
+import os, shutil, sys
+with open(sys.argv[1], "rb") as source, open(sys.argv[2], "xb") as target:
+ try:
+ shutil.copyfileobj(source, target)
+ except BaseException:
+ os.unlink(sys.argv[2])
+ raise
+PYTHON
+ owned_artifacts+=("$2")
+}
+export GOFLAGS=-buildvcs=false
+unset GOCACHE
+GOOS=linux GOARCH=arm64 CGO_ENABLED=0 nice -n 10 go build -o "$scratch/console" ./cmd/console
+stage_artifact "$scratch/console" "$artifact"
+tags=rootrunner,realconsole
+sha256sum "$artifact"
+if [ -n "${PLAN14_WOODSTAR_SOURCE:-}" ]; then
+ mkdir "$scratch/woodstar"
+ git -C "$PLAN14_WOODSTAR_SOURCE" rev-parse HEAD
+ git -C "$PLAN14_WOODSTAR_SOURCE" archive HEAD go.mod go.sum cmd internal | tar -x -C "$scratch/woodstar"
+ (cd "$scratch/woodstar" && GOOS=linux GOARCH=arm64 CGO_ENABLED=0 nice -n 10 go build -mod=readonly -o "$scratch/unfurl" ./cmd/unfurl)
+ stage_artifact "$scratch/unfurl" "$woodstar_artifact"
+ sha256sum "$woodstar_artifact"
+ tags=$tags,realwoodstar
+fi
+nice -n 10 python3 -u "$runner" --worktree "$worktree" --pkg ./internal/host --tags "$tags" "$@"