WIP claim Plan 14 track E checkpoint 47 (not for merge)
4a: canonical unit/shape binding, reserved named-unit launch and stop components.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
24 files changed,
+1040
−50
internal/host/claimjournal_containment.go+21 −1internal/host/claimlaunch_authority.go+3 −0internal/host/claimlaunch_root.go+14 −8internal/host/claimunit_binding.go+200 −0internal/host/claimunit_binding_source.go+43 −0internal/host/claimunit_binding_test.go+253 −0internal/host/claimunit_launch_test.go+73 −0internal/host/claimunit_placement.go+63 −0internal/host/plan14_installed_launch_root_linux_test.go+43 −2internal/host/plan14_unit_binding_root_linux_test.go+92 −0internal/host/rootgeneration_containment_linux.go+1 −2internal/host/rootgeneration_inventory.go+1 −2internal/host/rootgeneration_kernel_linux.go+1 −2internal/host/rootgeneration_retirement.go+1 −2internal/host/rootgenerations_launch.go+8 −0internal/instancectl/claimexec.go+6 −1internal/instancectl/claimlaunch_snapshot.go+5 −1internal/instancectl/claimunit.go+40 −13internal/instancectl/claimunit_bound.go+32 −0internal/instancectl/claimunit_bound_stop.go+21 −0internal/instancectl/claimunit_bound_test.go+91 −0internal/provision/plan14_e_write_census_test.go+2 −2internal/provision/testdata/plan14-root-reads-e.tsv+23 −13internal/release/rootgenerations.go+3 −1
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 298.647s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.685s
ok github.com/carlosframework/platform/cmd/moveprobe 93.217s
ok github.com/carlosframework/platform/cmd/zddprobe 0.709s
ok github.com/carlosframework/platform/internal/activator 250.460s
ok github.com/carlosframework/platform/internal/agentsock 0.015s
ok github.com/carlosframework/platform/internal/alerting 0.005s
ok github.com/carlosframework/platform/internal/analytics 0.018s
ok github.com/carlosframework/platform/internal/appconfig 2.457s
ok github.com/carlosframework/platform/internal/apperr 0.071s
ok github.com/carlosframework/platform/internal/applog 0.129s
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.716s
? 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.698s
ok github.com/carlosframework/platform/internal/changelog 0.301s
ok github.com/carlosframework/platform/internal/clamd 0.054s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 390.247s
? 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.310s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.002s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.435s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 26.704s
? 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.131s
ok github.com/carlosframework/platform/internal/gate 1.656s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.015s
ok github.com/carlosframework/platform/internal/geoip 0.031s
ok github.com/carlosframework/platform/internal/historyreadback 0.151s
ok github.com/carlosframework/platform/internal/host 148.120s
ok github.com/carlosframework/platform/internal/hostactors 0.525s
ok github.com/carlosframework/platform/internal/hostbox 2.292s
ok github.com/carlosframework/platform/internal/hostclaim 0.520s
ok github.com/carlosframework/platform/internal/hostevidence 0.005s
ok github.com/carlosframework/platform/internal/hostexec 4.964s
ok github.com/carlosframework/platform/internal/hostfence 0.028s
ok github.com/carlosframework/platform/internal/hostfiles 10.529s
ok github.com/carlosframework/platform/internal/hostfloor 0.016s
ok github.com/carlosframework/platform/internal/hostguard 1.025s
ok github.com/carlosframework/platform/internal/hostops 1.555s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.002s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.465s
ok github.com/carlosframework/platform/internal/hostresolve 0.143s
ok github.com/carlosframework/platform/internal/hostwrite 0.002s
ok github.com/carlosframework/platform/internal/infraview 0.008s
ok github.com/carlosframework/platform/internal/instancectl 4.176s
ok github.com/carlosframework/platform/internal/instanceexec 2.738s
ok github.com/carlosframework/platform/internal/instancefs 0.019s
ok github.com/carlosframework/platform/internal/instancekey 0.004s
ok github.com/carlosframework/platform/internal/instanceupdates 0.010s
ok github.com/carlosframework/platform/internal/layout 1.503s
ok github.com/carlosframework/platform/internal/lease 9.791s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.005s
ok github.com/carlosframework/platform/internal/ledger 0.011s
ok github.com/carlosframework/platform/internal/mail 0.164s
ok github.com/carlosframework/platform/internal/maintenance 0.083s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.518s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.067s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 20.974s
ok github.com/carlosframework/platform/internal/moves/archive 0.758s
ok github.com/carlosframework/platform/internal/objstore 0.184s
ok github.com/carlosframework/platform/internal/ops 0.184s
ok github.com/carlosframework/platform/internal/park 0.207s
ok github.com/carlosframework/platform/internal/privatefile 0.008s
ok github.com/carlosframework/platform/internal/provision 17.045s
ok github.com/carlosframework/platform/internal/publicrelease 0.007s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.450s
ok github.com/carlosframework/platform/internal/relay 7.243s
ok github.com/carlosframework/platform/internal/release 21.882s
ok github.com/carlosframework/platform/internal/rollout 3.786s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.006s
ok github.com/carlosframework/platform/internal/rootparticipation 0.007s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/runs 0.569s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.003s
ok github.com/carlosframework/platform/internal/schedule 2.502s
ok github.com/carlosframework/platform/internal/seeds 0.011s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.548s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.678s
ok github.com/carlosframework/platform/internal/status 0.039s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.200s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.556s
=== 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: 47120546 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.022s
=== RUN TestT4StorageProbeMinio
2026/10/15 16:53:34 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEC2303F342EE1, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.020s
minio contract: PASS
diff --git a/internal/host/claimjournal_containment.go b/internal/host/claimjournal_containment.go| index 17695f222..92b599d19 100644 |
| --- a/internal/host/claimjournal_containment.go |
| +++ b/internal/host/claimjournal_containment.go |
| @@ -50,6 +50,10 @@ func validateClaimGroups(parent string, groups []hostbox.LaunchGroup) error { |
| } |
| func validateClaimContainment(in hostbox.LaunchInventory, parent string) error { |
| + return validateClaimContainmentUnit(in, parent, instancectl.ClaimUnitControlGroup(in.Target.Host)) |
| +} |
| + |
| +func validateClaimContainmentUnit(in hostbox.LaunchInventory, parent, unitGroup string) error { |
| if err := validateClaimGroups(parent, in.Groups); err != nil { |
| return err |
| } |
| @@ -67,7 +71,7 @@ func validateClaimContainment(in hostbox.LaunchInventory, parent string) error { |
| return ErrClaimStopInventory |
| } |
| if hostbox.IsUnitReservation(r) { |
| - if r.Parent != (hostbox.LaunchGroup{}) || r.Group.RelativePath != instancectl.ClaimUnitControlGroup(in.Target.Host) { |
| + if r.Parent != (hostbox.LaunchGroup{}) || r.Group.RelativePath != unitGroup { |
| return ErrClaimStopInventory |
| } |
| continue |
| @@ -198,3 +202,19 @@ func validateStopClaimGroups(t hostguard.FenceTarget, j hostbox.StopKernelJourna |
| } |
| return validateClaimGroups(parent, groups) |
| } |
| + |
| +func validatePersistedUnitContainment(ctx context.Context, in hostbox.LaunchInventory, units ClaimLaunchUnits) error { |
| + agent, err := claimContainmentAgentRoot(ctx) |
| + if err != nil { |
| + return err |
| + } |
| + parent, err := claimContainmentParent(agent, in.Target) |
| + if err != nil { |
| + return err |
| + } |
| + _, group, err := claimLaunchPlacement(units, in.Target.Host) |
| + if err != nil { |
| + return err |
| + } |
| + return validateClaimContainmentUnit(in, parent, group) |
| +} |
diff --git a/internal/host/claimlaunch_authority.go b/internal/host/claimlaunch_authority.go| index 222db2b17..0df2243ff 100644 |
| --- a/internal/host/claimlaunch_authority.go |
| +++ b/internal/host/claimlaunch_authority.go |
| @@ -66,6 +66,9 @@ func (h *ClaimLaunchRoot) launchSpec(q hostbox.RoleLaunchRequest) (instancectl.L |
| // StartChecked after namespace/tier preparation and immediately before start. |
| func (h *ClaimLaunchRoot) authorize(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest, c *hostclaim.Claim) error { |
| t, r := q.Reservation.Target, *q.Reservation |
| + if err := h.checkUnitBinding(ctx, t, false); err != nil { |
| + return err |
| + } |
| if err := ctx.Err(); err != nil { |
| return err |
| } |
diff --git a/internal/host/claimlaunch_root.go b/internal/host/claimlaunch_root.go| index fa73dc375..086cc4b39 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 { |
| + unitBindings *claimUnitBindings |
| history rootHistoryAccess |
| generations *rootGenerationSource |
| installedProgram func(context.Context, hostbox.RoleLaunchRequest, instancectl.ClaimUnitStarted, ClaimLaunchObserved) (string, error) |
| @@ -152,7 +153,7 @@ func (h *ClaimLaunchRoot) launchRecord(ctx context.Context, peer StopPeer, t hos |
| if err != nil { |
| return err |
| } |
| - if err := validatePersistedClaimContainment(ctx, rec); err != nil { |
| + if err := validatePersistedUnitContainment(ctx, rec, h.Units); err != nil { |
| return err |
| } |
| if rec.ControllerPID != peer.PID || rec.ProcessStart != peer.ProcessStart { |
| @@ -186,7 +187,8 @@ func (h *ClaimLaunchRoot) startRecorded(ctx context.Context, peer StopPeer, q ho |
| if t.BootID != peer.BootID || t.ControllerID != peer.ControllerID { |
| return hostbox.RoleLaunchGrant{}, ErrClaimStopIdentity |
| } |
| - if r.Group.RelativePath != instancectl.ClaimUnitControlGroup(t.Host) || !claimLaunchTier(q.MemoryMax) { |
| + _, group, placementErr := claimLaunchPlacement(h.Units, t.Host) |
| + if placementErr != nil || r.Group.RelativePath != group || !claimLaunchTier(q.MemoryMax) { |
| return hostbox.RoleLaunchGrant{}, hostbox.ErrRoleLaunchShape |
| } |
| actor, err := h.Actor(ctx) |
| @@ -285,7 +287,7 @@ func (h *ClaimLaunchRoot) launchLocked(ctx context.Context, peer StopPeer, q hos |
| if err != nil { |
| return hostbox.RoleLaunchGrant{}, err |
| } |
| - if err := validatePersistedClaimContainment(ctx, record); err != nil { |
| + if err := validatePersistedUnitContainment(ctx, record, h.Units); err != nil { |
| return hostbox.RoleLaunchGrant{}, err |
| } |
| if old.Group.RelativePath != r.Group.RelativePath { |
| @@ -378,7 +380,7 @@ func (h *ClaimLaunchRoot) launchLocked(ctx context.Context, peer StopPeer, q hos |
| if sameErr := h.Units.Same(cleanup, started); sameErr != nil { |
| stopErr = h.settleGone(cleanup, t, r.ID, started.InvocationID) |
| } else { |
| - stopErr = h.Units.Stop(cleanup, t.Host) |
| + stopErr = claimStopInvocation(cleanup, h.Units, t.Host, started.InvocationID) |
| if stopErr == nil { |
| stopErr = h.settleGone(cleanup, t, r.ID, started.InvocationID) |
| } |
| @@ -408,7 +410,11 @@ func sameUnitRequest(old, r hostbox.LaunchReservation) bool { |
| // serializes with a launch. A reservation root never saw is spent. |
| func (h *ClaimLaunchRoot) stop(ctx context.Context, peer StopPeer, r hostbox.LaunchReservation) error { |
| t := r.Target |
| - if r.Group.RelativePath != instancectl.ClaimUnitControlGroup(t.Host) { |
| + if err := h.checkUnitBinding(ctx, t, true); err != nil { |
| + return err |
| + } |
| + _, group, err := claimLaunchPlacement(h.Units, t.Host) |
| + if err != nil || r.Group.RelativePath != group { |
| return hostbox.ErrRoleLaunchShape |
| } |
| if t.BootID != peer.BootID || t.ControllerID != peer.ControllerID { |
| @@ -419,7 +425,7 @@ func (h *ClaimLaunchRoot) stop(ctx context.Context, peer StopPeer, r hostbox.Lau |
| if err != nil { |
| return err |
| } |
| - if err := validatePersistedClaimContainment(ctx, rec); err != nil { |
| + if err := validatePersistedUnitContainment(ctx, rec, h.Units); err != nil { |
| return err |
| } |
| if rec.ControllerPID != peer.PID || rec.ProcessStart != peer.ProcessStart { |
| @@ -440,7 +446,7 @@ func (h *ClaimLaunchRoot) stop(ctx context.Context, peer StopPeer, r hostbox.Lau |
| } else if !errors.Is(err, instancectl.ErrClaimUnitBusy) { |
| return err |
| } |
| - if err := h.Units.Stop(ctx, t.Host); err != nil { |
| + if err := claimStopInvocation(ctx, h.Units, t.Host, old.Group.InvocationID); err != nil { |
| return err |
| } |
| return h.settleGone(ctx, t, r.ID, old.Group.InvocationID) |
| @@ -512,7 +518,7 @@ func (k claimUnitStopKernel) settleUnits(ctx context.Context, t hostguard.FenceT |
| if err != nil { |
| return nil, err |
| } |
| - if err := validatePersistedClaimContainment(ctx, in); err != nil { |
| + if err := validatePersistedUnitContainment(ctx, in, k.Units); err != nil { |
| return nil, err |
| } |
| ids := []string{} |
diff --git a/internal/host/claimunit_binding.go b/internal/host/claimunit_binding.go| new file mode 100644 |
| index 000000000..90979cac6 |
| --- /dev/null |
| +++ b/internal/host/claimunit_binding.go |
| @@ -0,0 +1,200 @@ |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "io" |
| + "net" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/release" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +type claimUnitShape struct { |
| + Host, Account, App, Kind, Unit, Backing, Socket, Addr, DBPath string |
| +} |
| + |
| +type claimUnitBinding struct { |
| + Schema int |
| + Owner hostguard.RootInstallation |
| + Shape claimUnitShape |
| + Retiring, Retired bool |
| +} |
| + |
| +type claimUnitBindings struct { |
| + state rootstate.Store |
| + installation func(context.Context, string) (hostguard.RootInstallation, error) |
| + canonical func(context.Context, string) (claimUnitShape, error) |
| +} |
| + |
| +func (s claimUnitShape) valid() bool { |
| + if !provision.ValidHostShape(s.Host) || !release.ValidAccount(s.Account) || !release.ValidApp(s.App) || s.Kind != "instance" || !instancectl.ValidBoundClaimUnit(s.Unit) { |
| + return false |
| + } |
| + if s.Backing != "" && (s.Backing != registry.BackingUnit || s.Unit != "carlos-app@"+s.Host+".service") { |
| + return false |
| + } |
| + if (s.Socket == "") == (s.Addr == "") { |
| + return false |
| + } |
| + for _, p := range []string{s.Socket, s.DBPath} { |
| + if p != "" && (!filepath.IsAbs(p) || filepath.Clean(p) != p || p == "/" || strings.ContainsAny(p, "\x00\r\n")) { |
| + return false |
| + } |
| + } |
| + if s.Addr != "" { |
| + host, port, err := net.SplitHostPort(s.Addr) |
| + if err != nil || port == "" || net.ParseIP(host) == nil || !net.ParseIP(host).IsLoopback() { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
| + |
| +func (s claimUnitShape) route() registry.Route { |
| + return registry.Route{Host: s.Host, Account: s.Account, App: s.App, Kind: s.Kind, Unit: s.Unit, Backing: s.Backing, Socket: s.Socket, Addr: s.Addr, DBPath: s.DBPath} |
| +} |
| + |
| +func (s claimUnitShape) matches(r registry.Route) bool { |
| + return s.Host == r.Host && s.Account == r.Account && s.App == r.App && s.Kind == r.Kind && s.Unit == r.Unit && s.Backing == r.Backing && s.Socket == r.Socket && s.Addr == r.Addr && s.DBPath == r.DBPath && r.Addrs == "" && r.Tunnel == "" && r.Seed == "" && !r.Replica |
| +} |
| + |
| +func unitBindingOwner(in hostguard.RootInstallation) hostguard.RootInstallation { |
| + in.Artifacts = hostguard.InstallationArtifacts{} |
| + in.Registration = hostguard.InstallationRegistration{} |
| + in.InventoryRevision = 0 |
| + in.Empty = false |
| + return in |
| +} |
| + |
| +func (b claimUnitBinding) valid() bool { |
| + in := b.Owner |
| + return b.Schema == 1 && b.Shape.valid() && in == unitBindingOwner(in) && in.Host == b.Shape.Host && in.Account == b.Shape.Account && in.App == b.Shape.App && in.Incarnation != "" && in.Stamp > 0 && in.Migration > 0 && in.Record != "" && in.Box != "" && in.Enrollment != "" && (!b.Retired || b.Retiring) |
| +} |
| + |
| +func (s *claimUnitBindings) read(host string) (claimUnitBinding, error) { |
| + var b claimUnitBinding |
| + if s == nil || s.state == nil || !provision.ValidHostShape(host) { |
| + return b, os.ErrPermission |
| + } |
| + raw, err := s.state.Read(host + ".json") |
| + if err != nil { |
| + return b, err |
| + } |
| + dec := json.NewDecoder(bytes.NewReader(raw)) |
| + dec.DisallowUnknownFields() |
| + if len(raw) > rootstate.MaxRecord || dec.Decode(&b) != nil || dec.Decode(new(any)) != io.EOF || !b.valid() || b.Owner.Host != host { |
| + return claimUnitBinding{}, os.ErrPermission |
| + } |
| + return b, nil |
| +} |
| + |
| +func (s *claimUnitBindings) write(b claimUnitBinding) error { |
| + if !b.valid() { |
| + return os.ErrPermission |
| + } |
| + raw, err := json.Marshal(b) |
| + if err != nil { |
| + return err |
| + } |
| + return s.state.Write(b.Owner.Host+".json", raw) |
| +} |
| + |
| +func (s *claimUnitBindings) current(ctx context.Context, host string) (claimUnitBinding, error) { |
| + var b claimUnitBinding |
| + if s == nil || s.installation == nil || s.canonical == nil { |
| + return b, os.ErrPermission |
| + } |
| + in, err := s.installation(ctx, host) |
| + if err != nil { |
| + return b, err |
| + } |
| + shape, err := s.canonical(ctx, host) |
| + if err != nil { |
| + return b, err |
| + } |
| + b = claimUnitBinding{Schema: 1, Owner: unitBindingOwner(in), Shape: shape} |
| + if in.InventoryRevision == 0 || b.Owner.Host != host || !b.valid() { |
| + return claimUnitBinding{}, os.ErrPermission |
| + } |
| + return b, nil |
| +} |
| + |
| +func (s *claimUnitBindings) establish(ctx context.Context, host string) (claimUnitBinding, error) { |
| + if s == nil || s.state == nil { |
| + return claimUnitBinding{}, os.ErrPermission |
| + } |
| + unlock, err := s.state.Lock(ctx, "bindings") |
| + if err != nil { |
| + return claimUnitBinding{}, err |
| + } |
| + defer unlock() |
| + next, err := s.current(ctx, host) |
| + if err != nil { |
| + return next, err |
| + } |
| + old, err := s.read(host) |
| + if err == nil { |
| + if old != next { |
| + return claimUnitBinding{}, os.ErrPermission |
| + } |
| + } else if !errors.Is(err, os.ErrNotExist) { |
| + return claimUnitBinding{}, err |
| + } |
| + // Replay syncs the original binding before relying on an earlier rename. |
| + return next, s.write(next) |
| +} |
| + |
| +func (s *claimUnitBindings) check(ctx context.Context, host string, candidate *registry.Route) (claimUnitBinding, error) { |
| + saved, err := s.read(host) |
| + if err != nil { |
| + return saved, err |
| + } |
| + current, err := s.current(ctx, host) |
| + if err != nil { |
| + return claimUnitBinding{}, err |
| + } |
| + if saved.Retiring || saved.Retired || saved != current || (candidate != nil && !saved.Shape.matches(*candidate)) { |
| + return claimUnitBinding{}, os.ErrPermission |
| + } |
| + return saved, nil |
| +} |
| + |
| +func (s *claimUnitBindings) retire(ctx context.Context, captured claimUnitBinding, verify func(claimUnitBinding) error) error { |
| + if s == nil || s.state == nil || !captured.valid() || captured.Retiring || verify == nil { |
| + return os.ErrPermission |
| + } |
| + unlock, err := s.state.Lock(ctx, "bindings") |
| + if err != nil { |
| + return err |
| + } |
| + defer unlock() |
| + old, err := s.read(captured.Owner.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if old.Owner != captured.Owner || old.Shape != captured.Shape { |
| + return os.ErrPermission |
| + } |
| + if old.Retired { |
| + return s.write(old) |
| + } |
| + old.Retiring = true |
| + if err := s.write(old); err != nil { |
| + return err |
| + } |
| + if err := verify(old); err != nil { |
| + return err |
| + } |
| + old.Retired = true |
| + return s.write(old) |
| +} |
diff --git a/internal/host/claimunit_binding_source.go b/internal/host/claimunit_binding_source.go| new file mode 100644 |
| index 000000000..60e56e977 |
| --- /dev/null |
| +++ b/internal/host/claimunit_binding_source.go |
| @@ -0,0 +1,43 @@ |
| +package host |
| + |
| +import ( |
| + "bytes" |
| + "encoding/json" |
| + "io" |
| + "os" |
| + |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func readClaimUnitShapes(path string) (map[string]claimUnitShape, error) { |
| + f, err := rootstate.OpenAuthorityFile(path) |
| + if err != nil { |
| + return nil, err |
| + } |
| + defer f.Close() |
| + raw, err := io.ReadAll(io.LimitReader(f, rootstate.MaxRecord+1)) |
| + if err != nil { |
| + return nil, err |
| + } |
| + return decodeClaimUnitShapes(raw) |
| +} |
| + |
| +func decodeClaimUnitShapes(raw []byte) (map[string]claimUnitShape, error) { |
| + var config struct { |
| + Schema int |
| + Units []claimUnitShape |
| + } |
| + dec := json.NewDecoder(bytes.NewReader(raw)) |
| + dec.DisallowUnknownFields() |
| + if len(raw) > rootstate.MaxRecord || dec.Decode(&config) != nil || dec.Decode(new(any)) != io.EOF || config.Schema != 1 || len(config.Units) > 4096 { |
| + return nil, os.ErrPermission |
| + } |
| + hosts, units := map[string]claimUnitShape{}, map[string]bool{} |
| + for _, s := range config.Units { |
| + if !s.valid() || hosts[s.Host].Host != "" || units[s.Unit] { |
| + return nil, os.ErrPermission |
| + } |
| + hosts[s.Host], units[s.Unit] = s, true |
| + } |
| + return hosts, nil |
| +} |
diff --git a/internal/host/claimunit_binding_test.go b/internal/host/claimunit_binding_test.go| new file mode 100644 |
| index 000000000..6304da08f |
| --- /dev/null |
| +++ b/internal/host/claimunit_binding_test.go |
| @@ -0,0 +1,253 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "errors" |
| + "os" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func plan14UnitBindingFixture(t *testing.T, unit, backing, socket string) (*claimUnitBindings, hostguard.RootInstallation, claimUnitShape) { |
| + t.Helper() |
| + in := hostguard.RootInstallation{Host: "shop.example", Account: "bab", App: "shop", Record: "apps/bab/shop/instances/shop.example.json", Incarnation: "inc", Stamp: 1, Migration: 1, InventoryRevision: 1, Box: "box", Enrollment: "enrollment"} |
| + shape := claimUnitShape{Host: in.Host, Account: in.Account, App: in.App, Kind: "instance", Unit: unit, Backing: backing, Socket: socket, DBPath: "/data/instances/shop/data.db"} |
| + if socket == "" { |
| + shape.Addr = "127.0.0.1:8090" |
| + } |
| + b := &claimUnitBindings{state: &rollbackMemory{files: map[string][]byte{}}, installation: func(context.Context, string) (hostguard.RootInstallation, error) { return in, nil }, canonical: func(context.Context, string) (claimUnitShape, error) { return shape, nil }} |
| + return b, in, shape |
| +} |
| + |
| +func TestPlan14CanonicalUnitBinding(t *testing.T) { |
| + for _, tc := range []struct{ name, unit, backing, socket string }{ |
| + {"named", "console.service", "", ""}, |
| + {"socket", "woodstar-atbridge.service", "", "/run/carlos/woodstar.sock"}, |
| + {"whole-app", "carlos-app@shop.example.service", "unit", ""}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + b, in, shape := plan14UnitBindingFixture(t, tc.unit, tc.backing, tc.socket) |
| + saved, err := b.establish(t.Context(), in.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + row := shape.route() |
| + if _, err := b.check(t.Context(), in.Host, &row); err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, _ := b.state.Read(in.Host + ".json") |
| + for _, mutate := range []func(*registry.Route){ |
| + func(r *registry.Route) { r.Unit = "other.service" }, func(r *registry.Route) { r.Backing = "exec" }, |
| + func(r *registry.Route) { r.Socket = "/run/other.sock" }, func(r *registry.Route) { r.Addr = "127.0.0.1:9000" }, |
| + func(r *registry.Route) { r.DBPath = "/data/sibling.db" }, func(r *registry.Route) { r.Account = "other" }, |
| + func(r *registry.Route) { r.Tunnel = "other" }, func(r *registry.Route) { r.Kind = "sidecar" }, |
| + } { |
| + forged := row |
| + mutate(&forged) |
| + if _, err := b.check(t.Context(), in.Host, &forged); err == nil { |
| + t.Fatal("accepted changed shape", forged) |
| + } |
| + } |
| + b.canonical = func(context.Context, string) (claimUnitShape, error) { |
| + changed := shape |
| + changed.Unit = "other.service" |
| + return changed, nil |
| + } |
| + if _, err := b.establish(t.Context(), in.Host); err == nil { |
| + t.Fatal("rebound a live installation") |
| + } |
| + if _, err := b.check(t.Context(), in.Host, &row); err == nil { |
| + t.Fatal("accepted changed canonical configuration") |
| + } |
| + now, _ := b.state.Read(in.Host + ".json") |
| + if string(now) != string(raw) { |
| + t.Fatal("replaced binding") |
| + } |
| + b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } |
| + successor := in |
| + successor.Stamp++ |
| + b.installation = func(context.Context, string) (hostguard.RootInstallation, error) { return successor, nil } |
| + if _, err := b.check(t.Context(), in.Host, &row); err == nil { |
| + t.Fatal("adopted successor") |
| + } |
| + if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { return os.ErrPermission }); err == nil { |
| + t.Fatal("retired without stop") |
| + } |
| + pending, _ := b.read(in.Host) |
| + if !pending.Retiring || pending.Retired { |
| + t.Fatal("missing durable retirement intent") |
| + } |
| + if _, err := b.establish(t.Context(), in.Host); err == nil { |
| + t.Fatal("overwrote predecessor retirement") |
| + } |
| + called := false |
| + if err := b.retire(t.Context(), saved, func(got claimUnitBinding) error { |
| + called = true |
| + if got.Owner != saved.Owner || got.Shape != shape { |
| + t.Fatal("retirement retargeted") |
| + } |
| + return nil |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !called { |
| + t.Fatal("no stop verification") |
| + } |
| + if _, err := b.check(t.Context(), in.Host, &row); err == nil { |
| + t.Fatal("retired binding granted work") |
| + } |
| + if _, err := b.establish(t.Context(), in.Host); err == nil { |
| + t.Fatal("implicit successor grant") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14CanonicalUnitBindingRefusals(t *testing.T) { |
| + b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "") |
| + for _, raw := range []string{"", "{}", `{"Schema":1,"unknown":1}`} { |
| + b.state.Write(in.Host+".json", []byte(raw)) |
| + if _, err := b.establish(t.Context(), in.Host); err == nil { |
| + t.Fatal("corrupt history became permission") |
| + } |
| + } |
| + b.state.Remove(in.Host + ".json") |
| + b.canonical = func(context.Context, string) (claimUnitShape, error) { return claimUnitShape{}, os.ErrNotExist } |
| + if _, err := b.establish(t.Context(), in.Host); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal(err) |
| + } |
| + if _, err := b.state.Read(in.Host + ".json"); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("created binding without root config", err) |
| + } |
| +} |
| + |
| +func TestPlan14UnitBindingRetirementCrash(t *testing.T) { |
| + b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "") |
| + saved, err := b.establish(t.Context(), in.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { return os.ErrPermission }); err == nil { |
| + t.Fatal("lost stop accepted") |
| + } |
| + restarted := *b |
| + if _, err := restarted.check(t.Context(), in.Host, nil); err == nil { |
| + t.Fatal("crash reopened admission") |
| + } |
| + if err := restarted.retire(t.Context(), saved, func(claimUnitBinding) error { return nil }); err != nil { |
| + t.Fatal(err) |
| + } |
| + raw, _ := b.state.Read(in.Host + ".json") |
| + var record claimUnitBinding |
| + if err := json.Unmarshal(raw, &record); err != nil || !record.Retired { |
| + t.Fatal(record, err) |
| + } |
| +} |
| + |
| +func TestPlan14UnitBindingConfig(t *testing.T) { |
| + _, _, s := plan14UnitBindingFixture(t, "console.service", "", "") |
| + raw, _ := json.Marshal(struct { |
| + Schema int |
| + Units []claimUnitShape |
| + }{1, []claimUnitShape{s}}) |
| + if got, err := decodeClaimUnitShapes(raw); err != nil || got[s.Host] != s { |
| + t.Fatal(got, err) |
| + } |
| + for _, kind := range []string{"duplicate host", "duplicate unit", "tunnel", "unknown field", "trailing", "unsafe addr", "template", "backing"} { |
| + t.Run(kind, func(t *testing.T) { |
| + other := s |
| + other.Host = "sibling.example" |
| + config := struct { |
| + Schema int |
| + Units []claimUnitShape |
| + }{1, []claimUnitShape{s}} |
| + switch kind { |
| + case "duplicate host": |
| + config.Units = append(config.Units, s) |
| + case "duplicate unit": |
| + config.Units = append(config.Units, other) |
| + case "unsafe addr": |
| + config.Units[0].Addr = "0.0.0.0:8090" |
| + case "template": |
| + config.Units[0].Unit = "x@.service" |
| + case "backing": |
| + config.Units[0].Backing = "unit" |
| + } |
| + body, _ := json.Marshal(config) |
| + switch kind { |
| + case "tunnel": |
| + body = []byte(`{"Schema":1,"Units":[{"Tunnel":"x"}]}`) |
| + case "unknown field": |
| + body = []byte(`{"Schema":1,"Units":[],"Authority":true}`) |
| + case "trailing": |
| + body = append(body, []byte(`{}`)...) |
| + } |
| + if _, err := decodeClaimUnitShapes(body); err == nil { |
| + t.Fatal("accepted invalid config") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +type plan14UnitBindingFault struct { |
| + rootstate.Store |
| + fail bool |
| + writes int |
| +} |
| + |
| +func (s *plan14UnitBindingFault) Write(name string, body []byte) error { |
| + s.writes++ |
| + if err := s.Store.Write(name, body); err != nil { |
| + return err |
| + } |
| + if s.fail { |
| + return os.ErrInvalid |
| + } |
| + return nil |
| +} |
| + |
| +func TestPlan14UnitBindingDurability(t *testing.T) { |
| + b, in, _ := plan14UnitBindingFixture(t, "console.service", "", "") |
| + fault := &plan14UnitBindingFault{Store: b.state, fail: true} |
| + b.state = fault |
| + if _, err := b.establish(t.Context(), in.Host); err == nil { |
| + t.Fatal("reported unsynced creation") |
| + } |
| + if _, err := b.establish(t.Context(), in.Host); err == nil { |
| + t.Fatal("duplicate creation skipped durability") |
| + } |
| + fault.fail = false |
| + saved, err := b.establish(t.Context(), in.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + fault.fail = true |
| + effects := 0 |
| + if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { effects++; return nil }); err == nil { |
| + t.Fatal("reported unsynced retirement intent") |
| + } |
| + if effects != 0 { |
| + t.Fatal("effects before durable intent") |
| + } |
| + if _, err := b.check(t.Context(), in.Host, nil); err == nil { |
| + t.Fatal("visible retirement intent reopened") |
| + } |
| + fault.fail = false |
| + if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { effects++; return nil }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if effects != 1 { |
| + t.Fatal(effects) |
| + } |
| + fault.fail = true |
| + if err := b.retire(t.Context(), saved, func(claimUnitBinding) error { effects++; return nil }); err == nil { |
| + t.Fatal("duplicate completion skipped durability") |
| + } |
| + if effects != 1 { |
| + t.Fatal("replayed completed effect") |
| + } |
| +} |
diff --git a/internal/host/claimunit_launch_test.go b/internal/host/claimunit_launch_test.go| new file mode 100644 |
| index 000000000..e68a4be3a |
| --- /dev/null |
| +++ b/internal/host/claimunit_launch_test.go |
| @@ -0,0 +1,73 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| +) |
| + |
| +type plan14NamedLaunchUnits struct { |
| + *launchUnits |
| + host, unit string |
| +} |
| + |
| +func (u plan14NamedLaunchUnits) Placement(host string) (string, string, error) { |
| + if host != u.host { |
| + return "", "", instancectl.ErrClaimUnit |
| + } |
| + return u.unit, instancectl.BoundClaimUnitControlGroup(host, u.unit), nil |
| +} |
| + |
| +func TestPlan14NamedLaunchBindingGate(t *testing.T) { |
| + for _, attack := range []string{"none", "missing", "canonical before", "canonical at exec", "owner", "retiring", "wrong group"} { |
| + t.Run(attack, func(t *testing.T) { |
| + f, _, _ := generationLaunchFixture(t) |
| + unit := "plan14-bound.service" |
| + f.units.started.Unit = unit |
| + f.units.started.ControlGroup = instancectl.BoundClaimUnitControlGroup(f.target.Host, unit) |
| + f.req.Reservation.Group.RelativePath = f.units.started.ControlGroup |
| + f.root.Units = plan14NamedLaunchUnits{f.units, f.target.Host, unit} |
| + b, _, _ := plan14UnitBindingFixture(t, 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) |
| + } |
| + shape := claimUnitShape{Host: in.Host, Account: in.Account, App: in.App, Kind: "instance", Unit: unit, Addr: "127.0.0.1:8090"} |
| + b.installation = func(context.Context, string) (hostguard.RootInstallation, error) { return in, nil } |
| + b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } |
| + saved, err := b.establish(t.Context(), in.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.root.unitBindings = b |
| + switch attack { |
| + case "missing": |
| + f.root.unitBindings = nil |
| + case "canonical before": |
| + shape.Unit = "replacement.service" |
| + case "canonical at exec": |
| + f.units.beforeCheck = func() { shape.Unit = "replacement.service" } |
| + case "owner": |
| + in.Stamp++ |
| + case "retiring": |
| + saved.Retiring = true |
| + if err := b.write(saved); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "wrong group": |
| + f.req.Reservation.Group.RelativePath = instancectl.ClaimUnitControlGroup(f.target.Host) |
| + } |
| + _, err = f.root.Serve(t.Context(), f.peer, f.req) |
| + started, _ := f.units.counts() |
| + if attack == "none" { |
| + if err != nil || started != 1 { |
| + t.Fatal("bound launch", err, started) |
| + } |
| + } else if err == nil || started != 0 { |
| + t.Fatal("authority attack executed", err, started) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/claimunit_placement.go b/internal/host/claimunit_placement.go| new file mode 100644 |
| index 000000000..c8aae4dd6 |
| --- /dev/null |
| +++ b/internal/host/claimunit_placement.go |
| @@ -0,0 +1,63 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| + "github.com/carlosframework/platform/internal/release" |
| + "os" |
| +) |
| + |
| +func claimLaunchPlacement(units ClaimLaunchUnits, host string) (string, string, error) { |
| + if bound, ok := units.(interface { |
| + Placement(string) (string, string, error) |
| + }); ok { |
| + return bound.Placement(host) |
| + } |
| + return instancectl.UnitName(instancectl.RoleRun, host), instancectl.ClaimUnitControlGroup(host), nil |
| +} |
| + |
| +func claimStopInvocation(ctx context.Context, units ClaimLaunchUnits, host, invocation string) error { |
| + if exact, ok := units.(interface { |
| + StopInvocation(context.Context, string, string) error |
| + }); ok { |
| + return exact.StopInvocation(ctx, host, invocation) |
| + } |
| + return units.Stop(ctx, host) |
| +} |
| + |
| +func generationBindingGroup(b release.GenerationBinding) string { |
| + if b.Unit == "" { |
| + return instancectl.ClaimUnitControlGroup(b.Binding.Host) |
| + } |
| + if !instancectl.ValidBoundClaimUnit(b.Unit) { |
| + return "" |
| + } |
| + return instancectl.BoundClaimUnitControlGroup(b.Binding.Host, b.Unit) |
| +} |
| + |
| +func (h *ClaimLaunchRoot) checkUnitBinding(ctx context.Context, t hostguard.FenceTarget, cleanup bool) error { |
| + unit, _, err := claimLaunchPlacement(h.Units, t.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if unit == instancectl.UnitName(instancectl.RoleRun, t.Host) { |
| + return nil |
| + } |
| + if h.unitBindings == nil { |
| + return os.ErrPermission |
| + } |
| + var b claimUnitBinding |
| + if cleanup { |
| + b, err = h.unitBindings.read(t.Host) |
| + } else { |
| + b, err = h.unitBindings.check(ctx, t.Host, nil) |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + if b.Owner.Host != t.Host || b.Owner.Incarnation != t.Incarnation || b.Owner.Stamp != t.Stamp || b.Owner.Box != t.Box || b.Owner.Enrollment != t.Enrollment || b.Shape.Unit != unit { |
| + return os.ErrPermission |
| + } |
| + return nil |
| +} |
diff --git a/internal/host/plan14_installed_launch_root_linux_test.go b/internal/host/plan14_installed_launch_root_linux_test.go| index 9261d4ce6..a10a33daf 100644 |
| --- a/internal/host/plan14_installed_launch_root_linux_test.go |
| +++ b/internal/host/plan14_installed_launch_root_linux_test.go |
| @@ -44,6 +44,16 @@ func plan14InstalledTenant() { |
| } |
| func TestPlan14InstalledClaimLaunchRootRunner(t *testing.T) { |
| + plan14InstalledClaimLaunch(t, "") |
| +} |
| + |
| +func TestPlan14NamedClaimLaunchRootRunner(t *testing.T) { |
| + for _, unit := range []string{"plan14-named.service", "plan14-socket.service", "backing"} { |
| + t.Run(unit, func(t *testing.T) { plan14InstalledClaimLaunch(t, unit) }) |
| + } |
| +} |
| + |
| +func plan14InstalledClaimLaunch(t *testing.T, boundUnit string) { |
| if dir := os.Getenv("PLAN14_INSTALLED_RETIRE_CHILD"); dir != "" { |
| plan14InstalledRetire(t, dir) |
| return |
| @@ -83,6 +93,9 @@ func TestPlan14InstalledClaimLaunchRootRunner(t *testing.T) { |
| t.Setenv("CARLOS_DATA_DIR", "/data") |
| t.Setenv("CARLOS_OPT_DIR", "/opt/carlos") |
| host := f.target.Host |
| + if boundUnit == "backing" { |
| + boundUnit = "carlos-app@" + host + ".service" |
| + } |
| for _, p := range []string{provision.InstanceDir("/data", host), provision.InstanceRunDir(host), "/run/carlos-control/exe", "/run/carlos/logs"} { |
| if err := os.MkdirAll(p, 0755); err != nil { |
| t.Fatal(err) |
| @@ -108,7 +121,16 @@ func TestPlan14InstalledClaimLaunchRootRunner(t *testing.T) { |
| } |
| lines = append(lines, line) |
| } |
| + unit := instancectl.UnitName(instancectl.RoleRun, host) |
| unitPath := "/run/systemd/system/carlos-instance@.service" |
| + if boundUnit != "" { |
| + unit = boundUnit |
| + unitPath = filepath.Join("/run/systemd/system", unit) |
| + for i := range lines { |
| + lines[i] = strings.ReplaceAll(strings.ReplaceAll(lines[i], "%i", provision.InstanceID(host)), "%I", provision.InstanceID(host)) |
| + } |
| + 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) |
| @@ -117,7 +139,6 @@ func TestPlan14InstalledClaimLaunchRootRunner(t *testing.T) { |
| t.Fatal(err) |
| } |
| out.Close() |
| - unit := instancectl.UnitName(instancectl.RoleRun, host) |
| t.Cleanup(func() { |
| ctl(context.Background(), "stop", unit) |
| ctl(context.Background(), "stop", instancectl.SliceName(host)) |
| @@ -185,11 +206,31 @@ func TestPlan14InstalledClaimLaunchRootRunner(t *testing.T) { |
| if err := f.root.Guard.EstablishRoot(t.Context(), old, func(*hostclaim.Claim) error { return nil }); err != nil { |
| t.Fatal(err) |
| } |
| + if boundUnit != "" { |
| + bindings, err := rootstate.Open(filepath.Join(dir, "unit-bindings"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer bindings.Close() |
| + shape := claimUnitShape{Host: host, Account: old.Account, App: old.App, Kind: "instance", Unit: boundUnit, Socket: provision.NestedSocket(host), DBPath: provision.NestedDBPath("/data", host)} |
| + if strings.HasPrefix(boundUnit, "carlos-app@") { |
| + shape.Backing = "unit" |
| + } |
| + f.root.unitBindings = &claimUnitBindings{state: bindings, installation: func(context.Context, string) (hostguard.RootInstallation, error) { return old, nil }, canonical: func(context.Context, string) (claimUnitShape, error) { return shape, nil }} |
| + if _, err := f.root.unitBindings.establish(t.Context(), host); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| f.root.Clock = execlease.SystemClock{} |
| f.root.DataRoot = "/data" |
| f.root.ReadyBudget = 10 * time.Second |
| f.root.ReadyPoll = 20 * time.Millisecond |
| - f.root.Units = instancectl.ClaimUnits{Systemctl: ctl, CgroupRoot: "/sys/fs/cgroup", ExeRoot: "/run/carlos-control/exe", UnitRoot: "/run/systemd/system", DataRoot: "/data", WaitBudget: 20 * time.Second} |
| + f.root.Units = instancectl.ClaimUnits{BoundUnit: boundUnit, BoundHost: func() string { |
| + if boundUnit != "" { |
| + return host |
| + } |
| + return "" |
| + }(), Systemctl: ctl, CgroupRoot: "/sys/fs/cgroup", ExeRoot: "/run/carlos-control/exe", UnitRoot: "/run/systemd/system", DataRoot: "/data", WaitBudget: 20 * time.Second} |
| f.root.Kernel = linuxClaimLaunchKernel{OptRoot: "/opt/carlos", CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc"} |
| store := release.NewDirStore(filepath.Join(dir, "deployment")) |
| rec.Schema = 1 |
diff --git a/internal/host/plan14_unit_binding_root_linux_test.go b/internal/host/plan14_unit_binding_root_linux_test.go| new file mode 100644 |
| index 000000000..683481f53 |
| --- /dev/null |
| +++ b/internal/host/plan14_unit_binding_root_linux_test.go |
| @@ -0,0 +1,92 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "encoding/json" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "syscall" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/rootstate" |
| +) |
| + |
| +func TestPlan14UnitBindingRootRunner(t *testing.T) { |
| + dir := generationRootRunnerDir(t) |
| + b, in, shape := plan14UnitBindingFixture(t, "plan14-binding.service", "", "") |
| + config := filepath.Join(dir, "units.json") |
| + raw, _ := json.Marshal(struct { |
| + Schema int |
| + Units []claimUnitShape |
| + }{1, []claimUnitShape{shape}}) |
| + if err := os.WriteFile(config, raw, 0644); err != nil { |
| + t.Fatal(err) |
| + } |
| + store, err := rootstate.Open(filepath.Join(dir, "bindings"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer store.Close() |
| + b.state = store |
| + b.canonical = func(ctx context.Context, host string) (claimUnitShape, error) { |
| + all, err := readClaimUnitShapes(config) |
| + if err != nil { |
| + return claimUnitShape{}, err |
| + } |
| + s, ok := all[host] |
| + if !ok { |
| + return s, os.ErrNotExist |
| + } |
| + return s, nil |
| + } |
| + bound, err := b.establish(t.Context(), in.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + row := shape.route() |
| + if _, err := b.check(t.Context(), in.Host, &row); err != nil { |
| + t.Fatal(err) |
| + } |
| + child := exec.Command("/bin/sh", "-c", `printf '{}' > "$1"`, "binding-writer", config) |
| + child.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: 65534, Gid: 65534}} |
| + if err := child.Run(); err == nil { |
| + t.Fatal("service credential replaced root config") |
| + } |
| + if _, err := b.check(t.Context(), in.Host, &row); err != nil { |
| + t.Fatal("hostile writer changed binding", err) |
| + } |
| + if err := os.Chmod(config, 0666); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := b.check(t.Context(), in.Host, &row); err == nil { |
| + t.Fatal("service-writable canonical source accepted") |
| + } |
| + if err := os.Chmod(config, 0644); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Chown(config, 65534, 65534); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := b.check(t.Context(), in.Host, &row); err == nil { |
| + t.Fatal("service-owned canonical source accepted") |
| + } |
| + if err := os.Chown(config, 0, 0); err != nil { |
| + t.Fatal(err) |
| + } |
| + successor := in |
| + successor.Stamp++ |
| + b.installation = func(context.Context, string) (hostguard.RootInstallation, error) { return successor, nil } |
| + if _, err := b.check(t.Context(), in.Host, &row); err == nil { |
| + t.Fatal("successor reused binding") |
| + } |
| + if err := b.retire(t.Context(), bound, func(claimUnitBinding) error { return nil }); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := b.establish(t.Context(), in.Host); err == nil { |
| + t.Fatal("retirement recreated authority") |
| + } |
| +} |
diff --git a/internal/host/rootgeneration_containment_linux.go b/internal/host/rootgeneration_containment_linux.go| index a7f098ad5..c22b8fad8 100644 |
| --- a/internal/host/rootgeneration_containment_linux.go |
| +++ b/internal/host/rootgeneration_containment_linux.go |
| @@ -9,7 +9,6 @@ import ( |
| "strconv" |
| "strings" |
| - "github.com/carlosframework/platform/internal/instancectl" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootstate" |
| "golang.org/x/sys/unix" |
| @@ -74,7 +73,7 @@ func (k generationKernel) containedBinding(ctx context.Context, path string, r r |
| } |
| b, p := r.Binding, r.Process |
| boot, err := k.boot() |
| - if err != nil || boot != b.Target.BootID || p.Group != instancectl.ClaimUnitControlGroup(b.Binding.Host) { |
| + if err != nil || boot != b.Target.BootID || p.Group != generationBindingGroup(b) { |
| return os.ErrPermission |
| } |
| var group unix.Stat_t |
diff --git a/internal/host/rootgeneration_inventory.go b/internal/host/rootgeneration_inventory.go| index b635f33ad..71147c296 100644 |
| --- a/internal/host/rootgeneration_inventory.go |
| +++ b/internal/host/rootgeneration_inventory.go |
| @@ -10,7 +10,6 @@ import ( |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostguard" |
| - "github.com/carlosframework/platform/internal/instancectl" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootparticipation" |
| @@ -118,7 +117,7 @@ func (v generationReconciler) reconcile(ctx context.Context, records []release.G |
| if err != nil { |
| return zero, err |
| } |
| - if matched[i] || b.Binding.Host != in.Host || b.Binding.Account != in.Account || b.Binding.App != in.App || b.Record != in.Record || b.Installation != rootHistoryInstallation(in) || b.ControllerPID != launch.ControllerPID || b.ControllerStart != launch.ProcessStart || b.Inputs != unit.Inputs || b.Invocation != unit.InvocationID || b.Executable.Device != unit.Executable.Device || b.Executable.Inode != unit.Executable.Inode || unit.Group.RelativePath != instancectl.ClaimUnitControlGroup(t.Host) { |
| + if matched[i] || b.Binding.Host != in.Host || b.Binding.Account != in.Account || b.Binding.App != in.App || b.Record != in.Record || b.Installation != rootHistoryInstallation(in) || b.ControllerPID != launch.ControllerPID || b.ControllerStart != launch.ProcessStart || b.Inputs != unit.Inputs || b.Invocation != unit.InvocationID || b.Executable.Device != unit.Executable.Device || b.Executable.Inode != unit.Executable.Inode || unit.Group.RelativePath != generationBindingGroup(b) { |
| return zero, os.ErrPermission |
| } |
| if unit.State != hostbox.UnitIntended && unit.State != hostbox.UnitStarted && unit.State != hostbox.UnitSettled { |
diff --git a/internal/host/rootgeneration_kernel_linux.go b/internal/host/rootgeneration_kernel_linux.go| index 5bcbb0fae..cfb22640a 100644 |
| --- a/internal/host/rootgeneration_kernel_linux.go |
| +++ b/internal/host/rootgeneration_kernel_linux.go |
| @@ -16,7 +16,6 @@ import ( |
| "strings" |
| "github.com/carlosframework/platform/internal/hostbox" |
| - "github.com/carlosframework/platform/internal/instancectl" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootstate" |
| "golang.org/x/sys/unix" |
| @@ -530,7 +529,7 @@ func productionGenerationAbsence(ctx context.Context, r release.GenerationReserv |
| } |
| func generationGroupGone(ctx context.Context, root string, r release.GenerationReservation) error { |
| - group := instancectl.ClaimUnitControlGroup(r.Binding.Binding.Host) |
| + group := generationBindingGroup(r.Binding) |
| path := filepath.Join(root, group) |
| if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) { |
| return nil |
diff --git a/internal/host/rootgeneration_retirement.go b/internal/host/rootgeneration_retirement.go| index 2868c794e..3ca90ac9e 100644 |
| --- a/internal/host/rootgeneration_retirement.go |
| +++ b/internal/host/rootgeneration_retirement.go |
| @@ -6,7 +6,6 @@ import ( |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostguard" |
| - "github.com/carlosframework/platform/internal/instancectl" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootstate" |
| ) |
| @@ -56,7 +55,7 @@ func (v generationRetirement) closure(ctx context.Context, r release.GenerationR |
| if unit.ID != b.Attempt { |
| continue |
| } |
| - if !hostbox.IsUnitReservation(unit) || (unit.State != hostbox.UnitIntended && unit.State != hostbox.UnitStarted && unit.State != hostbox.UnitSettled) || !hostbox.SameLaunchOwner(unit.Target, target) || unit.Inputs != b.Inputs || unit.InvocationID != b.Invocation || unit.Executable.Device != b.Executable.Device || unit.Executable.Inode != b.Executable.Inode || unit.Group.RelativePath != instancectl.ClaimUnitControlGroup(t.Host) { |
| + if !hostbox.IsUnitReservation(unit) || (unit.State != hostbox.UnitIntended && unit.State != hostbox.UnitStarted && unit.State != hostbox.UnitSettled) || !hostbox.SameLaunchOwner(unit.Target, target) || unit.Inputs != b.Inputs || unit.InvocationID != b.Invocation || unit.Executable.Device != b.Executable.Device || unit.Executable.Inode != b.Executable.Inode || unit.Group.RelativePath != generationBindingGroup(b) { |
| return false, os.ErrPermission |
| } |
| p := r.Process |
diff --git a/internal/host/rootgenerations_launch.go b/internal/host/rootgenerations_launch.go| index 9761c5b0b..62ca32720 100644 |
| --- a/internal/host/rootgenerations_launch.go |
| +++ b/internal/host/rootgenerations_launch.go |
| @@ -13,6 +13,7 @@ import ( |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/instancectl" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootstate" |
| @@ -113,6 +114,13 @@ func (s *rootGenerationSource) resolve(ctx context.Context, h *ClaimLaunchRoot, |
| Installation: rootHistoryInstallation(in), Record: in.Record, RecordETag: etag, |
| Attempt: q.Reservation.ID, Inputs: q.Reservation.Inputs, Invocation: q.Reservation.InvocationID, ControllerPID: peer.PID, ControllerStart: peer.ProcessStart, |
| Version: version, Digest: q.Spec.Digest, Path: q.Executable, Tree: tree, Executable: id, TreeIdentity: treeID} |
| + unit, group, err := claimLaunchPlacement(h.Units, t.Host) |
| + if err != nil || group != q.Reservation.Group.RelativePath { |
| + return release.GenerationBinding{}, os.ErrPermission |
| + } |
| + if unit != instancectl.UnitName(instancectl.RoleRun, t.Host) { |
| + b.Unit = unit |
| + } |
| return b, nil |
| } |
diff --git a/internal/instancectl/claimexec.go b/internal/instancectl/claimexec.go| index 567ba9f0c..6ff0c6992 100644 |
| --- a/internal/instancectl/claimexec.go |
| +++ b/internal/instancectl/claimexec.go |
| @@ -10,6 +10,7 @@ import ( |
| "path/filepath" |
| "time" |
| + "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/safefile" |
| ) |
| @@ -75,7 +76,11 @@ func (u ClaimUnits) prepareExecGate(ctx context.Context, host string, p ClaimExe |
| if err != nil { |
| return fail(err) |
| } |
| - body = append(body, []byte("BindReadOnlyPaths="+socket+":"+ClaimExecSocket+"\nBindReadOnlyPaths="+filepath.Join(source, "permit.json")+":"+ClaimExecSnapshot+"\nBindReadOnlyPaths=/proc/sys/kernel/random/boot_id:"+ClaimExecBootID+"\nExecStart=\nExecStart="+SystemBinary+" instance-exec -claim-reservation="+ClaimExecSnapshot+" run %I\n")...) |
| + instance := "%I" |
| + if u.BoundUnit != "" { |
| + instance = provision.InstanceID(host) |
| + } |
| + body = append(body, []byte("BindReadOnlyPaths="+socket+":"+ClaimExecSocket+"\nBindReadOnlyPaths="+filepath.Join(source, "permit.json")+":"+ClaimExecSnapshot+"\nBindReadOnlyPaths=/proc/sys/kernel/random/boot_id:"+ClaimExecBootID+"\nExecStart=\nExecStart="+SystemBinary+" instance-exec -claim-reservation="+ClaimExecSnapshot+" run "+instance+"\n")...) |
| if err = safefile.WriteFile(filepath.Join(dropin, "60-claim-launch.conf"), body, 0644); err != nil { |
| return fail(err) |
| } |
diff --git a/internal/instancectl/claimlaunch_snapshot.go b/internal/instancectl/claimlaunch_snapshot.go| index 9d50aa481..9a7f7f187 100644 |
| --- a/internal/instancectl/claimlaunch_snapshot.go |
| +++ b/internal/instancectl/claimlaunch_snapshot.go |
| @@ -25,7 +25,11 @@ func (u ClaimUnits) launchPaths(host string) (string, string, error) { |
| if !provision.ValidHostShape(host) { |
| return "", "", ErrClaimUnit |
| } |
| - return filepath.Join(u.ExeRoot, ".claim", provision.InstanceID(host)), filepath.Join(u.UnitRoot, UnitName(RoleRun, host)+".d"), nil |
| + unit, _, err := u.Placement(host) |
| + if err != nil { |
| + return "", "", err |
| + } |
| + return filepath.Join(u.ExeRoot, ".claim", provision.InstanceID(host)), filepath.Join(u.UnitRoot, unit+".d"), nil |
| } |
| func (u ClaimUnits) freezeLaunch(ctx context.Context, host string, launch Launch) error { |
diff --git a/internal/instancectl/claimunit.go b/internal/instancectl/claimunit.go| index aa6832f46..9e0ec697e 100644 |
| --- a/internal/instancectl/claimunit.go |
| +++ b/internal/instancectl/claimunit.go |
| @@ -23,11 +23,12 @@ import ( |
| // idle. Root calls it only after validating the reservation under the host |
| // guard; it adds no request surface of its own. |
| type ClaimUnits struct { |
| - Systemctl func(context.Context, ...string) (string, error) |
| - CgroupRoot string |
| - ExeRoot string |
| - UnitRoot string |
| - DataRoot string |
| + BoundHost, BoundUnit string |
| + Systemctl func(context.Context, ...string) (string, error) |
| + CgroupRoot string |
| + ExeRoot string |
| + UnitRoot string |
| + DataRoot string |
| // Room is the disk floor check; nil skips it, as a nil Statfs does. |
| Room func() error |
| WaitBudget time.Duration |
| @@ -94,7 +95,11 @@ func (u ClaimUnits) show(ctx context.Context, unit string, props ...string) (map |
| // cgroupEmpty reads the unit's cgroup.events: an absent group is empty. |
| func (u ClaimUnits) cgroupEmpty(host string) error { |
| - events := filepath.Join(u.CgroupRoot, parentSlices, SliceName(host), UnitName(RoleRun, host), "cgroup.events") |
| + _, group, err := u.Placement(host) |
| + if err != nil { |
| + return err |
| + } |
| + events := filepath.Join(u.CgroupRoot, group, "cgroup.events") |
| b, err := os.ReadFile(events) |
| if errors.Is(err, os.ErrNotExist) { |
| return nil |
| @@ -133,7 +138,13 @@ func (u ClaimUnits) start(ctx context.Context, in ClaimUnit, launch *Launch, che |
| !filepath.IsAbs(in.Executable) || filepath.Clean(in.Executable) != in.Executable || in.Executable == SystemBinary || strings.HasPrefix(in.Executable, LiveDir+"/") { |
| return ClaimUnitStarted{}, ErrClaimUnit |
| } |
| - unit := UnitName(RoleRun, in.Host) |
| + unit, group, err := u.Placement(in.Host) |
| + if err != nil { |
| + return ClaimUnitStarted{}, err |
| + } |
| + if u.BoundUnit != "" && (permit == nil || launch == nil || check == nil) { |
| + return ClaimUnitStarted{}, ErrClaimUnit |
| + } |
| before, err := u.show(ctx, unit, "ActiveState", "InvocationID") |
| if err != nil { |
| return ClaimUnitStarted{}, err |
| @@ -155,9 +166,13 @@ func (u ClaimUnits) start(ctx context.Context, in ClaimUnit, launch *Launch, che |
| if err := sliceTier(ctx, u.Systemctl, in.Host, tier); err != nil { |
| return ClaimUnitStarted{}, err |
| } |
| + var invocation string |
| refuse := func(cause error) (ClaimUnitStarted, error) { |
| cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), u.budget()) |
| defer cancel() |
| + if u.BoundUnit != "" { |
| + return ClaimUnitStarted{}, errors.Join(cause, u.StopInvocation(cleanup, in.Host, invocation)) |
| + } |
| return ClaimUnitStarted{}, errors.Join(cause, u.Stop(cleanup, in.Host)) |
| } |
| if launch != nil { |
| @@ -183,7 +198,6 @@ func (u ClaimUnits) start(ctx context.Context, in ClaimUnit, launch *Launch, che |
| } |
| ready, cancel := context.WithTimeout(ctx, u.budget()) |
| defer cancel() |
| - var invocation string |
| for { |
| after, err := u.show(ready, unit, "InvocationID", "MainPID", "ControlGroup", "ActiveState") |
| if err != nil { |
| @@ -198,10 +212,10 @@ func (u ClaimUnits) start(ctx context.Context, in ClaimUnit, launch *Launch, che |
| } |
| pid, _ := strconv.Atoi(after["MainPID"]) |
| if id != "" && pid > 0 && after["ActiveState"] == "active" { |
| - if after["ControlGroup"] != "/"+ClaimUnitControlGroup(in.Host) { |
| + if after["ControlGroup"] != "/"+group { |
| return refuse(ErrClaimUnit) |
| } |
| - started := ClaimUnitStarted{Unit: unit, InvocationID: id, ControlGroup: ClaimUnitControlGroup(in.Host), MainPID: pid} |
| + started := ClaimUnitStarted{Unit: unit, InvocationID: id, ControlGroup: group, MainPID: pid} |
| if gate != nil { |
| if err := gate.authorize(ready, u, started, check); err != nil { |
| cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), u.budget()) |
| @@ -229,7 +243,10 @@ func (u ClaimUnits) Stop(ctx context.Context, host string) error { |
| if u.Systemctl == nil || u.CgroupRoot == "" || !provision.ValidHostShape(host) { |
| return ErrClaimUnit |
| } |
| - unit := UnitName(RoleRun, host) |
| + unit, _, err := u.Placement(host) |
| + if err != nil { |
| + return err |
| + } |
| if out, err := u.Systemctl(ctx, "stop", unit); err != nil { |
| return fmt.Errorf("stop %s: %s: %w", unit, strings.TrimSpace(out), err) |
| } |
| @@ -259,11 +276,15 @@ func (u ClaimUnits) Gone(ctx context.Context, host, invocation string) error { |
| if u.Systemctl == nil || u.CgroupRoot == "" || !provision.ValidHostShape(host) { |
| return ErrClaimUnit |
| } |
| - state, err := u.show(ctx, UnitName(RoleRun, host), "ActiveState", "InvocationID") |
| + unit, _, err := u.Placement(host) |
| if err != nil { |
| return err |
| } |
| - if invocation != "" && state["InvocationID"] != "" && state["InvocationID"] != invocation { |
| + state, err := u.show(ctx, unit, "ActiveState", "InvocationID") |
| + if err != nil { |
| + return err |
| + } |
| + if u.BoundUnit == "" && invocation != "" && state["InvocationID"] != "" && state["InvocationID"] != invocation { |
| return nil |
| } |
| if !idleState(state["ActiveState"]) { |
| @@ -295,6 +316,12 @@ func (u ClaimUnits) Same(ctx context.Context, s ClaimUnitStarted) error { |
| if u.Systemctl == nil || s.Unit == "" || s.InvocationID == "" || s.MainPID <= 0 { |
| return ErrClaimUnit |
| } |
| + if u.BoundUnit != "" { |
| + unit, group, err := u.Placement(u.BoundHost) |
| + if err != nil || s.Unit != unit || s.ControlGroup != group { |
| + return ErrClaimUnit |
| + } |
| + } |
| state, err := u.show(ctx, s.Unit, "InvocationID", "MainPID", "ControlGroup", "ActiveState") |
| if err != nil { |
| return err |
diff --git a/internal/instancectl/claimunit_bound.go b/internal/instancectl/claimunit_bound.go| new file mode 100644 |
| index 000000000..ec708f30c |
| --- /dev/null |
| +++ b/internal/instancectl/claimunit_bound.go |
| @@ -0,0 +1,32 @@ |
| +package instancectl |
| + |
| +import ( |
| + "path" |
| + "strings" |
| + |
| + "github.com/carlosframework/platform/internal/provision" |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +func BoundClaimUnitControlGroup(host, unit string) string { |
| + return path.Join(parentSlices, SliceName(host), unit) |
| +} |
| + |
| +func (u ClaimUnits) Placement(host string) (string, string, error) { |
| + if !provision.ValidHostShape(host) { |
| + return "", "", ErrClaimUnit |
| + } |
| + if u.BoundHost == "" && u.BoundUnit == "" { |
| + return UnitName(RoleRun, host), ClaimUnitControlGroup(host), nil |
| + } |
| + if u.BoundHost != host || !ValidBoundClaimUnit(u.BoundUnit) { |
| + return "", "", ErrClaimUnit |
| + } |
| + return u.BoundUnit, BoundClaimUnitControlGroup(host, u.BoundUnit), nil |
| +} |
| + |
| +func ValidBoundClaimUnit(unit string) bool { |
| + return registry.ValidUnitName(unit) && strings.HasSuffix(unit, ".service") && |
| + !strings.HasPrefix(unit, "-") && !strings.HasSuffix(unit, "@.service") && |
| + !strings.HasPrefix(unit, "carlos-instance") && !strings.ContainsAny(unit, "%\\ \t\n\r\x00/") |
| +} |
diff --git a/internal/instancectl/claimunit_bound_stop.go b/internal/instancectl/claimunit_bound_stop.go| new file mode 100644 |
| index 000000000..5fa33820b |
| --- /dev/null |
| +++ b/internal/instancectl/claimunit_bound_stop.go |
| @@ -0,0 +1,21 @@ |
| +package instancectl |
| + |
| +import "context" |
| + |
| +func (u ClaimUnits) StopInvocation(ctx context.Context, host, invocation string) error { |
| + if u.BoundUnit == "" && u.BoundHost == "" { |
| + return u.Stop(ctx, host) |
| + } |
| + unit, group, err := u.Placement(host) |
| + if err != nil || invocation == "" || u.Systemctl == nil { |
| + return ErrClaimUnit |
| + } |
| + state, err := u.show(ctx, unit, "ActiveState", "InvocationID", "ControlGroup") |
| + if err != nil { |
| + return err |
| + } |
| + if state["InvocationID"] != invocation || state["ControlGroup"] != "/"+group { |
| + return ErrClaimUnitBusy |
| + } |
| + return u.Stop(ctx, host) |
| +} |
diff --git a/internal/instancectl/claimunit_bound_test.go b/internal/instancectl/claimunit_bound_test.go| new file mode 100644 |
| index 000000000..d85ed4670 |
| --- /dev/null |
| +++ b/internal/instancectl/claimunit_bound_test.go |
| @@ -0,0 +1,91 @@ |
| +package instancectl |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +func TestPlan14BoundUnitPlacement(t *testing.T) { |
| + for _, name := range []string{"console.service", "woodstar-atbridge.service", "carlos-app@shop.example.service"} { |
| + t.Run(name, func(t *testing.T) { |
| + u, ctl, in, _ := claimUnitFixture(t) |
| + u.BoundHost, u.BoundUnit = in.Host, name |
| + ctl.group = "/" + BoundClaimUnitControlGroup(in.Host, name) |
| + ctl.events = filepath.Join(u.CgroupRoot, strings.TrimPrefix(ctl.group, "/")) |
| + if err := os.MkdirAll(ctl.events, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + ctl.setPopulated(false) |
| + // A bound unit must never be started by the unreserved entrypoint. |
| + if _, err := u.Start(t.Context(), in); err == nil { |
| + t.Fatal("unreserved named launch") |
| + } |
| + if strings.Contains(ctl.joined(), "start ") { |
| + t.Fatal(ctl.joined()) |
| + } |
| + if err := u.Gone(t.Context(), in.Host, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + ctl.state, ctl.invocation = "active", "replacement" |
| + ctl.setPopulated(true) |
| + if err := u.Gone(t.Context(), in.Host, "old"); !errors.Is(err, ErrClaimUnitBusy) { |
| + t.Fatalf("replacement with populated tree accepted: %v", err) |
| + } |
| + ctl.state = "inactive" |
| + if err := u.Gone(t.Context(), in.Host, "old"); !errors.Is(err, ErrClaimUnitBusy) { |
| + t.Fatalf("descendants accepted: %v", err) |
| + } |
| + ctl.setPopulated(false) |
| + if err := u.Gone(t.Context(), in.Host, "old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + before := ctl.joined() |
| + if err := u.Stop(context.Background(), "sibling.example"); err == nil { |
| + t.Fatal("binding crossed hosts") |
| + } |
| + if ctl.joined() != before { |
| + t.Fatal("foreign host reached systemd") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestPlan14BoundUnitNames(t *testing.T) { |
| + for _, name := range []string{"../console.service", "console.service/child", "x\n.service", "x@.service", "x.socket", "--x.service", "carlos-instance-control.service"} { |
| + u, ctl, in, _ := claimUnitFixture(t) |
| + u.BoundHost, u.BoundUnit = in.Host, name |
| + if err := u.Gone(t.Context(), in.Host, ""); err == nil { |
| + t.Fatalf("accepted %q", name) |
| + } |
| + if ctl.joined() != "" { |
| + t.Fatal("invalid binding reached systemd") |
| + } |
| + } |
| +} |
| + |
| +func TestPlan14BoundUnitExactStop(t *testing.T) { |
| + u, ctl, in, _ := claimUnitFixture(t) |
| + u.BoundHost, u.BoundUnit = in.Host, "console.service" |
| + ctl.group = "/" + BoundClaimUnitControlGroup(in.Host, u.BoundUnit) |
| + ctl.invocation, ctl.state = "replacement", "active" |
| + if err := u.StopInvocation(t.Context(), in.Host, "old"); err == nil { |
| + t.Fatal("stopped replacement") |
| + } |
| + if strings.Contains(ctl.joined(), "stop ") { |
| + t.Fatal(ctl.joined()) |
| + } |
| + if err := u.StopInvocation(t.Context(), in.Host, ""); err == nil { |
| + t.Fatal("stopped unknown invocation") |
| + } |
| + ctl.invocation = "old" |
| + if err := u.StopInvocation(t.Context(), in.Host, "old"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !strings.Contains(ctl.joined(), "stop console.service") { |
| + t.Fatal(ctl.joined()) |
| + } |
| +} |
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go| index 9eb2816aa..36244a77c 100644 |
| --- a/internal/provision/plan14_e_write_census_test.go |
| +++ b/internal/provision/plan14_e_write_census_test.go |
| @@ -6,8 +6,8 @@ var plan14EWriteCensusOverlay = []censusEntry{ |
| {"internal/hostbox/journalservice.go", "type JournalRequest", "key-site", "key:RecordKey", "0da545d1fabfb3f49b57c6d997c90e59b9e1103ee2a0c6b0148f476b2620b856"}, |
| {"internal/host/rootgeneration_requirements.go", "generationLocalObligations", "key-site", "key:RecordKey", "7bf08b3afdf12e81188123e0257ef90cea91091a8ab3bfb975e9a0200619dd1b"}, |
| {"internal/host/rootgeneration_requirements.go", "generationRequiredInventory", "key-site", "key:RecordKey", "bbcbc143061ea6990ec8bc4e34f1a52a07e82039ea44340497d5bc6aab067913"}, |
| - {"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "9f400d6ea9358dbc0cf8468d4818bb87fc4c3d85fc83876904a5c764979439b8"}, |
| - {"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "d69a3c0c86b03d5469708ac8bf084f4f516c0b61a18519d46cc3c0a0616f5381"}, |
| + {"internal/host/rootgeneration_inventory.go", "generationReconciler.reconcile", "key-site", "key:RecordKey", "1e668bbef6945deb4f0f3a988b5e1a1ecf44e9b85d838c2e01f6a5052b75bbed"}, |
| + {"internal/host/rootgenerations_launch.go", "rootGenerationSource.resolve", "key-site", "key:RecordKey", "2c05d8bfe16ac42bdc612c0008e2ebf584d3ab5970547a02eb78a15fa1b5449c"}, |
| {"internal/historyreadback/workflow.go", "Operation.deliverGrant", "nonrecord", "sel:Put", "fb410ef6274ef7de0997a804c837ad1ecf29d8826ba1c4dcda00c2ae986fc434"}, |
| {"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "4f083cb29dd1bd8f7d2eeed20f30c9b51387082ac8c8ab1745efbb8a0270ac43"}, |
| {"internal/host/rootinventory.go", "ClaimRootHTTP.recoverRootInventory", "key-site", "key:RecordKey", "79d1472b90bc7290e655bfe5e8857bad9fd0666c72b88364e89e4e198e60f535"}, |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index 347961812..ec24b90d7 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-e.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-e.tsv |
| @@ -54,8 +54,8 @@ 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 3e511e1f76238fe1abf1189f47786f60a6fc0eb6c52ef00df85bf0731483e2c5 |
| -@coverage internal/release 6517cdba66a04ddf585b6bceb510402516a9bbb6ef37922b8d616c52ae781edf |
| +@coverage internal/host f7295d9eaa466a1b8ca6daf9ec5176b604538815739fec2b12d0fe20b87b4f8b |
| +@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 |
| internal/host/legacyrestart.go restartEffectFailed authority B3 E8 merge ruling 5: optional identity failures preserve the same restart outcome; no second restart; real not-up takes main rollback. Fixtures: TestPlan14ERecoveryRestartOutcome, TestPlan14ERecoveryBindingRefusalKeepsPromotedFix, TestPlan14ERecoveryNotUpPrecedesBinding. b68bb65884a14fe0cf78a65f56ded4ea41584bba99af9195827d1ea191b340a9 |
| @@ -185,18 +185,18 @@ internal/historyreadback/readback.go RequestStatus authority E1 E17: operator sa |
| @coverage . dcc873ca955a35bf69ae09641d50576790123ec17aaf01637f0cfbbcf85530ce |
| @coverage internal/historyreadback eaca4d4caacf499db8a4c4e8044d565eec5bb42e0fc765b66f6c7559fbb04842 |
| internal/host/claimlaunch_production.go WithProductionClaimLaunch 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. 183f783e1998ffbe49fec813501dfcb9238ffae3b8a09dc545cf88d1b1609dd0 |
| -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. file:Read@2340,file:Read@2499,file:Read@707 ab5d6ab6c237ed569904210a69ce0f2bdef564bbd8ff1a65f427c503f52abb48 |
| +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. file:Read@2813 f24033673d1d9b8c36304c204a271a7729e94b0e61620f3df6ea1f80537da4c2 |
| -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. 14f50101580ce4b094e0a321db3db200994da9c6708745c2f7b3a8fd3409adfd |
| +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. bf0bd36504cca10325b1f10a6f1ed094bbaccfc1a0bf4391e006af53ae2a9f94 |
| 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 |
| internal/host/rootgenerations_launch.go rootGenerationSource.reserve 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. c4269e65bcac751cd1a51a439e48eda77b055320554b3489d605fea47fa7f3a8 |
| -internal/host/rootgenerations_launch.go rootGenerationSource.resolve authority E2 E20: read the atomic root installation under the existing host exclusion without recursive lock acquisition; retain fresh claim, canonical record, signed artifact and final reservation checks. Fixtures: TestPlan14GenerationLaunchDoesNotReenterRootLock, TestPlan14GenerationLaunchRefusals, TestPlan14RootDispatchRejectsRegistryMutation. file:Open@2501 d69a3c0c86b03d5469708ac8bf084f4f516c0b61a18519d46cc3c0a0616f5381 |
| +internal/host/rootgenerations_launch.go rootGenerationSource.resolve authority E2 E20: read the atomic root installation under the existing host exclusion without recursive lock acquisition; retain fresh claim, canonical record, signed artifact and final reservation checks. Fixtures: TestPlan14GenerationLaunchDoesNotReenterRootLock, TestPlan14GenerationLaunchRefusals, TestPlan14RootDispatchRejectsRegistryMutation. 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:Open@2501 2c05d8bfe16ac42bdc612c0008e2ebf584d3ab5970547a02eb78a15fa1b5449c |
| internal/host/rootgenerations_launch.go type generationLaunchPermit 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. 52b30a79d7ced7875f435eff76d3c924ff4e460d3a2360cda5a9f12c407a2442 |
| internal/host/rootgenerations_launch.go type rootGenerationSource 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. 59a336027ef7750d36f1a36a27cf3d0449395493c05789291c4ed9bc16724e5f |
| -internal/release/rootgenerations.go GenerationBinding.valid 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. 636d5eb63f80ec3f3f2b87b859c96ce09700948ea3f3a5d58f04e5b1cf5ac3d3 |
| +internal/release/rootgenerations.go GenerationBinding.valid 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 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. deb92d925b7291f15f42a2ae7856fafb79258ea42ba8faaee4cb3c07d2dabc52 |
| internal/release/rootgenerations.go GenerationProcess.valid 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. f5790b759daeea8f3dee287a84f2061d72927ebb5a48ea303917392061e180ea |
| internal/release/rootgenerations.go RootGenerations.Check authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@78 8ee72f46a5a45879f0625b90e11057d758b73586e5913067d2cd9493705fff83 |
| internal/release/rootgenerations.go RootGenerations.Read 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:Read@206,file:Read@26 d77a3e6d7f8bdc2c16973910b3089aa7fb7c3c40b2d0b94f2ca7cdf32f9463b6 |
| @@ -205,7 +205,7 @@ internal/release/rootgenerations.go RootGenerations.Started authority E2 E20: ex |
| internal/release/rootgenerations.go RootGenerations.write authority E2 E23: versioned generation completeness binds exact reservations to independently supplied installation, launch and kernel evidence. Reservation mutations invalidate before commit; every destructive read reconciles again and retries sync. Production reconciliation remains unavailable. Fixtures: TestPlan14GenerationCompleteness, TestPlan14GenerationCompletenessInvalidation, TestPlan14GenerationCompletenessDurability, TestPlan14GenerationCompletenessCertificateSyncFailure, TestPlan14GenerationCompletenessPinConsumer, TestPlan14GenerationCompletenessProcessCrash; TestPlan14GenerationCompletenessCheckedRootStore passed on the E25 root runner. 3b7ee54d0f46efb1507f9a5f421b979047d8c0eb654ac5f31b1c3c42f67763c3 |
| internal/release/rootgenerations.go const RootGenerationsDir 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. e235f24f29b1f8511349cae97fbbc3f3472b49fdc6781c781d5b6f93ff3d45ff |
| internal/release/rootgenerations.go generationKey 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. cdd7e0d8cdff508772ab54e421a4ab914dbb15ea5d97847f35a93544f0a9259a |
| -internal/release/rootgenerations.go type GenerationBinding 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. 9bd76e1f6ef4d809df1a59719a1880215c910b0cab78449ad8cd79a678da401a |
| +internal/release/rootgenerations.go type GenerationBinding 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 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. bae2ebf9e17d26594533f6240b92355abbd74db11d6e4230fafb994912f35ec3 |
| internal/release/rootgenerations.go type GenerationOwner 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. 6a6c3576622656c2bc4b65075c8f5d70b29968203101a1916926adc48b20bce5 |
| internal/release/rootgenerations.go type GenerationProcess 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. 81994fee584ed8780e94cf1f8e471316a04154176c5d29cd96acbe0870daadd5 |
| internal/release/rootgenerations.go type GenerationReservation authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. 1917db4836b3b18b45ce6bca1a55f6d5a8082e5de26af1710d0da25db0809551 |
| @@ -228,7 +228,7 @@ internal/host/rootinstallation.go rootInstallationHostApp authority E2,J1 E20: r |
| internal/hostguard/rootview.go RootAuthority.Installation authority E2,J1 E20: read the atomic root installation under the existing host exclusion without recursive lock acquisition; retain fresh claim, canonical record, signed artifact and final reservation checks. Fixtures: TestPlan14GenerationLaunchDoesNotReenterRootLock, TestPlan14GenerationLaunchRefusals, TestPlan14RootDispatchRejectsRegistryMutation. d819256332950ec26c1b541f8be21a6e8cea839708fcbd0f7b8367d118574f92 |
| internal/release/rootgenerations.go RootGenerations.Retire authority E2 E20: exact reservation closure precedes retirement verification; retained tombstones forbid replay and require sync recommit. Retired records release only their own pins; completeness remains a separate mandatory verifier, still unavailable in production. Fixtures: TestPlan14GenerationRetirementReleasesExactPin, TestPlan14GenerationRetirementRefusals, TestPlan14GenerationClosedReplayRefused, TestPlan14GenerationRetirementProcessCrash, TestPlan14GenerationRetirementKeepsIndependentReference, TestPlan14GenerationClaimsOffRestageMatchesMain. file:Read@297 21bb00d11d6fd8474b4d0a556bd1af7e69dbf6bd75755daeb0f14481166f2a90 |
| @coverage internal/hostguard 470bdfe8e4e0b988846c3b2b56353ad48af545ead27b810721e1cf0e3c7c17ac |
| -internal/host/rootgeneration_kernel_linux.go generationGroupGone authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. file:Lstat@218 a4778a7342e51a58eb736c15463e75c93349690d1f07b0d840d201ae8f57ecc6 |
| +internal/host/rootgeneration_kernel_linux.go generationGroupGone authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. 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:Lstat@194 e2832d7383ea707b31e78d9d4487ccc565715c9be64e056ab1fe097d03073601 |
| internal/host/rootgeneration_kernel_linux.go generationIDRef authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. 949b4dddb141d7eafd4edc70b41f8dd337069f699ab30d86c46bc6fce986d40b |
| internal/host/rootgeneration_kernel_linux.go generationKernel.absent authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. file:Readlink@312 18269bf4b64e05138dff05e7894d4925bc24bcc012fa64a336723d981a9c52dd |
| internal/host/rootgeneration_kernel_linux.go generationKernel.boot authority E2 E21: bounded kernel reader retains executable, sibling, descriptor, mapping and mount references; checks controller boot/start and cgroup emptiness. Unknown nonroot tasks, foreign namespaces, missing kernel state or renamed trees refuse. Fixtures: TestPlan14GenerationKernelReferences, TestPlan14GenerationKernelMappingIdentity, TestPlan14GenerationKernelControllerIdentity, TestPlan14GenerationKernelRunningExecutable, TestPlan14GenerationKernelCgroup, TestPlan14GenerationKernelRetainedFiles, TestPlan14GenerationKernelSiblingAlias. Real proc executable tests are not full privileged namespace acceptance. c3cc84ed2af21ee0e57e91160561a3e4996e75990df2617a14865b084b176ced |
| @@ -301,13 +301,13 @@ internal/host/rootgeneration_census_other.go productionGenerationKernelCensus au |
| 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 |
| internal/host/rootgeneration_inventory.go generationInventoryDigest 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. 36ff5a31c6a013ac907d85e59b754cf4088df4600646ba7df177cf14e990177a |
| internal/host/rootgeneration_inventory.go generationOwner 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. 69f89153837761f13116937b330ffbf406891299d5580296ea4aade905aa02ce |
| -internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 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. 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. 9f400d6ea9358dbc0cf8468d4818bb87fc4c3d85fc83876904a5c764979439b8 |
| +internal/host/rootgeneration_inventory.go generationReconciler.reconcile authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 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. 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. 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. 1e668bbef6945deb4f0f3a988b5e1a1ecf44e9b85d838c2e01f6a5052b75bbed |
| internal/host/rootgeneration_inventory.go openGenerationLaunchInventory authority E2 E33: census-only generation journal reads accept private root-owned ancestry without chmod or repair. Every ancestor remains descriptor-checked, root-owned and non-writable by group/other; leaves remain single-link regular root-owned files, checked for DB/WAL/SHM. Public-file callers remain unchanged. Fixtures: TestPlan14AuthorityFilePrivateAncestors, TestPlan14AuthorityFileAbsoluteBoundary; TestPlan14GenerationPrivateJournalRootRunner and TestPlan14GenerationRequiredArtifactsRootRunner are runner-pending. Pre-flight 76, 78, 82 and 89. 2b54531e16282caeb0ddbc1dc92b18d6cb2db652589eb3757daefe10daa63a53 |
| internal/host/rootgeneration_inventory.go productionGenerationInventory authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. PROPOSED E46: local root installation obligations, exact root intention and inventory revision; service SQL is a deny-only consistency check. Executing identity: root instance-control or root participation/update. 9f093b0f02244e14e6726e51a7ad57fe83297646e36ee8817ed739e987c93cec |
| internal/host/rootgeneration_inventory.go type generationInventory authority E2 E38: reconcile completed root retirement against the retained stop receipt and exact inventory; inspect parked identities, require retired rows absent or an independently inventoried successor, and retain generation pins. Fixtures: TestPlan14GenerationParkedPayload, TestPlan14GenerationParkedCrash, TestPlan14GenerationParkedKeepsReservation, TestPlan14GenerationParkedReadback; TestPlan14GenerationParkedRootRunner is runner-pending. Timer producer matches retained entries once and preserves successors on replay: TestPlan14GenerationTimerRetirement. No claims-off path changes. 7a1e16370c99823e83e11e7824162082f85abf82e5dd5e18f029c82ab9e26f08 |
| internal/host/rootgeneration_inventory.go type generationReconciler 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. a979cdff1cd6311cf57d4a93e4501fb8dc2f65772d0013e622175b2a1f610d91 |
| internal/hostguard/generationinventory.go RootAuthority.GenerationInstallations authority E2 E37: retain independently initialized state-directory and timer requirements in the root installation transaction; preserve them through crash/reopen and rebind. Unknown history cannot certify completeness. Recommit under root-host exclusion before generation use. Canonical record and root retained obligations constrain agent candidate lists; timer enumeration is deny-only. Pre-flight 33, 80, 83. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationRegistryRows, TestPlan14RootTimerInventory, TestPlan14InventoryRequirementsRegistration, TestPlan14InventoryRequirementsUnknownAndRatchet, TestPlan14InventoryArtifactsCrash, TestPlan14InventoryRequirementsHistoryIdentity. Changed RequiredArtifacts and ExactRetirement root-runner witnesses await acceptance; remaining E2 launch/parked/containment integration is incomplete. 4219e7952cc7c83513f810bae72dbed60384f8bddfd3d121de92eb8b1f82e866 |
| -internal/host/rootgeneration_retirement.go generationRetirement.closure authority E2 E25: validate installation, controller, exact unit and process before retaining an active or prelaunch pin. Only closed writers plus a settled unit reach durable retirement and independent kernel absence. Use the checked read-only journal and continue past active pins. Fixtures: TestPlan14GenerationRetirementKeepsActivePin, TestPlan14GenerationRetirementKeepsPrelaunchPin, TestPlan14GenerationRetirementContinuesPastActivePin, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Running-tenant completeness remains unfinished. f698ed949535287a55d5a8fc103f7dc5bf0f2c5020e27ff96552f42852024d03 |
| +internal/host/rootgeneration_retirement.go generationRetirement.closure authority E2 E25: validate installation, controller, exact unit and process before retaining an active or prelaunch pin. Only closed writers plus a settled unit reach durable retirement and independent kernel absence. Use the checked read-only journal and continue past active pins. Fixtures: TestPlan14GenerationRetirementKeepsActivePin, TestPlan14GenerationRetirementKeepsPrelaunchPin, TestPlan14GenerationRetirementContinuesPastActivePin, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Running-tenant completeness remains unfinished. 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. b4b22db574eff86964cbb9a20f9f60e7ccb5c0adcd79656c936c929d4b0f95a5 |
| internal/host/rootgeneration_retirement.go generationRetirement.retireAll authority E2 E25: validate installation, controller, exact unit and process before retaining an active or prelaunch pin. Only closed writers plus a settled unit reach durable retirement and independent kernel absence. Use the checked read-only journal and continue past active pins. Fixtures: TestPlan14GenerationRetirementKeepsActivePin, TestPlan14GenerationRetirementKeepsPrelaunchPin, TestPlan14GenerationRetirementContinuesPastActivePin, TestPlan14GenerationRetirementBindingProcessCrash, TestPlan14GenerationRetirementCensusGate. Running-tenant completeness remains unfinished. c2ef0459f6836cceddd5dab65127f10323ffc797fe738d2c5f6040fadb1a6465 |
| internal/host/rootgeneration_kernel_linux.go generationTaskFDs authority E2 E25: keep the descriptor-listing handle open through inspection, including when scanning the current process. Check every listed descriptor against paths and artifact identities. Fixtures: TestPlan14GenerationKernelOwnDescriptors and TestPlan14GenerationKernelReferences. Root-runner acceptance passed at E27: TestPlan14GenerationKernelCensusRootRunner and TestPlan14GenerationKernelRetirementRootRunner. file:Open@143,file:Readdirnames@367 06417eff13beac0b675b55f2664183f938afee33a733d51bb571c9a0ad3d50dc |
| internal/host/rootgeneration_inventory.go generationPayloadBindings authority E2 E29: current and retained predecessor payloads must have completed materialization for every artifact kind, including route and timer. Tests: TestPlan14GenerationRegistryPayloadUnfinished, TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor. E27 checked root reader passed; extended physical/root-unit witnesses passed at E29. Required-artifact derivation and running-tenant containment remain incomplete. PROPOSED E46: local root installation obligations, exact root intention and inventory revision; service SQL is a deny-only consistency check. Executing identity: root instance-control or root participation/update. 58aaf33ef5fd544aeedb9d152058c86f319f594a79e607267f952dde3d1812e5 |
| @@ -379,7 +379,7 @@ internal/host/rootinventory_timer.go ClaimRootHTTP.verifyNewTimerIntentions hint |
| 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 |
| internal/host/claimlaunch_kernel_linux.go linuxClaimLaunchKernel.Observe authority C,G,J4 root-input;kept=C,G;scope=protected-decision-only 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:Lstat@430,file:Stat@1766,file:Stat@1862,file:Stat@1961,file:Stat@683 ec9cda265914cb10785a3a37cfa6929eb625be4575e98ace147640a26c984337 |
| internal/host/rootgeneration_containment_linux.go generationIdentity authority E2 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:Stat@102 13091ead40faf400cbd20f1d5d308fdfd4a3d7c605ef3eba4f5f279af8203258 |
| -internal/host/rootgeneration_containment_linux.go generationKernel.containedBinding authority E2 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. 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:Lstat@393,file:Readlink@1400,file:Readlink@1557,file:Readlink@1726,file:Readlink@3254,file:Stat@2449 09b91d059735787d01ce3747a1ed8af5069ea0b19fa2f1dd4c7ad47e0dad6fae |
| +internal/host/rootgeneration_containment_linux.go generationKernel.containedBinding authority E2 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. 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. 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:Lstat@369,file:Readlink@1376,file:Readlink@1533,file:Readlink@1702,file:Readlink@3230,file:Stat@2425 863bd8226570019d23faca8b5737e2b03d5cc608e1ce105e5ffbf445d5d78c73 |
| internal/host/systemapply.go systemApplier.activationGuard authority E2 PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. whole:Route@870 3dff6208f27c8a74f94df68e389b75cfe18d6e17a14a61a7f701597484bc29e2 |
| internal/host/systemapply.go systemApplier.restoreFiles authority E2 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:Stat@226 418eaa7dd1fd35f6d5640c7afd5385273088883e301066c7ac9c444b70d62cac |
| internal/instanceexec/claim_generation_linux.go ScopeClaimGeneration authority E2 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:Syscall@2236 0300932372f83c15b3c5716394128757476554c0603724b9f5801cae1a1a5ab7 |
| @@ -391,7 +391,7 @@ internal/host/rootgeneration_containment_linux.go generationKernel.containedTask |
| internal/host/claimstop_run.go claimActivationReady authority E2 PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. 82d893dc252c02813cd492f44dab8adc75b0e6fe5eaf4b7314c17759fdd5c83e |
| internal/host/rootreleaselock.go withClaimReleaseCensus authority E2 PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. 35bcc5e18e89f9755cf1f749bbaa4cbf493380b75500183687476abd71c946b6 |
| internal/rootparticipation/ledger.go IsAcquisitionRecovery authority E2 PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. d71ec87152a9ba780aaf6e08fd54288bd6da40d61ad96495e7c3b29a0be200cd |
| -@coverage internal/instancectl ede13946f9019158fd631e09a4543d3c73ee2e353b70945a900e963a688b1c33 |
| +@coverage internal/instancectl e2a238c3bd1267e3084705ccb5b2e0bff4b3feed82accbb48de8bd4dd68b9e29 |
| @coverage internal/instanceexec 1a4b9c7a5c57c99ecb39b56c724f08f7e0226dc4809f31000e49e4b38cb4d059 |
| @fields A,Account,Accounts,AddedAt,Addr,Addrs,Alias,Aliases,Alive,AllowAbsent,App,Archives,Args,At,Attempts,B,Backing,Bucket,BuildState,Callback,CallbackError,CallbackFailed,Canary,Caps,CertExpiresAt,Changed,Channel,CheckedAt,ClaimIncarnation,ClaimOwner,ClaimOwnership,ClaimRowID,ClaimStamp,Class,Contents,CreatedAt,DBPath,DNSCheckedAt,DNSStatus,Deadline,Demand,Digest,DoneBefore,Due,DueAt,Env,EnvBefore,Environment,Error,ErrorKind,ExcludeHosts,ExpectGood,ExpectOp,Expected,F,Failed,FenceGen,Fleet,FleetBox,FleetBoxes,Fleets,From,FromDB,FromOp,FromSock,Gate,GoodDemand,GoodDigest,GoodVersion,Hash,Health,Held,Hibernate,HoldOp,Host,Hosts,ID,Id,IdValue,Idle,IdleSeconds,ImportGate,Journal,Keep,Kind,Label,LastAt,LastError,LastMs,LastStatus,Limit,MaxRun,MaxRunSeconds,Method,Ms,Name,Next,NextAt,NextDue,NextGood,NextValue,Now,Number,Number64,Observed,ObservedValue,Old,On,Op,Original,Owner,Ownership,P,Path,Paths,Platform,Pool,Prefix,Prev,PrevVersion,Provisioned,RecordKey,Region,Registration,Replica,Replicas,Replication,Retirement,Route,Routes,Row,Rows,Seed,SeedID,SeedSource,ServingVersion,Slug,Socket,Spec,Sqid,State,Status,StatusNamed,Steering,StringMap,T,TValue,Tags,Target,Targets,Text,Timer,Timers,Times,To,ToDB,ToOp,ToSock,TokenHash,Tunnel,Unit,UpdateSettings,Ver,Version,W,WakeIncarnation,Wildcards,closed,conn,connect,ctx,d,dec,done,enc,full,input,mu,output,path,remote,sql,wait |
| internal/host/agent.go runAgent authority B,C,D,F rev3=census-ruling-seeded-instances;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body with wireSeedPlacement called in the activator start's Configure step, before armMovesPass, and the startup sweep's result stored for the import rollback in Recover; read sites unchanged, offsets moved; main's B,C,D,F authority retained PROPOSED E46: captured claim selection and root journal mutation; root RPC outside service host exclusion; SQL/route data never grants root authority. Executing identity: service controller; root for inventory/retirement callbacks. file:Exec@13802,file:Open@731,registry:App@10065,whole:Route@10057,whole:Route@23109 098dab554d79d132385030a61a266947c39d91b9c2670a3be1141e7eacfbab72 |
| @@ -429,3 +429,13 @@ internal/schedule/timer_mutation.go type timerMutator hint E2 PROPOSED E46: capt |
| internal/host/rootgeneration_inventory.go productionLocalGenerationInventory authority E2 PROPOSED E46: root participation/recovery reads retained root census, fences, inventory and kernel evidence; no deployment-store network dependency. SQL remains deny-only. 552c334ed06138a709143cec14ab5caf961bc4f2d9b7656623851c12b18e7958 |
| internal/host/rootgeneration_inventory.go readProductionGenerationInventory authority E2 PROPOSED E46: root participation/recovery reads retained root census, fences, inventory and kernel evidence; no deployment-store network dependency. SQL remains deny-only. 0a9180acd9e78efc5905fe37df25d877c7343fcec037786f699b03182acaedc3 |
| internal/host/rootgeneration_requirements.go generationLocalObligations authority E2 PROPOSED E46: root participation/recovery reads retained root census, fences, inventory and kernel evidence; no deployment-store network dependency. SQL remains deny-only. 7bf08b3afdf12e81188123e0257ef90cea91091a8ab3bfb975e9a0200619dd1b |
| +internal/host/claimlaunch_root.go ClaimLaunchRoot.launchRecord authority C,G,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:Read@121 d6bd008ff77de97a134a4d35d877d9cba351e02a99e2fd798c8a84fe1acc32dd |
| +internal/host/claimlaunch_root.go ClaimLaunchRoot.stop authority C,G,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:Read@508 dfca275187d117d3c8e44094c5d8ad8953239e32c61af25f5dc7aed7a1b728fe |
| +internal/host/claimlaunch_root.go claimUnitStopKernel.settleUnits authority C,G,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:Read@219 82c5a4ea6295e61d08f7a77a26e42040af6fff953265d8055d2454e28dff5ef1 |
| +internal/host/claimunit_binding.go claimUnitBindings.check authority E2 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. whole:Route@342,whole:Route@384 ade4c1bc0458015b450959ac11e28696c19491e24a76f334a5fb4f9c3ce8e767 |
| +internal/host/claimunit_binding.go claimUnitBindings.read authority E2 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@211 3ce1ba6c82c41f1ad4ac005066746fcefa8a679ff587d07fa8794ca743a77685 |
| +internal/host/claimunit_binding.go claimUnitShape.matches hint E2 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. registry:Account@98,registry:Addr@229,registry:Addrs@263,registry:App@120,registry:Backing@182,registry:DBPath@251,registry:Host@75,registry:Kind@139,registry:Replica@315,registry:Seed@298,registry:Socket@207,registry:Tunnel@280,registry:Unit@159 3846a08b4ea9297cb75bb7ebd25e07c9a63b8ada6a53ec8b49f2dce4e323ad6b |
| +internal/host/claimunit_binding_source.go readClaimUnitShapes authority E2 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:ReadAll@188 77b2f5b6157118d78a66945e5bfc942d5ef485d2b7a39e52d3b28b8a5b10a0b9 |
| +internal/instancectl/claimexec.go ClaimUnits.prepareExecGate 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@995 1b95e2b44b392d930ec8bd921b6d0382b47b3530a3d020646ced26e940e6f523 |
| +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 |
diff --git a/internal/release/rootgenerations.go b/internal/release/rootgenerations.go| index afdda3b26..1207316c0 100644 |
| --- a/internal/release/rootgenerations.go |
| +++ b/internal/release/rootgenerations.go |
| @@ -9,6 +9,7 @@ import ( |
| "errors" |
| "os" |
| "path/filepath" |
| + "strings" |
| "github.com/carlosframework/platform/internal/rootstate" |
| ) |
| @@ -21,6 +22,7 @@ type GenerationOwner struct { |
| } |
| type GenerationBinding struct { |
| + Unit string `json:",omitempty"` |
| Binding HistoryBinding |
| Target GenerationOwner |
| Installation, Attempt, Inputs, Invocation string |
| @@ -63,7 +65,7 @@ func generationKey(b GenerationBinding) string { |
| func (b GenerationBinding) valid() bool { |
| t := b.Target |
| digest, err := hex.DecodeString(b.Digest) |
| - return b.Binding.valid() && t.Host == b.Binding.Host && t.Incarnation != "" && t.Box != "" && t.Enrollment != "" && t.BootID != "" && t.ControllerID != "" && t.Stamp > 0 && t.Session > 0 && |
| + return (b.Unit == "" || (filepath.Base(b.Unit) == b.Unit && strings.HasSuffix(b.Unit, ".service") && !strings.ContainsAny(b.Unit, "\x00\r\n/\\"))) && b.Binding.valid() && t.Host == b.Binding.Host && t.Incarnation != "" && t.Box != "" && t.Enrollment != "" && t.BootID != "" && t.ControllerID != "" && t.Stamp > 0 && t.Session > 0 && |
| b.Record != "" && b.RecordETag != "" && b.Installation != "" && b.Attempt != "" && b.Inputs != "" && b.Invocation != "" && b.ControllerPID > 0 && b.ControllerStart != "" && |
| ValidVersion(b.Version) && err == nil && len(digest) == sha256.Size && hex.EncodeToString(digest) == b.Digest && |
| filepath.IsAbs(b.Path) && filepath.Clean(b.Path) == b.Path && b.Tree == filepath.Dir(b.Path) && |