WIP claim Plan 14 track E checkpoint 49 (not for merge)
4a: flat named socket, durable unit materialization, root program readback, exact retirement, predecessor handover; all three shapes pass on the runner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
37 files changed,
+1712
−41
internal/activator/rolelaunch.go+1 −0internal/activator/rolelaunch_bound_linux_test.go+50 −0internal/activator/rolelaunch_root_linux.go+24 −5internal/host/claimlaunch_authority.go+6 −1internal/host/claimlaunch_root.go+7 −0internal/host/claimlaunch_transport.go+12 −0internal/host/claimowners.go+2 −1internal/host/claimproduction.go+22 −2internal/host/claimretire_transport.go+4 −0internal/host/claimroleassembly.go+3 −0internal/host/claimroleassembly_linux.go+1 −0internal/host/claimunit_binding.go+7 −1internal/host/claimunit_definition.go+27 −1internal/host/claimunit_definition_test.go+23 −0internal/host/claimunit_files_linux.go+130 −0internal/host/claimunit_files_other.go+18 −0internal/host/claimunit_handover.go+72 −0internal/host/claimunit_handover_test.go+87 −0internal/host/claimunit_inputs.go+99 −0internal/host/claimunit_inputs_test.go+107 −0internal/host/claimunit_materialize.go+198 −0internal/host/claimunit_materialize_test.go+127 −0internal/host/claimunit_production.go+70 −4internal/host/claimunit_program.go+1 −1internal/host/claimunit_program_test.go+6 −0internal/host/claimunit_ready_linux.go+126 −0internal/host/claimunit_ready_linux_test.go+40 −0internal/host/claimunit_ready_other.go+13 −0internal/host/claimunit_retire.go+45 −0internal/host/claimunit_retire_test.go+77 −0internal/host/claimwakeassembly.go+8 −1internal/host/plan14_installed_launch_root_linux_test.go+158 −9internal/host/plan14_materialized_unit_root_linux_test.go+39 −0internal/host/plan14_root_generations_test.go+7 −1internal/hostbox/rolelaunch.go+19 −3internal/hostbox/rolelaunch_inputs_test.go+30 −0internal/provision/testdata/plan14-root-reads-e.tsv+46 −11
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 281.678s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.636s
ok github.com/carlosframework/platform/cmd/moveprobe 92.301s
ok github.com/carlosframework/platform/cmd/zddprobe 0.705s
ok github.com/carlosframework/platform/internal/activator 249.425s
ok github.com/carlosframework/platform/internal/agentsock 0.017s
ok github.com/carlosframework/platform/internal/alerting 0.005s
ok github.com/carlosframework/platform/internal/analytics 0.017s
ok github.com/carlosframework/platform/internal/appconfig 2.425s
ok github.com/carlosframework/platform/internal/apperr 0.080s
ok github.com/carlosframework/platform/internal/applog 0.142s
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.978s
? 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.694s
ok github.com/carlosframework/platform/internal/changelog 0.309s
ok github.com/carlosframework/platform/internal/clamd 0.062s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 386.234s
? 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.423s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.008s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.002s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.373s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.003s
ok github.com/carlosframework/platform/internal/edge 26.448s
? 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.127s
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.017s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/historyreadback 0.148s
ok github.com/carlosframework/platform/internal/host 143.713s
ok github.com/carlosframework/platform/internal/hostactors 0.425s
ok github.com/carlosframework/platform/internal/hostbox 2.327s
ok github.com/carlosframework/platform/internal/hostclaim 0.492s
ok github.com/carlosframework/platform/internal/hostevidence 0.015s
ok github.com/carlosframework/platform/internal/hostexec 4.728s
ok github.com/carlosframework/platform/internal/hostfence 0.032s
ok github.com/carlosframework/platform/internal/hostfiles 10.538s
ok github.com/carlosframework/platform/internal/hostfloor 0.015s
ok github.com/carlosframework/platform/internal/hostguard 0.992s
ok github.com/carlosframework/platform/internal/hostops 1.539s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.015s
ok github.com/carlosframework/platform/internal/hostquarantine 0.015s
ok github.com/carlosframework/platform/internal/hostrelocate 0.441s
ok github.com/carlosframework/platform/internal/hostresolve 0.149s
ok github.com/carlosframework/platform/internal/hostwrite 0.016s
ok github.com/carlosframework/platform/internal/infraview 0.015s
ok github.com/carlosframework/platform/internal/instancectl 5.220s
ok github.com/carlosframework/platform/internal/instanceexec 2.747s
ok github.com/carlosframework/platform/internal/instancefs 0.014s
ok github.com/carlosframework/platform/internal/instancekey 0.014s
ok github.com/carlosframework/platform/internal/instanceupdates 0.025s
ok github.com/carlosframework/platform/internal/layout 1.512s
ok github.com/carlosframework/platform/internal/lease 9.810s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.005s
ok github.com/carlosframework/platform/internal/ledger 0.012s
ok github.com/carlosframework/platform/internal/mail 0.170s
ok github.com/carlosframework/platform/internal/maintenance 0.105s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.501s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.070s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 20.961s
ok github.com/carlosframework/platform/internal/moves/archive 0.756s
ok github.com/carlosframework/platform/internal/objstore 0.180s
ok github.com/carlosframework/platform/internal/ops 0.169s
ok github.com/carlosframework/platform/internal/park 0.221s
ok github.com/carlosframework/platform/internal/privatefile 0.006s
ok github.com/carlosframework/platform/internal/provision 15.944s
ok github.com/carlosframework/platform/internal/publicrelease 0.008s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.470s
ok github.com/carlosframework/platform/internal/relay 7.229s
ok github.com/carlosframework/platform/internal/release 21.464s
ok github.com/carlosframework/platform/internal/rollout 3.691s
ok github.com/carlosframework/platform/internal/rolloutwords 0.004s
ok github.com/carlosframework/platform/internal/rootaws 0.006s
ok github.com/carlosframework/platform/internal/rootparticipation 0.006s
ok github.com/carlosframework/platform/internal/rootstate 0.027s
ok github.com/carlosframework/platform/internal/runs 0.568s
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.460s
ok github.com/carlosframework/platform/internal/seeds 0.017s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.222s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.382s
ok github.com/carlosframework/platform/internal/status 0.036s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.212s
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.003s
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.001s
ok github.com/carlosframework/platform/internal/vet 1.546s
=== 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: 47251618 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/15 18:27:32 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEC750E823C67A, 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/activator/rolelaunch.go b/internal/activator/rolelaunch.go| index dc6b40c7b..135b33c02 100644 |
| --- a/internal/activator/rolelaunch.go |
| +++ b/internal/activator/rolelaunch.go |
| @@ -31,6 +31,7 @@ type RootRoleLauncher interface { |
| } |
| type RoleLaunch struct { |
| + Unit *hostbox.UnitLaunchInputs |
| Spec *hostbox.LaunchSpec |
| // Root carries a reservation to root's isolated launcher. Nil keeps |
| // every claimed launch refused. |
diff --git a/internal/activator/rolelaunch_bound_linux_test.go b/internal/activator/rolelaunch_bound_linux_test.go| new file mode 100644 |
| index 000000000..e8f245be7 |
| --- /dev/null |
| +++ b/internal/activator/rolelaunch_bound_linux_test.go |
| @@ -0,0 +1,50 @@ |
| +package activator |
| + |
| +import ( |
| + "os/exec" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| +) |
| + |
| +func TestPlan14BoundRoleLaunch(t *testing.T) { |
| + for _, attack := range []string{"none", "args", "env", "spec", "unit"} { |
| + t.Run(attack, func(t *testing.T) { |
| + f := newRootLaunchFixture(t) |
| + f.launch.Unit = &hostbox.UnitLaunchInputs{Unit: "woodstar.service", Args: []string{"serve"}, Env: []string{"ROOT_ENV=yes"}} |
| + f.launch.Spec = &hostbox.LaunchSpec{Args: []string{"serve"}, Env: []string{"ROOT_ENV=yes"}} |
| + f.build = func() *exec.Cmd { |
| + cmd := exec.Command(f.exe, "serve") |
| + cmd.Env = []string{"ROOT_ENV=yes"} |
| + if attack == "args" { |
| + cmd.Args[1] = "shell" |
| + } |
| + if attack == "env" { |
| + cmd.Env = []string{"ROOT_ENV=no"} |
| + } |
| + return cmd |
| + } |
| + if attack == "spec" { |
| + f.launch.Spec.Args = []string{"shell"} |
| + } |
| + if attack == "unit" { |
| + f.launch.Unit.Unit = "../other.service" |
| + } |
| + _, _, settle, _, err := f.launch.Start(t.Context(), "main", f.attempt, f.build, f.before) |
| + if attack != "none" { |
| + if err == nil || len(f.root.launches) != 0 { |
| + t.Fatal("changed launch reached root", err) |
| + } |
| + return |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer settle(t.Context()) |
| + if len(f.root.launches) != 1 || f.root.launches[0].Reservation.Group.RelativePath != instancectl.BoundClaimUnitControlGroup("tenant.test", "woodstar.service") { |
| + t.Fatal("wrong placement", f.root.launches) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/activator/rolelaunch_root_linux.go b/internal/activator/rolelaunch_root_linux.go| index d92c39eff..f87651a9c 100644 |
| --- a/internal/activator/rolelaunch_root_linux.go |
| +++ b/internal/activator/rolelaunch_root_linux.go |
| @@ -10,6 +10,7 @@ import ( |
| "os" |
| "os/exec" |
| "path/filepath" |
| + "slices" |
| "strconv" |
| "strings" |
| "time" |
| @@ -56,14 +57,30 @@ func (l RoleLaunch) startIsolated(ctx context.Context, role string, attempt host |
| } |
| }() |
| reservation := hostbox.LaunchReservation{ID: attempt.ID + ":" + role, Role: role, InvocationID: attempt.InvocationID, Inputs: attempt.LaunchInputs, Target: l.Target, Group: hostbox.LaunchGroup{RelativePath: instancectl.ClaimUnitControlGroup(host)}} |
| + if l.Unit != nil { |
| + if !instancectl.ValidBoundClaimUnit(l.Unit.Unit) { |
| + return fail(hostbox.ErrRoleLaunchShape) |
| + } |
| + reservation.Group.RelativePath = instancectl.BoundClaimUnitControlGroup(host, l.Unit.Unit) |
| + } |
| err := l.Work.With(ctx, l.Identity, func() error { |
| cmd = build() |
| if cmd == nil { |
| return hostwrite.ErrUnavailable |
| } |
| - argv, socket, err := isolatedArgv(cmd, host, l.LaunchDir, l.RunDir) |
| - if err != nil { |
| - return err |
| + var argv []string |
| + var socket string |
| + var err error |
| + if l.Unit != nil { |
| + if l.Spec == nil || len(cmd.Args) == 0 || !slices.Equal(cmd.Args[1:], l.Unit.Args) || !slices.Equal(cmd.Env, l.Unit.Env) || !slices.Equal(l.Spec.Args, l.Unit.Args) || !slices.Equal(l.Spec.Env, l.Unit.Env) { |
| + return hostbox.ErrRoleLaunchShape |
| + } |
| + argv = append([]string{filepath.Join(instancectl.LiveDir, host)}, l.Unit.Args...) |
| + } else { |
| + argv, socket, err = isolatedArgv(cmd, host, l.LaunchDir, l.RunDir) |
| + if err != nil { |
| + return err |
| + } |
| } |
| if l.Initialize != nil { |
| if err = l.Initialize(ctx); err != nil { |
| @@ -90,8 +107,10 @@ func (l RoleLaunch) startIsolated(ctx context.Context, role string, attempt host |
| if err = os.MkdirAll(l.RunDir, 0o750|os.ModeSetgid); err != nil { |
| return err |
| } |
| - if err = os.Remove(socket); err != nil && !errors.Is(err, os.ErrNotExist) { |
| - return err |
| + if socket != "" { |
| + if err = os.Remove(socket); err != nil && !errors.Is(err, os.ErrNotExist) { |
| + return err |
| + } |
| } |
| raw, err := json.MarshalIndent(instancectl.Launch{Schema: instancectl.LaunchSchema, Host: host, Argv: argv, Env: append([]string(nil), cmd.Env...)}, "", " ") |
| if err != nil { |
diff --git a/internal/host/claimlaunch_authority.go b/internal/host/claimlaunch_authority.go| index 31bc1eafe..dbd92a35d 100644 |
| --- a/internal/host/claimlaunch_authority.go |
| +++ b/internal/host/claimlaunch_authority.go |
| @@ -176,7 +176,12 @@ func (h *ClaimLaunchRoot) waitReady(ctx context.Context, s instancectl.ClaimUnit |
| if err := h.Units.Same(ctx, s); err != nil { |
| return ClaimLaunchObserved{}, err |
| } |
| - return got, nil |
| + if h.boundReady == nil || h.boundReady(ctx, s) == nil { |
| + if err := h.Units.Same(ctx, s); err != nil { |
| + return ClaimLaunchObserved{}, err |
| + } |
| + return got, nil |
| + } |
| } |
| select { |
| case <-ctx.Done(): |
diff --git a/internal/host/claimlaunch_root.go b/internal/host/claimlaunch_root.go| index 1848f1760..85489af03 100644 |
| --- a/internal/host/claimlaunch_root.go |
| +++ b/internal/host/claimlaunch_root.go |
| @@ -62,6 +62,7 @@ type ClaimLaunchObserved struct { |
| // roster identity, its local launch journal and its own view of the kernel. |
| // A request only names which reservation to run. |
| type ClaimLaunchRoot struct { |
| + boundReady func(context.Context, instancectl.ClaimUnitStarted) error |
| unitProgram func(claimUnitShape) (claimUnitProgram, error) |
| unitBindings *claimUnitBindings |
| history rootHistoryAccess |
| @@ -100,6 +101,12 @@ func (h *ClaimLaunchRoot) Serve(ctx context.Context, peer StopPeer, q hostbox.Ro |
| } |
| out := hostbox.RoleLaunchResponse{V: hostbox.RoleLaunchV, Protocol: hostbox.RoleLaunchProtocol} |
| switch q.Op { |
| + case hostbox.RoleLaunchInputs: |
| + inputs, err := h.boundInputs(ctx, *q.Route) |
| + if err != nil { |
| + return hostbox.RoleLaunchResponse{}, err |
| + } |
| + out.Inputs = inputs |
| case hostbox.RoleLaunchCapability: |
| if q.Kind != "" && q.Kind != hostclaim.KindServing { |
| if (q.Kind != hostclaim.KindLanding && q.Kind != hostclaim.KindRelocating) || h.Attempts[q.Kind] == nil { |
diff --git a/internal/host/claimlaunch_transport.go b/internal/host/claimlaunch_transport.go| index 89b070213..2740b7a2b 100644 |
| --- a/internal/host/claimlaunch_transport.go |
| +++ b/internal/host/claimlaunch_transport.go |
| @@ -11,6 +11,7 @@ import ( |
| "github.com/carlosframework/platform/internal/activator" |
| "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/registry" |
| ) |
| // ClaimRoleRootClient is the agent's client of POST /claim/launch on the |
| @@ -124,3 +125,14 @@ func (c *ClaimRoleRootClient) KindCapability(ctx context.Context, kind string) e |
| } |
| return nil |
| } |
| + |
| +func (c *ClaimRoleRootClient) UnitInputs(ctx context.Context, route registry.Route) (*hostbox.UnitLaunchInputs, error) { |
| + out, err := c.call(ctx, hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchInputs, Route: &route}) |
| + if err != nil { |
| + return nil, err |
| + } |
| + if out.Protocol != hostbox.RoleLaunchProtocol { |
| + return nil, hostbox.ErrChanged |
| + } |
| + return out.Inputs, nil |
| +} |
diff --git a/internal/host/claimowners.go b/internal/host/claimowners.go| index 75bf335ae..e4bb1ecbf 100644 |
| --- a/internal/host/claimowners.go |
| +++ b/internal/host/claimowners.go |
| @@ -103,6 +103,7 @@ func claimRelocationC1(getenv func(string) string, movesC1 bool, r ClaimRelocati |
| // environment, the cgroup limit, the live version, and the config and |
| // release identities the provider's InputPolicy accepts for that route. |
| type ClaimServingLaunch struct { |
| + Unit *hostbox.UnitLaunchInputs |
| Identity hostwrite.Identity |
| Executable string |
| ExecutableSHA256 string |
| @@ -424,7 +425,7 @@ func (s *claimServingOwners) Select(ctx context.Context, rt registry.Route, o ho |
| if len(roles) == 0 { |
| roles = []string{"main"} |
| } |
| - in := ClaimLaunchInputs{Roles: roles, Identity: l.Identity, Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: append([]string(nil), l.Env...), DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit, |
| + in := ClaimLaunchInputs{Unit: l.Unit, Roles: roles, Identity: l.Identity, Executable: l.Executable, ExecutableSHA256: l.ExecutableSHA256, Args: append([]string(nil), l.Args...), Env: append([]string(nil), l.Env...), DBPath: rt.DBPath, Version: l.Version, Staged: l.Staged, Limit: l.Limit, |
| Admission: hostclaim.Admission{ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: s.Box, Mode: "cold", NewGeneration: generation, Region: s.Region}} |
| if claimEmptyBootstrap(h) { |
| in.Empty = true |
diff --git a/internal/host/claimproduction.go b/internal/host/claimproduction.go| index 6d112723c..53f40ddbf 100644 |
| --- a/internal/host/claimproduction.go |
| +++ b/internal/host/claimproduction.go |
| @@ -16,6 +16,7 @@ import ( |
| "github.com/carlosframework/platform/internal/activator" |
| "github.com/carlosframework/platform/internal/appconfig" |
| "github.com/carlosframework/platform/internal/boxid" |
| + "github.com/carlosframework/platform/internal/controlsock" |
| "github.com/carlosframework/platform/internal/genselect" |
| "github.com/carlosframework/platform/internal/hostactors" |
| "github.com/carlosframework/platform/internal/hostbox" |
| @@ -179,7 +180,7 @@ func (ProductionClaimOwnerProvider) OpenClaimOwners(ctx context.Context, store r |
| // The token mint is the only write this source makes. It runs under the |
| // same guard and claim store the assembly's WorkSource uses. |
| tokens := claimAdminTokens{Guard: hostguard.Guard{DataDir: r.DataDir, Claims: hostguard.StoreReader{Store: store}}, Claims: store, Verify: out.Identity.Verify, Local: out.Identity.Local(local), Policy: hostbox.Policy{Held: hostbox.RegistryHoldPolicy(r.Registry)}.Check, Now: time.Now} |
| - out.Serving.Launch = claimServingLaunchSource(backend, tokens) |
| + out.Serving.Launch = claimServingLaunchSource(backend, tokens, NewClaimRoleRootClient(controlsock.Path).UnitInputs) |
| out.Serving.Markers = marker |
| if hostwrite.Enabled(claimEntryEnv(ctx)) && moves.Enabled(claimEntryEnv(ctx)) { |
| out.moveRead = &claimMoveReadAccess{check: claimBoundIdentity(out.Identity.Verify, actor), actor: actor, route: r.Registry.Get, launch: claimMoveLaunchSource(backend)} |
| @@ -281,6 +282,16 @@ func claimInputPolicy(route func(string) (*registry.Route, error), store release |
| if id.Release != "" && id.Config == "" { |
| return release.CheckRouteReleaseIdentity(ctx, store, pub, *rt, id) |
| } |
| + if rt.Unit != "" || rt.Backing != "" { |
| + snapshot, err := claimBoundSnapshot(ctx, backend, *rt, NewClaimRoleRootClient(controlsock.Path).UnitInputs) |
| + if err != nil { |
| + return err |
| + } |
| + if id != snapshot.Identity { |
| + return hostbox.ErrChanged |
| + } |
| + return nil |
| + } |
| snapshot, err := backend.SnapshotClaimLaunch(ctx, *rt) |
| if err != nil { |
| return err |
| @@ -511,8 +522,17 @@ func (t claimAdminTokens) With(ctx context.Context, host string, effect func() e |
| // authorized token initialization, then the read-only snapshot with the |
| // replica gate (SelectClaimLaunch), then a declined box's launch gate |
| // (declinedClaimGate). |
| -func claimServingLaunchSource(backend *activator.ExecBackend, tokens claimAdminTokens) func(context.Context, registry.Route) (ClaimServingLaunch, error) { |
| +func claimServingLaunchSource(backend *activator.ExecBackend, tokens claimAdminTokens, readers ...claimUnitInputReader) func(context.Context, registry.Route) (ClaimServingLaunch, error) { |
| return func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) { |
| + if len(readers) != 0 { |
| + unit, err := readers[0](ctx, rt) |
| + if err != nil { |
| + return ClaimServingLaunch{}, err |
| + } |
| + if unit != nil { |
| + return claimBoundSnapshot(ctx, backend, rt, func(context.Context, registry.Route) (*hostbox.UnitLaunchInputs, error) { return unit, nil }) |
| + } |
| + } |
| if err := backend.EnsureClaimAdminToken(ctx, rt, func(ctx context.Context, effect func() error) error { return tokens.With(ctx, rt.Host, effect) }); err != nil { |
| return ClaimServingLaunch{}, err |
| } |
diff --git a/internal/host/claimretire_transport.go b/internal/host/claimretire_transport.go| index d53fad314..ca6ded624 100644 |
| --- a/internal/host/claimretire_transport.go |
| +++ b/internal/host/claimretire_transport.go |
| @@ -22,6 +22,7 @@ import ( |
| ) |
| type ClaimRootHTTP struct { |
| + retireBoundUnit func(context.Context, hostguard.FenceTarget) error |
| history rootHistoryAccess |
| executionRuntime *claimExecutionRuntime |
| Execution *execlease.RootBroker |
| @@ -163,6 +164,9 @@ func (h ClaimRootHTTP) retire(ctx context.Context, target hostguard.FenceTarget) |
| a.Stop = func(context.Context, registry.Route) (hostguard.FenceTarget, error) { return target, nil } |
| a.Retirement.Filesystem = hostbox.TypedParker{Roots: h.Roots, ParkedRoots: h.Roots, Incarnation: target.Incarnation, Host: target.Host, Retirement: target.FenceID} |
| a.Retirement.Authority = hostbox.InstallationCleanup{Guard: a.Reconciler.Guard, Verify: verifyRetirementReceipt} |
| + if h.retireBoundUnit != nil { |
| + a.Retirement.Authority = claimBoundUnitCleanup{inner: a.Retirement.Authority, retire: h.retireBoundUnit} |
| + } |
| a.Retirement.Ack = retirementAcknowledgement(h.Stop.Coordinator.Evidence) |
| return a.Retire(ctx, row) |
| } |
diff --git a/internal/host/claimroleassembly.go b/internal/host/claimroleassembly.go| index 3a108aa60..02cc601f1 100644 |
| --- a/internal/host/claimroleassembly.go |
| +++ b/internal/host/claimroleassembly.go |
| @@ -94,6 +94,9 @@ func (a *ClaimRoleAssembly) PreflightLaunch(host string, in ClaimLaunchInputs) e |
| if err != nil { |
| return err |
| } |
| + if in.Unit != nil { |
| + return claimBoundInputMatch(host, in) |
| + } |
| // The same validator startIsolated runs on the built command. |
| _, socket, db, err := activator.IsolatedArgv(host, paths.LaunchDir, paths.RunDir, in.Args, in.Env) |
| if err != nil { |
diff --git a/internal/host/claimroleassembly_linux.go b/internal/host/claimroleassembly_linux.go| index ad98346c0..fdf59e3e2 100644 |
| --- a/internal/host/claimroleassembly_linux.go |
| +++ b/internal/host/claimroleassembly_linux.go |
| @@ -157,6 +157,7 @@ func (a *ClaimRoleAssembly) Launch(ctx context.Context, o hostclaim.Observed, wo |
| return nil, hostwrite.ErrUnavailable |
| } |
| launch := *pre |
| + launch.Unit = in.Unit |
| launch.Spec = &hostbox.LaunchSpec{Identity: in.Identity, Executable: in.Executable, Digest: in.ExecutableSHA256, Args: append([]string(nil), in.Args...), Env: append([]string(nil), in.Env...)} |
| launch.Target, launch.Work, launch.Identity = target, work, in.Identity |
| var mu sync.Mutex |
diff --git a/internal/host/claimunit_binding.go b/internal/host/claimunit_binding.go| index 8221dc55b..4910a370d 100644 |
| --- a/internal/host/claimunit_binding.go |
| +++ b/internal/host/claimunit_binding.go |
| @@ -32,6 +32,7 @@ type claimUnitBinding struct { |
| } |
| type claimUnitBindings struct { |
| + handover func(context.Context, claimUnitBinding, claimUnitBinding) error |
| state rootstate.Store |
| installation func(context.Context, string) (hostguard.RootInstallation, error) |
| canonical func(context.Context, string) (claimUnitShape, error) |
| @@ -173,7 +174,12 @@ func (s *claimUnitBindings) establish(ctx context.Context, host string) (claimUn |
| old, err := s.read(host) |
| if err == nil { |
| if old != next { |
| - return claimUnitBinding{}, os.ErrPermission |
| + if !old.Retired || old.Shape != next.Shape || old.Owner == next.Owner || s.handover == nil { |
| + return claimUnitBinding{}, os.ErrPermission |
| + } |
| + if err := s.handover(ctx, old, next); err != nil { |
| + return claimUnitBinding{}, err |
| + } |
| } |
| } else if !errors.Is(err, os.ErrNotExist) { |
| return claimUnitBinding{}, err |
diff --git a/internal/host/claimunit_definition.go b/internal/host/claimunit_definition.go| index 9c72d987b..6aa089861 100644 |
| --- a/internal/host/claimunit_definition.go |
| +++ b/internal/host/claimunit_definition.go |
| @@ -24,6 +24,13 @@ func claimBoundUnitDefinition(s claimUnitShape) ([]byte, error) { |
| // With no live root gate, even a direct systemctl restart must execute no |
| // tenant code. The temporary reservation drop-in replaces this command. |
| body = strings.Replace(body, "ExecStart="+instancectl.SystemBinary+" instance-exec run "+provision.InstanceID(s.Host), "ExecStart=/usr/bin/false", 1) |
| + if s.Socket != "" && s.Socket != provision.NestedSocket(s.Host) { |
| + if !claimFlatSocket(s) { |
| + return nil, os.ErrPermission |
| + } |
| + body = strings.Replace(body, "TemporaryFileSystem=/run/carlos:ro\n", "", 1) |
| + body += "\n[Service]\nBindPaths=" + provision.InstanceRunDir(s.Host) + ":/run/carlos\nReadWritePaths=/run/carlos\n" |
| + } |
| if s.Addr != "" { |
| body += "\n[Service]\nIPAddressAllow=localhost\n" |
| } |
| @@ -35,7 +42,7 @@ func verifyClaimBoundUnitDefinition(s claimUnitShape) error { |
| if err != nil { |
| return err |
| } |
| - f, err := rootstate.OpenAuthorityFile(filepath.Join("/run/systemd/system", s.Unit)) |
| + f, err := rootstate.OpenTrustedFile(filepath.Join("/run/systemd/system", s.Unit)) |
| if err != nil { |
| return err |
| } |
| @@ -49,3 +56,22 @@ func verifyClaimBoundUnitDefinition(s claimUnitShape) error { |
| } |
| return nil |
| } |
| + |
| +func claimFlatSocket(s claimUnitShape) bool { |
| + if filepath.Dir(s.Socket) != provision.SocketDir || !strings.HasSuffix(s.Socket, ".sock") { |
| + return false |
| + } |
| + for _, r := range filepath.Base(s.Socket) { |
| + if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.') { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
| + |
| +func claimSocketTarget(s claimUnitShape) string { |
| + if claimFlatSocket(s) { |
| + return filepath.Join(provision.InstanceRunDir(s.Host), filepath.Base(s.Socket)) |
| + } |
| + return s.Socket |
| +} |
diff --git a/internal/host/claimunit_definition_test.go b/internal/host/claimunit_definition_test.go| index bffe259b6..46570b851 100644 |
| --- a/internal/host/claimunit_definition_test.go |
| +++ b/internal/host/claimunit_definition_test.go |
| @@ -33,6 +33,7 @@ func TestPlan14BoundUnitDefinition(t *testing.T) { |
| } |
| } |
| shape.Host = "woodstar-media.example" |
| + shape.Socket = provision.NestedSocket(shape.Host) |
| raw, err := claimBoundUnitDefinition(shape) |
| if err != nil { |
| t.Fatal(err) |
| @@ -41,3 +42,25 @@ func TestPlan14BoundUnitDefinition(t *testing.T) { |
| t.Fatal("named unit used unescaped slice") |
| } |
| } |
| + |
| +func TestPlan14FlatBoundSocketDefinition(t *testing.T) { |
| + _, _, shape := plan14UnitBindingFixture(t, "woodstar-media.service", "", "/run/carlos/woodstar-media.sock") |
| + shape.Program = strings.Repeat("a", 64) |
| + raw, err := claimBoundUnitDefinition(shape) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + own := provision.InstanceRunDir(shape.Host) |
| + if !strings.Contains(string(raw), "BindPaths="+own+":/run/carlos\n") || !strings.Contains(string(raw), "ReadWritePaths=/run/carlos\n") { |
| + t.Fatal("flat path has no private run view") |
| + } |
| + if strings.Contains(string(raw), "BindPaths=/run/carlos:/run/carlos") || strings.Contains(string(raw), "TemporaryFileSystem=/run/carlos:ro") { |
| + t.Fatal("flat socket exposes siblings") |
| + } |
| + for _, socket := range []string{"/run/carlos/../other.sock", "/run/carlos/other/path.sock", "/run/carlos/space name.sock", "/run/carlos/percent%name.sock", "/elsewhere/app.sock"} { |
| + shape.Socket = socket |
| + if _, err := claimBoundUnitDefinition(shape); err == nil { |
| + t.Fatal("unsafe socket", socket) |
| + } |
| + } |
| +} |
diff --git a/internal/host/claimunit_files_linux.go b/internal/host/claimunit_files_linux.go| new file mode 100644 |
| index 000000000..32b1bfd31 |
| --- /dev/null |
| +++ b/internal/host/claimunit_files_linux.go |
| @@ -0,0 +1,130 @@ |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "errors" |
| + "io" |
| + "os" |
| + "path/filepath" |
| + |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "github.com/carlosframework/platform/internal/safefile" |
| + "golang.org/x/sys/unix" |
| +) |
| + |
| +type claimUnitFiles struct{ dir string } |
| + |
| +func (f claimUnitFiles) read(unit string) ([]byte, rootstate.FileIdentity, error) { |
| + if !instancectl.ValidBoundClaimUnit(unit) { |
| + return nil, rootstate.FileIdentity{}, os.ErrPermission |
| + } |
| + file, err := rootstate.OpenTrustedFile(filepath.Join(f.dir, unit)) |
| + if err != nil { |
| + return nil, rootstate.FileIdentity{}, err |
| + } |
| + defer file.Close() |
| + raw, err := io.ReadAll(io.LimitReader(file, rootstate.MaxRecord+1)) |
| + if err != nil { |
| + return nil, rootstate.FileIdentity{}, err |
| + } |
| + if len(raw) > rootstate.MaxRecord { |
| + return nil, rootstate.FileIdentity{}, os.ErrPermission |
| + } |
| + id, err := rootstate.Identity(file) |
| + return raw, id, err |
| +} |
| +func (f claimUnitFiles) create(unit string, body []byte) error { |
| + return f.write(unit, rootstate.FileIdentity{}, body) |
| +} |
| +func (f claimUnitFiles) replace(unit string, id rootstate.FileIdentity, body []byte) error { |
| + if id == (rootstate.FileIdentity{}) { |
| + return os.ErrPermission |
| + } |
| + return f.write(unit, id, body) |
| +} |
| +func (f claimUnitFiles) write(unit string, id rootstate.FileIdentity, body []byte) error { |
| + if !instancectl.ValidBoundClaimUnit(unit) { |
| + return os.ErrPermission |
| + } |
| + checked, err := rootstate.Open(f.dir, false, true) |
| + if err != nil { |
| + return err |
| + } |
| + defer checked.Close() |
| + parent, err := safefile.OpenDir(f.dir) |
| + if err != nil { |
| + return err |
| + } |
| + defer parent.Close() |
| + tmp, err := os.CreateTemp(f.dir, ".claim-unit-") |
| + if err != nil { |
| + return err |
| + } |
| + name := filepath.Base(tmp.Name()) |
| + defer unix.Unlinkat(int(parent.Fd()), name, 0) |
| + _, err = tmp.Write(body) |
| + if err == nil { |
| + err = tmp.Chmod(0644) |
| + } |
| + if err == nil { |
| + err = tmp.Sync() |
| + } |
| + if err = errors.Join(err, tmp.Close()); err != nil { |
| + return err |
| + } |
| + flags := uint(unix.RENAME_NOREPLACE) |
| + if id != (rootstate.FileIdentity{}) { |
| + _, current, err := f.read(unit) |
| + if err != nil { |
| + return err |
| + } |
| + if current != id { |
| + return os.ErrPermission |
| + } |
| + flags = 0 |
| + } |
| + if err = unix.Renameat2(int(parent.Fd()), name, int(parent.Fd()), unit, flags); err != nil { |
| + return err |
| + } |
| + if err = parent.Sync(); err != nil { |
| + return err |
| + } |
| + got, _, err := f.read(unit) |
| + if err != nil { |
| + return err |
| + } |
| + if !bytes.Equal(got, body) { |
| + return os.ErrPermission |
| + } |
| + return nil |
| +} |
| + |
| +// The flat name is only a routing projection. Root launch and containment |
| +// retain the binding and the private run directory independently of it. |
| +func projectClaimBoundSocket(s claimUnitShape) error { |
| + if !claimFlatSocket(s) { |
| + return nil |
| + } |
| + parent, err := safefile.OpenDir(filepath.Dir(s.Socket)) |
| + if err != nil { |
| + return err |
| + } |
| + defer parent.Close() |
| + name := filepath.Base(s.Socket) |
| + target := claimSocketTarget(s) |
| + if err := unix.Symlinkat(target, int(parent.Fd()), name); err != nil { |
| + if !errors.Is(err, unix.EEXIST) { |
| + return err |
| + } |
| + var buf [4096]byte |
| + n, err := unix.Readlinkat(int(parent.Fd()), name, buf[:]) |
| + if err != nil { |
| + return err |
| + } |
| + if string(buf[:n]) != target { |
| + return os.ErrPermission |
| + } |
| + } |
| + return parent.Sync() |
| +} |
diff --git a/internal/host/claimunit_files_other.go b/internal/host/claimunit_files_other.go| new file mode 100644 |
| index 000000000..950f74ccb |
| --- /dev/null |
| +++ b/internal/host/claimunit_files_other.go |
| @@ -0,0 +1,18 @@ |
| +//go:build !linux |
| + |
| +package host |
| + |
| +import ( |
| + "github.com/carlosframework/platform/internal/rootstate" |
| + "os" |
| +) |
| + |
| +type claimUnitFiles struct{ dir string } |
| + |
| +func (claimUnitFiles) read(string) ([]byte, rootstate.FileIdentity, error) { |
| + return nil, rootstate.FileIdentity{}, os.ErrPermission |
| +} |
| +func (claimUnitFiles) create(string, []byte) error { return os.ErrPermission } |
| +func (claimUnitFiles) replace(string, rootstate.FileIdentity, []byte) error { return os.ErrPermission } |
| + |
| +func projectClaimBoundSocket(claimUnitShape) error { return os.ErrPermission } |
diff --git a/internal/host/claimunit_handover.go b/internal/host/claimunit_handover.go| new file mode 100644 |
| index 000000000..239dee805 |
| --- /dev/null |
| +++ b/internal/host/claimunit_handover.go |
| @@ -0,0 +1,72 @@ |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +// The caller holds the binding lock and has resolved the successor from the |
| +// current root installation. A route edit never reaches this transition. |
| +func (m claimUnitMaterializer) handover(ctx context.Context, old, next claimUnitBinding) error { |
| + if m.state == nil || m.read == nil || m.replace == nil || m.idle == nil || !old.valid() || !next.valid() || !old.Retired || next.Retiring || next.Retired || old.Shape != next.Shape || old.Owner == next.Owner { |
| + return os.ErrPermission |
| + } |
| + unlock, err := m.state.Lock(ctx, "materialize") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlock() |
| + r, err := m.load(old.Owner.Host) |
| + if err != nil { |
| + return err |
| + } |
| + want, err := claimOwnedUnitDefinition(next) |
| + if err != nil { |
| + return err |
| + } |
| + previous := old |
| + previous.Retiring, previous.Retired = false, false |
| + if r.Binding == previous { |
| + if !r.Retired || r.Identity == (rootstate.FileIdentity{}) { |
| + return os.ErrPermission |
| + } |
| + raw, id, err := m.read(old.Shape.Unit) |
| + if err != nil { |
| + return err |
| + } |
| + if id != r.Identity || !bytes.Equal(raw, claimRetiredUnitDefinition(old)) { |
| + return os.ErrPermission |
| + } |
| + r = claimUnitMaterialization{Binding: next, Digest: claimUnitDigest(want), Prior: old, PriorIdentity: r.Identity} |
| + } |
| + if r.Binding != next || r.Prior != old || r.Digest != claimUnitDigest(want) || r.Retiring || r.Retired { |
| + return os.ErrPermission |
| + } |
| + if err = m.save(r); err != nil { |
| + return err |
| + } |
| + raw, id, err := m.read(next.Shape.Unit) |
| + if err != nil { |
| + return err |
| + } |
| + if r.Identity == (rootstate.FileIdentity{}) && id == r.PriorIdentity && bytes.Equal(raw, claimRetiredUnitDefinition(old)) { |
| + if err = m.idle(next.Shape.Host); err != nil { |
| + return err |
| + } |
| + if err = m.replace(next.Shape.Unit, r.PriorIdentity, want); err != nil { |
| + return err |
| + } |
| + raw, id, err = m.read(next.Shape.Unit) |
| + if err != nil { |
| + return err |
| + } |
| + } |
| + if !bytes.Equal(raw, want) || id == (rootstate.FileIdentity{}) || (r.Identity != (rootstate.FileIdentity{}) && r.Identity != id) { |
| + return os.ErrPermission |
| + } |
| + r.Identity = id |
| + return m.save(r) |
| +} |
diff --git a/internal/host/claimunit_handover_test.go b/internal/host/claimunit_handover_test.go| new file mode 100644 |
| index 000000000..f0c812de4 |
| --- /dev/null |
| +++ b/internal/host/claimunit_handover_test.go |
| @@ -0,0 +1,87 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14UnitPredecessorHandover(t *testing.T) { |
| + for _, attack := range []string{"none", "not retired", "same owner", "shape", "replacement", "lost materialization"} { |
| + t.Run(attack, func(t *testing.T) { |
| + b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") |
| + shape.Program = strings.Repeat("a", 64) |
| + b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } |
| + b.installation = func(context.Context, string) (hostguard.RootInstallation, error) { return in, nil } |
| + files := &plan14UnitFiles{} |
| + state := &plan14UnitBindingFault{Store: &rollbackMemory{files: map[string][]byte{}}} |
| + m := claimUnitMaterializer{idle: func(string) error { return nil }, state: state, read: files.read, create: files.create, replace: files.replace} |
| + b.handover = m.handover |
| + old, err := b.establish(t.Context(), in.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = m.install(t.Context(), old); err != nil { |
| + t.Fatal(err) |
| + } |
| + if attack != "not retired" { |
| + if err = b.retire(t.Context(), old, func(claimUnitBinding) error { return m.retire(t.Context(), old, func() error { return nil }) }); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if attack != "same owner" { |
| + in.Stamp++ |
| + } |
| + if attack == "shape" { |
| + shape.Addr = "127.0.0.1:9999" |
| + } |
| + if attack == "replacement" { |
| + files.id.Inode++ |
| + } |
| + original := m.replace |
| + if attack == "lost materialization" { |
| + m.replace = func(unit string, id rootstate.FileIdentity, body []byte) error { |
| + if err := original(unit, id, body); err != nil { |
| + return err |
| + } |
| + return os.ErrInvalid |
| + } |
| + b.handover = m.handover |
| + } |
| + next, err := b.establish(t.Context(), in.Host) |
| + if attack != "none" && attack != "lost materialization" { |
| + if err == nil { |
| + t.Fatal("unsafe handover accepted", next) |
| + } |
| + return |
| + } |
| + if attack == "lost materialization" { |
| + if err == nil { |
| + t.Fatal("lost replacement reported success") |
| + } |
| + m.replace = original |
| + b.handover = m.handover |
| + next, err = b.establish(t.Context(), in.Host) |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if next.Owner.Stamp != in.Stamp || next.Retired || next.Retiring { |
| + t.Fatal(next) |
| + } |
| + if err = m.verify(next); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = b.retire(t.Context(), old, func(claimUnitBinding) error { t.Fatal("predecessor reached successor"); return nil }); err == nil { |
| + t.Fatal("stale retirement accepted") |
| + } |
| + if _, err = b.establish(t.Context(), in.Host); err != nil { |
| + t.Fatal("replay", err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/claimunit_inputs.go b/internal/host/claimunit_inputs.go| new file mode 100644 |
| index 000000000..5c1c5d8ed |
| --- /dev/null |
| +++ b/internal/host/claimunit_inputs.go |
| @@ -0,0 +1,99 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "slices" |
| + |
| + "github.com/carlosframework/platform/internal/activator" |
| + "github.com/carlosframework/platform/internal/cgroup" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +func (h *ClaimLaunchRoot) boundInputs(ctx context.Context, rt registry.Route) (*hostbox.UnitLaunchInputs, error) { |
| + if h.unitBindings == nil || h.unitProgram == nil { |
| + return nil, os.ErrPermission |
| + } |
| + var result *hostbox.UnitLaunchInputs |
| + err := func() error { |
| + b, err := h.unitBindings.read(rt.Host) |
| + if errors.Is(err, os.ErrNotExist) { |
| + if _, e := h.unitBindings.canonical(ctx, rt.Host); !errors.Is(e, os.ErrNotExist) { |
| + return os.ErrPermission |
| + } |
| + if rt.Unit != "" || rt.Backing != "" || rt.Tunnel != "" { |
| + return hostbox.ErrUnsupported |
| + } |
| + return nil |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + b, err = h.unitBindings.check(ctx, rt.Host, &rt) |
| + if err != nil { |
| + return err |
| + } |
| + p, err := h.unitProgram(b.Shape) |
| + if err != nil { |
| + return err |
| + } |
| + result = &hostbox.UnitLaunchInputs{Unit: b.Shape.Unit, Socket: b.Shape.Socket, Addr: b.Shape.Addr, DBPath: b.Shape.DBPath, Program: b.Shape.Program, Args: append([]string(nil), p.Args...), Env: append([]string(nil), p.Env...)} |
| + return nil |
| + }() |
| + return result, err |
| +} |
| + |
| +type claimUnitInputReader func(context.Context, registry.Route) (*hostbox.UnitLaunchInputs, error) |
| + |
| +func claimBoundSnapshot(ctx context.Context, backend *activator.ExecBackend, rt registry.Route, read claimUnitInputReader) (ClaimServingLaunch, error) { |
| + var out ClaimServingLaunch |
| + unit, err := read(ctx, rt) |
| + if err != nil { |
| + return out, err |
| + } |
| + if unit == nil { |
| + return out, hostbox.ErrUnsupported |
| + } |
| + if err = validateClaimUnitInputs(rt.Host, unit); err != nil { |
| + return out, err |
| + } |
| + id, err := release.ResolveLive(backend.OptDir, rt.Host) |
| + if err != nil { |
| + return out, err |
| + } |
| + if id.Version == "" || !claimHexDigest(id.Digest) { |
| + return out, hostbox.ErrChanged |
| + } |
| + out = ClaimServingLaunch{Unit: unit, Identity: hostwrite.Identity{Config: unit.Program, Release: id.Digest}, Executable: release.LiveLink(backend.OptDir, rt.Host), ExecutableSHA256: id.Digest, Args: unit.Args, Env: unit.Env, Version: id.Version, Staged: id, Limit: cgroup.TierDefault, Roles: []string{"main"}} |
| + return out, nil |
| +} |
| + |
| +func validateClaimUnitInputs(host string, in *hostbox.UnitLaunchInputs) error { |
| + if in == nil || !claimHexDigest(in.Program) || !provision.ValidHostShape(host) || filepath.Dir(in.DBPath) != provision.InstanceDir("/data", host) { |
| + return hostbox.ErrRoleLaunchShape |
| + } |
| + if !instancectl.ValidBoundClaimUnit(in.Unit) || (in.Socket == "") == (in.Addr == "") || filepath.Clean(in.DBPath) != in.DBPath { |
| + return hostbox.ErrRoleLaunchShape |
| + } |
| + if in.Socket != "" && in.Socket != provision.NestedSocket(host) && !claimFlatSocket(claimUnitShape{Socket: in.Socket}) { |
| + return hostbox.ErrRoleLaunchShape |
| + } |
| + return nil |
| +} |
| + |
| +func claimBoundInputMatch(host string, in ClaimLaunchInputs) error { |
| + if err := validateClaimUnitInputs(host, in.Unit); err != nil { |
| + return err |
| + } |
| + if in.DBPath != in.Unit.DBPath || in.Identity.Config != in.Unit.Program || !slices.Equal(in.Args, in.Unit.Args) || !slices.Equal(in.Env, in.Unit.Env) { |
| + return hostbox.ErrRoleLaunchShape |
| + } |
| + return nil |
| +} |
diff --git a/internal/host/claimunit_inputs_test.go b/internal/host/claimunit_inputs_test.go| new file mode 100644 |
| index 000000000..714a5b5a6 |
| --- /dev/null |
| +++ b/internal/host/claimunit_inputs_test.go |
| @@ -0,0 +1,107 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "os" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +func TestPlan14BoundUnitInputs(t *testing.T) { |
| + for _, unit := range []string{"console.service", "woodstar-atbridge.service", "carlos-app@shop.example.service"} { |
| + t.Run(unit, func(t *testing.T) { |
| + f, _, _ := generationLaunchFixture(t) |
| + b, _, shape := plan14UnitBindingFixture(t, unit, "", "") |
| + if unit == "woodstar-atbridge.service" { |
| + shape.Addr = "" |
| + shape.Socket = "/run/carlos/woodstar-atbridge.sock" |
| + } |
| + if unit == "carlos-app@shop.example.service" { |
| + shape.Backing = "unit" |
| + } |
| + in, err := f.root.Guard.Root.Installation(t.Context(), f.target.Host, hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + program := claimUnitProgram{Schema: 1, Host: in.Host, Args: []string{"serve"}, Env: []string{"SECRET=root-config"}} |
| + raw, _ := json.Marshal(program) |
| + shape.Program = claimUnitDigest(raw) |
| + b.installation = func(context.Context, string) (hostguard.RootInstallation, error) { return in, nil } |
| + b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } |
| + if _, err = b.establish(t.Context(), in.Host); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.root.unitBindings = b |
| + f.root.unitProgram = func(s claimUnitShape) (claimUnitProgram, error) { return decodeClaimUnitProgram(raw, s) } |
| + route := shape.route() |
| + locked, cancel := context.WithTimeout(t.Context(), time.Second) |
| + defer cancel() |
| + if err := f.root.Guard.WithLocal(locked, in.Host, func() error { _, err := f.root.boundInputs(locked, route); return err }); err != nil { |
| + t.Fatal("input view reacquired host lock", err) |
| + } |
| + q := hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchInputs, Route: &route} |
| + out, err := f.root.Serve(t.Context(), f.peer, q) |
| + if err != nil || out.Inputs == nil || out.Inputs.Unit != unit || out.Inputs.Program != shape.Program || out.Inputs.Env[0] != program.Env[0] { |
| + t.Fatal(out, err) |
| + } |
| + for _, change := range []func(*registry.Route){ |
| + func(r *registry.Route) { r.Unit = "other.service" }, func(r *registry.Route) { r.Unit = ""; r.Backing = "" }, |
| + func(r *registry.Route) { r.Backing = "exec" }, func(r *registry.Route) { r.Socket = "/run/carlos/other.sock" }, |
| + func(r *registry.Route) { r.Addr = "127.0.0.1:9999" }, func(r *registry.Route) { r.DBPath = "/data/other.db" }, |
| + func(r *registry.Route) { r.Tunnel = "tunnel" }, func(r *registry.Route) { r.Seed = "seed" }, func(r *registry.Route) { r.Replica = true }, |
| + } { |
| + forged := route |
| + change(&forged) |
| + q.Route = &forged |
| + if _, err = f.root.Serve(t.Context(), f.peer, q); err == nil { |
| + t.Fatal("changed route accepted", forged) |
| + } |
| + } |
| + q.Route = &route |
| + raw = append(raw, ' ') |
| + if _, err = f.root.Serve(t.Context(), f.peer, q); err == nil { |
| + t.Fatal("changed root program accepted") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14BoundColdSelection(t *testing.T) { |
| + f := newAdminTokenFixture(t, "box-1") |
| + unit := &hostbox.UnitLaunchInputs{Unit: "woodstar.service", Socket: "/run/carlos/woodstar.sock", DBPath: provision.NestedDBPath("/data", f.rt.Host), Program: strings.Repeat("a", 64), Args: []string{"serve"}, Env: []string{"ROOT_PROGRAM=yes"}} |
| + f.rt.Unit = unit.Unit |
| + f.rt.Socket = unit.Socket |
| + f.rt.DBPath = unit.DBPath |
| + source := claimServingLaunchSource(f.backend, f.tokens, func(context.Context, registry.Route) (*hostbox.UnitLaunchInputs, error) { return unit, nil }) |
| + launch, err := source(t.Context(), f.rt) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if launch.Unit != unit || launch.Identity.Config != unit.Program || launch.Identity.Release != launch.Staged.Digest || launch.ExecutableSHA256 != launch.Staged.Digest { |
| + t.Fatal(launch) |
| + } |
| + in := ClaimLaunchInputs{Unit: unit, Roles: launch.Roles, Identity: launch.Identity, Executable: launch.Executable, ExecutableSHA256: launch.ExecutableSHA256, Args: launch.Args, Env: launch.Env, DBPath: unit.DBPath, Limit: launch.Limit} |
| + if err = shapeAssembly().PreflightLaunch(f.rt.Host, in); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, change := range []func(*ClaimLaunchInputs){ |
| + func(in *ClaimLaunchInputs) { in.Args = []string{"shell"} }, func(in *ClaimLaunchInputs) { in.Env = []string{"ROOT_PROGRAM=no"} }, |
| + func(in *ClaimLaunchInputs) { in.Identity.Config = "different" }, func(in *ClaimLaunchInputs) { in.DBPath = "/data/sibling.db" }, |
| + } { |
| + bad := in |
| + change(&bad) |
| + if err = shapeAssembly().PreflightLaunch(f.rt.Host, bad); err == nil { |
| + t.Fatal("changed bound inputs accepted") |
| + } |
| + } |
| + if _, err = os.Stat(f.envPath()); !os.IsNotExist(err) { |
| + t.Fatal("bound selection minted ordinary environment", err) |
| + } |
| +} |
diff --git a/internal/host/claimunit_materialize.go b/internal/host/claimunit_materialize.go| new file mode 100644 |
| index 000000000..ab623d23d |
| --- /dev/null |
| +++ b/internal/host/claimunit_materialize.go |
| @@ -0,0 +1,198 @@ |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "errors" |
| + "io" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +type claimUnitMaterialization struct { |
| + Prior claimUnitBinding |
| + PriorIdentity rootstate.FileIdentity |
| + Binding claimUnitBinding |
| + Digest string |
| + Identity rootstate.FileIdentity |
| + Retiring, Retired bool |
| +} |
| + |
| +type claimUnitMaterializer struct { |
| + idle func(string) error |
| + state rootstate.Store |
| + read func(string) ([]byte, rootstate.FileIdentity, error) |
| + create func(string, []byte) error |
| + replace func(string, rootstate.FileIdentity, []byte) error |
| +} |
| + |
| +func claimUnitDigest(b []byte) string { sum := sha256.Sum256(b); return hex.EncodeToString(sum[:]) } |
| + |
| +func (m claimUnitMaterializer) load(host string) (claimUnitMaterialization, error) { |
| + var r claimUnitMaterialization |
| + raw, err := m.state.Read(host + ".unit.json") |
| + if err != nil { |
| + return r, err |
| + } |
| + dec := json.NewDecoder(bytes.NewReader(raw)) |
| + dec.DisallowUnknownFields() |
| + if dec.Decode(&r) != nil || dec.Decode(new(any)) != io.EOF || !r.Binding.valid() || r.Binding.Owner.Host != host || !claimHexDigest(r.Digest) || (r.Retired && !r.Retiring) { |
| + return r, os.ErrPermission |
| + } |
| + if r.Prior != (claimUnitBinding{}) && (!r.Prior.valid() || !r.Prior.Retired || r.Prior.Shape != r.Binding.Shape || r.Prior.Owner == r.Binding.Owner || r.PriorIdentity == (rootstate.FileIdentity{})) { |
| + return r, os.ErrPermission |
| + } |
| + return r, nil |
| +} |
| +func (m claimUnitMaterializer) save(r claimUnitMaterialization) error { |
| + raw, err := json.Marshal(r) |
| + if err != nil { |
| + return err |
| + } |
| + return m.state.Write(r.Binding.Owner.Host+".unit.json", raw) |
| +} |
| +func claimOwnedUnitDefinition(b claimUnitBinding) ([]byte, error) { |
| + raw, err := claimBoundUnitDefinition(b.Shape) |
| + if err != nil { |
| + return nil, err |
| + } |
| + owner, _ := json.Marshal(b.Owner) |
| + return append([]byte("# claim-owner="+claimUnitDigest(owner)+"\n"), raw...), nil |
| +} |
| +func claimRetiredUnitDefinition(b claimUnitBinding) []byte { |
| + owner, _ := json.Marshal(b.Owner) |
| + return []byte("# claim-retired=" + claimUnitDigest(owner) + "\n[Unit]\nRefuseManualStart=yes\n[Service]\nType=oneshot\nExecStart=/usr/bin/false\nRestart=no\n") |
| +} |
| + |
| +func (m claimUnitMaterializer) install(ctx context.Context, b claimUnitBinding) error { |
| + if m.state == nil || m.read == nil || m.create == nil || m.idle == nil || !b.valid() || b.Retiring || b.Retired { |
| + return os.ErrPermission |
| + } |
| + unlock, err := m.state.Lock(ctx, "materialize") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlock() |
| + want, err := claimOwnedUnitDefinition(b) |
| + if err != nil { |
| + return err |
| + } |
| + r, err := m.load(b.Owner.Host) |
| + if errors.Is(err, os.ErrNotExist) { |
| + if _, _, e := m.read(b.Shape.Unit); !errors.Is(e, os.ErrNotExist) { |
| + if e != nil { |
| + return e |
| + } |
| + return os.ErrPermission |
| + } |
| + r = claimUnitMaterialization{Binding: b, Digest: claimUnitDigest(want)} |
| + } else if err != nil { |
| + return err |
| + } |
| + if r.Binding != b || r.Digest != claimUnitDigest(want) || r.Retiring || r.Retired { |
| + return os.ErrPermission |
| + } |
| + if r.Identity == (rootstate.FileIdentity{}) { |
| + if err := m.idle(b.Shape.Host); err != nil { |
| + return err |
| + } |
| + } |
| + if err = m.save(r); err != nil { |
| + return err |
| + } |
| + raw, id, err := m.read(b.Shape.Unit) |
| + if errors.Is(err, os.ErrNotExist) && r.Identity == (rootstate.FileIdentity{}) { |
| + if err = m.create(b.Shape.Unit, want); err != nil { |
| + return err |
| + } |
| + raw, id, err = m.read(b.Shape.Unit) |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + if !bytes.Equal(raw, want) || id == (rootstate.FileIdentity{}) || (r.Identity != (rootstate.FileIdentity{}) && r.Identity != id) { |
| + return os.ErrPermission |
| + } |
| + r.Identity = id |
| + return m.save(r) |
| +} |
| + |
| +func (m claimUnitMaterializer) verify(b claimUnitBinding) error { |
| + r, err := m.load(b.Owner.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if r.Binding != b || r.Retiring || r.Retired || r.Identity == (rootstate.FileIdentity{}) { |
| + return os.ErrPermission |
| + } |
| + raw, id, err := m.read(b.Shape.Unit) |
| + if err != nil { |
| + return err |
| + } |
| + want, err := claimOwnedUnitDefinition(b) |
| + if err != nil { |
| + return err |
| + } |
| + if id != r.Identity || !bytes.Equal(raw, want) || claimUnitDigest(raw) != r.Digest { |
| + return os.ErrPermission |
| + } |
| + return nil |
| +} |
| + |
| +func (m claimUnitMaterializer) retire(ctx context.Context, b claimUnitBinding, gone func() error) error { |
| + if m.state == nil || m.read == nil || m.replace == nil || gone == nil || !b.valid() { |
| + return os.ErrPermission |
| + } |
| + unlock, err := m.state.Lock(ctx, "materialize") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlock() |
| + r, err := m.load(b.Owner.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if r.Binding != b || r.Identity == (rootstate.FileIdentity{}) { |
| + return os.ErrPermission |
| + } |
| + raw, id, err := m.read(b.Shape.Unit) |
| + if err != nil { |
| + return err |
| + } |
| + tombstone := claimRetiredUnitDefinition(b) |
| + if r.Retiring && bytes.Equal(raw, tombstone) { |
| + if r.Retired && r.Identity != id { |
| + return os.ErrPermission |
| + } |
| + r.Identity = id |
| + r.Retired = true |
| + return m.save(r) |
| + } |
| + if r.Retired || id != r.Identity || claimUnitDigest(raw) != r.Digest { |
| + return os.ErrPermission |
| + } |
| + r.Retiring = true |
| + if err = m.save(r); err != nil { |
| + return err |
| + } |
| + if err = gone(); err != nil { |
| + return err |
| + } |
| + if err = m.replace(b.Shape.Unit, r.Identity, tombstone); err != nil { |
| + return err |
| + } |
| + raw, id, err = m.read(b.Shape.Unit) |
| + if err != nil { |
| + return err |
| + } |
| + if !bytes.Equal(raw, tombstone) { |
| + return os.ErrPermission |
| + } |
| + r.Identity = id |
| + r.Retired = true |
| + return m.save(r) |
| +} |
diff --git a/internal/host/claimunit_materialize_test.go b/internal/host/claimunit_materialize_test.go| new file mode 100644 |
| index 000000000..ae9445d21 |
| --- /dev/null |
| +++ b/internal/host/claimunit_materialize_test.go |
| @@ -0,0 +1,127 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +type plan14UnitFiles struct { |
| + body []byte |
| + id rootstate.FileIdentity |
| + creates, retires int |
| +} |
| + |
| +func (f *plan14UnitFiles) read(string) ([]byte, rootstate.FileIdentity, error) { |
| + if f.body == nil { |
| + return nil, rootstate.FileIdentity{}, os.ErrNotExist |
| + } |
| + return append([]byte(nil), f.body...), f.id, nil |
| +} |
| +func (f *plan14UnitFiles) create(_ string, b []byte) error { |
| + if f.body != nil { |
| + return os.ErrExist |
| + } |
| + f.body = append([]byte(nil), b...) |
| + f.id = rootstate.FileIdentity{Device: 1, Inode: 2} |
| + f.creates++ |
| + return nil |
| +} |
| +func (f *plan14UnitFiles) replace(_ string, id rootstate.FileIdentity, b []byte) error { |
| + if id != f.id { |
| + return os.ErrPermission |
| + } |
| + f.body = append([]byte(nil), b...) |
| + f.id.Inode++ |
| + f.retires++ |
| + return nil |
| +} |
| +func TestPlan14UnitMaterialization(t *testing.T) { |
| + for _, attack := range []string{"none", "foreign file", "busy install", "replacement", "changed bytes", "lost intent", "lost materialized", "busy", "retired"} { |
| + t.Run(attack, func(t *testing.T) { |
| + b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") |
| + shape.Program = strings.Repeat("a", 64) |
| + b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } |
| + binding, err := b.establish(t.Context(), in.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + files := &plan14UnitFiles{} |
| + state := &plan14UnitBindingFault{Store: &rollbackMemory{files: map[string][]byte{}}} |
| + m := claimUnitMaterializer{idle: func(string) error { return nil }, state: state, read: files.read, create: files.create, replace: files.replace} |
| + if attack == "foreign file" { |
| + files.body = []byte("foreign") |
| + files.id = rootstate.FileIdentity{Device: 1, Inode: 9} |
| + } |
| + if attack == "busy install" { |
| + m.idle = func(string) error { return os.ErrPermission } |
| + } |
| + if attack == "lost intent" { |
| + state.fail = true |
| + } |
| + err = m.install(t.Context(), binding) |
| + if attack == "foreign file" || attack == "lost intent" || attack == "busy install" { |
| + if err == nil || files.creates != 0 { |
| + t.Fatal("effect before durable intent", err, files.creates) |
| + } |
| + return |
| + } |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if files.creates != 1 { |
| + t.Fatal(files.creates) |
| + } |
| + if err = m.install(t.Context(), binding); err != nil || files.creates != 1 { |
| + t.Fatal("replay", err) |
| + } |
| + switch attack { |
| + case "replacement": |
| + files.id.Inode++ |
| + case "changed bytes": |
| + files.body = append(files.body, 'x') |
| + case "lost materialized": |
| + record, err := m.load(in.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + record.Identity = rootstate.FileIdentity{} |
| + if err = m.save(record); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err = m.install(t.Context(), binding); err != nil || files.creates != 1 { |
| + t.Fatal("lost materialization recovery", err) |
| + } |
| + } |
| + gone := func() error { |
| + if attack == "busy" { |
| + return os.ErrPermission |
| + } |
| + return nil |
| + } |
| + err = m.retire(t.Context(), binding, gone) |
| + if attack == "replacement" || attack == "changed bytes" || attack == "busy" { |
| + if err == nil || files.retires != 0 { |
| + t.Fatal("retired foreign or running unit", err) |
| + } |
| + return |
| + } |
| + if err != nil || files.retires != 1 { |
| + t.Fatal("retire", err, files.retires) |
| + } |
| + if !strings.Contains(string(files.body), "RefuseManualStart=yes") { |
| + t.Fatal("retirement exposed old unit") |
| + } |
| + if err = m.retire(t.Context(), binding, gone); err != nil || files.retires != 1 { |
| + t.Fatal("retire replay", err) |
| + } |
| + if err = m.install(t.Context(), binding); !errors.Is(err, os.ErrPermission) { |
| + t.Fatal("retired unit reinstalled", err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/claimunit_production.go b/internal/host/claimunit_production.go| index 8dc7ac57d..96006cf13 100644 |
| --- a/internal/host/claimunit_production.go |
| +++ b/internal/host/claimunit_production.go |
| @@ -15,6 +15,10 @@ var openClaimUnitBindings = func() (rootstate.Store, error) { |
| return rootstate.Open("/var/lib/carlos-claims/unit-bindings", true, false) |
| } |
| +var openClaimUnitMaterializations = func() (rootstate.Store, error) { |
| + return rootstate.Open("/var/lib/carlos-claims/unit-materializations", true, false) |
| +} |
| + |
| var productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { |
| shapes, err := readClaimUnitShapes("/etc/carlos/claim-units.json") |
| if errors.Is(err, os.ErrNotExist) { |
| @@ -58,7 +62,7 @@ func bindProductionClaimUnits(ctx context.Context, h *ClaimRootWithLaunch, a *Cl |
| } |
| shape, ok := current[host] |
| if !ok { |
| - return claimUnitShape{}, os.ErrPermission |
| + return claimUnitShape{}, os.ErrNotExist |
| } |
| return shape, nil |
| } |
| @@ -84,23 +88,85 @@ func bindProductionClaimUnits(ctx context.Context, h *ClaimRootWithLaunch, a *Cl |
| } |
| return in, nil |
| } |
| + materialState, err := openClaimUnitMaterializations() |
| + if err != nil { |
| + state.Close() |
| + return err |
| + } |
| + files := claimUnitFiles{dir: "/etc/systemd/system"} |
| + material := claimUnitMaterializer{idle: func(host string) error { |
| + binding, err := b.read(host) |
| + if err != nil { |
| + return err |
| + } |
| + unit := base |
| + unit.BoundHost, unit.BoundUnit = host, binding.Shape.Unit |
| + return unit.Gone(ctx, host, "") |
| + }, state: materialState, read: files.read, create: files.create, replace: files.replace} |
| + b.handover = material.handover |
| hosts := make([]string, 0, len(shapes)) |
| for host := range shapes { |
| hosts = append(hosts, host) |
| } |
| sort.Strings(hosts) |
| for _, host := range hosts { |
| - if err := guard.WithLocal(ctx, host, func() error { _, err := b.establish(ctx, host); return err }); err != nil { |
| + if err := guard.WithLocal(ctx, host, func() error { |
| + binding, err := b.establish(ctx, host) |
| + if err != nil { |
| + return err |
| + } |
| + if err := material.install(ctx, binding); err != nil { |
| + return err |
| + } |
| + return projectClaimBoundSocket(binding.Shape) |
| + }); err != nil { |
| + state.Close() |
| + materialState.Close() |
| + return err |
| + } |
| + } |
| + if len(hosts) != 0 { |
| + if _, err := base.Systemctl(ctx, "daemon-reload"); err != nil { |
| state.Close() |
| + materialState.Close() |
| return err |
| } |
| } |
| - dispatch := claimUnitDispatch{base: base, bindings: b, shapes: productionClaimUnitShapes, definition: verifyClaimBoundUnitDefinition} |
| + dispatch := claimUnitDispatch{base: base, bindings: b, shapes: productionClaimUnitShapes, definition: func(shape claimUnitShape) error { |
| + binding, err := b.read(shape.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if binding.Shape != shape { |
| + return os.ErrPermission |
| + } |
| + return material.verify(binding) |
| + }} |
| h.Launch.Units, h.Launch.unitBindings = dispatch, b |
| h.Launch.unitProgram = readClaimUnitProgram |
| + h.Launch.boundReady = func(ctx context.Context, started instancectl.ClaimUnitStarted) error { |
| + for host, shape := range shapes { |
| + if shape.Unit == started.Unit { |
| + binding, err := b.check(ctx, host, nil) |
| + if err != nil { |
| + return err |
| + } |
| + return claimBoundUnitReady(ctx, started, binding.Shape) |
| + } |
| + } |
| + return nil |
| + } |
| + h.Root.retireBoundUnit = func(ctx context.Context, target hostguard.FenceTarget) error { |
| + if err := retireClaimBoundUnit(ctx, target, b, material, dispatch.Gone); err != nil { |
| + return err |
| + } |
| + _, err := base.Systemctl(ctx, "daemon-reload") |
| + return err |
| + } |
| kernel, ok := h.Root.Stop.Coordinator.Kernel.(claimUnitStopKernel) |
| if !ok { |
| state.Close() |
| + materialState.Close() |
| return os.ErrPermission |
| } |
| kernel.Units = dispatch |
| @@ -114,7 +180,7 @@ func bindProductionClaimUnits(ctx context.Context, h *ClaimRootWithLaunch, a *Cl |
| if closePrevious != nil { |
| err = closePrevious(ctx) |
| } |
| - return errors.Join(err, state.Close()) |
| + return errors.Join(err, state.Close(), materialState.Close()) |
| } |
| return nil |
| } |
diff --git a/internal/host/claimunit_program.go b/internal/host/claimunit_program.go| index cac2be0d8..f85e17f81 100644 |
| --- a/internal/host/claimunit_program.go |
| +++ b/internal/host/claimunit_program.go |
| @@ -92,7 +92,7 @@ func (h *ClaimLaunchRoot) boundLaunchSpec(q hostbox.RoleLaunchRequest) (instance |
| if err != nil { |
| return launch, true, err |
| } |
| - if !slices.Equal(p.Args, q.Spec.Args) || !slices.Equal(p.Env, q.Spec.Env) { |
| + if q.Spec.Identity.Config != b.Shape.Program || !slices.Equal(p.Args, q.Spec.Args) || !slices.Equal(p.Env, q.Spec.Env) { |
| return launch, true, os.ErrPermission |
| } |
| return instancectl.Launch{Schema: instancectl.LaunchSchema, Host: host, Argv: append([]string{filepath.Join(instancectl.LiveDir, host)}, p.Args...), Env: append([]string(nil), p.Env...)}, true, nil |
diff --git a/internal/host/claimunit_program_test.go b/internal/host/claimunit_program_test.go| index e93ddae6b..99d9ed934 100644 |
| --- a/internal/host/claimunit_program_test.go |
| +++ b/internal/host/claimunit_program_test.go |
| @@ -31,6 +31,7 @@ func TestPlan14BoundProgram(t *testing.T) { |
| f.root.unitProgram = func(shape claimUnitShape) (claimUnitProgram, error) { return decodeClaimUnitProgram(raw, shape) } |
| f.root.Units = plan14NamedLaunchUnits{f.units, in.Host, s.Unit} |
| f.req.Spec.Args, f.req.Spec.Env = p.Args, p.Env |
| + f.req.Spec.Identity.Config = s.Program |
| launch, err := f.root.launchSpec(f.req) |
| if err != nil || len(launch.Argv) != 4 || launch.Argv[1] != "console" { |
| t.Fatal(launch, err) |
| @@ -56,6 +57,11 @@ func TestPlan14BoundProgram(t *testing.T) { |
| t.Fatal("service-selected environment accepted") |
| } |
| f.req.Spec.Env = p.Env |
| + f.req.Spec.Identity.Config = "forged-config" |
| + if _, err := f.root.launchSpec(f.req); err == nil { |
| + t.Fatal("service-selected program identity accepted") |
| + } |
| + f.req.Spec.Identity.Config = s.Program |
| f.req.Spec.Args = []string{"shell"} |
| if _, err := f.root.launchSpec(f.req); err == nil { |
| t.Fatal("service-selected command accepted") |
diff --git a/internal/host/claimunit_ready_linux.go b/internal/host/claimunit_ready_linux.go| new file mode 100644 |
| index 000000000..14e26ef17 |
| --- /dev/null |
| +++ b/internal/host/claimunit_ready_linux.go |
| @@ -0,0 +1,126 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/binary" |
| + "fmt" |
| + "net" |
| + "net/http" |
| + "os" |
| + "path/filepath" |
| + "strconv" |
| + "strings" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/instancectl" |
| +) |
| + |
| +func claimProcTCPAddress(a *net.TCPAddr, ipv6 bool) string { |
| + ip := a.IP.To4() |
| + if ipv6 { |
| + ip = a.IP.To16() |
| + } |
| + if ip == nil { |
| + return "" |
| + } |
| + var b strings.Builder |
| + for i := 0; i < len(ip); i += 4 { |
| + fmt.Fprintf(&b, "%08X", binary.NativeEndian.Uint32(ip[i:i+4])) |
| + } |
| + return fmt.Sprintf("%s:%04X", b.String(), a.Port) |
| +} |
| + |
| +func claimTCPPeer(conn net.Conn, group string) error { |
| + local, ok := conn.LocalAddr().(*net.TCPAddr) |
| + if !ok { |
| + return os.ErrPermission |
| + } |
| + remote, ok := conn.RemoteAddr().(*net.TCPAddr) |
| + if !ok { |
| + return os.ErrPermission |
| + } |
| + pids, err := os.ReadFile(filepath.Join("/sys/fs/cgroup", group, "cgroup.procs")) |
| + if err != nil { |
| + return err |
| + } |
| + inodes := map[string]bool{} |
| + for _, network := range []string{"tcp", "tcp6"} { |
| + raw, err := os.ReadFile("/proc/net/" + network) |
| + if err != nil { |
| + return err |
| + } |
| + for _, line := range strings.Split(string(raw), "\n") { |
| + fields := strings.Fields(line) |
| + if len(fields) > 9 && fields[1] == claimProcTCPAddress(remote, network == "tcp6") && fields[2] == claimProcTCPAddress(local, network == "tcp6") && fields[3] == "01" && fields[9] != "0" { |
| + inodes["socket:["+fields[9]+"]"] = true |
| + } |
| + } |
| + } |
| + for _, pid := range strings.Fields(string(pids)) { |
| + n, err := strconv.Atoi(pid) |
| + if err != nil || n <= 1 { |
| + return os.ErrPermission |
| + } |
| + dir := filepath.Join("/proc", pid, "fd") |
| + entries, err := os.ReadDir(dir) |
| + if err != nil { |
| + return err |
| + } |
| + for _, entry := range entries { |
| + target, err := os.Readlink(filepath.Join(dir, entry.Name())) |
| + if err == nil && inodes[target] { |
| + return nil |
| + } |
| + } |
| + } |
| + return os.ErrPermission |
| +} |
| + |
| +func claimBoundUnitReady(ctx context.Context, s instancectl.ClaimUnitStarted, shape claimUnitShape) error { |
| + transport := &http.Transport{DisableKeepAlives: true, DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { |
| + network, endpoint := "unix", claimSocketTarget(shape) |
| + if shape.Addr != "" { |
| + network, endpoint = "tcp", shape.Addr |
| + } |
| + conn, err := (&net.Dialer{}).DialContext(ctx, network, endpoint) |
| + if err != nil { |
| + return nil, err |
| + } |
| + if network == "unix" { |
| + err = claimProbePeer(conn, s.MainPID, "0::/"+s.ControlGroup) |
| + } else { |
| + for until := time.Now().Add(500 * time.Millisecond); ; { |
| + err = claimTCPPeer(conn, s.ControlGroup) |
| + if err == nil || time.Now().After(until) { |
| + break |
| + } |
| + select { |
| + case <-ctx.Done(): |
| + conn.Close() |
| + return nil, ctx.Err() |
| + case <-time.After(10 * time.Millisecond): |
| + } |
| + } |
| + } |
| + if err != nil { |
| + conn.Close() |
| + return nil, err |
| + } |
| + return conn, nil |
| + }} |
| + defer transport.CloseIdleConnections() |
| + request, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://carlos/", nil) |
| + if err != nil { |
| + return err |
| + } |
| + client := &http.Client{Transport: transport, Timeout: time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} |
| + response, err := client.Do(request) |
| + if err != nil { |
| + return err |
| + } |
| + defer response.Body.Close() |
| + if response.StatusCode >= 500 { |
| + return os.ErrPermission |
| + } |
| + return nil |
| +} |
diff --git a/internal/host/claimunit_ready_linux_test.go b/internal/host/claimunit_ready_linux_test.go| new file mode 100644 |
| index 000000000..c5bf89df6 |
| --- /dev/null |
| +++ b/internal/host/claimunit_ready_linux_test.go |
| @@ -0,0 +1,40 @@ |
| +package host |
| + |
| +import ( |
| + "net" |
| + "testing" |
| +) |
| + |
| +func TestPlan14BoundTCPPeerAddress(t *testing.T) { |
| + for _, c := range []struct { |
| + ip, want string |
| + ipv6 bool |
| + }{ |
| + {"127.0.0.1", "0100007F:1F90", false}, {"::1", "00000000000000000000000001000000:1F90", true}, |
| + } { |
| + if got := claimProcTCPAddress(&net.TCPAddr{IP: net.ParseIP(c.ip), Port: 8080}, c.ipv6); got != c.want { |
| + t.Fatal(got, c.want) |
| + } |
| + } |
| +} |
| + |
| +func TestPlan14BoundTCPPeerRefusesForeignGroup(t *testing.T) { |
| + ln, err := net.Listen("tcp", "127.0.0.1:0") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer ln.Close() |
| + client, err := net.Dial("tcp", ln.Addr().String()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer client.Close() |
| + server, err := ln.Accept() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer server.Close() |
| + if err := claimTCPPeer(client, "plan14-no-such-unit"); err == nil { |
| + t.Fatal("foreign group supplied readiness") |
| + } |
| +} |
diff --git a/internal/host/claimunit_ready_other.go b/internal/host/claimunit_ready_other.go| new file mode 100644 |
| index 000000000..2581cfd22 |
| --- /dev/null |
| +++ b/internal/host/claimunit_ready_other.go |
| @@ -0,0 +1,13 @@ |
| +//go:build !linux |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "os" |
| +) |
| + |
| +func claimBoundUnitReady(context.Context, instancectl.ClaimUnitStarted, claimUnitShape) error { |
| + return os.ErrPermission |
| +} |
diff --git a/internal/host/claimunit_retire.go b/internal/host/claimunit_retire.go| new file mode 100644 |
| index 000000000..e973cc531 |
| --- /dev/null |
| +++ b/internal/host/claimunit_retire.go |
| @@ -0,0 +1,45 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| +) |
| + |
| +type claimBoundUnitCleanup struct { |
| + inner hostbox.RetirementAuthority |
| + retire func(context.Context, hostguard.FenceTarget) error |
| +} |
| + |
| +func (c claimBoundUnitCleanup) WithCleanup(ctx context.Context, t hostguard.FenceTarget, fn func() error) error { |
| + return c.inner.WithCleanup(ctx, t, func() error { |
| + if err := c.retire(ctx, t); err != nil { |
| + return err |
| + } |
| + return fn() |
| + }) |
| +} |
| + |
| +func retireClaimBoundUnit(ctx context.Context, t hostguard.FenceTarget, b *claimUnitBindings, m claimUnitMaterializer, gone func(context.Context, string, string) error) error { |
| + captured, err := b.read(t.Host) |
| + if errors.Is(err, os.ErrNotExist) { |
| + // An unconfigured ordinary host has no named placement to retire. |
| + if _, e := b.canonical(ctx, t.Host); errors.Is(e, os.ErrNotExist) { |
| + return nil |
| + } |
| + return os.ErrPermission |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + if captured.Owner.Host != t.Host || captured.Owner.Incarnation != t.Incarnation || captured.Owner.Stamp != t.Stamp || captured.Owner.Box != t.Box || captured.Owner.Enrollment != t.Enrollment || gone == nil { |
| + return os.ErrPermission |
| + } |
| + captured.Retiring, captured.Retired = false, false |
| + return b.retire(ctx, captured, func(claimUnitBinding) error { |
| + return m.retire(ctx, captured, func() error { return gone(ctx, t.Host, "") }) |
| + }) |
| +} |
diff --git a/internal/host/claimunit_retire_test.go b/internal/host/claimunit_retire_test.go| new file mode 100644 |
| index 000000000..e55094f42 |
| --- /dev/null |
| +++ b/internal/host/claimunit_retire_test.go |
| @@ -0,0 +1,77 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14UnitRetirementExactOwner(t *testing.T) { |
| + for _, attack := range []string{"none", "successor", "running", "lost replace", "lost final write"} { |
| + t.Run(attack, func(t *testing.T) { |
| + b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") |
| + shape.Program = strings.Repeat("a", 64) |
| + b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } |
| + captured, err := b.establish(t.Context(), in.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + files := &plan14UnitFiles{} |
| + ms := &plan14UnitBindingFault{Store: &rollbackMemory{files: map[string][]byte{}}} |
| + material := claimUnitMaterializer{idle: func(string) error { return nil }, state: ms, read: files.read, create: files.create, replace: files.replace} |
| + if err = material.install(t.Context(), captured); err != nil { |
| + t.Fatal(err) |
| + } |
| + target := hostguard.FenceTarget{Host: in.Host, Incarnation: in.Incarnation, Stamp: in.Stamp, Box: in.Box, Enrollment: in.Enrollment} |
| + if attack == "successor" { |
| + target.Stamp++ |
| + } |
| + gone := func(context.Context, string, string) error { |
| + if attack == "running" { |
| + return os.ErrPermission |
| + } |
| + return nil |
| + } |
| + original := material.replace |
| + material.replace = func(unit string, id rootstate.FileIdentity, body []byte) error { |
| + if err := original(unit, id, body); err != nil { |
| + return err |
| + } |
| + if attack == "lost replace" { |
| + return os.ErrInvalid |
| + } |
| + if attack == "lost final write" { |
| + ms.fail = true |
| + } |
| + return nil |
| + } |
| + err = retireClaimBoundUnit(t.Context(), target, b, material, gone) |
| + if attack == "none" { |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + } else if err == nil { |
| + t.Fatal("attack accepted") |
| + } |
| + if attack == "successor" || attack == "running" { |
| + if files.retires != 0 { |
| + t.Fatal("foreign or running retired") |
| + } |
| + return |
| + } |
| + material.replace = original |
| + ms.fail = false |
| + if err = retireClaimBoundUnit(t.Context(), target, b, material, gone); err != nil { |
| + t.Fatal("recover", err) |
| + } |
| + retired, err := b.read(in.Host) |
| + if err != nil || !retired.Retired || files.retires != 1 { |
| + t.Fatal(retired, err, files.retires) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/claimwakeassembly.go b/internal/host/claimwakeassembly.go| index b5eb48b44..ec73a92e4 100644 |
| --- a/internal/host/claimwakeassembly.go |
| +++ b/internal/host/claimwakeassembly.go |
| @@ -27,6 +27,7 @@ import ( |
| // ClaimLaunchInputs is an in-process adapter result, never a new persisted |
| // held-input format. The moves adapter retains its MoveInputs and manifest. |
| type ClaimLaunchInputs struct { |
| + Unit *hostbox.UnitLaunchInputs |
| Roles []string |
| Identity hostwrite.Identity |
| Baseline *genselect.Plan |
| @@ -248,7 +249,7 @@ func (w *ClaimWakeAssembly) Build(ctx context.Context, source hostbox.WorkSource |
| if err := w.admitsOperation(o, p, writer.Box); err != nil { |
| return err |
| } |
| - if rt.Seed != "" || rt.Backing != "" || rt.Unit != "" || rt.IsSidecar() || rt.Tunnel != "" { |
| + if rt.Seed != "" || rt.IsSidecar() || rt.Tunnel != "" { |
| return hostbox.ErrUnsupported |
| } |
| prior, _, err := w.pendingRecovery(call, o) |
| @@ -259,6 +260,9 @@ func (w *ClaimWakeAssembly) Build(ctx context.Context, source hostbox.WorkSource |
| if err != nil { |
| return err |
| } |
| + if (rt.Unit != "" || rt.Backing != "") && in.Unit == nil { |
| + return hostbox.ErrUnsupported |
| + } |
| if len(in.Roles) != 1 || in.Roles[0] != "main" || in.Executable == "" || len(in.ExecutableSHA256) != 64 || in.DBPath == "" || in.Identity.Config == "" || in.Identity.Release == "" || in.Admission.Mode != "cold" || (in.Baseline == nil && !in.Empty) { |
| return hostbox.ErrUnsupported |
| } |
| @@ -906,6 +910,9 @@ func (w *ClaimWakeAssembly) readiness(o hostclaim.Observed, p activator.StartPol |
| launch := a.launch |
| a.mu.Unlock() |
| if launch != nil && launch.Root != nil { |
| + if in.Unit != nil { |
| + return nil |
| + } |
| return claimPollReady(ctx, rt.Socket, "http://carlos/", func(conn net.Conn) error { return launch.PeerInRole(ctx, conn, cmd) }, func(status int) bool { return status < 500 }) |
| } |
| return w.Ready(ctx, cmd, rt, in) |
diff --git a/internal/host/plan14_installed_launch_root_linux_test.go b/internal/host/plan14_installed_launch_root_linux_test.go| index 5ae46f185..2cc335811 100644 |
| --- a/internal/host/plan14_installed_launch_root_linux_test.go |
| +++ b/internal/host/plan14_installed_launch_root_linux_test.go |
| @@ -76,7 +76,15 @@ func TestPlan14BoundRouteProgramRootRunner(t *testing.T) { |
| } |
| } |
| +func TestPlan14MaterializedBoundRouteRootRunner(t *testing.T) { |
| + for _, unit := range []string{"tcp", "socket-program", "backing-program"} { |
| + t.Run(unit, func(t *testing.T) { plan14InstalledClaimLaunch(t, "materialized:"+unit) }) |
| + } |
| +} |
| + |
| func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| + materialized := strings.HasPrefix(boundUnit, "materialized:") |
| + boundUnit = strings.TrimPrefix(boundUnit, "materialized:") |
| programMode := boundUnit == "tcp" || boundUnit == "socket-program" || boundUnit == "backing-program" |
| tcpMode := boundUnit == "tcp" || boundUnit == "backing-program" |
| if boundUnit == "tcp" { |
| @@ -136,7 +144,11 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| endpoint = ln.Addr().String() |
| ln.Close() |
| } |
| - program := claimUnitProgram{Schema: 1, Host: host, Args: []string{"plan14-tenant"}, Env: []string{"PLAN14_TENANT_SOCKET=" + filepath.Join(provision.InstanceRunDir(host), "app.sock")}} |
| + socketEndpoint := filepath.Join(provision.InstanceRunDir(host), "app.sock") |
| + if materialized && !tcpMode { |
| + socketEndpoint = "/run/carlos/woodstar-test.sock" |
| + } |
| + program := claimUnitProgram{Schema: 1, Host: host, Args: []string{"plan14-tenant"}, Env: []string{"PLAN14_TENANT_SOCKET=" + socketEndpoint}} |
| if tcpMode { |
| program.Env = append(program.Env, "PLAN14_TENANT_ADDR="+endpoint) |
| } |
| @@ -155,6 +167,9 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| t.Fatal(err) |
| } |
| } |
| + if materialized { |
| + plan14MaterializedDependencies(t, ctl) |
| + } |
| template, err := unitsFS.ReadFile("units/carlos-instance@.service") |
| if err != nil { |
| t.Fatal(err) |
| @@ -187,14 +202,19 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| } |
| f.req.Reservation.Group.RelativePath = instancectl.BoundClaimUnitControlGroup(host, unit) |
| } |
| - out, err := os.OpenFile(unitPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) |
| - if err != nil { |
| - t.Fatal(err) |
| + if materialized { |
| + unitPath = filepath.Join("/etc/systemd/system", unit) |
| } |
| - if _, err := out.WriteString(strings.Join(lines, "\n")); err != nil { |
| - t.Fatal(err) |
| + if !materialized { |
| + out, err := os.OpenFile(unitPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := out.WriteString(strings.Join(lines, "\n")); err != nil { |
| + t.Fatal(err) |
| + } |
| + out.Close() |
| } |
| - out.Close() |
| t.Cleanup(func() { |
| ctl(context.Background(), "stop", unit) |
| ctl(context.Background(), "stop", instancectl.SliceName(host)) |
| @@ -274,6 +294,9 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| } |
| if programMode { |
| shape.Program = hex.EncodeToString(programSum[:]) |
| + if materialized && !tcpMode { |
| + shape.Socket = socketEndpoint |
| + } |
| if tcpMode { |
| shape.Addr, shape.Socket = endpoint, "" |
| } |
| @@ -310,6 +333,29 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| } |
| return "" |
| }(), Systemctl: ctl, CgroupRoot: "/sys/fs/cgroup", ExeRoot: "/run/carlos-control/exe", UnitRoot: "/run/systemd/system", DataRoot: "/data", WaitBudget: 20 * time.Second} |
| + var material *claimUnitMaterializer |
| + if materialized { |
| + ms, err := rootstate.Open(filepath.Join(dir, "unit-materializations"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer ms.Close() |
| + files := claimUnitFiles{dir: "/etc/systemd/system"} |
| + material = &claimUnitMaterializer{idle: func(host string) error { return f.root.Units.Gone(t.Context(), host, "") }, state: ms, read: files.read, create: files.create, replace: files.replace} |
| + binding, err := f.root.unitBindings.read(host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := material.install(t.Context(), binding); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := projectClaimBoundSocket(binding.Shape); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := ctl(t.Context(), "daemon-reload"); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| if programMode { |
| base := f.root.Units.(instancectl.ClaimUnits) |
| base.BoundHost, base.BoundUnit = "", "" |
| @@ -318,6 +364,9 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| t.Fatal(err) |
| } |
| f.root.Units = claimUnitDispatch{base: base, bindings: f.root.unitBindings, shapes: func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{host: binding.Shape}, nil }, definition: func(s claimUnitShape) error { |
| + if material != nil { |
| + return material.verify(binding) |
| + } |
| // Dependencies and output are runner adapters; all isolation settings |
| // and the permanent no-reservation refusal remain in the unit. |
| raw, err := os.ReadFile(unitPath) |
| @@ -330,6 +379,15 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| return nil |
| }} |
| } |
| + if materialized { |
| + f.root.boundReady = func(ctx context.Context, started instancectl.ClaimUnitStarted) error { |
| + b, err := f.root.unitBindings.check(ctx, host, nil) |
| + if err != nil { |
| + return err |
| + } |
| + return claimBoundUnitReady(ctx, started, b.Shape) |
| + } |
| + } |
| f.root.Kernel = linuxClaimLaunchKernel{OptRoot: "/opt/carlos", CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc"} |
| store := release.NewDirStore(filepath.Join(dir, "deployment")) |
| rec.Schema = 1 |
| @@ -369,6 +427,19 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| f.req.Spec.Env = []string{"CARLOS_ADMIN_TOKEN=test", "PLAN14_TENANT_SOCKET=" + filepath.Join(provision.InstanceRunDir(host), "app.sock")} |
| if programMode { |
| f.req.Spec.Args, f.req.Spec.Env = program.Args, program.Env |
| + f.req.Spec.Identity.Config = hex.EncodeToString(programSum[:]) |
| + if materialized { |
| + b, err := f.root.unitBindings.read(host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + route := b.Shape.route() |
| + out, err := f.root.Serve(t.Context(), f.peer, hostbox.RoleLaunchRequest{V: hostbox.RoleLaunchV, Op: hostbox.RoleLaunchInputs, Route: &route}) |
| + if err != nil || out.Inputs == nil { |
| + t.Fatal("bound admission inputs", err) |
| + } |
| + f.req.Spec.Args, f.req.Spec.Env = out.Inputs.Args, out.Inputs.Env |
| + } |
| } |
| inputs, err := f.req.Spec.Hash() |
| if err != nil { |
| @@ -400,13 +471,14 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| }) |
| if err != nil { |
| logs, _ := exec.Command("journalctl", "-u", unit, "--no-pager", "-n", "80").CombinedOutput() |
| - t.Fatalf("production launch: %v\n%s", err, logs) |
| + output, _ := os.ReadFile(provision.LogSocket(host, "run")) |
| + t.Fatalf("production launch: %v\n%s\noutput: %s", err, logs, output) |
| } |
| transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { |
| if tcpMode { |
| return (&net.Dialer{}).DialContext(ctx, "tcp", endpoint) |
| } |
| - return (&net.Dialer{}).DialContext(ctx, "unix", filepath.Join(provision.InstanceRunDir(host), "app.sock")) |
| + return (&net.Dialer{}).DialContext(ctx, "unix", socketEndpoint) |
| }} |
| defer transport.CloseIdleConnections() |
| client := &http.Client{Transport: transport, Timeout: 3 * time.Second} |
| @@ -420,6 +492,19 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| t.Fatal(resp.Status) |
| } |
| checkSibling() |
| + if materialized { |
| + b, err := f.root.unitBindings.read(host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + unrelated := instancectl.ClaimUnitStarted{Unit: response.Grant.Unit, MainPID: response.Grant.PID, ControlGroup: instancectl.ClaimUnitControlGroup("unrelated.example")} |
| + if b.Shape.Socket != "" { |
| + unrelated.MainPID = os.Getpid() |
| + } |
| + if err := claimBoundUnitReady(t.Context(), unrelated, b.Shape); err == nil { |
| + t.Fatal("readiness accepted unrelated process or group") |
| + } |
| + } |
| generations, err := rootstate.Open(filepath.Join(dir, "generations"), false, false) |
| if err != nil { |
| t.Fatal(err) |
| @@ -537,6 +622,70 @@ func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| t.Logf("%s", output) |
| } |
| checkSibling() |
| + if material != nil { |
| + original, err := os.ReadFile(unitPath) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + saved := unitPath + ".plan14-original" |
| + if _, err := os.Lstat(saved); !os.IsNotExist(err) { |
| + t.Fatal("fixture predecessor exists", err) |
| + } |
| + if err := os.Rename(unitPath, saved); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { os.Remove(saved) }) |
| + files := claimUnitFiles{dir: filepath.Dir(unitPath)} |
| + if err := files.create(unit, original); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := retireClaimBoundUnit(t.Context(), f.target, f.root.unitBindings, *material, f.root.Units.Gone); err == nil { |
| + t.Fatal("same-byte successor retired") |
| + } |
| + after, err := os.ReadFile(unitPath) |
| + if err != nil || string(after) != string(original) { |
| + t.Fatal("successor changed", err) |
| + } |
| + if err := os.Remove(unitPath); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Rename(saved, unitPath); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := retireClaimBoundUnit(t.Context(), f.target, f.root.unitBindings, *material, f.root.Units.Gone); err != nil { |
| + t.Fatal("unit retirement", err) |
| + } |
| + if _, err := ctl(t.Context(), "daemon-reload"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := ctl(t.Context(), "start", unit); err == nil { |
| + t.Fatal("retired unit accepted start") |
| + } |
| + binding, err := f.root.unitBindings.read(host) |
| + if err != nil || !binding.Retired { |
| + t.Fatal(binding, err) |
| + } |
| + for _, leaf := range []string{"launch.json", "permit.json", "exec.sock"} { |
| + if _, err := os.Lstat(filepath.Join("/run/carlos-control/exe/.claim", provision.InstanceID(host), leaf)); !os.IsNotExist(err) { |
| + t.Fatal("retired environment remains", leaf, err) |
| + } |
| + } |
| + // The root installation/claim source remains a fixture here. The durable |
| + // file transition and old-owner refusal use the real production adapters. |
| + f.root.unitBindings.handover = material.handover |
| + old.Stamp++ |
| + successor, err := f.root.unitBindings.establish(t.Context(), host) |
| + if err != nil { |
| + t.Fatal("successor binding", err) |
| + } |
| + if err := material.verify(successor); err != nil { |
| + t.Fatal("successor definition", err) |
| + } |
| + if err := retireClaimBoundUnit(t.Context(), f.target, f.root.unitBindings, *material, f.root.Units.Gone); err == nil { |
| + t.Fatal("old owner retired successor") |
| + } |
| + checkSibling() |
| + } |
| } |
| func plan14InstalledRetire(t *testing.T, dir string) { |
diff --git a/internal/host/plan14_materialized_unit_root_linux_test.go b/internal/host/plan14_materialized_unit_root_linux_test.go| new file mode 100644 |
| index 000000000..4c59fdbd5 |
| --- /dev/null |
| +++ b/internal/host/plan14_materialized_unit_root_linux_test.go |
| @@ -0,0 +1,39 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| +) |
| + |
| +func plan14MaterializedDependencies(t *testing.T, ctl func(context.Context, ...string) (string, error)) { |
| + t.Helper() |
| + units := []struct{ name, body string }{ |
| + {"carlos-edge.service", "[Service]\nType=simple\nExecStart=/bin/sleep infinity\n"}, |
| + {"data-instances.mount", "[Mount]\nWhat=/data/instances\nWhere=/data/instances\nType=none\nOptions=bind,nosymfollow\n"}, |
| + {"run-carlos-instances.mount", "[Mount]\nWhat=/run/carlos/instances\nWhere=/run/carlos/instances\nType=none\nOptions=bind,nosymfollow\n"}, |
| + {"run-carlos-logs.mount", "[Mount]\nWhat=/run/carlos/logs\nWhere=/run/carlos/logs\nType=none\nOptions=bind,nosymfollow\n"}, |
| + } |
| + for _, u := range units { |
| + path := filepath.Join("/run/systemd/system", u.name) |
| + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err = f.WriteString(u.body); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.Close() |
| + t.Cleanup(func() { |
| + ctl(context.Background(), "stop", u.name) |
| + os.Remove(path) |
| + ctl(context.Background(), "daemon-reload") |
| + }) |
| + } |
| + if out, err := ctl(t.Context(), "daemon-reload"); err != nil { |
| + t.Fatal(out, err) |
| + } |
| +} |
diff --git a/internal/host/plan14_root_generations_test.go b/internal/host/plan14_root_generations_test.go| index 15d24415c..0faa04bbd 100644 |
| --- a/internal/host/plan14_root_generations_test.go |
| +++ b/internal/host/plan14_root_generations_test.go |
| @@ -310,10 +310,16 @@ func TestPlan14GenerationReservationProcessCrash(t *testing.T) { |
| } |
| func TestPlan14GenerationProductionWiring(t *testing.T) { |
| + t.Setenv("PATH", t.TempDir()) |
| oldOpen, oldShapes := openClaimUnitBindings, productionClaimUnitShapes |
| + oldMaterial := openClaimUnitMaterializations |
| openClaimUnitBindings = func() (rootstate.Store, error) { return &rollbackMemory{files: map[string][]byte{}}, nil } |
| + openClaimUnitMaterializations = func() (rootstate.Store, error) { return &rollbackMemory{files: map[string][]byte{}}, nil } |
| productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil } |
| - t.Cleanup(func() { openClaimUnitBindings, productionClaimUnitShapes = oldOpen, oldShapes }) |
| + t.Cleanup(func() { |
| + openClaimUnitBindings, productionClaimUnitShapes = oldOpen, oldShapes |
| + openClaimUnitMaterializations = oldMaterial |
| + }) |
| f, _, _ := generationLaunchFixture(t) |
| root := ClaimRootHTTP{Launches: f.journal, Stop: ClaimStopHTTP{Authenticate: func(context.Context, net.Conn) (StopPeer, error) { return f.peer, nil }, Coordinator: ClaimStopCoordinator{Kernel: stopKernelFunc(nil), Guard: f.root.Guard}}} |
| assembly := &ClaimAssembly{Claims: f.root.generations.store, ManagedRoots: map[string]string{"opt": f.root.generations.optDir, "data": "/data"}, Local: f.root.Local} |
diff --git a/internal/hostbox/rolelaunch.go b/internal/hostbox/rolelaunch.go| index e1026c611..b9615be8c 100644 |
| --- a/internal/hostbox/rolelaunch.go |
| +++ b/internal/hostbox/rolelaunch.go |
| @@ -11,6 +11,7 @@ import ( |
| "strings" |
| "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/registry" |
| ) |
| // The reservation-bearing extension of main's isolated root launch protocol |
| @@ -27,6 +28,7 @@ const ( |
| RoleLaunchCapability = "capability" |
| RoleLaunchStart = "start" |
| RoleLaunchStop = "stop" |
| + RoleLaunchInputs = "unit-inputs" |
| // Unit reservation states. Only root writes them, in its local launch |
| // journal. None of the agent's journal operations moves a unit |
| @@ -47,6 +49,7 @@ var ErrRoleLaunchShape = fmt.Errorf("%w: role launch shape is not carried by the |
| // agent asks root to run, the build it pinned and the slice tier; stop names |
| // the same reservation; capability carries nothing. |
| type RoleLaunchRequest struct { |
| + Route *registry.Route `json:"route,omitempty"` |
| Kind string `json:"kind,omitempty"` |
| Spec *LaunchSpec `json:"spec,omitempty"` |
| V int `json:"v"` |
| @@ -58,10 +61,16 @@ type RoleLaunchRequest struct { |
| // RoleLaunchResponse answers every call with the protocol root speaks. A |
| // start answers with the grant root journaled. |
| +type UnitLaunchInputs struct { |
| + Unit, Socket, Addr, DBPath, Program string |
| + Args, Env []string |
| +} |
| + |
| type RoleLaunchResponse struct { |
| - V int `json:"v"` |
| - Protocol string `json:"protocol"` |
| - Grant *RoleLaunchGrant `json:"grant,omitempty"` |
| + Inputs *UnitLaunchInputs `json:"inputs,omitempty"` |
| + V int `json:"v"` |
| + Protocol string `json:"protocol"` |
| + Grant *RoleLaunchGrant `json:"grant,omitempty"` |
| } |
| // RoleLaunchGrant is root's record of the unit it started for a reservation: |
| @@ -96,7 +105,14 @@ func (q RoleLaunchRequest) Validate() error { |
| if q.V != RoleLaunchV { |
| return ErrRoleLaunchShape |
| } |
| + if q.Op != RoleLaunchInputs && q.Route != nil { |
| + return ErrRoleLaunchShape |
| + } |
| switch q.Op { |
| + case RoleLaunchInputs: |
| + if q.Route == nil || !component(q.Route.Host) || q.Kind != "" || q.Spec != nil || q.Reservation != nil || q.Executable != "" || q.MemoryMax != 0 { |
| + return ErrRoleLaunchShape |
| + } |
| case RoleLaunchCapability: |
| if q.Spec != nil || q.Reservation != nil || q.Executable != "" || q.MemoryMax != 0 { |
| return ErrRoleLaunchShape |
diff --git a/internal/hostbox/rolelaunch_inputs_test.go b/internal/hostbox/rolelaunch_inputs_test.go| new file mode 100644 |
| index 000000000..d12c42ce1 |
| --- /dev/null |
| +++ b/internal/hostbox/rolelaunch_inputs_test.go |
| @@ -0,0 +1,30 @@ |
| +package hostbox |
| + |
| +import ( |
| + "bytes" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "testing" |
| +) |
| + |
| +func TestPlan14UnitInputsProtocol(t *testing.T) { |
| + q := RoleLaunchRequest{V: RoleLaunchV, Op: RoleLaunchInputs, Route: ®istry.Route{Host: "shop.example", Unit: "console.service"}} |
| + raw, err := EncodeRoleLaunchRequest(q) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + decoded, err := DecodeRoleLaunchRequest(bytes.NewReader(raw)) |
| + if err != nil || decoded.Route.Unit != q.Route.Unit { |
| + t.Fatal(decoded, err) |
| + } |
| + for _, mutate := range []func(*RoleLaunchRequest){ |
| + func(q *RoleLaunchRequest) { q.Route = nil }, func(q *RoleLaunchRequest) { q.Op = RoleLaunchCapability }, |
| + func(q *RoleLaunchRequest) { q.Op = RoleLaunchStop }, func(q *RoleLaunchRequest) { q.MemoryMax = 1 }, |
| + func(q *RoleLaunchRequest) { q.Executable = "/app" }, func(q *RoleLaunchRequest) { q.Kind = "serving" }, func(q *RoleLaunchRequest) { q.Spec = &LaunchSpec{} }, |
| + } { |
| + bad := q |
| + mutate(&bad) |
| + if bad.Validate() == nil { |
| + t.Fatal("mixed protocol request accepted", bad) |
| + } |
| + } |
| +} |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index 283b2bfee..e1bf4aebc 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-e.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-e.tsv |
| @@ -54,7 +54,6 @@ internal/release/roothistory_replace.go replacementReconstruction authority E1 S |
| internal/release/signedhead.go StageSignedHead authority E1 Canonical route is supplied by the root unit verifier. Verifies signed channel, digest ratchet and manifest/artifact before staging; unavailable pin census never authorizes restaging differing bytes. Fixtures: TestPlan14Incident20261006WithLedger and TestPlan14SignedNamedRecoveryAuthority. registry:Account@202,registry:Account@302,registry:Account@403,registry:App@214,registry:App@415,registry:Channel@222,registry:Host@434 ca02f04e55556ba0233dfa0848b0c533a9d9aa06cad6abd33cfd2968b3bd838a |
| internal/rootstate/link_linux.go ReadTrustedLink authority A,E1 Checks the full root-owned parent chain before descriptor-based live-link reading. Fixture: TestPlan14TrustedLiveLink; privileged acceptance remains required. file:Open@156 90f8e8de4e2c71f07b5d749cbe4bb1c1dfa426576a82138823449f56fc9ec5ab |
| internal/rootstate/link_linux.go directory.readLink authority A,E1 Pins the symlink inode with O_PATH and checks owner/type/link count before readlinkat. Fixture: TestPlan14TrustedLiveLink. file:Fstat@356,file:Openat@183,file:Readlinkat@554 a8559fe63dce48830916004ecfe7fbbe5d419568c7daf7a3073cf1b5bb12c3d7 |
| -@coverage internal/host 20d945384e2acd968e65c69ad218b960dc1428993a3b281092aac9390a4b90a7 |
| @coverage internal/release 329e742eddf1a5da3cd8f4a6b76a2eda2e38e5e6f0132d66e7b3830cfa95e77f |
| @coverage internal/rootstate a773d6dbc0e4150e0054f7a94d0e1493c88d0d57f4f4530138b3427e2c217bc5 |
| internal/release/roothistory_status.go RootHistory.MigrationStatus authority E1 Exact completion binds the accepted authorization digest from durable private history to enrolled-root signed readback. Pending originals, changed evidence and canonical sources cannot supply retirement completion. Fixtures: TestPlan14HistoryReconstructionCompletion, TestPlan14HistoryRetirementCompletion, TestPlan14ReadReconstructionCompletion, TestPlan14ReadRetirementCompletion, TestPlan14RecoveryRetirementProductionCallback, TestPlan14RootHistoryReconstructionCrash, TestPlan14HistoryRetirementProcessCrash. file:Read@1062,file:Read@3891 f0ab7ad2d2d70f8c22ebfd575f5a1c6931f7eba30ccdba80ad43d0efffc88154 |
| @@ -159,7 +158,6 @@ internal/host/rootrestart_requests_source.go rootRestartRequestSource.inventory |
| internal/host/rootrestart_requests_source.go rootRestartRequestSource.target authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. registry:Unit@1211,whole:Route@891 301b62efbf0281f713cbed7fb15cd244b7d2c479882f4ad930c8d482b51177ff |
| internal/host/rootrestart_requests_source.go type rootRestartRequestSource authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. db7ff5c46b6db3fe88657fddf8b4e17324b896fce9b21042d7f0f63140c1d776 |
| internal/release/rootrestart_history.go RootRestarts.Bindings authority E1 E15: census-only root unit-restart consumption uses retained installations, canonical records, exact requests, invocation evidence and admission after durable intent under release exclusion. No-census effects use the original consumer. Fixtures: TestPlan14RootRestartProductionConsumption, TestPlan14RootRestartProductionHeldRollover, TestPlan14RootRestartProductionAdmissionAfterIntent, TestPlan14RootRestartProductionUnknown, TestPlan14RootRestartProductionSyncFailure, TestPlan14RootRestartProductionCrash, TestPlan14RootRestartRequestCensusGate. file:Read@324 d22b7e6b5710ca0025639395101933bec5ce0bd38768ebcf44a0805f1f159b08 |
| -internal/host/claimretire_transport.go type ClaimRootHTTP authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. e87bb168975b9a7c4334de1c1fc027c556d69597122a6134770e770c719a45be |
| internal/host/claimunit.go claimUnitContent authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 38dcfeb754659895bc28b024bc3186c6b12edeb17d087546507fa1299eb8e331 |
| internal/host/roothistory_creation.go productionCreationHistory authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. file:Open@289,file:Open@368 a26026dd565e4d1d046591fa1534ded1b64ccb8eafbef65a4d4ec899c0c471d3 |
| internal/host/roothistory_creation.go rootHistoryCreation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:Open@190,file:Open@300,whole:Route@535 49c28b2886b2ef03b818b2ad7d9a7f6fec65637f86e4d2f94e54394cac032bcc |
| @@ -188,7 +186,6 @@ internal/host/claimlaunch_production.go WithProductionClaimLaunch authority E1,E |
| internal/host/claimlaunch_root.go ClaimLaunchRoot.launchLocked authority E1,E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. file:Read@2348,file:Read@2507,file:Read@707 8857198d935629758593ec447afac75c9b28ecaf09743bb8f304f93ea868c66e |
| internal/host/claimlaunch_root.go ClaimLaunchRoot.start authority E1,E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 78796fbb8f2516f96d1de84cfdd97f294cbcbd21ee63e8b47aa4e2fc2bad3980 |
| internal/host/claimlaunch_root.go ClaimLaunchRoot.startRecorded authority E1,E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. file:Read@2865 f0bc2975c60b0476090e265227b6428b3e7ab0bb41c6a0feb9a9a2244642fbc9 |
| -internal/host/claimlaunch_root.go type ClaimLaunchRoot authority E1,E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. f1a0ee3e297b8f85b63f0f5361d065d3cd21448552f1574ec54f6256409fe3f3 |
| internal/host/rootgenerations_launch.go generationLaunchPermit.check authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. 788e600dd37fec60c5b98dc42b23c182c337df064088a041e2e20b68573942b5 |
| internal/host/rootgenerations_launch.go generationLaunchPermit.started authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. PROPOSED E45: root uses retained reservation identities and live kernel containment checks; unrecognized processes still refuse. Current positive root acceptance requires rerun after proc-mount tightening. a7635d02d9cb682be64a0df3f8932336f5e874260898df9013ff7ffd7e3d7462 |
| internal/host/rootgenerations_launch.go productionRootGenerations authority E2 E18: claims-on launch records the canonical installation, deployment revision, signed build, executable and containing tree before start under release exclusion; both final spawn checks reread the same reservation and identities. Root process evidence never retires the reservation. This is the launch prerequisite only; complete pins and retirement remain pending. Fixtures: TestPlan14GenerationReservedBeforeClaimSpawn, TestPlan14GenerationLaunchRefusals, TestPlan14GenerationStartedSyncFailureRetainsPin, TestPlan14GenerationReservationProcessCrash, TestPlan14GenerationProductionWiring, TestPlan14GenerationReservationDurability, TestPlan14GenerationReservationConflicts, TestPlan14GenerationObservationKeepsReservation, TestPlan14GenerationRecordRefusals. file:Open@346 a1132403ace7213de9e12720d5a393a3bb85045d1d2c53f0c37ea2ae8e933b04 |
| @@ -295,7 +292,6 @@ internal/host/rootgeneration_census_linux.go generationKernel.census authority E |
| internal/host/rootgeneration_inventory.go generationInventoryAt authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance passed at E27; running-tenant containment remains unfinished. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@173,file:Open@533 57f4c32cd65afcefad93a41d5266e2b12acc624ff808f47d40e31a79bbc7a827 |
| internal/hostbox/generationinventory.go LaunchJournal.GenerationLaunches authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:QueryContext@270 6694bd3a35b8d91f53e67134145c44854086f08593110ed1b905bca9587be290 |
| internal/hostbox/generationinventory.go OpenGenerationLaunchReader authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:Lstat@218,file:Open@283 552d8629a7932c4ae5dca9d2beef4559f7e8a2864259d11e2ff98c699fdcd999 |
| -@coverage internal/hostbox b76083441c876c66209ec032b57e7dde6eb51dd41597f9d2d5462dffa3736abe |
| internal/host/rootgeneration_census_linux.go productionGenerationKernelCensus authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. PROPOSED E45: root uses retained reservation identities and live kernel containment checks; unrecognized processes still refuse. Current positive root acceptance requires rerun after proc-mount tightening. e0c2041104c3a10eebc0183e67602069fd33954f8d0c8efb058261cd54551900 |
| internal/host/rootgeneration_census_other.go productionGenerationKernelCensus authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. a1217cf881e9d05336fc16d6050f7f7a1a2184a0c20ff2988d0cf0f9012eaa45 |
| internal/host/rootgeneration_inventory.go generationInstallation authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 3e5e15d50752510acb44b10eb29621757a87bff54ceae015f704dd7f3a848ecb |
| @@ -346,7 +342,6 @@ 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@11828,whole:Route@13334,whole:Route@360,whole:Route@4164,whole:Route@6157,whole:Route@6338 706f2a4268a06dae2eca22093f841750139c5bd9dcaa0e91657621c5eea1bbe9 |
| 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 2666d54bdc876904d9c2c3d6de5c2f82dc688f07082d92d84ddfc7e01cda6a7e |
| -@coverage internal/activator 3a1d82354a14988963fec3a1048ac366cf33c939f7dbbc466374635a19370593 |
| 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@3935,whole:Route@4065,whole:Route@4600,whole:Route@604,whole:Route@685,whole:Route@751 4a04a9b93fede307dc676ff367a826134e5b754c0ef417214f17439e6304cfda |
| 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 |
| @@ -373,7 +368,6 @@ internal/host/rootlaunchverifier.go rootLaunchVerifier.verify authority E2 PROPO |
| internal/rootparticipation/production.go Ledger.ActivationReady authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. 99a3a8f61a8b769da5bce734d6d8a937b861ed38ef1e4f9cc1eb2b4d473286fb |
| @coverage internal/hostfloor 37459f6d8d47ed5ec3892e22483e34abde2dabc0775cf0846b6fe00fb66dcd75 |
| @coverage internal/rootparticipation d867cfb3ad2b0432f7cee72f29805bc3420e28fe0edd5b89b469f154e4371694 |
| -internal/host/claimretire_transport.go ClaimRootHTTP.retire authority C,G,J1 PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. registry:ClaimOwnership@1081,registry:Host@1062,registry:Host@792,whole:ClaimOwnership@1085,whole:ClaimOwnership@894,whole:Route@1830,whole:Route@854 425059d65adaafa5680ad92974559fbeb56ba0a3a7ff3867bb63937648989e06 |
| internal/host/rootinventory_timer.go ClaimRootHTTP.verifyInventoryTimer hint C,J1 PROPOSED E45 / R-E45-5: root executes a deny-only consistency check of service-writable timer rows. Registry absence is not ordering authority. Root intention authority resides in the retained inventory and fence; producer lifecycle remains incomplete. PROPOSED E46: R-E45-5: service-writable timer SQL is deny-only; root pending/inventory supplies ownership. Executing identity: root instance-control or root participation/update. registry:ClaimIncarnation@1006,registry:ClaimIncarnation@980,registry:ClaimOwner@1072,registry:ClaimOwner@299,registry:ClaimRowID@1107,registry:ClaimStamp@1032,registry:ClaimStamp@1052,registry:Host@936,registry:Name@958,registry:Registration@1144,whole:Timer@709 7af2d533a789e21c0306910c072aa186df0460cddb0d4b5490bbd26b0a19943d |
| internal/host/rootinventory_timer.go ClaimRootHTTP.verifyNewTimerIntentions hint E2 PROPOSED E45 / R-E45-5: root executes a deny-only consistency check of service-writable timer rows. Registry absence is not ordering authority. Root intention authority resides in the retained inventory and fence; producer lifecycle remains incomplete. PROPOSED E46: R-E45-5: service-writable timer SQL is deny-only; root pending/inventory supplies ownership. Executing identity: root instance-control or root participation/update. whole:Timer@710 43a9e0925e79a484c680f7ca04c1ad19c13cb9863a5e9caf03104259bf9c3ec8 |
| internal/host/claimlaunch_execpermit.go ClaimLaunchRoot.execPermit authority D Root participation bounds the executable permit; TestPlan14ExecutionFinalLaunchUsesRootParticipation covers initial and final checks. PROPOSED E45: root uses retained reservation identities and live kernel containment checks; unrecognized processes still refuse. Current positive root acceptance requires rerun after proc-mount tightening. file:Read@157 cf0aa8db12ea1d451ce4e729572dc8c628ee22453bee25dae62c5ff13e85e769 |
| @@ -440,17 +434,58 @@ internal/instancectl/claimexec.go ClaimUnits.prepareExecGate authority C,G,J2,J4 |
| internal/instancectl/claimunit.go ClaimUnits.cgroupEmpty authority C,G,J2,J4,E2 root-input;kept=C,G;scope=protected-decision-only PROPOSED E47: root-owned immutable unit/shape binding, reserved launch placement and exact invocation stop; retained generation binding carries the canonical unit. Default unit paths remain unchanged. Production named-route wiring and unit/env/timer retirement are not complete. file:ReadFile@196 9e874fe959a3a381442e2900e8184f91ef803dd4f47839de8dbd6d3ab395d03e |
| internal/host/claimlaunch_authority.go ClaimLaunchRoot.authorize authority D,E2 Both root launch authorizations read root participation and match authenticated peer; TestPlan14ExecutionFinalLaunchUsesRootParticipation. PROPOSED E47: final named-unit launch authorization rechecks the retained root binding and refuses missing, retired, foreign or changed canonical shape. 9e00346c2012343327a230067f2e35f9f960569fbb3b6ee0dc401e7f566cbef6 |
| internal/host/claimlaunch_authority.go ClaimLaunchRoot.launchSpec authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 4703055ca1e26021e448839b43b5e281a020e492aac61dadca12f03675a688a4 |
| -internal/host/claimunit_binding.go claimUnitBindings.establish authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 8001dce0f3c38b1c08845b8a536870e8d644114401670b8ca4950562b3c3517e |
| internal/host/claimunit_binding.go claimUnitShape.valid authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 725731eb019d6a4e612a7a94bcebc5619139405081053d725a4c67f763d536d4 |
| -internal/host/claimunit_definition.go claimBoundUnitDefinition authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. file:ReadFile@165 2ef162905f3ca438ac3c186c92cd04059ba4deb07da29badd9d31a02d352ff00 |
| -internal/host/claimunit_definition.go verifyClaimBoundUnitDefinition authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. file:ReadAll@285 107db8628ffc00a68a079040c507484372c36815e20de082f4964caac958597e |
| internal/host/claimunit_dispatch.go claimUnitDispatch.StartReserved authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 02dfbc7e31f9a13ba210d2bed3181da5e1874ae388c2d1060cbdb57c398a04e5 |
| internal/host/claimunit_dispatch.go claimUnitDispatch.forStart authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. ca62826e85ad57e88e68739cf35bee81c4caa0e541f1883d4440ebbdaefc4890 |
| internal/host/claimunit_dispatch.go claimUnitDispatch.selected authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. d167dd0c3f8f2b32995fd4d330e9fd3d15054e6abcbc77ce9408a0bf3b2c8196 |
| -internal/host/claimunit_production.go bindProductionClaimUnits authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. file:Read@1270 bf9467dfc3f9819e7157fad6af0ae86c3d0bdb05f56fcd55fb68438e73699712 |
| internal/host/claimunit_production.go var openClaimUnitBindings authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. file:Open@70 f332e5eeef4116b6a9242f7f5c6c47bef78639a042ef0db3993a54dfae71963c |
| -internal/host/claimunit_program.go ClaimLaunchRoot.boundLaunchSpec authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. e80de9bd3222f06b6a7999ab9aa160a7a5edbb07f58eb7ba6ecba2ef77e66b7e |
| internal/host/claimunit_program.go readClaimUnitProgram authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. file:ReadAll@353 28ee039bab9f2461f10797082d8cfafe751f094271f377d04565e410127808ec |
| internal/instancectl/claimexec.go ClaimUnits.pinExecPeer authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 6966c6e7ca873e312051e94c473f59331b33a1bf6b1017a2a83cd05c66ec01c4 |
| internal/instancectl/claimexec.go claimExecGate.authorize authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 96964cb1eb39b1dd8ecbd47e9e6139e92a61bc72a83ebacfe9cf5886b6d08712 |
| internal/instancectl/claimunit.go ClaimUnits.start authority E2 PROPOSED E48: root; retained canonical unit and program ownership, checked root configuration, deny-only candidate matching, reservation-bearing per-host dispatch, and invocation/cgroup-bound executable peer confirmation. No no-census path is added. Full named-route admission, flat sockets and unit/env/timer retirement remain incomplete. 415efaf2a2a3b304cc9b623c19f0f772e941939322f5752eb8c74ed49e93b1dd |
| + |
| +# PROPOSED E49 |
| +internal/activator/rolelaunch_root_linux.go RoleLaunch.startIsolated hint E2 PROPOSED E49: agent (service UID); 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. file:Fstat@2605 a5e700f18823b68fdc17c599e7818b0bc0ffe1dd3d88bd3303693301b325fe78 |
| +internal/host/claimlaunch_root.go ClaimLaunchRoot.Serve 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. whole:Route@665 1f7cc03f7ad8adca38e728c5b3ec5517d40c7915c1deee3efc08acb929412059 |
| +internal/host/claimlaunch_root.go type ClaimLaunchRoot 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. cba734c41024098ee92239f2cca401803b9f7c0fd855065b9285d90189ac3db4 |
| +internal/host/claimlaunch_transport.go ClaimRoleRootClient.UnitInputs hint E2 PROPOSED E49: agent (service UID); 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. whole:Route@226,whole:Route@234 40a7733f6f7684a849c6cfd1e867cc732f9dc15a2d8cd1fff912adc0069f9229 |
| +internal/host/claimowners.go claimServingOwners.Select hint E2 PROPOSED E49: agent (service UID); 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. registry:DBPath@1081,registry:DBPath@461,registry:Host@424,whole:Route@555 19654b1aa1b28c4bc0f71601dd176980ab610160f7fcda8f628d5de5835dc131 |
| +internal/host/claimproduction.go ProductionClaimOwnerProvider.OpenClaimOwners authority E2 PROPOSED E49: agent (service UID); 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. file:Open@4292 a8d876818f5dfb66808c341f1edf88eab6f3f5da6e98a77f7602e4467e6c9cc7 |
| +internal/host/claimproduction.go claimInputPolicy authority E2 PROPOSED E49: root and agent (caller-dependent); 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. registry:Backing@1130,registry:Unit@1113,whole:Route@1096,whole:Route@1203,whole:Route@1437,whole:Route@618,whole:Route@979 14a881c1169490c73f3bc95cba8c7af21386af3761d28dab8281b23d34934c45 |
| +internal/host/claimproduction.go claimServingLaunchSource hint E2 PROPOSED E49: agent (service UID); 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. registry:Host@695,whole:Route@1137,whole:Route@328,whole:Route@457,whole:Route@612,whole:Route@819 43aa8735763509e59253ace1b1718380dcce6171965a6f6a98843ba25ea4b6b8 |
| +internal/host/claimretire_transport.go ClaimRootHTTP.retire 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. registry:ClaimOwnership@1081,registry:Host@1062,registry:Host@792,whole:ClaimOwnership@1085,whole:ClaimOwnership@894,whole:Route@1971,whole:Route@854 38ea70052f78cc0d9fdb95cf1bcbb30cae504c7a833ab45769fedf26978c50c4 |
| +internal/host/claimretire_transport.go type ClaimRootHTTP 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. 674c85dcb180b4818e5c6117cbed7ef7301c8094a9806d854762521d8d41d315 |
| +internal/host/claimroleassembly.go ClaimRoleAssembly.PreflightLaunch hint E2 PROPOSED E49: agent (service UID); 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. data-literal@200 5bf02fb5b6911924dbd18f063fec53552a994bcc9ae790a67db5786fa877de1e |
| +internal/host/claimroleassembly_linux.go ClaimRoleAssembly.Launch hint E2 PROPOSED E49: agent (service UID); 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. file:Command@5897,file:EvalSymlinks@5446,file:Fstat@3529,file:Lstat@2269,file:OpenFile@3119,file:OpenFile@3306,file:Read@711 6f5cf726038aad19bf8247979e4073d57c804ced0c655cd6e0787b7749ac0d40 |
| +internal/host/claimunit_definition.go claimBoundUnitDefinition 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. file:ReadFile@165 b71194042fe61687350b9dc960744579a8d357d0f66fedc693d8589710f90837 |
| +internal/host/claimunit_definition.go verifyClaimBoundUnitDefinition 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. file:ReadAll@283 41521fe1d039d29440b9dcd067dc202407525912118d8ede300dc4b9ae34795c |
| +internal/host/claimunit_files_linux.go claimUnitFiles.read 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. file:ReadAll@354 cca15dca6e1d79a12975a567a497008abc6a1e57962ea609631dcdd0d2902760 |
| +internal/host/claimunit_files_linux.go claimUnitFiles.write 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. file:CreateTemp@381,file:Open@182 6367edf5ddca8081479ff9fae8b536dca9df86a265ce0e873269752f5297ef48 |
| +internal/host/claimunit_files_linux.go projectClaimBoundSocket hint 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. file:Readlinkat@433 4f3c8daab1f68102e7be9511d1ec29d89c584c99aed79641f2b740031e503ab4 |
| +internal/host/claimunit_inputs.go ClaimLaunchRoot.boundInputs 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. registry:Backing@483,registry:Host@294,registry:Host@385,registry:Host@651,registry:Tunnel@503,registry:Unit@466,whole:Route@661 52dcc6f753bca0dc33b5f49cfcc7df880c1b1c0121be77c18243eded84dc08e4 |
| +internal/host/claimunit_inputs.go claimBoundSnapshot hint E2 PROPOSED E49: agent (service UID); 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. registry:Host@339,registry:Host@437,registry:Host@728,whole:Route@205 3dca2da123f0cff4415c6ef9ef7102b002452e6dbe3b7f526ded14062c51acca |
| +internal/host/claimunit_inputs.go validateClaimUnitInputs hint E2 PROPOSED E49: agent (service UID); 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. data-literal@212 3388a4b9a3ea4b26bb2ce8f4a3ab649ffbfed92fd5e3d217bc04bddcc15d32b5 |
| +internal/host/claimunit_production.go var openClaimUnitMaterializations 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. file:Open@78 9946ebde7a32ac9a87a2d2a9966e45e9ae3559da214706bbb4edd2db3089aa3c |
| +internal/host/claimunit_ready_linux.go claimTCPPeer 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. file:ReadDir@1048,file:ReadFile@241,file:ReadFile@436,file:Readlink@1152 4d30fb4b107ef5768405f689d8b1c40849c8ebe60fe516eb122ef97bf3f9a358 |
| +internal/host/claimwakeassembly.go ClaimWakeAssembly.Build hint E2 PROPOSED E49: agent (service UID); 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. file:Command@9122,file:Prepare@6839,file:Read@1097,file:Read@1191,registry:Backing@2684,registry:Host@11736,registry:Host@3164,registry:Host@3767,registry:Host@3812,registry:Host@4519,registry:Host@5240,registry:Host@6287,registry:Host@6995,registry:Host@7668,registry:Host@8320,registry:Seed@2404,registry:Tunnel@2439,registry:Unit@2667,whole:Route@2611,whole:Route@5640,whole:Route@9260 c3f6ebbf3e063068c5c3322e9edd1eef1f1d09a1024e278e96e77b0e8d1bef15 |
| +internal/host/claimwakeassembly.go ClaimWakeAssembly.readiness hint E2 PROPOSED E49: agent (service UID); 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. registry:Socket@472,whole:Route@1132,whole:Route@653 923fed5500b805bcf31f5d53a5a5fce2859e7bedd68bd22c06b1087ec0a4e635 |
| +internal/hostbox/rolelaunch.go RoleLaunchRequest.Validate hint E2 PROPOSED E49: root/server and agent/client; 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. registry:Host@255,whole:Route@136,whole:Route@228,whole:Route@257 ee67fcc3a1d69ee0acb05a32446cc6cd80e97964fe095fc5c83ab4f85935b26c |
| +@coverage internal/activator 5076bca3ca2fda41c60b04aa927ac99c8756291e6a9b1a1e2417fa35ee81ad24 |
| +@coverage internal/hostbox 0b04568917345170ab505138980c1cb4f0484c146837b81daa44fea12429af0b |
| +internal/host/claimlaunch_authority.go ClaimLaunchRoot.waitReady authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. 9fa747b55dbd3b9a00b01679bcb973344fba2d9775d50bc4063286e31ee12d3e |
| +internal/host/claimunit_materialize.go claimUnitMaterializer.install authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. e1159782780b090f5c6adb853eebdb56e05cc1af6595fe13ec96e21e12a8767f |
| +internal/host/claimunit_materialize.go claimUnitMaterializer.verify authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. 87b28d127a9df0ddacce0856ab6eac8d6967b2dc85d9af32b2938dbaa3ec5ab1 |
| +internal/host/claimunit_materialize.go claimUnitMaterializer.retire authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. 8f65239a6f2d0c826258fe6446f746fabcf060d633f29b694ac2c6ef2045b0d0 |
| +internal/host/claimunit_retire.go retireClaimBoundUnit authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. f7982c0ef43a68af9c0195ecb424a0f58911416a9b422b725eddecc8391f8857 |
| +internal/host/claimunit_retire.go claimBoundUnitCleanup.WithCleanup authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. b1bd5b012973d2abdd29ffcbc65ab6980eef0cbb75caefde7ea62e0738c81b7a |
| +internal/host/claimunit_ready_linux.go claimBoundUnitReady authority E2 PROPOSED E49: root; delegated retained binding, exact unit identity and receipt-authorized retirement or per-connection kernel readiness. No service candidate grants authority. 62e76b8a9ab27be350f3da6d75224600c6cb02d3259e2bd9efbd52e1df28eb95 |
| + |
| +# PROPOSED E49 |
| +internal/host/claimunit_binding.go claimUnitBindings.establish 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. 0b414d85d3aa277f24f37eb02186b90b7392585e384e3e253868f84bf1b77ff1 |
| +internal/host/claimunit_materialize.go claimUnitMaterializer.load 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. file:Read@130 0ba6babf0f42abfcc6f8f3ff562f7258211e6405cbbaa8ee90aa987757d3741b |
| +internal/host/claimunit_program.go ClaimLaunchRoot.boundLaunchSpec 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. 7738f116f2fffb3d2ded6c31c2a7680ab509bba27d52fac85e3ea64f416318f4 |
| +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. 06834223f9165c117262813eebef7a1512d23305c937345f30dec1c938a358e4 |
| + |
| +# 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. file:Read@1268 3a422899fe24fa8d00b80e8fe1c902f044f69f1c4f3f3176fea22e6b9ae0ff54 |
| +@coverage internal/host 940f90be9477b700499cf2d21daad88c3d3ed759d87fb7a9b0008a6efa2560f3 |