carlos / platform Public

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>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev dd5357e2a2c1a9554cd31d5bcfa1ceadbebc667f parent 6286ad9
24 files changed, +1040 −50
  • internal/host/claimjournal_containment.go +21 −1
  • internal/host/claimlaunch_authority.go +3 −0
  • internal/host/claimlaunch_root.go +14 −8
  • internal/host/claimunit_binding.go +200 −0
  • internal/host/claimunit_binding_source.go +43 −0
  • internal/host/claimunit_binding_test.go +253 −0
  • internal/host/claimunit_launch_test.go +73 −0
  • internal/host/claimunit_placement.go +63 −0
  • internal/host/plan14_installed_launch_root_linux_test.go +43 −2
  • internal/host/plan14_unit_binding_root_linux_test.go +92 −0
  • internal/host/rootgeneration_containment_linux.go +1 −2
  • internal/host/rootgeneration_inventory.go +1 −2
  • internal/host/rootgeneration_kernel_linux.go +1 −2
  • internal/host/rootgeneration_retirement.go +1 −2
  • internal/host/rootgenerations_launch.go +8 −0
  • internal/instancectl/claimexec.go +6 −1
  • internal/instancectl/claimlaunch_snapshot.go +5 −1
  • internal/instancectl/claimunit.go +40 −13
  • internal/instancectl/claimunit_bound.go +32 −0
  • internal/instancectl/claimunit_bound_stop.go +21 −0
  • internal/instancectl/claimunit_bound_test.go +91 −0
  • internal/provision/plan14_e_write_census_test.go +2 −2
  • internal/provision/testdata/plan14-root-reads-e.tsv +23 −13
  • internal/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) &&