WIP claim Plan 14 track E checkpoint 46 (not for merge)
M3 network-free boot recovery; R-E45-4 timer producers, atomic re-registration, abandoned intentions; installed claim launch and exact pin release accepted on the root runner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
40 files changed,
+1694
−91
internal/host/agent.go+4 −0internal/host/claimassembly.go+4 −0internal/host/plan14_generation_creation_test.go+9 −0internal/host/plan14_generation_registry_root_linux_test.go+18 −0internal/host/plan14_generation_requirements_linux_test.go+15 −0internal/host/plan14_generation_requirements_root_linux_test.go+32 −1internal/host/plan14_generation_retirement_test.go+38 −0internal/host/plan14_generation_timer_retirement_test.go+25 −1internal/host/plan14_installed_launch_root_linux_test.go+390 −0internal/host/plan14_timer_intention_test.go+183 −3internal/host/rootgeneration_inventory.go+16 −1internal/host/rootgeneration_parked.go+12 −2internal/host/rootgeneration_requirements.go+36 −10internal/host/rootgeneration_rows.go+3 −0internal/host/rootinventory.go+15 −2internal/host/rootinventory_timer.go+6 −3internal/host/rootinventory_timer_mutation.go+114 −0internal/host/rootinventory_timer_recovery.go+51 −0internal/host/rootlaunchverifier.go+1 −1internal/hostbox/coldhandover.go+19 −0internal/hostbox/coldhandover_test.go+14 −1internal/hostbox/inventory_expected.go+36 −1internal/hostbox/inventoryscope.go+3 −0internal/hostbox/journalservice.go+1 −0internal/hostbox/reconcile_adapter.go+2 −2internal/hostbox/registryrows.go+1 −1internal/hostbox/stopinventory.go+1 −0internal/hostbox/timer_registration.go+134 −0internal/hostbox/typedretire.go+12 −8internal/hostbox/work.go+2 −0internal/hostguard/rootinventory_settle.go+34 −0internal/hostguard/rootinventory_settle_test.go+71 −0internal/provision/plan14_e_write_census_test.go+14 −4internal/provision/testdata/plan14-root-reads-e.tsv+52 −16internal/registry/claimrows.go+9 −1internal/schedule/claimtimers.go+35 −0internal/schedule/converge.go+54 −29internal/schedule/plan14_timer_producers_test.go+126 −0internal/schedule/runner.go+2 −4internal/schedule/timer_mutation.go+100 −0
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 281.558s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.664s
ok github.com/carlosframework/platform/cmd/moveprobe 92.227s
ok github.com/carlosframework/platform/cmd/zddprobe 0.704s
ok github.com/carlosframework/platform/internal/activator 250.548s
ok github.com/carlosframework/platform/internal/agentsock 0.018s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.022s
ok github.com/carlosframework/platform/internal/appconfig 2.436s
ok github.com/carlosframework/platform/internal/apperr 0.071s
ok github.com/carlosframework/platform/internal/applog 0.140s
ok github.com/carlosframework/platform/internal/awsinv 0.007s
ok github.com/carlosframework/platform/internal/boxchoice 0.007s
ok github.com/carlosframework/platform/internal/boxid 0.678s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.008s
ok github.com/carlosframework/platform/internal/caps 0.011s
ok github.com/carlosframework/platform/internal/certwarm 0.003s
ok github.com/carlosframework/platform/internal/cgroup 0.696s
ok github.com/carlosframework/platform/internal/changelog 0.312s
ok github.com/carlosframework/platform/internal/clamd 0.055s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 387.313s
? 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.422s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.010s
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.004s
ok github.com/carlosframework/platform/internal/domains 2.307s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.005s
ok github.com/carlosframework/platform/internal/edge 26.630s
? 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.655s
ok github.com/carlosframework/platform/internal/genselect 0.014s
ok github.com/carlosframework/platform/internal/geo 0.018s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/historyreadback 0.149s
ok github.com/carlosframework/platform/internal/host 144.459s
ok github.com/carlosframework/platform/internal/hostactors 0.372s
ok github.com/carlosframework/platform/internal/hostbox 2.264s
ok github.com/carlosframework/platform/internal/hostclaim 0.487s
ok github.com/carlosframework/platform/internal/hostevidence 0.006s
ok github.com/carlosframework/platform/internal/hostexec 4.857s
ok github.com/carlosframework/platform/internal/hostfence 0.038s
ok github.com/carlosframework/platform/internal/hostfiles 10.519s
ok github.com/carlosframework/platform/internal/hostfloor 0.013s
ok github.com/carlosframework/platform/internal/hostguard 1.019s
ok github.com/carlosframework/platform/internal/hostops 1.507s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.004s
ok github.com/carlosframework/platform/internal/hostquarantine 0.004s
ok github.com/carlosframework/platform/internal/hostrelocate 0.427s
ok github.com/carlosframework/platform/internal/hostresolve 0.144s
ok github.com/carlosframework/platform/internal/hostwrite 0.004s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 4.180s
ok github.com/carlosframework/platform/internal/instanceexec 2.732s
ok github.com/carlosframework/platform/internal/instancefs 0.008s
ok github.com/carlosframework/platform/internal/instancekey 0.002s
ok github.com/carlosframework/platform/internal/instanceupdates 0.009s
ok github.com/carlosframework/platform/internal/layout 1.477s
ok github.com/carlosframework/platform/internal/lease 9.786s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.005s
ok github.com/carlosframework/platform/internal/ledger 0.017s
ok github.com/carlosframework/platform/internal/mail 0.154s
ok github.com/carlosframework/platform/internal/maintenance 0.086s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.523s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.070s
ok github.com/carlosframework/platform/internal/metrics 0.004s
ok github.com/carlosframework/platform/internal/moves 21.006s
ok github.com/carlosframework/platform/internal/moves/archive 0.764s
ok github.com/carlosframework/platform/internal/objstore 0.170s
ok github.com/carlosframework/platform/internal/ops 0.176s
ok github.com/carlosframework/platform/internal/park 0.204s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 16.909s
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.520s
ok github.com/carlosframework/platform/internal/relay 7.232s
ok github.com/carlosframework/platform/internal/release 22.146s
ok github.com/carlosframework/platform/internal/rollout 3.759s
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.568s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.484s
ok github.com/carlosframework/platform/internal/seeds 0.011s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.437s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.725s
ok github.com/carlosframework/platform/internal/status 0.043s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.189s
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.003s
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.536s
=== 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:20:16 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEC05F35076213, 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.00s)
PASS
ok github.com/carlosframework/platform/internal/host 0.020s
minio contract: PASS
diff --git a/internal/host/agent.go b/internal/host/agent.go| index aa3263a64..257204721 100644 |
| --- a/internal/host/agent.go |
| +++ b/internal/host/agent.go |
| @@ -193,6 +193,7 @@ import ( |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/rootparticipation" |
| "github.com/carlosframework/platform/internal/rootstate" |
| + "github.com/carlosframework/platform/internal/schedule" |
| "github.com/carlosframework/platform/internal/status" |
| "github.com/carlosframework/platform/internal/version" |
| ) |
| @@ -418,6 +419,9 @@ func runAgent(args []string, buildClaims ClaimFactory) { |
| // Asked inside Acquire, before the lease (amadan #213). |
| ConfigGate: configGate(), |
| } |
| + if claims.Writes.Enabled { |
| + eb.Timers = schedule.ClaimTimers{TimerStore: reg, Writes: claims.Writes, Context: agentCtx} |
| + } |
| if logSink != nil { // a typed-nil in the interface would NOT be nil |
| eb.Logs = logSink |
| } |
diff --git a/internal/host/claimassembly.go b/internal/host/claimassembly.go| index 3df628bb4..7089f1c64 100644 |
| --- a/internal/host/claimassembly.go |
| +++ b/internal/host/claimassembly.go |
| @@ -4,6 +4,7 @@ import ( |
| "context" |
| "errors" |
| "fmt" |
| + "github.com/carlosframework/platform/internal/registry" |
| "net/http" |
| "os" |
| "path/filepath" |
| @@ -160,6 +161,9 @@ func (a ClaimAssembly) Build(ctx context.Context) (ClaimProviders, error) { |
| a.Guard.Claims = hostguard.StoreReader{Store: a.Claims} |
| // Continue is the moves C1 session table, nil unless both flags are on (D1). |
| source := hostbox.WorkSource{FleetHomeRegion: actor.FleetHomeRegion, HomeRegion: actor.HomeRegion, Guard: a.Guard, Box: actor.Box, Region: park.PlacementRegion(actor.Region), Now: time.Now, Policy: a.Policy.Check, Local: a.Identity.Local(a.Local), Continue: a.Wake.Continue} |
| + source.Timer = func(ctx context.Context, work hostwrite.Work, timer registry.Timer, expected string, remove bool) (*registry.Timer, error) { |
| + return a.Reconcile.MutateTimer(ctx, work, timer, expected, remove) |
| + } |
| a.Reconcile.Reconciler.Claims = a.Claims |
| a.Reconcile.Reconciler.Guard = a.Guard |
| a.Reconcile.Reconciler.Writes = hostwrite.Config{Enabled: true, Selector: source} |
diff --git a/internal/host/plan14_generation_creation_test.go b/internal/host/plan14_generation_creation_test.go| index 396d2ecab..b19138928 100644 |
| --- a/internal/host/plan14_generation_creation_test.go |
| +++ b/internal/host/plan14_generation_creation_test.go |
| @@ -13,6 +13,12 @@ import ( |
| ) |
| func TestPlan14GenerationEmptyInstallationRequiresCreation(t *testing.T) { |
| + plan14GenerationCreation(t, false) |
| +} |
| +func TestPlan14GenerationLocalRecoveryCreation(t *testing.T) { |
| + plan14GenerationCreation(t, true) |
| +} |
| +func plan14GenerationCreation(t *testing.T, local bool) { |
| for _, attack := range []string{"none", "missing", "erased", "corrupt", "consumed", "wrong-installation", "wrong-host", "visible-sync", "changed-during-kernel"} { |
| t.Run(attack, func(t *testing.T) { |
| f, disk, rec := generationLaunchFixture(t) |
| @@ -47,6 +53,9 @@ func TestPlan14GenerationEmptyInstallationRequiresCreation(t *testing.T) { |
| return nil |
| } |
| v := generationReconciler{read: func(ctx context.Context) (generationInventory, error) { |
| + if local { |
| + return generationLocalObligations(ctx, snapshot, fresh) |
| + } |
| return generationRequiredInventory(ctx, f.root.generations.store, snapshot, fresh) |
| }, kernel: func(context.Context) (string, string, error) { |
| if active && attack == "changed-during-kernel" { |
diff --git a/internal/host/plan14_generation_registry_root_linux_test.go b/internal/host/plan14_generation_registry_root_linux_test.go| index 6829112e4..ba422fc13 100644 |
| --- a/internal/host/plan14_generation_registry_root_linux_test.go |
| +++ b/internal/host/plan14_generation_registry_root_linux_test.go |
| @@ -4,6 +4,7 @@ package host |
| import ( |
| "fmt" |
| + "github.com/carlosframework/platform/internal/instanceexec" |
| "os" |
| "os/exec" |
| "os/user" |
| @@ -13,6 +14,23 @@ import ( |
| // The test binary must take the same private registry entrypoint as carlos. |
| func TestMain(m *testing.M) { |
| + if os.Getenv("PLAN14_TENANT_SOCKET") != "" { |
| + plan14InstalledTenant() |
| + return |
| + } |
| + if len(os.Args) > 1 { |
| + switch os.Args[1] { |
| + case "instance-exec": |
| + instanceexec.RunExec(os.Args[2:]) |
| + return |
| + case "instance-exit": |
| + instanceexec.RunExit(os.Args[2:]) |
| + return |
| + case "plan14-tenant": |
| + plan14InstalledTenant() |
| + return |
| + } |
| + } |
| if len(os.Args) > 1 && os.Args[1] == registryChildCommand { |
| if err := RunRegistryChild(os.Args[2:]); err != nil { |
| fmt.Fprintln(os.Stderr, err) |
diff --git a/internal/host/plan14_generation_requirements_linux_test.go b/internal/host/plan14_generation_requirements_linux_test.go| index 6a262ddf1..43dbde977 100644 |
| --- a/internal/host/plan14_generation_requirements_linux_test.go |
| +++ b/internal/host/plan14_generation_requirements_linux_test.go |
| @@ -8,6 +8,7 @@ import ( |
| "errors" |
| "os" |
| "path/filepath" |
| + "strings" |
| "testing" |
| "time" |
| @@ -21,7 +22,18 @@ import ( |
| ) |
| func TestPlan14GenerationRequiredArtifacts(t *testing.T) { |
| + plan14GenerationRequiredArtifacts(t, false) |
| +} |
| + |
| +func TestPlan14GenerationLocalRecoveryObligations(t *testing.T) { |
| + plan14GenerationRequiredArtifacts(t, true) |
| +} |
| + |
| +func plan14GenerationRequiredArtifacts(t *testing.T, local bool) { |
| for _, attack := range []string{"none", "service", "record-next-pass", "route-omitted", "state-omitted", "historical-state-omitted", "requirements-missing", "requirements-schema", "requirements-malformed", "state-child-only", "state-wrong-host", "state-wrong-root", "state-file", "duplicate-route", "duplicate-state", "false-empty", "predecessor-omitted", "record-schema", "record-kind", "record-missing", "record-malformed", "record-account", "record-app", "record-host", "record-incarnation", "changed-record-during-kernel"} { |
| + if local && (strings.HasPrefix(attack, "record-") || attack == "changed-record-during-kernel") { |
| + continue |
| + } |
| t.Run(attack, func(t *testing.T) { |
| f, disk, rec := generationLaunchFixture(t) |
| if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| @@ -100,6 +112,9 @@ func TestPlan14GenerationRequiredArtifacts(t *testing.T) { |
| } |
| attackActive := false |
| v := generationReconciler{read: func(ctx context.Context) (generationInventory, error) { |
| + if local { |
| + return generationLocalObligations(ctx, snapshot, nil) |
| + } |
| return generationRequiredInventory(ctx, store, snapshot, nil) |
| }, kernel: func(context.Context) (string, string, error) { |
| if attackActive && attack == "changed-record-during-kernel" { |
diff --git a/internal/host/plan14_generation_requirements_root_linux_test.go b/internal/host/plan14_generation_requirements_root_linux_test.go| index f326a9f7e..12ad48166 100644 |
| --- a/internal/host/plan14_generation_requirements_root_linux_test.go |
| +++ b/internal/host/plan14_generation_requirements_root_linux_test.go |
| @@ -3,6 +3,7 @@ |
| package host |
| import ( |
| + "context" |
| "encoding/json" |
| "os" |
| "os/exec" |
| @@ -22,6 +23,14 @@ import ( |
| ) |
| func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) { |
| + plan14RequiredArtifactsRootRunner(t, false) |
| +} |
| + |
| +func TestPlan14BootLocalRecoveryRootRunner(t *testing.T) { |
| + plan14RequiredArtifactsRootRunner(t, true) |
| +} |
| + |
| +func plan14RequiredArtifactsRootRunner(t *testing.T, local bool) { |
| const childKey = "PLAN14_E30_REQUIRED_CHILD" |
| dir := os.Getenv(childKey) |
| if dir == "" { |
| @@ -43,7 +52,7 @@ func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^TestPlan14GenerationRequiredArtifactsRootRunner$", "-test.v", "-test.timeout=2m") |
| + cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^"+t.Name()+"$", "-test.v", "-test.timeout=2m") |
| cmd.Dir = dir |
| cmd.Env = append(os.Environ(), childKey+"="+dir) |
| if out, err := cmd.CombinedOutput(); err != nil { |
| @@ -212,6 +221,15 @@ func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) { |
| return c |
| } |
| source := productionRootGenerationPins() |
| + if local { |
| + t.Setenv("CARLOS_DEPLOYMENT_DIR", "") |
| + t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "") |
| + if err := registerProductionLaunchVerifier(); err != nil { |
| + t.Fatal(err) |
| + } |
| + source = generationPinSource{open: func() (rootstate.Store, error) { return rootstate.Open(release.RootGenerationsDir, false, false) }, reconcile: (generationReconciler{read: productionLocalGenerationInventory, kernel: productionGenerationKernelCensus}).reconcile} |
| + } |
| + |
| pinsUnderRelease := func() (release.StagedPaths, error) { |
| var pins release.StagedPaths |
| err := withClaimRootRelease(t.Context(), true, func() (rootstate.Store, error) { return rootstate.Open(rootReleasePrivate, false, false) }, func() error { |
| @@ -240,6 +258,18 @@ func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) { |
| } |
| } |
| accept() |
| + if local { |
| + state, err := rootstate.Open(rootparticipation.Namespace, false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer state.Close() |
| + l := &rootparticipation.Ledger{State: state, VerifyLaunches: (rootLaunchVerifier{exclude: participationRelease, open: func() (rootstate.Store, error) { return rootstate.Open(release.RootGenerationsDir, false, false) }, reconcile: generationReconciler{read: productionLocalGenerationInventory, kernel: productionGenerationKernelCensus}}).verify, ExcludeLaunches: participationRelease.With} |
| + a := &systemApplier{participation: l, dataDir: dataDir()} |
| + if err := a.controllersRecovered(context.Background()); err != nil { |
| + t.Fatal("boot participation recovery without deployment storage", err) |
| + } |
| + } |
| for _, omit := range []string{"route", "state", "timer", "historical-state", "timer-both"} { |
| t.Run(omit, func(t *testing.T) { |
| initial := certificate() |
| @@ -375,6 +405,7 @@ func generationRunnerEmptyRegistry(t *testing.T, opt string, uid, gid int) { |
| t.Fatal(err) |
| } |
| source := productionRootGenerationPins() |
| + |
| check := func(t *testing.T, refuse bool) { |
| t.Helper() |
| err := withClaimRootRelease(t.Context(), true, func() (rootstate.Store, error) { return rootstate.Open(rootReleasePrivate, false, false) }, func() error { |
diff --git a/internal/host/plan14_generation_retirement_test.go b/internal/host/plan14_generation_retirement_test.go| index f99ea4b88..a024a8d42 100644 |
| --- a/internal/host/plan14_generation_retirement_test.go |
| +++ b/internal/host/plan14_generation_retirement_test.go |
| @@ -88,3 +88,41 @@ func TestPlan14GenerationRetirementReleasesExactPin(t *testing.T) { |
| t.Fatal("swept reservation replayed") |
| } |
| } |
| + |
| +func TestPlan14InstalledPinClosure(t *testing.T) { |
| + for _, state := range []string{"running", "controller-open", "closed"} { |
| + t.Run(state, func(t *testing.T) { |
| + f, disk, _ := generationLaunchFixture(t) |
| + if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| + t.Fatal(err) |
| + } |
| + r := readGenerationLaunch(t, disk) |
| + if state != "running" { |
| + if err := f.root.stop(t.Context(), f.peer, *f.req.Reservation); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if state == "closed" { |
| + if err := f.journal.CloseWriters(t.Context(), f.target); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + calls := 0 |
| + retire := generationRetirement{guard: f.root.Guard, journal: f.journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { |
| + calls++ |
| + return strings.Repeat("b", 64), nil |
| + }} |
| + ledger := &release.RootGenerations{Store: disk} |
| + if err := retire.retire(t.Context(), ledger, r); err != nil { |
| + t.Fatal(err) |
| + } |
| + got, err := ledger.Read(r.Binding) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if (got.Retirement != "") != (state == "closed") || (calls > 0) != (state == "closed") { |
| + t.Fatal("pin crossed incomplete closure", state, got, calls) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/host/plan14_generation_timer_retirement_test.go b/internal/host/plan14_generation_timer_retirement_test.go| index 8233873aa..b85e6a551 100644 |
| --- a/internal/host/plan14_generation_timer_retirement_test.go |
| +++ b/internal/host/plan14_generation_timer_retirement_test.go |
| @@ -6,12 +6,13 @@ import ( |
| "testing" |
| "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| "github.com/carlosframework/platform/internal/registry" |
| ) |
| func TestPlan14GenerationTimerRetirement(t *testing.T) { |
| t.Setenv("CARLOS_HOST_CLAIM", "true") |
| - for _, name := range []string{"exact", "replacement", "unrecorded", "legacy-unrecorded"} { |
| + for _, name := range []string{"exact", "replacement", "unrecorded", "legacy-unrecorded", "abandoned", "intended-row", "withdrawn"} { |
| mismatch := name == "replacement" || name == "unrecorded" |
| t.Run(name, func(t *testing.T) { |
| coordinator, req, _ := claimStopFixture(t) |
| @@ -51,6 +52,17 @@ func TestPlan14GenerationTimerRetirement(t *testing.T) { |
| if name == "unrecorded" || name == "legacy-unrecorded" { |
| in.Entries = in.Entries[:1] |
| } |
| + if name == "abandoned" || name == "intended-row" { |
| + in.Entries[1].Materialization = "intended" |
| + } |
| + if name == "withdrawn" { |
| + in.Entries[1].TimerRetired = true |
| + } |
| + if name == "abandoned" || name == "withdrawn" { |
| + if err := reg.DeleteTimerOwnedRegistration(timer); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| if err := inventory.WriteStopInventory(t.Context(), in); err != nil { |
| t.Fatal(err) |
| } |
| @@ -60,6 +72,8 @@ func TestPlan14GenerationTimerRetirement(t *testing.T) { |
| // must already be durable, and cannot be confused with the remote result. |
| if err := root.retire(t.Context(), result.Target); err == nil { |
| t.Fatal("missing claim accepted") |
| + } else { |
| + t.Logf("retirement: %v", err) |
| } |
| got, err := reg.Get(req.Host) |
| if err != nil { |
| @@ -82,6 +96,16 @@ func TestPlan14GenerationTimerRetirement(t *testing.T) { |
| if err != nil || len(records) != 1 || !records[0].Complete { |
| t.Fatal("missing local completion", records, err) |
| } |
| + if name != "legacy-unrecorded" { |
| + receipt, err := coordinator.Guard.FenceReceiptFor(t.Context(), result.Target) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + snapshot := generationInventory{Retirements: records, Payloads: []hostbox.GenerationPayload{{Inventory: in}}, Installations: []hostguard.FenceView{{Host: req.Host, Receipts: []hostguard.FenceReceipt{receipt}}}} |
| + if _, err := generationPhysicalPayloads(t.Context(), snapshot); err != nil { |
| + t.Fatal("completed timer retirement still blocks completeness", err) |
| + } |
| + } |
| if len(records[0].Inventory.Artifacts) != 2 { |
| t.Fatal("duplicated timer", records[0]) |
| } |
diff --git a/internal/host/plan14_installed_launch_root_linux_test.go b/internal/host/plan14_installed_launch_root_linux_test.go| new file mode 100644 |
| index 000000000..9261d4ce6 |
| --- /dev/null |
| +++ b/internal/host/plan14_installed_launch_root_linux_test.go |
| @@ -0,0 +1,390 @@ |
| +//go:build linux && rootrunner |
| + |
| +package host |
| + |
| +import ( |
| + "context" |
| + "crypto/ed25519" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "encoding/json" |
| + "fmt" |
| + "net" |
| + "net/http" |
| + "os" |
| + "os/exec" |
| + "path/filepath" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + execlease "github.com/carlosframework/platform/internal/hostexec" |
| + "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" |
| +) |
| + |
| +func plan14InstalledTenant() { |
| + socket := os.Getenv("PLAN14_TENANT_SOCKET") |
| + if socket == "" { |
| + os.Exit(2) |
| + } |
| + ln, err := net.Listen("unix", socket) |
| + if err != nil { |
| + fmt.Fprintln(os.Stderr, err) |
| + os.Exit(1) |
| + } |
| + if err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("running")) })); err != nil { |
| + os.Exit(1) |
| + } |
| +} |
| + |
| +func TestPlan14InstalledClaimLaunchRootRunner(t *testing.T) { |
| + if dir := os.Getenv("PLAN14_INSTALLED_RETIRE_CHILD"); dir != "" { |
| + plan14InstalledRetire(t, dir) |
| + return |
| + } |
| + dir := generationRootRunnerDir(t) |
| + t.Setenv("TMPDIR", dir) |
| + uid, gid := generationRunnerRegistryUser(t) |
| + ctl := func(ctx context.Context, args ...string) (string, error) { |
| + out, err := exec.CommandContext(ctx, "systemctl", args...).CombinedOutput() |
| + return strings.TrimSpace(string(out)), err |
| + } |
| + // These fixture paths must be absent. Install only this runner's units and |
| + // filesystem roots; retain the deployed unit's hardening and launch entrypoint. |
| + for _, p := range []string{"/data", "/opt/carlos", "/run/carlos", "/run/carlos-control", "/etc/carlos"} { |
| + if err := os.Mkdir(p, 0755); err != nil { |
| + t.Fatal("fixture root already exists", p, err) |
| + } |
| + p := p |
| + t.Cleanup(func() { |
| + if err := os.RemoveAll(p); err != nil { |
| + t.Error(err) |
| + } |
| + }) |
| + } |
| + binary, err := os.Executable() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + body, err := os.ReadFile(binary) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(instancectl.SystemBinary, body, 0755); err != nil { |
| + t.Fatal(err) |
| + } |
| + f, _, rec := generationLaunchFixture(t) |
| + t.Setenv("CARLOS_DATA_DIR", "/data") |
| + t.Setenv("CARLOS_OPT_DIR", "/opt/carlos") |
| + host := f.target.Host |
| + 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) |
| + } |
| + } |
| + for _, p := range []string{provision.InstanceDir("/data", host), provision.InstanceRunDir(host)} { |
| + if err := os.Chown(p, uid, gid); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + template, err := unitsFS.ReadFile("units/carlos-instance@.service") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var lines []string |
| + for _, line := range strings.Split(string(template), "\n") { |
| + key, _, _ := strings.Cut(line, "=") |
| + switch key { |
| + case "BindsTo", "Requires", "After": |
| + continue |
| + case "StandardOutput": |
| + line = "StandardOutput=journal" |
| + } |
| + lines = append(lines, line) |
| + } |
| + unitPath := "/run/systemd/system/carlos-instance@.service" |
| + out, err := os.OpenFile(unitPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := out.WriteString(strings.Join(lines, "\n")); err != nil { |
| + t.Fatal(err) |
| + } |
| + out.Close() |
| + unit := instancectl.UnitName(instancectl.RoleRun, host) |
| + t.Cleanup(func() { |
| + ctl(context.Background(), "stop", unit) |
| + ctl(context.Background(), "stop", instancectl.SliceName(host)) |
| + ctl(context.Background(), "reset-failed", unit) |
| + os.Remove(unitPath) |
| + os.RemoveAll(filepath.Join("/run/systemd/system", unit+".d")) |
| + os.RemoveAll(filepath.Join("/run/systemd/system", instancectl.SliceName(host)+".d")) |
| + ctl(context.Background(), "daemon-reload") |
| + }) |
| + if out, err := ctl(t.Context(), "daemon-reload"); err != nil { |
| + t.Fatal(out, err) |
| + } |
| + // Use a real controller lifetime, which must be joined before exact release. |
| + controller := exec.Command("/bin/sleep", "240") |
| + if err := controller.Start(); err != nil { |
| + t.Fatal(err) |
| + } |
| + joined := false |
| + join := func() { |
| + if !joined { |
| + controller.Process.Kill() |
| + controller.Wait() |
| + joined = true |
| + } |
| + } |
| + t.Cleanup(join) |
| + f.peer.PID = controller.Process.Pid |
| + f.peer.ProcessStart, err = claimLaunchProcessStart("/proc", f.peer.PID) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + launch, err := f.journal.Read(t.Context(), f.target) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.target.BootID, f.peer.BootID = strings.TrimSpace(string(boot)), strings.TrimSpace(string(boot)) |
| + f.req.Reservation.Target = f.target |
| + launch.Target = f.target |
| + f.claims.set(func(c *hostclaim.Claim) { c.Holder.WakeRetry.Attempt.BootID = f.peer.BootID }) |
| + launch.ControllerPID, launch.ProcessStart = f.peer.PID, f.peer.ProcessStart |
| + // Open a fresh journal with the controller identity production will check. |
| + journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { journal.Close() }) |
| + if err := journal.Prelaunch(t.Context(), launch); err != nil { |
| + t.Fatal(err) |
| + } |
| + f.journal, f.root.Journal = journal, journal |
| + state, err := rootstate.Open(filepath.Join(dir, "fences"), true, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer state.Close() |
| + old, err := f.root.Guard.Root.Installation(t.Context(), host, hostguard.Captured{Incarnation: f.target.Incarnation, Stamp: f.target.Stamp}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.root.Guard.Root = hostguard.NewRootAuthority(state) |
| + if err := f.root.Guard.EstablishRoot(t.Context(), old, func(*hostclaim.Claim) error { return nil }); 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.Kernel = linuxClaimLaunchKernel{OptRoot: "/opt/carlos", CgroupRoot: "/sys/fs/cgroup", ProcRoot: "/proc"} |
| + store := release.NewDirStore(filepath.Join(dir, "deployment")) |
| + rec.Schema = 1 |
| + plan14PutRecord(t, store, rec) |
| + pub, priv, err := ed25519.GenerateKey(nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, v := range []string{"v1", "v3"} { |
| + if err := release.StageArtifact("/opt/carlos", rec.Account, v, rec.App, body); err != nil { |
| + t.Fatal(err) |
| + } |
| + m := release.Manifest{App: rec.App, Version: v, Kind: "binary", Artifacts: map[string]release.Artifact{release.AdoptPlatform(): {}}} |
| + if err := release.Ship(t.Context(), store, rec.Account, m, map[string][]byte{release.AdoptPlatform(): body}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := release.Promote(t.Context(), store, priv, rec.Account, rec.App, "canary/console", v); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + path := release.VersionPath("/opt/carlos", rec.Account, "v1", rec.App) |
| + file, err := rootstate.OpenTrustedFile(path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + id, err := rootstate.Identity(file) |
| + file.Close() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + digest := sha256.Sum256(body) |
| + f.req.Executable = path |
| + f.req.Reservation.Executable = hostbox.LaunchFile{Device: id.Device, Inode: id.Inode} |
| + f.req.Spec.Executable = path |
| + f.req.Spec.Digest = hex.EncodeToString(digest[:]) |
| + f.req.Spec.Args = []string{"--socket", filepath.Join(provision.InstanceRunDir(host), "app.sock"), "--db", provision.NestedDBPath("/data", host)} |
| + f.req.Spec.Env = []string{"CARLOS_ADMIN_TOKEN=test", "PLAN14_TENANT_SOCKET=" + filepath.Join(provision.InstanceRunDir(host), "app.sock")} |
| + inputs, err := f.req.Spec.Hash() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + f.req.Reservation.Inputs = inputs |
| + f.claims.set(func(c *hostclaim.Claim) { c.Holder.WakeRetry.Attempt.LaunchInputs = inputs }) |
| + f.root.generations = &rootGenerationSource{store: store, optDir: "/opt/carlos", pub: func() (ed25519.PublicKey, error) { return pub, nil }, open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "generations"), true, false) }, openFile: rootstate.OpenTrustedFile} |
| + lock := &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "release"), true, false) }} |
| + if err := f.req.Validate(); err != nil { |
| + t.Fatal("request", err) |
| + } |
| + if _, err := f.root.launchSpec(f.req); err != nil { |
| + t.Fatal("launch spec", err) |
| + } |
| + if err := f.root.Kernel.Verify(f.req.Executable, f.req.Reservation.Executable); err != nil { |
| + t.Fatal("kernel build", err) |
| + } |
| + var response hostbox.RoleLaunchResponse |
| + err = lock.With(t.Context(), func(ctx context.Context) error { |
| + var err error |
| + response, err = f.root.Serve(ctx, f.peer, f.req) |
| + return err |
| + }) |
| + if err != nil { |
| + logs, _ := exec.Command("journalctl", "-u", unit, "--no-pager", "-n", "80").CombinedOutput() |
| + t.Fatalf("production launch: %v\n%s", err, logs) |
| + } |
| + transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { |
| + return (&net.Dialer{}).DialContext(ctx, "unix", filepath.Join(provision.InstanceRunDir(host), "app.sock")) |
| + }} |
| + defer transport.CloseIdleConnections() |
| + client := &http.Client{Transport: transport, Timeout: 3 * time.Second} |
| + resp, err := client.Get("http://tenant/") |
| + if err != nil { |
| + t.Fatal("installed tenant did not serve", err) |
| + } |
| + resp.Body.Close() |
| + transport.CloseIdleConnections() |
| + if resp.StatusCode != 200 { |
| + t.Fatal(resp.Status) |
| + } |
| + generations, err := rootstate.Open(filepath.Join(dir, "generations"), false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer generations.Close() |
| + ledger := &release.RootGenerations{Store: generations} |
| + records, err := ledger.Reservations(t.Context()) |
| + if err != nil || len(records) != 1 { |
| + t.Fatal(records, err) |
| + } |
| + v1 := records[0] |
| + if v1.Process.PID != response.Grant.PID || v1.Process.Containment != 1 || v1.Process.TenantPID == 0 { |
| + t.Fatal("missing production process evidence", v1) |
| + } |
| + pending := v1.Binding |
| + pending.Version = "v3" |
| + pending.Attempt = "pending-v3" |
| + pending.Invocation = "pending-v3" |
| + pending.Path = release.VersionPath("/opt/carlos", rec.Account, "v3", rec.App) |
| + pending.Tree = filepath.Dir(pending.Path) |
| + for _, v := range []struct { |
| + path string |
| + id *rootstate.FileIdentity |
| + }{{pending.Path, &pending.Executable}, {pending.Tree, &pending.TreeIdentity}} { |
| + file, err := os.Open(v.path) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + *v.id, err = rootstate.Identity(file) |
| + file.Close() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := lock.With(t.Context(), func(ctx context.Context) error { _, err := ledger.Reserve(pending); return err }); err != nil { |
| + t.Fatal(err) |
| + } |
| + retire := generationRetirement{guard: f.root.Guard, journal: journal, kernel: productionGenerationAbsence} |
| + if err := lock.With(t.Context(), func(ctx context.Context) error { return retire.retire(ctx, ledger, v1) }); err != nil { |
| + t.Fatal(err) |
| + } |
| + still, err := ledger.Read(v1.Binding) |
| + if err != nil || still.Retirement != "" { |
| + t.Fatal("live pin retired", err) |
| + } |
| + stop := f.req |
| + stop.Op = hostbox.RoleLaunchStop |
| + stop.Spec, stop.Executable, stop.MemoryMax = nil, "", 0 |
| + if _, err := f.root.Serve(t.Context(), f.peer, stop); err != nil { |
| + t.Fatal("production stop", err) |
| + } |
| + if err := journal.CloseWriters(t.Context(), f.target); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := lock.With(t.Context(), func(ctx context.Context) error { return retire.retire(ctx, ledger, v1) }); err == nil { |
| + t.Fatal("living controller released pin") |
| + } |
| + join() |
| + raw, _ := json.Marshal(f.target) |
| + if err := os.WriteFile(filepath.Join(dir, "target.json"), raw, 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + // Isolate the census from unrelated runner daemons, as the generation suite |
| + // does. The installed process and its controller were stopped and joined in |
| + // the outer namespace first; the child uses the retained root records. |
| + cmd := exec.CommandContext(t.Context(), "unshare", "--mount", "--pid", "--fork", "--mount-proc", binary, "-test.run=^TestPlan14InstalledClaimLaunchRootRunner$", "-test.v", "-test.timeout=2m") |
| + cmd.Env = append(os.Environ(), "PLAN14_INSTALLED_RETIRE_CHILD="+dir) |
| + if output, err := cmd.CombinedOutput(); err != nil { |
| + t.Fatalf("exact pin release: %v\n%s", err, output) |
| + } else { |
| + t.Logf("%s", output) |
| + } |
| +} |
| + |
| +func plan14InstalledRetire(t *testing.T, dir string) { |
| + state, err := rootstate.Open(filepath.Join(dir, "fences"), false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer state.Close() |
| + journal, err := openGenerationLaunchInventory(filepath.Join(dir, "launch.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer journal.Close() |
| + generations, err := rootstate.Open(filepath.Join(dir, "generations"), false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer generations.Close() |
| + ledger := &release.RootGenerations{Store: generations} |
| + records, err := ledger.Reservations(t.Context()) |
| + if err != nil || len(records) != 2 { |
| + t.Fatal(records, err) |
| + } |
| + var v1, v3 release.GenerationReservation |
| + for _, r := range records { |
| + if r.Binding.Version == "v1" { |
| + v1 = r |
| + } else { |
| + v3 = r |
| + } |
| + } |
| + if err := os.Mkdir(filepath.Join(dir, "retire-locks"), 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + verifier := generationRetirement{guard: hostguard.Guard{DataDir: "/data", LockDir: filepath.Join(dir, "retire-locks"), Root: hostguard.NewRootAuthority(state)}, journal: journal, kernel: productionGenerationAbsence} |
| + lock := &release.ReleaseExclusion{Open: func() (rootstate.Store, error) { return rootstate.Open(filepath.Join(dir, "release"), false, false) }} |
| + if err := lock.With(t.Context(), func(ctx context.Context) error { return verifier.retire(ctx, ledger, v1) }); err != nil { |
| + t.Fatal(err) |
| + } |
| + released, err := ledger.Read(v1.Binding) |
| + if err != nil || released.Retirement == "" { |
| + t.Fatal("exact pin not released", released, err) |
| + } |
| + pending, err := ledger.Read(v3.Binding) |
| + if err != nil || pending != v3 { |
| + t.Fatal("pending counterexample changed", pending, err) |
| + } |
| +} |
diff --git a/internal/host/plan14_timer_intention_test.go b/internal/host/plan14_timer_intention_test.go| index 5dfa6eaca..e3687b7b5 100644 |
| --- a/internal/host/plan14_timer_intention_test.go |
| +++ b/internal/host/plan14_timer_intention_test.go |
| @@ -5,6 +5,8 @@ import ( |
| "crypto/sha256" |
| "database/sql" |
| "encoding/hex" |
| + "encoding/json" |
| + "errors" |
| "fmt" |
| "os" |
| "path/filepath" |
| @@ -14,9 +16,12 @@ import ( |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/rootstate" |
| + "github.com/carlosframework/platform/internal/schedule" |
| + "time" |
| ) |
| func TestPlan14TimerIntentionBeforeMaterialization(t *testing.T) { |
| @@ -41,7 +46,7 @@ func (s *timerSyncFault) Write(name string, body []byte) error { |
| } |
| func plan14TimerIntention(t *testing.T, open func(*testing.T) rootstate.Store) { |
| - for _, attack := range []string{"none", "already-materialized", "late-intention", "changed-registration", "changed-row", "omit-intention", "crash-after-intention", "crash-after-row", "intention-sync", "materialization-sync"} { |
| + for _, attack := range []string{"none", "already-materialized", "late-intention", "changed-registration", "changed-row", "omit-intention", "crash-after-intention", "crash-after-row", "intention-sync", "materialization-sync", "replace", "remove", "replace-sync", "remove-sync", "replace-crash", "remove-crash", "replace-advanced-crash", "remove-advanced-crash", "producer", "bootstrap"} { |
| t.Run(attack, func(t *testing.T) { |
| t.Setenv("CARLOS_HOST_CLAIM", "1") |
| f := newRecoveryFixture(t) |
| @@ -61,10 +66,30 @@ func plan14TimerIntention(t *testing.T, open func(*testing.T) rootstate.Store) { |
| defer reg.Close() |
| sum := sha256.Sum256([]byte(fmt.Sprintf("%s\x00%s\x00%d\x00%s", o.Host, h.Incarnation, h.Stamp, "box-1"))) |
| owner := registry.ClaimOwnership{ClaimIncarnation: h.Incarnation, ClaimStamp: h.Stamp, ClaimOwner: hex.EncodeToString(sum[:]), ClaimRowID: "route"} |
| + var bootstrap *registry.Timer |
| + if attack == "bootstrap" { |
| + if err := reg.UpsertTimer(registry.Timer{Host: o.Host, Name: "daily", Kind: "schedule", Path: "/daily", Spec: "every 1h0m0s", Attempts: 2, NextAt: 17}); err != nil { |
| + t.Fatal(err) |
| + } |
| + bootstrap, err = reg.GetTimer(o.Host, "daily") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + owned := owner |
| + owned.ClaimRowID = bootstrap.Registration |
| + if err := reg.AdoptClaimTimer(t.Context(), *bootstrap, owned); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| if err := reg.Upsert(registry.Route{Host: o.Host, Account: h.Account, App: h.App, DBPath: provision.NestedDBPath(f.guard.DataDir, o.Host), ClaimOwnership: owner}); err != nil { |
| t.Fatal(err) |
| } |
| in := hostbox.StopInventory{Complete: true, Host: o.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: "box-1", Enrollment: "en", DirectoryRevision: "1", Entries: []hostbox.StopInventoryEntry{{Kind: "route", ArtifactID: "route", ManagedRoot: "registry", RelativePath: o.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}}} |
| + artifacts := hostguard.InstallationArtifacts{Schema: 1, StateDirectory: true} |
| + if bootstrap != nil { |
| + artifacts.Timers = bootstrap.Name |
| + in.Entries = append(in.Entries, hostbox.StopInventoryEntry{Kind: "timer", ArtifactID: bootstrap.Registration, ManagedRoot: "registry", RelativePath: o.Host + "/" + bootstrap.Name, RowIdentity: bootstrap.Registration, Ownership: owner.ClaimOwner, Materialization: "materialized"}) |
| + } |
| if err := journals.Inventory.WriteStopInventory(t.Context(), in); err != nil { |
| t.Fatal(err) |
| } |
| @@ -72,13 +97,26 @@ func plan14TimerIntention(t *testing.T, open func(*testing.T) rootstate.Store) { |
| state := &timerSyncFault{Store: open(t)} |
| defer state.Close() |
| g.Root = hostguard.NewRootAuthority(state) |
| - if err := g.EstablishRoot(t.Context(), hostguard.RootInstallation{Artifacts: hostguard.InstallationArtifacts{Schema: 1, StateDirectory: true}, Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Account: h.Account, App: h.App, Record: h.Key, Box: "box-1", Enrollment: "en", Migration: 1, InventoryRevision: 1}, func(*hostclaim.Claim) error { return nil }); err != nil { |
| + if err := g.EstablishRoot(t.Context(), hostguard.RootInstallation{Artifacts: artifacts, Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Account: h.Account, App: h.App, Record: h.Key, Box: "box-1", Enrollment: "en", Migration: 1, InventoryRevision: 1}, func(*hostclaim.Claim) error { return nil }); err != nil { |
| t.Fatal(err) |
| } |
| actor := hostbox.Actor{Box: "box-1", Enrollment: "en", Revision: 1} |
| root := ClaimRootHTTP{Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: g, Evidence: f.claims}}, Journals: journals, InventoryActor: func(context.Context) (hostbox.Actor, error) { return actor, nil }, Roots: map[string]string{"data": g.DataDir}, Reconcile: hostbox.ReconcileAdapter{Registry: reg, Census: &hostbox.EmptyInstallationCensus{}}} |
| var last hostbox.JournalRequest |
| _, client := hostbox.RemoteJournals(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { |
| + raw, err := json.Marshal(q) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + q = hostbox.JournalRequest{} |
| + if err := json.Unmarshal(raw, &q); err != nil { |
| + t.Fatal(err) |
| + } |
| + if q.Op == "inventory-version" { |
| + out, err := journals.Inventory.SnapshotRequest(ctx, q.Target) |
| + out.V, out.ID = 1, q.ID |
| + return out, err |
| + } |
| last = q |
| return root.commitRootInventory(ctx, q) |
| }) |
| @@ -92,6 +130,60 @@ func plan14TimerIntention(t *testing.T, open func(*testing.T) rootstate.Store) { |
| } |
| timer := registry.Timer{Host: o.Host, Name: "daily", Kind: "schedule", Spec: "every 1h0m0s", Path: "/daily", Registration: "planned", ClaimOwnership: owner} |
| timer.ClaimRowID = timer.Registration |
| + if attack == "producer" || attack == "bootstrap" { |
| + serviceGuard := f.guard |
| + if err := serviceGuard.Activate(t.Context(), o.Host, actor.Box, nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + serviceGuard.FenceView = g.Root.View |
| + adapter := hostbox.ReconcileAdapter{Registry: reg, Inventory: client, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1", Reconciler: hostbox.Reconciler{Guard: serviceGuard, Claims: f.claims}} |
| + source := hostbox.WorkSource{Guard: serviceGuard, Box: actor.Box, Region: "ie", Now: time.Now, Policy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil }, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }, Timer: adapter.MutateTimer} |
| + work, err := source.Select(t.Context(), o.Host, "timers") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + producer := schedule.SelectedTimers{Store: reg, Work: work, Context: t.Context(), Host: o.Host} |
| + timer.Registration = "" |
| + timer.ClaimOwnership = registry.ClaimOwnership{} |
| + if err := producer.UpsertTimer(timer); err != nil { |
| + t.Fatal("producer registration", err) |
| + } |
| + first, err := reg.GetTimer(o.Host, timer.Name) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := producer.UpsertTimer(timer); err != nil { |
| + t.Fatal("producer replacement", err) |
| + } |
| + second, err := reg.GetTimer(o.Host, timer.Name) |
| + if err != nil || first.Registration == second.Registration { |
| + t.Fatal("producer identity", second, err) |
| + } |
| + if err := producer.DeleteTimer(o.Host, timer.Name); err != nil { |
| + t.Fatal("producer withdrawal", err) |
| + } |
| + if _, err := reg.GetTimer(o.Host, timer.Name); !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("producer deletion", err) |
| + } |
| + if err := producer.UpsertTimer(timer); err != nil { |
| + t.Fatal("producer registration after withdrawal", err) |
| + } |
| + imported := timer |
| + imported.Registration = "imported" |
| + imported.Attempts, imported.LastAt, imported.NextAt, imported.CreatedAt = 3, 7, 11, 13 |
| + if err := producer.ReplaceTimers(o.Host, []registry.Timer{imported}); err != nil { |
| + t.Fatal("import", err) |
| + } |
| + if err := producer.ReplaceTimers(o.Host, []registry.Timer{imported}); err != nil { |
| + t.Fatal("import replay", err) |
| + } |
| + got, err := reg.GetTimer(o.Host, imported.Name) |
| + if err != nil || got.Registration != "imported" || got.Attempts != 3 || got.LastAt != 7 || got.NextAt != 11 || got.CreatedAt != 13 { |
| + t.Fatal("import identity and bookkeeping", got, err) |
| + } |
| + return |
| + } |
| + |
| row := func() { |
| t.Helper() |
| if err := reg.ReplaceTimers(o.Host, []registry.Timer{timer}); err != nil { |
| @@ -167,7 +259,7 @@ func plan14TimerIntention(t *testing.T, open func(*testing.T) rootstate.Store) { |
| expect.Revision++ |
| } |
| } |
| - valid := attack == "none" || attack == "crash-after-intention" || attack == "crash-after-row" || attack == "intention-sync" || attack == "materialization-sync" |
| + valid := attack == "none" || attack == "crash-after-intention" || attack == "crash-after-row" || attack == "intention-sync" || attack == "materialization-sync" || strings.HasPrefix(attack, "replace") || strings.HasPrefix(attack, "remove") |
| if (err == nil) != valid { |
| t.Fatalf("materialization valid=%t: %v", valid, err) |
| } |
| @@ -175,6 +267,94 @@ func plan14TimerIntention(t *testing.T, open func(*testing.T) rootstate.Store) { |
| if err := write(); err != nil { |
| t.Fatal("materialized replay", err) |
| } |
| + if strings.HasPrefix(attack, "replace") || strings.HasPrefix(attack, "remove") { |
| + mutation := hostbox.TimerMutation{Timer: timer, Expected: timer.Registration, Remove: strings.HasPrefix(attack, "remove")} |
| + if mutation.Remove { |
| + in.Entries[1].TimerRetired = true |
| + } else { |
| + mutation.Timer.Registration = "replacement" |
| + mutation.Timer.ClaimRowID = "replacement" |
| + in.Entries[1].RowIdentity = "replacement" |
| + } |
| + if strings.HasSuffix(attack, "sync") { |
| + state.phase = "complete" |
| + } |
| + if strings.HasSuffix(attack, "crash") { |
| + calls := 0 |
| + journals.Now = func() time.Time { |
| + calls++ |
| + if calls > 1 { |
| + return time.Now().Add(2 * hostbox.JournalRequestWindow) |
| + } |
| + return time.Now() |
| + } |
| + } |
| + revision, err := client.WriteTimerExpected(t.Context(), in, expect, mutation) |
| + if strings.HasSuffix(attack, "crash") { |
| + if err == nil { |
| + t.Fatal("journal fault did not interrupt mutation") |
| + } |
| + got, getErr := reg.GetTimer(timer.Host, timer.Name) |
| + if mutation.Remove { |
| + if !errors.Is(getErr, os.ErrNotExist) { |
| + t.Fatal("did not reach row removal", getErr) |
| + } |
| + } else if getErr != nil || got.Registration != mutation.Timer.Registration { |
| + t.Fatal("did not reach row replacement", got, getErr) |
| + } |
| + journals.Now = time.Now |
| + if strings.Contains(attack, "advanced") { |
| + key, _ := hostclaim.Key(o.Host) |
| + body, etag, err := f.claims.Get(t.Context(), key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := f.claims.Put(t.Context(), key, append(body, '\n'), etag); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := root.recoverRootInventory(t.Context()); err != nil { |
| + t.Fatal(err) |
| + } |
| + if strings.Contains(attack, "advanced") { |
| + snapshot, err := journals.Inventory.SnapshotRequest(t.Context(), expect.Target) |
| + if err != nil || snapshot.Revision != expect.Revision+1 || snapshot.Inventory.Entries[1] != in.Entries[1] { |
| + t.Fatal("advanced claim lost pending effect", snapshot, err) |
| + } |
| + pending, err := g.Root.PendingInstallations() |
| + if err != nil || len(pending) != 0 { |
| + t.Fatal("effect still pending", pending, err) |
| + } |
| + return |
| + } |
| + reply, replayErr := root.commitRootInventory(t.Context(), last) |
| + err, revision = replayErr, reply.Revision |
| + } |
| + |
| + if strings.HasSuffix(attack, "sync") { |
| + if err == nil { |
| + t.Fatal("acknowledged failed sync") |
| + } |
| + reply, replayErr := root.commitRootInventory(t.Context(), last) |
| + err, revision = replayErr, reply.Revision |
| + } |
| + if err != nil || revision != expect.Revision+1 { |
| + t.Fatal("timer mutation", revision, err) |
| + } |
| + expect.Revision = revision |
| + got, err := reg.GetTimer(o.Host, timer.Name) |
| + if mutation.Remove { |
| + if !errors.Is(err, os.ErrNotExist) { |
| + t.Fatal("timer survived", got, err) |
| + } |
| + } else if err != nil || got.Registration != "replacement" { |
| + t.Fatal("replacement missing", got, err) |
| + } |
| + if _, err := root.commitRootInventory(t.Context(), last); err != nil { |
| + t.Fatal("mutation replay", err) |
| + } |
| + } |
| + |
| got, err := journals.Inventory.ReadStopInventory(t.Context(), o.Host) |
| if err != nil || len(got.Entries) != 2 || got.Entries[1] != in.Entries[1] { |
| t.Fatal("lost exact timer", got, err) |
diff --git a/internal/host/rootgeneration_inventory.go b/internal/host/rootgeneration_inventory.go| index 008c06b72..b635f33ad 100644 |
| --- a/internal/host/rootgeneration_inventory.go |
| +++ b/internal/host/rootgeneration_inventory.go |
| @@ -218,7 +218,7 @@ func generationPayloadBindings(payloads []hostbox.GenerationPayload, installs ma |
| return os.ErrPermission |
| } |
| for _, entry := range in.Entries { |
| - if entry.Materialization != "materialized" { |
| + if entry.Materialization != "materialized" && !(entry.Kind == "timer" && entry.Materialization == "intended") { |
| return os.ErrPermission |
| } |
| } |
| @@ -241,6 +241,14 @@ func generationPayloadBindings(payloads []hostbox.GenerationPayload, installs ma |
| } |
| func productionGenerationInventory(ctx context.Context) (generationInventory, error) { |
| + return readProductionGenerationInventory(ctx, false) |
| +} |
| + |
| +func productionLocalGenerationInventory(ctx context.Context) (generationInventory, error) { |
| + return readProductionGenerationInventory(ctx, true) |
| +} |
| + |
| +func readProductionGenerationInventory(ctx context.Context, local bool) (generationInventory, error) { |
| snapshot, err := generationInventoryAt(ctx, rootparticipation.Namespace, "/var/lib/carlos-claims/authority/fences", "/var/lib/carlos-claims/journals.db") |
| if err != nil { |
| return generationInventory{}, err |
| @@ -252,6 +260,13 @@ func productionGenerationInventory(ctx context.Context) (generationInventory, er |
| if len(snapshot.Installations) == 0 { |
| return productionGenerationRegistryInventory(ctx, snapshot) |
| } |
| + if local { |
| + snapshot, err = generationLocalObligations(ctx, snapshot, productionGenerationCreation) |
| + if err != nil { |
| + return generationInventory{}, err |
| + } |
| + return productionGenerationRegistryInventory(ctx, snapshot) |
| + } |
| store, err := openDeployStore(ctx) |
| if err != nil { |
| return generationInventory{}, err |
diff --git a/internal/host/rootgeneration_parked.go b/internal/host/rootgeneration_parked.go| index b4eaba56f..99f2f159a 100644 |
| --- a/internal/host/rootgeneration_parked.go |
| +++ b/internal/host/rootgeneration_parked.go |
| @@ -66,10 +66,10 @@ func generationRetirements(ctx context.Context, snapshot generationInventory) (m |
| } |
| expected := map[hostbox.Artifact]bool{} |
| for _, e := range snapshot.Payloads[index].Inventory.Entries { |
| - if e.Ownership != j.Inventory.Ownership || e.Materialization != "materialized" { |
| + if e.Ownership != j.Inventory.Ownership || (e.Materialization != "materialized" && !(e.Kind == "timer" && e.Materialization == "intended")) { |
| return nil, os.ErrPermission |
| } |
| - a := hostbox.Artifact{Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Dev: e.Device, Ino: e.Inode, Type: e.Type} |
| + a := hostbox.Artifact{AllowAbsent: e.Kind == "timer" && (e.TimerRetired || e.Materialization == "intended"), Kind: hostbox.ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Dev: e.Device, Ino: e.Inode, Type: e.Type} |
| if expected[a] { |
| return nil, os.ErrPermission |
| } |
| @@ -106,6 +106,16 @@ func generationPhysicalPayloads(ctx context.Context, snapshot generationInventor |
| if err != nil { |
| return nil, err |
| } |
| + for i, payload := range snapshot.Payloads { |
| + if retired[i] != "" { |
| + continue |
| + } |
| + for _, entry := range payload.Inventory.Entries { |
| + if entry.Materialization != "materialized" { |
| + return nil, os.ErrPermission |
| + } |
| + } |
| + } |
| payloads := append([]hostbox.GenerationPayload(nil), snapshot.Payloads...) |
| for i, id := range retired { |
| in := &payloads[i].Inventory |
diff --git a/internal/host/rootgeneration_requirements.go b/internal/host/rootgeneration_requirements.go| index 4eb25f12e..b1d3db1e4 100644 |
| --- a/internal/host/rootgeneration_requirements.go |
| +++ b/internal/host/rootgeneration_requirements.go |
| @@ -22,13 +22,6 @@ func generationRequiredInventory(ctx context.Context, store release.Store, snaps |
| if store == nil { |
| return zero, os.ErrPermission |
| } |
| - installs := map[string]hostguard.FenceView{} |
| - for _, view := range snapshot.Installations { |
| - installs[view.Host] = view |
| - } |
| - if err := generationPayloadBindings(snapshot.Payloads, installs); err != nil { |
| - return zero, err |
| - } |
| snapshot.Records = nil |
| for _, view := range snapshot.Installations { |
| if err := ctx.Err(); err != nil { |
| @@ -53,6 +46,39 @@ func generationRequiredInventory(ctx context.Context, store release.Store, snaps |
| return zero, os.ErrPermission |
| } |
| snapshot.Records = append(snapshot.Records, generationRecord{Key: in.Record, ETag: etag, Record: *rec}) |
| + |
| + } |
| + return generationLocalObligations(ctx, snapshot, fresh) |
| +} |
| + |
| +func generationLocalObligations(ctx context.Context, snapshot generationInventory, fresh func(hostguard.RootInstallation) error) (generationInventory, error) { |
| + var zero generationInventory |
| + installs := map[string]hostguard.FenceView{} |
| + for _, view := range snapshot.Installations { |
| + installs[view.Host] = view |
| + } |
| + if err := generationPayloadBindings(snapshot.Payloads, installs); err != nil { |
| + return zero, err |
| + } |
| + if err := generationRetainedTimers(snapshot); err != nil { |
| + return zero, err |
| + } |
| + for _, view := range snapshot.Installations { |
| + if err := ctx.Err(); err != nil { |
| + return zero, err |
| + } |
| + in := view.Installation |
| + if !release.ValidAccount(in.Account) || !release.ValidApp(in.App) || in.Record != provision.RecordKey(in.Account, in.App, in.Host) { |
| + return zero, os.ErrPermission |
| + } |
| + if _, err := hostclaim.Key(in.Host); err != nil { |
| + return zero, err |
| + } |
| + for _, record := range snapshot.Records { |
| + if record.Key == in.Record && record.Record.DerivesPaths() { |
| + in.Artifacts.StateDirectory = true |
| + } |
| + } |
| for _, p := range snapshot.Payloads { |
| payload := p.Inventory |
| if payload.Host != in.Host { |
| @@ -78,14 +104,14 @@ func generationRequiredInventory(ctx context.Context, store release.Store, snaps |
| } |
| continue |
| } |
| - state, err := filepath.Rel(dataDir(), provision.InstanceDir(dataDir(), rec.Host)) |
| + state, err := filepath.Rel(dataDir(), provision.InstanceDir(dataDir(), in.Host)) |
| if err != nil || !filepath.IsLocal(state) || state == "." { |
| return zero, os.ErrPermission |
| } |
| routes, directories := 0, 0 |
| for _, entry := range payload.Entries { |
| if entry.Kind == "route" { |
| - if entry.ManagedRoot != "registry" || entry.RelativePath != rec.Host || entry.RowIdentity == "" { |
| + if entry.ManagedRoot != "registry" || entry.RelativePath != in.Host || entry.RowIdentity == "" { |
| return zero, os.ErrPermission |
| } |
| routes++ |
| @@ -97,7 +123,7 @@ func generationRequiredInventory(ctx context.Context, store release.Store, snaps |
| directories++ |
| } |
| } |
| - if routes != 1 || ((rec.DerivesPaths() || in.Artifacts.StateDirectory) && directories != 1) { |
| + if routes != 1 || (in.Artifacts.StateDirectory && directories != 1) { |
| return zero, os.ErrPermission |
| } |
| } |
diff --git a/internal/host/rootgeneration_rows.go b/internal/host/rootgeneration_rows.go| index 916208c29..417fcfa53 100644 |
| --- a/internal/host/rootgeneration_rows.go |
| +++ b/internal/host/rootgeneration_rows.go |
| @@ -62,6 +62,9 @@ func generationRegistryInventory(ctx context.Context, reg *registry.Registry, sn |
| if _, exists := expected[k]; exists { |
| return zero, os.ErrPermission |
| } |
| + if entry.TimerRetired { |
| + continue |
| + } |
| expected[k] = own |
| if len(expected) > 8192 { |
| return zero, os.ErrPermission |
diff --git a/internal/host/rootinventory.go b/internal/host/rootinventory.go| index d226e0cdf..5d938ff0c 100644 |
| --- a/internal/host/rootinventory.go |
| +++ b/internal/host/rootinventory.go |
| @@ -185,7 +185,7 @@ func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox |
| continue |
| } |
| if e.Kind == "timer" { |
| - if err := h.verifyInventoryTimer(in.Host, canonical.ClaimOwnership, e); err != nil { |
| + if err := h.verifyTimerMutationRow(in.Host, canonical.ClaimOwnership, e, q.Timer); err != nil { |
| return err |
| } |
| continue |
| @@ -231,7 +231,7 @@ func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox |
| if revision != q.Revision { |
| return hostbox.ErrChanged |
| } |
| - if err := h.verifyNewTimerIntentions(prior, in); err != nil { |
| + if err := h.verifyTimerMutation(prior, in, q.Timer); err != nil { |
| return err |
| } |
| if revision == 0 { |
| @@ -254,6 +254,9 @@ func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox |
| found := false |
| for _, next := range in.Entries { |
| if old.ArtifactID == next.ArtifactID { |
| + if q.Timer != nil && old.Kind == "timer" && old.RelativePath == q.Timer.Timer.Host+"/"+q.Timer.Timer.Name { |
| + old.RowIdentity, old.Materialization, old.TimerRetired = next.RowIdentity, next.Materialization, next.TimerRetired |
| + } |
| if old.Materialization == "intended" && next.Materialization == "materialized" { |
| old.Materialization = next.Materialization |
| old.Device = next.Device |
| @@ -286,6 +289,11 @@ func (h ClaimRootHTTP) commitRootInventoryPending(ctx context.Context, q hostbox |
| if replayed { |
| return nil |
| } |
| + if q.Timer != nil { |
| + if err := h.applyTimerMutation(*q.Timer); err != nil { |
| + return err |
| + } |
| + } |
| if firstLanding { |
| out, err = h.Journals.ExecuteRootInventory(ctx, actor.Box+"/"+actor.Enrollment, q, in, pending) |
| } else if pending { |
| @@ -373,6 +381,11 @@ func (h ClaimRootHTTP) recoverRootInventory(ctx context.Context) error { |
| if err = json.Unmarshal(raw, &q); err != nil { |
| return err |
| } |
| + if settled, settleErr := h.settleTimerInventory(ctx, q, raw); settled { |
| + continue |
| + } else if settleErr != nil { |
| + return settleErr |
| + } |
| if _, err = h.withRootInventoryHistory(ctx, q, true); err != nil { |
| if errors.Is(err, hostbox.ErrChanged) || errors.Is(err, hostbox.ErrJournalUncertain) { |
| err = errors.Join(err, h.Stop.Coordinator.Guard.AbandonRootInventory(ctx, q.Target.Host, raw, func(c *hostclaim.Claim) bool { |
diff --git a/internal/host/rootinventory_timer.go b/internal/host/rootinventory_timer.go| index 39f178e38..fa347e57b 100644 |
| --- a/internal/host/rootinventory_timer.go |
| +++ b/internal/host/rootinventory_timer.go |
| @@ -21,12 +21,12 @@ func (h ClaimRootHTTP) verifyInventoryTimer(host string, owner registry.ClaimOwn |
| return err |
| } |
| if timer == nil { |
| - if e.Materialization != "intended" { |
| + if e.Materialization != "intended" && !e.TimerRetired { |
| return hostbox.ErrTypedInventory |
| } |
| return nil |
| } |
| - if (e.Materialization != "intended" && e.Materialization != "materialized") || timer.Host != host || timer.Name != name || timer.ClaimIncarnation != owner.ClaimIncarnation || timer.ClaimStamp != owner.ClaimStamp || timer.ClaimOwner != e.Ownership || timer.ClaimRowID != e.RowIdentity || timer.Registration != e.RowIdentity { |
| + if e.TimerRetired || (e.Materialization != "intended" && e.Materialization != "materialized") || timer.Host != host || timer.Name != name || timer.ClaimIncarnation != owner.ClaimIncarnation || timer.ClaimStamp != owner.ClaimStamp || timer.ClaimOwner != e.Ownership || timer.ClaimRowID != e.RowIdentity || timer.Registration != e.RowIdentity { |
| return hostbox.ErrTypedInventory |
| } |
| return nil |
| @@ -34,6 +34,9 @@ func (h ClaimRootHTTP) verifyInventoryTimer(host string, owner registry.ClaimOwn |
| func (h ClaimRootHTTP) verifyNewTimerIntentions(prior, next hostbox.StopInventory) error { |
| for _, e := range next.Entries { |
| + if e.TimerRetired && e.Kind != "timer" { |
| + return hostbox.ErrTypedInventory |
| + } |
| if e.Kind != "timer" { |
| continue |
| } |
| @@ -47,7 +50,7 @@ func (h ClaimRootHTTP) verifyNewTimerIntentions(prior, next hostbox.StopInventor |
| if known { |
| continue |
| } |
| - if e.Materialization != "intended" { |
| + if e.Materialization != "intended" || e.TimerRetired { |
| return hostbox.ErrTypedInventory |
| } |
| row, err := h.Reconcile.Registry.GetTimer(next.Host, strings.TrimPrefix(e.RelativePath, next.Host+"/")) |
diff --git a/internal/host/rootinventory_timer_mutation.go b/internal/host/rootinventory_timer_mutation.go| new file mode 100644 |
| index 000000000..c5b0f5840 |
| --- /dev/null |
| +++ b/internal/host/rootinventory_timer_mutation.go |
| @@ -0,0 +1,114 @@ |
| +package host |
| + |
| +import ( |
| + "errors" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "github.com/carlosframework/platform/internal/schedule" |
| + "os" |
| +) |
| + |
| +func (h ClaimRootHTTP) verifyTimerMutationRow(host string, owner registry.ClaimOwnership, e hostbox.StopInventoryEntry, m *hostbox.TimerMutation) error { |
| + if m == nil || e.RelativePath != m.Timer.Host+"/"+m.Timer.Name { |
| + return h.verifyInventoryTimer(host, owner, e) |
| + } |
| + t := m.Timer |
| + if t.Host != host || !schedule.ValidName(t.Name) || !t.ClaimOwnership.Valid() || t.ClaimIncarnation != owner.ClaimIncarnation || t.ClaimStamp != owner.ClaimStamp || t.ClaimOwner != owner.ClaimOwner || t.ClaimRowID != t.Registration || t.Registration == "" || e.Kind != "timer" || e.ManagedRoot != "registry" || e.Ownership != owner.ClaimOwner || e.RowIdentity != t.Registration || e.Materialization != "materialized" || e.TimerRetired != m.Remove || e.Device != 0 || e.Inode != 0 || e.Type != 0 { |
| + return hostbox.ErrTypedInventory |
| + } |
| + if t.Kind != schedule.KindOnce && t.Kind != schedule.KindSchedule { |
| + return hostbox.ErrTypedInventory |
| + } |
| + if schedule.ValidPath(t.Path) != nil { |
| + return hostbox.ErrTypedInventory |
| + } |
| + row, err := h.Reconcile.Registry.GetTimer(host, t.Name) |
| + if errors.Is(err, os.ErrNotExist) { |
| + if m.Expected == "" || m.Remove { |
| + return nil |
| + } |
| + return hostbox.ErrChanged |
| + } |
| + if err != nil { |
| + return err |
| + } |
| + if row == nil || row.ClaimIncarnation != t.ClaimIncarnation || row.ClaimStamp != t.ClaimStamp || row.ClaimOwner != t.ClaimOwner || row.ClaimRowID != row.Registration || row.Kind != t.Kind { |
| + return hostbox.ErrChanged |
| + } |
| + if *row == t || row.Registration == m.Expected { |
| + return nil |
| + } |
| + return hostbox.ErrChanged |
| +} |
| + |
| +func (h ClaimRootHTTP) verifyTimerMutation(prior, next hostbox.StopInventory, m *hostbox.TimerMutation) error { |
| + if m == nil { |
| + return h.verifyNewTimerIntentions(prior, next) |
| + } |
| + var old, entry *hostbox.StopInventoryEntry |
| + name := m.Timer.Host + "/" + m.Timer.Name |
| + for i := range prior.Entries { |
| + e := &prior.Entries[i] |
| + if e.Kind == "timer" && e.RelativePath == name { |
| + if old != nil { |
| + return hostbox.ErrTypedInventory |
| + } |
| + old = e |
| + } |
| + } |
| + for i := range next.Entries { |
| + e := &next.Entries[i] |
| + if e.Kind == "timer" && e.RelativePath == name { |
| + if entry != nil { |
| + return hostbox.ErrTypedInventory |
| + } |
| + entry = e |
| + } |
| + } |
| + if entry == nil { |
| + return hostbox.ErrTypedInventory |
| + } |
| + if old == nil || old.TimerRetired { |
| + if m.Expected != "" || m.Remove { |
| + return hostbox.ErrChanged |
| + } |
| + } else if old.RowIdentity != m.Expected { |
| + return hostbox.ErrChanged |
| + } |
| + if old != nil && (old.ArtifactID != entry.ArtifactID || old.Ownership != entry.Ownership || old.ManagedRoot != entry.ManagedRoot) { |
| + return hostbox.ErrChanged |
| + } |
| + if m.Remove && (old == nil || old.RowIdentity != entry.RowIdentity) { |
| + return hostbox.ErrChanged |
| + } |
| + if !m.Remove && m.Expected != "" && m.Expected == entry.RowIdentity { |
| + return hostbox.ErrChanged |
| + } |
| + // Check every other new entry through the ordinary intention rules. |
| + filtered := next |
| + filtered.Entries = nil |
| + for _, e := range next.Entries { |
| + if e.RelativePath != name || e.Kind != "timer" { |
| + filtered.Entries = append(filtered.Entries, e) |
| + } |
| + } |
| + return h.verifyNewTimerIntentions(prior, filtered) |
| +} |
| + |
| +func (h ClaimRootHTTP) applyTimerMutation(m hostbox.TimerMutation) error { |
| + if m.Remove { |
| + err := h.Reconcile.Registry.DeleteTimerOwnedRegistration(m.Timer) |
| + if errors.Is(err, registry.ErrTimerChanged) { |
| + _, err = h.Reconcile.Registry.GetTimer(m.Timer.Host, m.Timer.Name) |
| + if errors.Is(err, os.ErrNotExist) { |
| + return nil |
| + } |
| + if err == nil { |
| + return hostbox.ErrChanged |
| + } |
| + } |
| + return err |
| + } |
| + _, err := h.Reconcile.Registry.MaterializeTimerRegistration(m.Timer, m.Expected) |
| + return err |
| +} |
diff --git a/internal/host/rootinventory_timer_recovery.go b/internal/host/rootinventory_timer_recovery.go| new file mode 100644 |
| index 000000000..66b5c65be |
| --- /dev/null |
| +++ b/internal/host/rootinventory_timer_recovery.go |
| @@ -0,0 +1,51 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "os" |
| +) |
| + |
| +// A row mutation can commit before the inventory journal. Settle that exact |
| +// root intention locally even if the claim has since advanced. This grants no |
| +// new timer write: SQL is checked only against the effect root already intended. |
| +func (h ClaimRootHTTP) settleTimerInventory(ctx context.Context, q hostbox.JournalRequest, raw []byte) (bool, error) { |
| + if q.Timer == nil { |
| + return false, nil |
| + } |
| + if h.Reconcile.Registry == nil || h.Journals == nil { |
| + return false, hostbox.ErrUnsupported |
| + } |
| + m := q.Timer |
| + applied := func() (bool, error) { |
| + row, err := h.Reconcile.Registry.GetTimer(m.Timer.Host, m.Timer.Name) |
| + if errors.Is(err, os.ErrNotExist) { |
| + return m.Remove, nil |
| + } |
| + if err != nil { |
| + return false, err |
| + } |
| + return !m.Remove && row != nil && *row == m.Timer, nil |
| + } |
| + done, err := applied() |
| + if err != nil || !done { |
| + return false, err |
| + } |
| + err = h.Stop.Coordinator.Guard.SettleRootInventory(ctx, q.Target.Host, raw, func(in hostguard.RootInstallation) error { |
| + if in.Host != q.Inventory.Host || in.Incarnation != q.Inventory.Incarnation || in.Stamp != q.Inventory.InstallationStamp || in.InventoryRevision != q.Revision+1 { |
| + return hostbox.ErrChanged |
| + } |
| + exact, err := applied() |
| + if err != nil { |
| + return err |
| + } |
| + if !exact { |
| + return hostbox.ErrChanged |
| + } |
| + _, err = h.Journals.CompletePendingInventory(ctx, in.Box+"/"+in.Enrollment, q) |
| + return err |
| + }) |
| + return err == nil, err |
| +} |
diff --git a/internal/host/rootlaunchverifier.go b/internal/host/rootlaunchverifier.go| index d0b11348d..c3fa6ac0b 100644 |
| --- a/internal/host/rootlaunchverifier.go |
| +++ b/internal/host/rootlaunchverifier.go |
| @@ -63,7 +63,7 @@ func registerProductionLaunchVerifier() error { |
| rootLaunchStartup.Do(func() { |
| v := rootLaunchVerifier{exclude: participationRelease, |
| open: func() (rootstate.Store, error) { return rootstate.Open(release.RootGenerationsDir, false, false) }, |
| - reconcile: generationReconciler{read: productionGenerationInventory, kernel: productionGenerationKernelCensus}, |
| + reconcile: generationReconciler{read: productionLocalGenerationInventory, kernel: productionGenerationKernelCensus}, |
| } |
| rootLaunchStartup.err = rootparticipation.RegisterLaunchVerifier(v.verify, participationRelease.With) |
| }) |
diff --git a/internal/hostbox/coldhandover.go b/internal/hostbox/coldhandover.go| index 166d489a0..9c023604e 100644 |
| --- a/internal/hostbox/coldhandover.go |
| +++ b/internal/hostbox/coldhandover.go |
| @@ -209,6 +209,25 @@ func (h ColdHandover) Run(ctx context.Context, prior hostguard.FenceTarget, next |
| // An intended path is an intent PrepareState persisted before its |
| // mkdir and never finalized. It carries no captured identity, so it |
| // is handled only by retireIntended below, never as an artifact. |
| + if entry.Kind == "timer" && (entry.Materialization == "intended" || entry.TimerRetired) { |
| + if entry.Device != 0 || entry.Inode != 0 || entry.Type != 0 || entry.ManagedRoot != "registry" || entry.RowIdentity == "" { |
| + return ErrTypedInventory |
| + } |
| + present := false |
| + for _, timer := range j.Before.Timers { |
| + if entry.RelativePath != timer.Host+"/"+timer.Name { |
| + continue |
| + } |
| + if entry.TimerRetired || timer.Registration != entry.RowIdentity || timer.ClaimRowID != entry.RowIdentity || timer.ClaimOwner != entry.Ownership { |
| + return ErrTypedInventory |
| + } |
| + present = true |
| + } |
| + if !present { |
| + inventory.Artifacts = append(inventory.Artifacts, Artifact{Kind: ArtifactTimer, Root: entry.ManagedRoot, Name: entry.RelativePath, Owner: entry.Ownership, RowID: entry.RowIdentity + "~" + entry.RowIdentity, AllowAbsent: true}) |
| + } |
| + continue |
| + } |
| if entry.Materialization == "intended" { |
| if entry.Kind != "path" || entry.Device != 0 || entry.Inode != 0 || !managedName(entry.RelativePath) || h.Roots[entry.ManagedRoot] == "" { |
| return ErrTypedInventory |
diff --git a/internal/hostbox/coldhandover_test.go b/internal/hostbox/coldhandover_test.go| index 0ab0ac6e7..945a2ce93 100644 |
| --- a/internal/hostbox/coldhandover_test.go |
| +++ b/internal/hostbox/coldhandover_test.go |
| @@ -16,6 +16,14 @@ import ( |
| ) |
| func TestColdHandoverParksFilesRestoresOwnedRowsAndNeverReplaysOverStartedSuccessor(t *testing.T) { |
| + coldHandoverTimers(t, false) |
| +} |
| + |
| +func TestPlan14ColdHandoverAbandonedTimers(t *testing.T) { |
| + coldHandoverTimers(t, true) |
| +} |
| + |
| +func coldHandoverTimers(t *testing.T, abandoned bool) { |
| t.Setenv("CARLOS_HOST_CLAIM", "1") |
| source, store, initial := workFixture(t) |
| ctx := t.Context() |
| @@ -74,6 +82,11 @@ func TestColdHandoverParksFilesRestoresOwnedRowsAndNeverReplaysOverStartedSucces |
| t.Fatal(err) |
| } |
| in := StopInventory{Complete: true, Host: old.Host, Incarnation: prior.Incarnation, InstallationStamp: prior.Stamp, Box: "box", Enrollment: "enrollment", DirectoryRevision: "1", Entries: []StopInventoryEntry{{Kind: "route", ArtifactID: owner.ClaimRowID, ManagedRoot: "registry", RelativePath: old.Host, RowIdentity: owner.ClaimRowID, Ownership: owner.ClaimOwner, Materialization: "materialized"}, {Kind: "path", ArtifactID: "database", ManagedRoot: "data", RelativePath: artifact.Name, Ownership: artifact.Owner, Materialization: "materialized", Device: artifact.Dev, Inode: artifact.Ino, Type: artifact.Type}}} |
| + if abandoned { |
| + for _, name := range []string{"abandoned", "withdrawn"} { |
| + in.Entries = append(in.Entries, StopInventoryEntry{Kind: "timer", ArtifactID: name, ManagedRoot: "registry", RelativePath: old.Host + "/" + name, Ownership: owner.ClaimOwner, RowIdentity: name, Materialization: "intended", TimerRetired: name == "withdrawn"}) |
| + } |
| + } |
| if err = inventory.WriteStopInventory(ctx, in); err != nil { |
| t.Fatal(err) |
| } |
| @@ -136,7 +149,7 @@ func TestColdHandoverParksFilesRestoresOwnedRowsAndNeverReplaysOverStartedSucces |
| t.Fatalf("timer bookkeeping not retained: %+v %v", after, err) |
| } |
| retained, err := inventory.ReadInstallation(ctx, old.Host, prior.Incarnation, prior.Stamp) |
| - if err != nil || len(retained.Entries) != 2 { |
| + if err != nil || len(retained.Entries) != len(in.Entries) { |
| t.Fatal("predecessor inventory discarded") |
| } |
| if err = os.WriteFile(dbPath, []byte("new-runtime-data"), 0600); err != nil { |
diff --git a/internal/hostbox/inventory_expected.go b/internal/hostbox/inventory_expected.go| index 20a950ef6..9bfbb54b4 100644 |
| --- a/internal/hostbox/inventory_expected.go |
| +++ b/internal/hostbox/inventory_expected.go |
| @@ -2,7 +2,9 @@ package hostbox |
| import ( |
| "context" |
| + "encoding/json" |
| "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/registry" |
| ) |
| type InventoryExpectation struct { |
| @@ -26,11 +28,23 @@ func (d *StopInventoryDB) Snapshot(ctx context.Context, host string, captured ho |
| return out.Inventory, out.Revision, err |
| } |
| +type TimerMutation struct { |
| + Timer registry.Timer |
| + Expected string |
| + Remove bool |
| +} |
| + |
| +func (d *StopInventoryDB) WriteTimerExpected(ctx context.Context, in StopInventory, expected InventoryExpectation, timer TimerMutation) (uint64, error) { |
| + return d.writeExpected(ctx, in, expected, &timer) |
| +} |
| func (d *StopInventoryDB) WriteExpected(ctx context.Context, in StopInventory, expected InventoryExpectation) (uint64, error) { |
| + return d.writeExpected(ctx, in, expected, nil) |
| +} |
| +func (d *StopInventoryDB) writeExpected(ctx context.Context, in StopInventory, expected InventoryExpectation, timer *TimerMutation) (uint64, error) { |
| if d == nil || d.remote == nil || expected.Target.Host != in.Host || expected.Target.Incarnation != in.Incarnation || expected.Target.Stamp != in.InstallationStamp { |
| return 0, ErrTypedInventory |
| } |
| - out, err := journalCall(ctx, d.remote, JournalRequest{Op: "inventory-write", Inventory: in, Revision: expected.Revision, Target: expected.Target, RecordKey: expected.RecordKey, RecordETag: expected.RecordETag}) |
| + out, err := journalCall(ctx, d.remote, JournalRequest{Timer: timer, Op: "inventory-write", Inventory: in, Revision: expected.Revision, Target: expected.Target, RecordKey: expected.RecordKey, RecordETag: expected.RecordETag}) |
| if err != nil { |
| return 0, err |
| } |
| @@ -51,3 +65,24 @@ func (d *StopInventoryDB) SnapshotRequest(ctx context.Context, target hostguard. |
| } |
| return d.inventoryVersion(ctx, target) |
| } |
| + |
| +func (m TimerMutation) MarshalJSON() ([]byte, error) { |
| + type wire TimerMutation |
| + return json.Marshal(struct { |
| + wire |
| + Registration string |
| + }{wire(m), m.Timer.Registration}) |
| +} |
| +func (m *TimerMutation) UnmarshalJSON(raw []byte) error { |
| + type wire TimerMutation |
| + var v struct { |
| + wire |
| + Registration string |
| + } |
| + if err := json.Unmarshal(raw, &v); err != nil { |
| + return err |
| + } |
| + *m = TimerMutation(v.wire) |
| + m.Timer.Registration = v.Registration |
| + return nil |
| +} |
diff --git a/internal/hostbox/inventoryscope.go b/internal/hostbox/inventoryscope.go| index 4f9c3f176..4b85d39f1 100644 |
| --- a/internal/hostbox/inventoryscope.go |
| +++ b/internal/hostbox/inventoryscope.go |
| @@ -24,6 +24,9 @@ func ValidateAgentInventory(in StopInventory, row *registry.Route, dataDir strin |
| } |
| seen := make(map[string]bool) |
| for _, e := range in.Entries { |
| + if e.TimerRetired && e.Kind != "timer" { |
| + return ErrTypedInventory |
| + } |
| key := e.ManagedRoot + "\x00" + e.RelativePath |
| if seen[key] || !managedName(e.RelativePath) || (row != nil && e.Ownership != row.ClaimOwner) { |
| return ErrTypedInventory |
diff --git a/internal/hostbox/journalservice.go b/internal/hostbox/journalservice.go| index 642251c50..bade850b0 100644 |
| --- a/internal/hostbox/journalservice.go |
| +++ b/internal/hostbox/journalservice.go |
| @@ -64,6 +64,7 @@ func journalPrepare(req JournalRequest, now time.Time) (JournalRequest, error) { |
| // ID is retained by the transport across retries. Root commits the request and |
| // its result in the same FULL-synchronous transaction as the journal change. |
| type JournalRequest struct { |
| + Timer *TimerMutation `json:"timer,omitempty"` |
| RecordKey string `json:"record_key,omitempty"` |
| RecordETag string `json:"record_etag,omitempty"` |
| Revision uint64 `json:"inventory_revision"` |
diff --git a/internal/hostbox/reconcile_adapter.go b/internal/hostbox/reconcile_adapter.go| index 51826df67..550cc9356 100644 |
| --- a/internal/hostbox/reconcile_adapter.go |
| +++ b/internal/hostbox/reconcile_adapter.go |
| @@ -310,10 +310,10 @@ func (a ReconcileAdapter) Retire(ctx context.Context, row registry.Route) error |
| } |
| inventory := RetirementInventory{Host: row.Host, Captured: hostguard.Captured{Incarnation: row.ClaimIncarnation, Stamp: row.ClaimStamp}, Ownership: row.ClaimOwner} |
| for _, e := range in.Entries { |
| - if e.Ownership != row.ClaimOwner || e.Materialization != "materialized" { |
| + if e.Ownership != row.ClaimOwner || (e.Materialization != "materialized" && !(a.RequireTimerInventory && e.Kind == "timer" && e.Materialization == "intended")) { |
| return ErrTypedInventory |
| } |
| - inventory.Artifacts = append(inventory.Artifacts, Artifact{Kind: ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Dev: e.Device, Ino: e.Inode, Type: e.Type}) |
| + inventory.Artifacts = append(inventory.Artifacts, Artifact{AllowAbsent: e.Kind == "timer" && (e.TimerRetired || e.Materialization == "intended"), Kind: ArtifactKind(e.Kind), Root: e.ManagedRoot, Name: e.RelativePath, Owner: e.Ownership, RowID: e.RowIdentity, Dev: e.Device, Ino: e.Inode, Type: e.Type}) |
| } |
| if err = checkTrustedJournalDir(a.Retirement.JournalDir, true); err != nil { |
| return err |
diff --git a/internal/hostbox/registryrows.go b/internal/hostbox/registryrows.go| index 642e430e5..e3aea9006 100644 |
| --- a/internal/hostbox/registryrows.go |
| +++ b/internal/hostbox/registryrows.go |
| @@ -22,7 +22,7 @@ func (r RegistryRows) RetireRowsJournal(ctx context.Context, id, owner string, r |
| if a.Root != "registry" || a.Owner != owner { |
| return ErrTypedInventory |
| } |
| - target := registry.ClaimRow{Kind: string(a.Kind), Host: r.Inventory.Host, Ownership: registry.ClaimOwnership{ClaimIncarnation: r.Inventory.Captured.Incarnation, ClaimStamp: r.Inventory.Captured.Stamp, ClaimOwner: owner, ClaimRowID: a.RowID}} |
| + target := registry.ClaimRow{AllowAbsent: a.AllowAbsent, Kind: string(a.Kind), Host: r.Inventory.Host, Ownership: registry.ClaimOwnership{ClaimIncarnation: r.Inventory.Captured.Incarnation, ClaimStamp: r.Inventory.Captured.Stamp, ClaimOwner: owner, ClaimRowID: a.RowID}} |
| if a.Kind == ArtifactRoute { |
| if a.Name != target.Host { |
| return ErrTypedInventory |
diff --git a/internal/hostbox/stopinventory.go b/internal/hostbox/stopinventory.go| index acc72d2e2..0053eb898 100644 |
| --- a/internal/hostbox/stopinventory.go |
| +++ b/internal/hostbox/stopinventory.go |
| @@ -1,6 +1,7 @@ |
| package hostbox |
| type StopInventoryEntry struct { |
| + TimerRetired bool `json:",omitempty"` |
| Device, Inode uint64 |
| Type uint32 |
| Kind string |
diff --git a/internal/hostbox/timer_registration.go b/internal/hostbox/timer_registration.go| new file mode 100644 |
| index 000000000..dcaed30d1 |
| --- /dev/null |
| +++ b/internal/hostbox/timer_registration.go |
| @@ -0,0 +1,134 @@ |
| +package hostbox |
| + |
| +import ( |
| + "context" |
| + "crypto/rand" |
| + "encoding/hex" |
| + "errors" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/hostguard" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "os" |
| +) |
| + |
| +func (w selectedWork) MutateTimer(ctx context.Context, t registry.Timer, expected string, remove bool) (*registry.Timer, error) { |
| + if w.source.Timer == nil { |
| + return nil, ErrUnsupported |
| + } |
| + return w.source.Timer(ctx, w, t, expected, remove) |
| +} |
| + |
| +func (a ReconcileAdapter) MutateTimer(ctx context.Context, work hostwrite.Work, t registry.Timer, expected string, remove bool) (*registry.Timer, error) { |
| + s, err := Selected(work) |
| + if err != nil || s.Host != t.Host || a.Registry == nil || !a.Inventory.Remote() { |
| + return nil, ErrUnsupported |
| + } |
| + in, revision, err := a.Inventory.Snapshot(ctx, s.Host, s.Captured) |
| + if err != nil { |
| + return nil, err |
| + } |
| + in.Entries = append([]StopInventoryEntry(nil), in.Entries...) |
| + var capture InventoryExpectation |
| + var record RecordSelection |
| + var old *registry.Timer |
| + err = work.With(ctx, hostwrite.Identity{}, func() error { |
| + obs, err := hostclaim.Read(ctx, a.Reconciler.Claims, s.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if obs.Claim == nil || obs.Claim.Holder == nil { |
| + return ErrChanged |
| + } |
| + h := obs.Claim.Holder |
| + _, etag, err := a.Reconciler.Claims.Get(ctx, h.Key) |
| + if err != nil { |
| + return err |
| + } |
| + record = RecordSelection{Host: s.Host, Key: h.Key, ETag: etag, Incarnation: s.Captured.Incarnation, Family: h.Family} |
| + capture, err = a.CaptureInventory(ctx, record, s.Captured, revision) |
| + if err != nil { |
| + return err |
| + } |
| + old, err = a.Registry.GetTimer(s.Host, t.Name) |
| + if err != nil && !errors.Is(err, os.ErrNotExist) { |
| + return err |
| + } |
| + if old == nil { |
| + if expected != "" || remove { |
| + return registry.ErrTimerChanged |
| + } |
| + } else { |
| + if old.Registration != expected { |
| + return registry.ErrTimerChanged |
| + } |
| + if old.Kind != t.Kind { |
| + return registry.ErrTimerKind |
| + } |
| + } |
| + rt, err := a.Registry.Get(s.Host) |
| + if err != nil { |
| + return err |
| + } |
| + if rt == nil || !rt.ClaimOwnership.Valid() || rt.ClaimIncarnation != s.Captured.Incarnation || rt.ClaimStamp != s.Captured.Stamp { |
| + return ErrChanged |
| + } |
| + if remove { |
| + if t.ClaimOwnership != old.ClaimOwnership || t.Registration != old.Registration { |
| + return ErrChanged |
| + } |
| + t = *old |
| + } else { |
| + if old != nil && t.Registration == "" { |
| + t.CreatedAt, t.LastAt, t.LastStatus, t.LastMs = old.CreatedAt, old.LastAt, old.LastStatus, old.LastMs |
| + } |
| + t.ClaimOwnership = rt.ClaimOwnership |
| + if t.Registration == "" { |
| + var id [16]byte |
| + if _, err := rand.Read(id[:]); err != nil { |
| + return err |
| + } |
| + t.Registration = hex.EncodeToString(id[:]) |
| + } |
| + t.ClaimRowID = t.Registration |
| + } |
| + return nil |
| + }) |
| + if err != nil { |
| + return nil, err |
| + } |
| + index := -1 |
| + for i, e := range in.Entries { |
| + if e.Kind == "timer" && e.RelativePath == t.Host+"/"+t.Name { |
| + if index >= 0 { |
| + return nil, ErrTypedInventory |
| + } |
| + index = i |
| + } |
| + } |
| + if !remove && old != nil && t.Registration == expected { |
| + if *old != t || index < 0 || in.Entries[index].RowIdentity != expected || in.Entries[index].Materialization != "materialized" || in.Entries[index].TimerRetired || in.Entries[index].Ownership != t.ClaimOwner { |
| + return nil, ErrChanged |
| + } |
| + err = work.With(ctx, hostwrite.Identity{}, func() error { return a.RevalidateInventory(ctx, record, s.Captured, capture, revision) }) |
| + return &t, err |
| + } |
| + entry := StopInventoryEntry{Kind: "timer", ArtifactID: t.Registration, ManagedRoot: "registry", RelativePath: t.Host + "/" + t.Name, Ownership: t.ClaimOwner, RowIdentity: t.Registration, Materialization: "materialized", TimerRetired: remove} |
| + if index < 0 { |
| + in.Entries = append(in.Entries, entry) |
| + } else { |
| + entry.ArtifactID = in.Entries[index].ArtifactID |
| + in.Entries[index] = entry |
| + } |
| + revision, err = a.Inventory.WriteTimerExpected(ctx, in, capture, TimerMutation{Timer: t, Expected: expected, Remove: remove}) |
| + if err != nil { |
| + return nil, err |
| + } |
| + err = work.With(ctx, hostwrite.Identity{}, func() error { |
| + return a.RevalidateInventory(ctx, record, hostguard.Captured{Incarnation: s.Captured.Incarnation, Stamp: s.Captured.Stamp}, capture, revision) |
| + }) |
| + if err != nil { |
| + return nil, err |
| + } |
| + return &t, nil |
| +} |
diff --git a/internal/hostbox/typedretire.go b/internal/hostbox/typedretire.go| index ab33a2137..3397d8ba6 100644 |
| --- a/internal/hostbox/typedretire.go |
| +++ b/internal/hostbox/typedretire.go |
| @@ -33,14 +33,15 @@ const ( |
| // Artifact binds a managed name to the installation identity which created it. |
| // Root is a configured root identifier, never an arbitrary absolute pathname. |
| type Artifact struct { |
| - Kind ArtifactKind `json:"kind"` |
| - Root string `json:"root"` |
| - Name string `json:"name"` |
| - Owner string `json:"owner"` |
| - RowID string `json:"row_id,omitempty"` |
| - Dev uint64 `json:"dev,omitempty"` |
| - Ino uint64 `json:"ino,omitempty"` |
| - Type uint32 `json:"type,omitempty"` |
| + AllowAbsent bool `json:"allow_absent,omitempty"` |
| + Kind ArtifactKind `json:"kind"` |
| + Root string `json:"root"` |
| + Name string `json:"name"` |
| + Owner string `json:"owner"` |
| + RowID string `json:"row_id,omitempty"` |
| + Dev uint64 `json:"dev,omitempty"` |
| + Ino uint64 `json:"ino,omitempty"` |
| + Type uint32 `json:"type,omitempty"` |
| } |
| type RetirementInventory struct { |
| @@ -119,6 +120,9 @@ func validateInventory(in RetirementInventory) error { |
| default: |
| return ErrTypedInventory |
| } |
| + if a.AllowAbsent && a.Kind != ArtifactTimer { |
| + return ErrTypedInventory |
| + } |
| key := string(a.Kind) + "\x00" + a.Root + "\x00" + a.Name + "\x00" + a.RowID |
| if seen[key] { |
| return ErrTypedInventory |
diff --git a/internal/hostbox/work.go b/internal/hostbox/work.go| index 9063c5c28..81a9870fe 100644 |
| --- a/internal/hostbox/work.go |
| +++ b/internal/hostbox/work.go |
| @@ -4,6 +4,7 @@ import ( |
| "context" |
| "errors" |
| "github.com/carlosframework/platform/internal/park" |
| + "github.com/carlosframework/platform/internal/registry" |
| "sync" |
| "time" |
| @@ -24,6 +25,7 @@ type Selection struct { |
| } |
| type WorkSource struct { |
| + Timer func(context.Context, hostwrite.Work, registry.Timer, string, bool) (*registry.Timer, error) |
| FleetHomeRegion string |
| HomeRegion string |
| Prepare func(context.Context, hostclaim.Observed) error |
diff --git a/internal/hostguard/rootinventory_settle.go b/internal/hostguard/rootinventory_settle.go| new file mode 100644 |
| index 000000000..72ec8b325 |
| --- /dev/null |
| +++ b/internal/hostguard/rootinventory_settle.go |
| @@ -0,0 +1,34 @@ |
| +package hostguard |
| + |
| +import ( |
| + "bytes" |
| + "context" |
| +) |
| + |
| +// SettleRootInventory completes only the already persisted installation intent. |
| +// The callback may finish its journal after independently proving its effects; |
| +// it must not create effects or infer an intention from service state. |
| +func (g Guard) SettleRootInventory(ctx context.Context, host string, intent []byte, settle func(RootInstallation) error) error { |
| + if g.Root == nil || settle == nil { |
| + return ErrInvalidInput |
| + } |
| + return g.readLocked(ctx, host, func(d *recordDir) error { |
| + r, err := d.read(host) |
| + if err != nil { |
| + return err |
| + } |
| + if r.pendingInstallation == nil || !bytes.Equal(r.pendingInventory, intent) { |
| + return ErrInvalidInput |
| + } |
| + in := *r.pendingInstallation |
| + if r.installation.Incarnation != in.Incarnation || r.installation.Stamp != in.Stamp || r.installation.InventoryRevision+1 != in.InventoryRevision || r.ActiveStamp != in.Stamp { |
| + return ErrRecordIncarnation |
| + } |
| + if err := settle(in); err != nil { |
| + return err |
| + } |
| + r.installation = in |
| + r.pendingInventory, r.pendingInstallation = nil, nil |
| + return d.write(r) |
| + }) |
| +} |
diff --git a/internal/hostguard/rootinventory_settle_test.go b/internal/hostguard/rootinventory_settle_test.go| new file mode 100644 |
| index 000000000..956902c15 |
| --- /dev/null |
| +++ b/internal/hostguard/rootinventory_settle_test.go |
| @@ -0,0 +1,71 @@ |
| +package hostguard |
| + |
| +import ( |
| + "errors" |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "testing" |
| +) |
| + |
| +func TestPlan14RootInventorySettlement(t *testing.T) { |
| + for _, change := range []string{"exact", "intent", "stamp", "revision", "callback"} { |
| + t.Run(change, func(t *testing.T) { |
| + f, target, captured := fenceFixture(t) |
| + f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "console" |
| + f.g.Root = NewRootAuthority(&rootTestStore{dir: t.TempDir()}) |
| + in := RootInstallation{Host: target.Host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Account: "bab", App: "console", Record: "record", Box: target.Box, Enrollment: target.Enrollment, Migration: 1, InventoryRevision: 1} |
| + if err := f.g.EstablishRoot(t.Context(), in, allow); err != nil { |
| + t.Fatal(err) |
| + } |
| + in.InventoryRevision++ |
| + if change == "revision" { |
| + in.InventoryRevision++ |
| + } |
| + fault := errors.New("interrupted") |
| + if err := f.g.CommitRootInventory(t.Context(), in, []byte("exact"), allow, nil, func(RootInstallation) error { return fault }); !errors.Is(err, fault) { |
| + t.Fatal(err) |
| + } |
| + // No fresh claim is available. Local settlement cannot clear a denial. |
| + f.obs.Claim = &hostclaim.Claim{} |
| + intent := []byte("exact") |
| + if change == "intent" { |
| + intent = []byte("forged") |
| + } |
| + r, err := f.g.Root.read(target.Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + r.quarantined = true |
| + if change == "stamp" { |
| + r.ActiveStamp++ |
| + } |
| + if err := f.g.Root.write(r); err != nil { |
| + t.Fatal(err) |
| + } |
| + calls := 0 |
| + err = f.g.SettleRootInventory(t.Context(), target.Host, intent, func(got RootInstallation) error { |
| + calls++ |
| + if got != in || change == "callback" { |
| + return fault |
| + } |
| + return nil |
| + }) |
| + if (err == nil) != (change == "exact") { |
| + t.Fatal(change, err) |
| + } |
| + if (change == "intent" || change == "revision" || change == "stamp") && calls != 0 { |
| + t.Fatal("unvalidated intent reached effect callback") |
| + } |
| + r, err = f.g.Root.read(target.Host) |
| + if err != nil || !r.quarantined { |
| + t.Fatal("settlement cleared denial", err) |
| + } |
| + pending, e := f.g.Root.PendingInstallations() |
| + if e != nil { |
| + t.Fatal(e) |
| + } |
| + if (len(pending) == 0) != (change == "exact") { |
| + t.Fatal("pending intention changed", pending) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/provision/plan14_e_write_census_test.go b/internal/provision/plan14_e_write_census_test.go| index c1e32ba31..9eb2816aa 100644 |
| --- a/internal/provision/plan14_e_write_census_test.go |
| +++ b/internal/provision/plan14_e_write_census_test.go |
| @@ -2,12 +2,15 @@ package provision |
| // Track E overlays the reviewed Part 1 write census without changing its baseline. |
| var plan14EWriteCensusOverlay = []censusEntry{ |
| - {"internal/host/rootgeneration_requirements.go", "generationRequiredInventory", "key-site", "key:RecordKey", "0ceac8b867717d959ccc6a66a44505a177ad56d09bd79d826c89a506eb8d00a8"}, |
| + {"internal/hostbox/inventory_expected.go", "StopInventoryDB.writeExpected", "key-site", "key:RecordKey", "6bd77e75ff73ac8e461bb0687b183e031e2864d8455f58513b71aade995cb2b1"}, |
| + {"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/historyreadback/workflow.go", "Operation.deliverGrant", "nonrecord", "sel:Put", "fb410ef6274ef7de0997a804c837ad1ecf29d8826ba1c4dcda00c2ae986fc434"}, |
| - {"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "b08d3c55f02e0b9e9c39455ce37f854a254c5f65af9fc74443fee414e2756f1b"}, |
| - {"internal/host/rootinventory.go", "ClaimRootHTTP.recoverRootInventory", "key-site", "key:RecordKey", "a828684ad31a409d3e1fc306216d907681f514cb813302eb965a040e5084feda"}, |
| + {"internal/host/rootinventory.go", "ClaimRootHTTP.commitRootInventoryPending", "key-site", "key:RecordKey", "4f083cb29dd1bd8f7d2eeed20f30c9b51387082ac8c8ab1745efbb8a0270ac43"}, |
| + {"internal/host/rootinventory.go", "ClaimRootHTTP.recoverRootInventory", "key-site", "key:RecordKey", "79d1472b90bc7290e655bfe5e8857bad9fd0666c72b88364e89e4e198e60f535"}, |
| {"internal/host/roothistory_creation.go", "rootRestartRequestSource.creation", "key-site", "key:RecordKey", "af79a66b746b0f42388010b38ea8130dca6a8d29c4ba5623bfc9c3cc40dd9601"}, |
| {"internal/host/rootrestart_requests.go", "rootRestartRequests.consume", "key-site", "key:RecordKey", "0a9fbbe6f201077e276298b7e666e1ee2a345f1c420335290fec38a676b1ad84"}, |
| {"internal/host/rootrestart_requests_source.go", "rootRestartRequestSource.inventory", "key-site", "key:ParseSidecarKey,key:RecordKey", "1ae4bd07fdbd2b0fccd29cc032b0de4a6c53bc52da7fcbc186a11f8b65b394f9"}, |
| @@ -21,7 +24,14 @@ var plan14EWriteCensusOverlay = []censusEntry{ |
| } |
| func plan14EWriteCensusInventory() []censusEntry { |
| - entries := append([]censusEntry(nil), plan9B8CensusInventory...) |
| + var entries []censusEntry |
| + for _, entry := range plan9B8CensusInventory { |
| + // E46 moved this key site to writeExpected; WriteExpected delegates. |
| + if entry.File == "internal/hostbox/inventory_expected.go" && entry.Unit == "StopInventoryDB.WriteExpected" { |
| + continue |
| + } |
| + entries = append(entries, entry) |
| + } |
| for _, replacement := range plan14EWriteCensusOverlay { |
| found := false |
| for i, entry := range entries { |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index e57ed2779..347961812 100644 |
| --- a/internal/provision/testdata/plan14-root-reads-e.tsv |
| +++ b/internal/provision/testdata/plan14-root-reads-e.tsv |
| @@ -54,7 +54,7 @@ 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 ccde94dd6c888c98f4f78e71beb82af7f64f8fbaddc1c9cb0de032fe610426a7 |
| +@coverage internal/host 3e511e1f76238fe1abf1189f47786f60a6fc0eb6c52ef00df85bf0731483e2c5 |
| @coverage internal/release 6517cdba66a04ddf585b6bceb510402516a9bbb6ef37922b8d616c52ae781edf |
| @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 |
| @@ -167,11 +167,11 @@ internal/host/roothistory_creation.go rootHistoryInstallation authority E2 E37: |
| internal/host/roothistory_creation.go rootRestartRequestSource.creation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. registry:Account@219,registry:App@243,registry:Host@133,registry:Host@194 af79a66b746b0f42388010b38ea8130dca6a8d29c4ba5623bfc9c3cc40dd9601 |
| internal/host/roothistory_creation.go type rootHistoryAccess authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 9c97bcd7878d79022ed942d8e6c00e944a00e0fa27a1bc5c732b4f4c10f4e9dd |
| internal/host/roothistory_launch.go ClaimLaunchRoot.rootInstalledProgram authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:OpenFile@541,file:Read@1043 e8d530dd1b8f613dfbd41d34a1b08b19aa3f74e0522c8f41db768ccd4e9e80d2 |
| -internal/host/rootinventory.go ClaimRootHTTP.commitRootInventoryPending 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. PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. file:Read@1383,file:Read@3423,registry:Account@4787,registry:Account@4802,registry:App@4823,registry:App@4834,registry:ClaimIncarnation@4293,registry:ClaimOwner@4343,registry:ClaimOwner@5398,registry:ClaimOwnership@4885,registry:ClaimOwnership@4907,registry:ClaimOwnership@5712,registry:ClaimOwnership@7795,registry:ClaimRowID@4507,registry:ClaimRowID@4584,registry:ClaimStamp@4321,registry:DBPath@4851,registry:DBPath@4865,registry:Host@4757,registry:Host@4769,whole:ClaimOwnership@4889,whole:ClaimOwnership@4917,whole:ClaimOwnership@5722,whole:ClaimOwnership@7806,whole:Route@3615,whole:Route@3998,whole:Route@4014,whole:Route@4069,whole:Route@4720,whole:Route@4733,whole:Route@4742,whole:Route@4980,whole:Route@5212,whole:Route@5960,whole:Route@6096,whole:Route@6320,whole:Route@7639,whole:Route@9046,whole:Route@9107,whole:Route@9647 b08d3c55f02e0b9e9c39455ce37f854a254c5f65af9fc74443fee414e2756f1b |
| +internal/host/rootinventory.go ClaimRootHTTP.commitRootInventoryPending 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. PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. 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. file:Read@1383,file:Read@3423,registry:Account@4787,registry:Account@4802,registry:App@4823,registry:App@4834,registry:ClaimIncarnation@4293,registry:ClaimOwner@4343,registry:ClaimOwner@5398,registry:ClaimOwnership@4885,registry:ClaimOwnership@4907,registry:ClaimOwnership@5714,registry:ClaimOwnership@7810,registry:ClaimRowID@4507,registry:ClaimRowID@4584,registry:ClaimStamp@4321,registry:DBPath@4851,registry:DBPath@4865,registry:Host@4757,registry:Host@4769,registry:Host@8286,registry:Name@8309,whole:ClaimOwnership@4889,whole:ClaimOwnership@4917,whole:ClaimOwnership@5724,whole:ClaimOwnership@7821,whole:Route@10010,whole:Route@3615,whole:Route@3998,whole:Route@4014,whole:Route@4069,whole:Route@4720,whole:Route@4733,whole:Route@4742,whole:Route@4980,whole:Route@5212,whole:Route@5971,whole:Route@6107,whole:Route@6331,whole:Route@7654,whole:Route@9305,whole:Route@9366,whole:Timer@8294,whole:Timer@8317 4f083cb29dd1bd8f7d2eeed20f30c9b51387082ac8c8ab1745efbb8a0270ac43 |
| internal/release/roothistory_creation.go RootHistory.ConsumeCreation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 7ec65199b5afc894ae4aefded6a40f485086d0f8a4c86391eca193f473dca2a1 |
| internal/release/roothistory_creation.go RootHistory.FreshInstallation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 71b326a36350139c9c592b0b5b4b91409831fcffdd0c61d68cf5ec2bf54bbd5b |
| internal/release/roothistory_creation.go RootHistory.RecordCreation authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 0b545ab63902eb8f2c8988991786c964a2af53d65eff46d8e53a0747da492b6d |
| -internal/host/rootinventory.go ClaimRootHTTP.recoverRootInventory authority C,E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. file:Read@1371,file:Read@707 a828684ad31a409d3e1fc306216d907681f514cb813302eb965a040e5084feda |
| +internal/host/rootinventory.go ClaimRootHTTP.recoverRootInventory authority C,E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. PROPOSED 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. file:Read@1512,file:Read@707 79d1472b90bc7290e655bfe5e8857bad9fd0666c72b88364e89e4e198e60f535 |
| internal/host/rootinventory.go ClaimRootHTTP.withRootInventoryHistory authority E1 E16: root first-registration creation evidence under release exclusion; exact installation identity is required for first-program adoption and is consumed before an attempt. Installed-program history comes from the verified claim launch. Fixtures: TestPlan14FreshInstallRegistration, TestPlan14FreshInstallProgramGate, TestPlan14FreshInstallCanonicalIdentity, TestPlan14FreshInstallLaunchObservation, TestPlan14InstalledLaunchProgramEvidence, TestPlan14FreshInstallProcessCrash, TestPlan14FreshInstallVisibleSync, TestClaimControlUnitInstallers. 81bc65e0a2018fcdaa8933231d44579429801c2265e6c87c274e9fd8e096c202 |
| history_cli.go historyCommandWithSave authority E1 E17: operator saves exact signed authorization and challenge before delivery, resumes without renewal, and verifies enrolled-root exact completion. Existing proof branches are unchanged. Tests: TestPlan14HistoryOperatorWorkflow, TestPlan14HistoryOperatorConflicts, TestPlan14HistoryOperatorExactCompletion, TestPlan14HistoryWorkflowProcessCrash, TestPlan14HistoryCommandResume, TestPlan14HistorySavedFiles, TestPlan14HistoryCLIDispatch. file:Read@2820 9d2f3a735e54cbc01cd4db714125aae9e491afad3d666495f3915de7d517aa2f |
| history_cli.go historyReadFile authority E1 E17: operator saves exact signed authorization and challenge before delivery, resumes without renewal, and verifies enrolled-root exact completion. Existing proof branches are unchanged. Tests: TestPlan14HistoryOperatorWorkflow, TestPlan14HistoryOperatorConflicts, TestPlan14HistoryOperatorExactCompletion, TestPlan14HistoryWorkflowProcessCrash, TestPlan14HistoryCommandResume, TestPlan14HistorySavedFiles, TestPlan14HistoryCLIDispatch. file:OpenFile@75,file:ReadAll@178 890cbecced68e8ebec71ddbba5682becd5318fa6d5e29f6dc8e5203f9fef6a6a |
| @@ -227,7 +227,7 @@ internal/release/rootgenerationpins.go type RootGenerationPins authority E2 E23: |
| internal/host/rootinstallation.go rootInstallationHostApp 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. file:Read@488,registry:Account@1062,registry:App@1091 0bbe17aff8c82841ae47fb01e060e1e1a80865e635d4a5c95dc5a638d2754d1c |
| 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 017a3a049b377b2099de8f37c594fa9f5b0e806d0e64fcb1102acff2475241e2 |
| +@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 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 |
| @@ -295,7 +295,7 @@ internal/host/rootgeneration_census_linux.go generationKernel.census authority E |
| internal/host/rootgeneration_inventory.go generationInventoryAt authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance passed at E27; running-tenant containment remains unfinished. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@173,file:Open@533 57f4c32cd65afcefad93a41d5266e2b12acc624ff808f47d40e31a79bbc7a827 |
| internal/hostbox/generationinventory.go LaunchJournal.GenerationLaunches authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:QueryContext@270 6694bd3a35b8d91f53e67134145c44854086f08593110ed1b905bca9587be290 |
| internal/hostbox/generationinventory.go OpenGenerationLaunchReader authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. file:Lstat@218,file:Open@283 552d8629a7932c4ae5dca9d2beef4559f7e8a2864259d11e2ff98c699fdcd999 |
| -@coverage internal/hostbox c3d05fdb9b9e5ed56d29df815e73bd857e8a2a6f31168deecf628c0bf82ccf1a |
| +@coverage internal/hostbox b76083441c876c66209ec032b57e7dde6eb51dd41597f9d2d5462dffa3736abe |
| internal/host/rootgeneration_census_linux.go productionGenerationKernelCensus authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. PROPOSED E45: root uses retained reservation identities and live kernel containment checks; unrecognized processes still refuse. Current positive root acceptance requires rerun after proc-mount tightening. e0c2041104c3a10eebc0183e67602069fd33954f8d0c8efb058261cd54551900 |
| internal/host/rootgeneration_census_other.go productionGenerationKernelCensus authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. a1217cf881e9d05336fc16d6050f7f7a1a2184a0c20ff2988d0cf0f9012eaa45 |
| internal/host/rootgeneration_inventory.go generationInstallation authority E2 E24: independent root installation and launch enumeration, exact reservation reconciliation and fresh quiescent whole-version-tree kernel census; missing sources, open unreserved launches, foreign namespaces and uncontained tasks refuse. Production pin source certifies this evidence before returning pins. Fixtures: TestPlan14GenerationInventoryCompleteness, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationInventoryLifecycle, TestPlan14GenerationInventoryProcessCrash, TestPlan14GenerationKernelCensusUnknownReference, TestPlan14GenerationLaunchInventory, TestPlan14GenerationReaderDoesNotInitialize, TestPlan14GenerationInstallationInventory. Checked production source/kernel witnesses passed on the E27 root runner. E29 physical-payload and installed-unit witnesses passed on the controller runner; E30 required-artifact witness pending. 3e5e15d50752510acb44b10eb29621757a87bff54ceae015f704dd7f3a848ecb |
| @@ -303,22 +303,22 @@ internal/host/rootgeneration_inventory.go generationInventoryDigest authority E2 |
| 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 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. 32fe8188ec5a22297f4e0aa68f2741839461637a2ef2e7a282f9c5115141db6d |
| +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.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. a110a5abe6b9ce876ddacad08d1eeef06753e13d7d0619d239a7b4d3c4e00e48 |
| +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 |
| internal/hostbox/generationpayload.go LaunchJournal.GenerationPayloads authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance passed at E27; running-tenant containment remains unfinished. file:QueryContext@272 c4aa5c5794f91bda7416b569e02bcd6003640b1256e5f8201aafc987fa681626 |
| internal/hostbox/generationpayload.go type GenerationPayload authority E2 E26: checked read-only payload enumeration includes inventory and revision orphans; completeness binds current and retained predecessor inventories, exact current revision and both source reads. Fixtures: TestPlan14GenerationPayloadCompleteness, TestPlan14GenerationPayloadRetainedPredecessor, TestPlan14GenerationInventoryPinConsumer, TestPlan14GenerationPayloadInventory, TestPlan14GenerationPayloadReaderDoesNotInitialize. Checked root reader acceptance passed at E27; running-tenant containment remains unfinished. 992921ba14660af87a8099649ac589a7c2587467c05df052710fa45a9a5aa8bb |
| internal/host/rootgeneration_payload_linux.go generationPayloadFiles authority E2 E28: retained data payloads require exact device/inode/type through no-symlink descriptor opens before and after kernel inspection. Missing, replaced and intended payloads remain unresolved. Tests: TestPlan14GenerationPayloadPhysicalIdentity; privileged TestPlan14GenerationPayloadPhysicalRootRunner remains runner-pending. Route/timer and running-tenant containment are not accepted by this check. file:Fstat@1333,file:Openat2@1120,file:Openat2@917 7344256ef22fb8dea05e47f70c05094008739c14ae0f95336573c4f23d3e1837 |
| internal/host/rootgeneration_payload_other.go generationPayloadFiles authority E2 E28: retained data payloads require exact device/inode/type through no-symlink descriptor opens before and after kernel inspection. Missing, replaced and intended payloads remain unresolved. Tests: TestPlan14GenerationPayloadPhysicalIdentity; privileged TestPlan14GenerationPayloadPhysicalRootRunner remains runner-pending. Route/timer and running-tenant containment are not accepted by this check. candidate:Kind@183,candidate:Kind@204 182189d8409113d671cdc76fc5d2d73e7056057bca230d546e65300c428b2c18 |
| -internal/host/rootgeneration_requirements.go generationRequiredInventory 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. 0ceac8b867717d959ccc6a66a44505a177ad56d09bd79d826c89a506eb8d00a8 |
| +internal/host/rootgeneration_requirements.go generationRequiredInventory 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. 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. bbcbc143061ea6990ec8bc4e34f1a52a07e82039ea44340497d5bc6aab067913 |
| internal/host/rootgeneration_requirements.go type generationRecord authority E2 E30: derive route and ordinary-instance state-container requirements from schema- and shape-validated canonical deployment records bound to retained installation incarnation; include record contents and ETag in both source reads and the certificate digest. Missing, mismatched, omitted or changed evidence refuses without retiring pins. Fixtures: TestPlan14GenerationRequiredArtifacts, TestPlan14GenerationProductionModeGate. Production fixed-path witness TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. Timer classes, parked artifacts, positive empty-installation evidence and running-tenant containment remain incomplete. ff50bc959af62afbd73b01e9640346f9c79d6b6578e03cb3a7d6bdff4d90fc28 |
| -internal/host/rootgeneration_rows.go generationRegistryInventory 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. registry:ClaimOwnership@2199,registry:ClaimOwnership@2578,registry:ClaimRowID@2631,registry:Host@2177,registry:Host@2556,registry:Name@2497,registry:Registration@2609,whole:ClaimOwnership@1475,whole:ClaimOwnership@1539,whole:ClaimOwnership@1553,whole:ClaimOwnership@1567,whole:ClaimOwnership@2076,whole:ClaimOwnership@2205,whole:ClaimOwnership@2223,whole:ClaimOwnership@2278,whole:ClaimOwnership@2527,whole:ClaimOwnership@2584,whole:ClaimOwnership@2602,whole:ClaimOwnership@2697,whole:ClaimOwnership@2725,whole:Route@2094,whole:Timer@2383,whole:Timer@2461 b4105502dc32425e8566a988cb45f11289b32c4ca4d5d78ab249c31f8d164ec7 |
| -@coverage internal/registry 3e9a5115c72f6fec616b43e24630d5f9d60bffbc591c7b3ebd178f2a4855f7ff |
| +internal/host/rootgeneration_rows.go generationRegistryInventory 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. registry:ClaimOwnership@2244,registry:ClaimOwnership@2623,registry:ClaimRowID@2676,registry:Host@2222,registry:Host@2601,registry:Name@2542,registry:Registration@2654,whole:ClaimOwnership@1475,whole:ClaimOwnership@1584,whole:ClaimOwnership@1598,whole:ClaimOwnership@1612,whole:ClaimOwnership@2121,whole:ClaimOwnership@2250,whole:ClaimOwnership@2268,whole:ClaimOwnership@2323,whole:ClaimOwnership@2572,whole:ClaimOwnership@2629,whole:ClaimOwnership@2647,whole:ClaimOwnership@2742,whole:ClaimOwnership@2770,whole:Route@2139,whole:Timer@2428,whole:Timer@2506 f1110311a6c7e1535c025aace601d79cbc28e9a3fd71e6b617e7c0098ebc82ff |
| +@coverage internal/registry 5cbb6c195a8dd94bcad3eb50492e2fc4b12c34c2bc6902c02b6644b2928deaef |
| internal/registry/remote.go serveRemote authority E2 Mixed declaration: E2 adds only the ArtifactHosts read operation; all existing dispatch and write behavior stays legacy. Original immutable legacy fingerprints retained. E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. file:Open@332,registry:CallbackError@1712,registry:CallbackFailed@1669,registry:Deadline@1295,registry:Deadline@1369,registry:Method@1022,registry:Method@1043,registry:Method@1070,registry:Method@1604,whole:remoteRequest@1426,whole:remoteRequest@835,whole:remoteRequest@922,whole:remoteResponse@1148,whole:remoteResponse@1194,whole:remoteResponse@1488,whole:remoteResponse@1786,whole:remoteResponse@1822,whole:remoteResponse@392,whole:remoteResponse@431 40254c291f7b1a89b28e39eb1e4ffb3aa5852fd00de076ced1d3f7c48885e6dd |
| internal/registry/artifacthosts.go Registry.ArtifactHosts authority E2 E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. file:Query@196,registry:Hosts@164,registry:remote@59,registry:remote@91,registry:sql@196,whole:remoteClient@61,whole:remoteClient@93 7310bda43df947e7b2d237dce83291dfe15596802d9db404f6ec0bb457cd6540 |
| internal/registry/remote_dispatch.go Registry.dispatchRemote hint E2 Mixed declaration: E2 adds only the ArtifactHosts read operation; all existing dispatch and write behavior stays legacy. Original immutable legacy fingerprints retained. E32: census-only E2 completeness discovers the bounded union of route and timer hosts, including inactive orphan timers, through the service-user read-only parser. Unknown hosts refuse even with an empty root installation list; discovery supplies no authority and never removes a retained obligation. Pre-flight 33, 80, 82, 83, 89 and 90. Fixtures: TestArtifactHosts, TestRemoteReadOnlyWALRegistry, TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryProductionReader; extended TestPlan14GenerationRequiredArtifactsRootRunner remains runner-pending. PROPOSED E45: service registry child executes explicit timer row materialization/removal; service-writable SQL is deny-only to root and cannot confer ownership. Root lifecycle producer wiring is pending. registry:A@6327,registry:Account@3790,registry:Accounts@2803,registry:Addr@4367,registry:Addrs@4375,registry:Alias@6375,registry:Alias@6439,registry:Alias@6527,registry:Aliases@6249,registry:Alive@8069,registry:Args@5951,registry:At@5392,registry:At@6458,registry:At@6536,registry:At@7817,registry:Attempts@7839,registry:B@2188,registry:Backing@4186,registry:BuildState@4593,registry:Changed@1996,registry:Changed@3159,registry:Changed@3246,registry:Changed@3335,registry:Changed@3406,registry:Changed@3488,registry:Changed@7749,registry:Channel@3850,registry:Channel@4833,registry:Channel@5237,registry:CheckedAt@5318,registry:Class@4305,registry:DoneBefore@8078,registry:Due@5721,registry:Due@5785,registry:Due@7861,registry:Env@4901,registry:Env@5148,registry:Env@5248,registry:ExcludeHosts@7544,registry:ExcludeHosts@7639,registry:ExpectGood@5517,registry:ExpectOp@3214,registry:Expected@1250,registry:Expected@289,registry:Expected@762,registry:Expected@939,registry:F@1968,registry:F@2029,registry:Fleet@2054,registry:Fleet@2246,registry:Fleet@2328,registry:Fleet@2409,registry:Fleet@2502,registry:FleetBoxes@2463,registry:Fleets@2112,registry:From@3961,registry:From@4022,registry:FromDB@4097,registry:FromOp@3302,registry:FromSock@4107,registry:Hash@4773,registry:Health@4384,registry:Host@1024,registry:Host@1135,registry:Host@272,registry:Host@2987,registry:Host@3028,registry:Host@3114,registry:Host@3190,registry:Host@3276,registry:Host@3368,registry:Host@3440,registry:Host@3532,registry:Host@3782,registry:Host@3842,registry:Host@3900,registry:Host@3953,registry:Host@4014,registry:Host@4089,registry:Host@4178,registry:Host@4232,registry:Host@4297,registry:Host@4359,registry:Host@4437,registry:Host@4512,registry:Host@4585,registry:Host@4650,registry:Host@4711,registry:Host@4765,registry:Host@4893,registry:Host@4947,registry:Host@5008,registry:Host@5070,registry:Host@5300,registry:Host@5384,registry:Host@5441,registry:Host@5509,registry:Host@5598,registry:Host@5658,registry:Host@5713,registry:Host@5777,registry:Host@5892,registry:Host@5943,registry:Host@6010,registry:Host@6085,registry:Host@6148,registry:Host@6745,registry:Host@6892,registry:Host@7180,registry:Host@7244,registry:Host@7303,registry:Host@7386,registry:Host@7469,registry:Host@7700,registry:Host@7785,registry:Host@7917,registry:Host@7998,registry:Host@8161,registry:Hosts@187,registry:Hosts@2866,registry:Hosts@4824,registry:Hosts@5139,registry:Hosts@5228,registry:Id@1244,registry:Id@1555,registry:Id@1654,registry:Id@1734,registry:Id@1885,registry:IdValue@2622,registry:Idle@6018,registry:Journal@1262,registry:Journal@1561,registry:Journal@1740,registry:Journal@442,registry:Keep@8177,registry:Kind@8006,registry:Kind@8169,registry:Label@2255,registry:Label@2337,registry:Label@2418,registry:Label@5016,registry:Limit@7535,registry:Limit@7630,registry:MaxRun@6026,registry:Method@151,registry:Ms@7833,registry:Name@1032,registry:Name@1143,registry:Name@2082,registry:Name@2641,registry:Name@7252,registry:Name@7311,registry:Name@7394,registry:Name@7708,registry:Name@7793,registry:Name@7925,registry:Next@301,registry:NextAt@7851,registry:NextGood@5531,registry:NextValue@1814,registry:NextValue@1912,registry:Now@1167,registry:Now@6219,registry:Now@7528,registry:Now@7623,registry:Now@7716,registry:Number64@2729,registry:Number@7966,registry:Number@8037,registry:Number@8122,registry:Observed@365,registry:ObservedValue@643,registry:Old@1807,registry:Old@1905,registry:On@5666,registry:On@6093,registry:On@6156,registry:Op@3132,registry:Op@3208,registry:Op@3376,registry:Op@3448,registry:Op@3540,registry:Owner@280,registry:Owner@377,registry:Owner@660,registry:P@6846,registry:Path@7077,registry:Paths@6774,registry:Prefix@6900,registry:Prev@5456,registry:Registration@1040,registry:Registration@1151,registry:Registration@7402,registry:Registration@7801,registry:Replica@4719,registry:Replicas@4658,registry:Retirement@1891,registry:Route@2964,registry:Routes@2922,registry:Routes@3566,registry:Routes@5819,registry:Routes@6186,registry:Rows@7188,registry:Seed@5900,registry:Sqid@2633,registry:Sqid@2696,registry:Sqid@2775,registry:Status@1056,registry:Status@2427,registry:Status@3122,registry:Status@3198,registry:Status@3284,registry:Status@3908,registry:Status@5308,registry:Status@6448,registry:Status@7823,registry:Status@7933,registry:Steering@4445,registry:StringMap@2539,registry:T@7128,registry:T@757,registry:T@851,registry:T@934,registry:TValue@3069,registry:Tags@2346,registry:Target@5078,registry:Targets@453,registry:Text@1460,registry:Text@1607,registry:Text@2743,registry:Text@6933,registry:Text@6996,registry:Timer@709,registry:Timer@7216,registry:Timer@891,registry:Timers@7439,registry:Timers@7498,registry:Timers@7587,registry:Times@8211,registry:To@3969,registry:To@4030,registry:ToDB@4119,registry:ToOp@3294,registry:ToSock@4127,registry:TokenHash@2264,registry:Tunnel@4955,registry:Unit@4240,registry:UpdateSettings@4520,registry:Ver@5449,registry:Ver@5606,registry:W@6679,registry:Wildcards@6573,whole:Account@2807,whole:ClaimOwnership@282,whole:ClaimOwnership@379,whole:ClaimOwnership@662,whole:ClaimRow@455,whole:ClaimRowArchive@561,whole:ClaimRowArchive@571,whole:ColdRows@1809,whole:ColdRows@1816,whole:ColdRows@1907,whole:ColdRows@1914,whole:Fleet@1970,whole:Fleet@2031,whole:Fleet@2058,whole:Fleet@2116,whole:FleetBox@2190,whole:FleetBox@2467,whole:GoodBuild@5519,whole:GoodBuild@5533,whole:Route@2926,whole:Route@2968,whole:Route@3071,whole:Route@3570,whole:Route@367,whole:Route@3725,whole:Route@3731,whole:Route@5823,whole:Route@6190,whole:RouteAlias@6253,whole:RouteAlias@6329,whole:RoutePath@6778,whole:RoutePath@6848,whole:Timer@645,whole:Timer@713,whole:Timer@7130,whole:Timer@7190,whole:Timer@7220,whole:Timer@7443,whole:Timer@7502,whole:Timer@759,whole:Timer@7591,whole:Timer@853,whole:Timer@895,whole:Timer@936,whole:WildcardDomain@6577,whole:WildcardDomain@6681 d1343daea2d7ea81eabd31ec2a0026c4d4831636618903ec7a719c20f1633646 |
| @@ -336,12 +336,12 @@ internal/hostguard/root.go Guard.CommitRootInventoryArtifacts authority E2 E37: |
| internal/hostguard/root.go type InstallationArtifacts 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. fab0473af135dbb33e0f4cf9709a07b5026feedd43709c1a681c650c3ae82dd5 |
| internal/hostguard/root.go type RootInstallation 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. 33adc00085827994991a0ba71e06cee27157eb04a870506b72c1f16ec3962606 |
| internal/hostguard/rootrebind.go Guard.RebindRootInventory 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. 15f4c0b9159f193f9bad5215fe0224ba02276a9b58d036c98b4c4fe190856216 |
| -internal/host/rootgeneration_parked.go generationPhysicalPayloads 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. 40f718df6477870f1c6e57b779a39bf7a27ae98b3e3afbcdaca32c16b76dc109 |
| +internal/host/rootgeneration_parked.go generationPhysicalPayloads 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: Root rejects unfinished live inventory but accepts intended timer entries only behind exact completed retirement. 29a39e3ac81a6f7762155600de35faeaf1d7355437983e88ce9078cdb60207f7 |
| internal/host/rootgeneration_parked.go generationReadRetirements 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. ba341f9665adbb204cf8ecf87a417b65022ce947291a4b99b1e8fa1bd16f3dec |
| internal/host/rootgeneration_parked.go generationRetirementRecords 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. file:Read@707 02eca3d34294974c09e93199efb37a223e7615eaa32faffdafb982b5d4ba53e7 |
| -internal/host/rootgeneration_parked.go generationRetirements 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. be9fd54e443994c423e0e315637b8cf05a361cbb32913b9bc9f891fbb65ad59f |
| +internal/host/rootgeneration_parked.go generationRetirements 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. 48eef6024e9437337e59956c4cf6393a5ca401196195f1e0c4a69674d70aed82 |
| internal/host/rootgeneration_parked.go productionGenerationRetirements 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. file:Open@221,file:Open@306 fe83ff81bb9966f50de7fb3a049b8acd8e87f5d86346809090fc46b5c1e65189 |
| -internal/hostbox/reconcile_adapter.go ReconcileAdapter.Retire authority C,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 E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. registry:ClaimIncarnation@1309,registry:ClaimIncarnation@322,registry:ClaimIncarnation@3417,registry:ClaimIncarnation@3439,registry:ClaimOwner@1366,registry:ClaimOwner@1434,registry:ClaimOwner@2190,registry:ClaimOwner@2596,registry:ClaimOwner@3497,registry:ClaimOwner@3513,registry:ClaimOwner@3751,registry:ClaimOwnership@147,registry:ClaimRowID@3531,registry:ClaimRowID@3774,registry:ClaimStamp@1338,registry:ClaimStamp@3463,registry:ClaimStamp@3479,registry:ClaimStamp@362,registry:Host@1257,registry:Host@2095,registry:Host@2641,registry:Host@2698,registry:Host@288,registry:Host@3336,registry:Host@3719,registry:Name@3736,registry:Registration@3553,registry:Registration@3795,whole:ClaimOwnership@151,whole:Route@1069,whole:Route@1152,whole:Route@231,whole:Timer@3402 42d0e63edc29cf8a83f8265fb1b0260a88334a7ebc3f7b9530d938d9ffa5e4f1 |
| +internal/hostbox/reconcile_adapter.go ReconcileAdapter.Retire authority C,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 E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. 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. registry:ClaimIncarnation@1309,registry:ClaimIncarnation@322,registry:ClaimIncarnation@3592,registry:ClaimIncarnation@3614,registry:ClaimOwner@1366,registry:ClaimOwner@1434,registry:ClaimOwner@2365,registry:ClaimOwner@2771,registry:ClaimOwner@3672,registry:ClaimOwner@3688,registry:ClaimOwner@3926,registry:ClaimOwnership@147,registry:ClaimRowID@3706,registry:ClaimRowID@3949,registry:ClaimStamp@1338,registry:ClaimStamp@362,registry:ClaimStamp@3638,registry:ClaimStamp@3654,registry:Host@1257,registry:Host@2270,registry:Host@2816,registry:Host@2873,registry:Host@288,registry:Host@3511,registry:Host@3894,registry:Name@3911,registry:Registration@3728,registry:Registration@3970,whole:ClaimOwnership@151,whole:Route@1069,whole:Route@1152,whole:Route@231,whole:Timer@3577 3559413c25c77eed78b72cb8614e01bfa7443db51993479dc02febb4e1be1e34 |
| internal/hostbox/retirement_timers.go retirementWithTimers authority C,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 E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. 67060d1e6077a5cb6a15ea2749e070e97f830177987570037564916f5ed9bda0 |
| internal/activator/activator.go Service.respawnWorkerLocked authority E2 E39: close the unsupported claimed worker path before backend entry, replacement or supervised stop. Claims-off paths remain unchanged. This is refusal coverage, not positive root-reserved worker launch support. Fixtures: TestPlan14GenerationWorkerStartBoundary, TestPlan14GenerationWorkerRealBackendRefusal, TestPlan14GenerationWorkerReplacementBoundary, TestPlan14GenerationWorkerSupervisionBoundary. Pre-flight 29 and E39 worker launch audit. whole:Route@11828,whole:Route@13334,whole:Route@360,whole:Route@4164,whole:Route@6157,whole:Route@6338 706f2a4268a06dae2eca22093f841750139c5bd9dcaa0e91657621c5eea1bbe9 |
| internal/activator/workerbuild.go Service.startWorker authority E2 E39: close the unsupported claimed worker path before backend entry, replacement or supervised stop. Claims-off paths remain unchanged. This is refusal coverage, not positive root-reserved worker launch support. Fixtures: TestPlan14GenerationWorkerStartBoundary, TestPlan14GenerationWorkerRealBackendRefusal, TestPlan14GenerationWorkerReplacementBoundary, TestPlan14GenerationWorkerSupervisionBoundary. Pre-flight 29 and E39 worker launch audit. registry:Host@1201,registry:Host@1379,registry:Host@2314,whole:Route@2420,whole:Route@931 be27c7d049a6009a8a22798569e457f5126f14d5e7c56aafb5b1bd260717f6f5 |
| @@ -356,7 +356,7 @@ internal/host/relocationinstall.go ClaimAssembly.relocationInstall authority B2, |
| internal/host/guardtransaction.go systemApplier.beginGuard authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Read@679,file:Read@903 7d3e508615e4469231a856637aa1d1a38d8a4c52ed5a65cd4334ebb914385ec1 |
| internal/host/guardtransaction.go systemApplier.checkGuardRecovery authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:ReadFile@433,file:ReadFile@660 25070bd97c1b3c9ce9fbdc8f44cec9680db72abf4ab9f50e603aad9b3a1ea4e2 |
| internal/host/rootexecution_runtime.go newClaimExecutionRuntime authority D Positive claim or fence mismatches are terminal; transport failures retain bounded retry eligibility. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Read@205 91514737432c25a2256b449c97d70fc61f9deb32726b9aea90e7ffdeb962a9db |
| -internal/host/rootlaunchverifier.go registerProductionLaunchVerifier authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:Open@191 20e6bdeb66c580f64b9a315a65bb647c17dde0fe49c4f2bc45adfcd2bdc6989f |
| +internal/host/rootlaunchverifier.go registerProductionLaunchVerifier authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. 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. file:Open@191 93460d1a688168268f9b22fb2dae3c4744e5f7b7cff909ec55624e989db0cdf9 |
| internal/host/rootlaunchverifier.go var participationRelease authority E2 PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. PROPOSED E45: root enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. file:Open@124,file:Open@76 5f8ff567328f13a1ddc69ed82af4037c18c2a83d48a369a144fe14e1824b3b14 |
| internal/host/systemapply.go systemApplier.Run authority D,F,J4 root-input;kept=D,F;scope=protected-decision-only 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 enforces census-gated release ordering, bounded acquisition and scoped recovery; registry timer primitives preserve supplied identity but producer integration remains pending. 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:OpenFile@483 6a5704be83e35979db7f2811b8ea7fd3d4c2c74c9b532f9f94f1d5482cbeb827 |
| internal/host/systemparticipation.go systemApplier.beginExecutableChange authority D Claims-on or retained root census uses root exclusion; claims-off with no census preserves main participation and recovery behavior. PROPOSED E43: root; release-held context and D3 reservation/census verification under release-before-participation exclusion; no-census effects stay on the legacy path. TestPlan14LaunchVerifierToken, TestPlan14LaunchVerifierCallerOrder, TestPlan14LaunchVerifierNoCensus, TestPlan14LaunchVerifierRootRunner. file:ReadFile@565 f4cacc520429d698655295d87a14326a4a3f3d11e70fa16536e2c3f554d6c307 |
| @@ -374,8 +374,8 @@ internal/rootparticipation/production.go Ledger.ActivationReady authority E2 PRO |
| @coverage internal/hostfloor 37459f6d8d47ed5ec3892e22483e34abde2dabc0775cf0846b6fe00fb66dcd75 |
| @coverage internal/rootparticipation d867cfb3ad2b0432f7cee72f29805bc3420e28fe0edd5b89b469f154e4371694 |
| internal/host/claimretire_transport.go ClaimRootHTTP.retire authority C,G,J1 PROPOSED E44: root timer intention must precede materialization; exact registration and row identity required; root retirement cannot infer ownership from registry enumeration. Tests: TestPlan14TimerIntentionBeforeMaterialization, TestPlan14GenerationTimerRetirement; root timer intention, sync-failure retry and retirement acceptance. Scheduler producer wiring remains incomplete. registry:ClaimOwnership@1081,registry:Host@1062,registry:Host@792,whole:ClaimOwnership@1085,whole:ClaimOwnership@894,whole:Route@1830,whole:Route@854 425059d65adaafa5680ad92974559fbeb56ba0a3a7ff3867bb63937648989e06 |
| -internal/host/rootinventory_timer.go ClaimRootHTTP.verifyInventoryTimer hint C,J1 PROPOSED E45 / R-E45-5: root executes a deny-only consistency check of service-writable timer rows. Registry absence is not ordering authority. Root intention authority resides in the retained inventory and fence; producer lifecycle remains incomplete. registry:ClaimIncarnation@943,registry:ClaimIncarnation@969,registry:ClaimOwner@1035,registry:ClaimOwner@299,registry:ClaimRowID@1070,registry:ClaimStamp@1015,registry:ClaimStamp@995,registry:Host@899,registry:Name@921,registry:Registration@1107,whole:Timer@709 f54f9aeaf48b8a258b9102d630719552a7e3ff495a53ab53123b79420723365f |
| -internal/host/rootinventory_timer.go ClaimRootHTTP.verifyNewTimerIntentions hint E2 PROPOSED E45 / R-E45-5: root executes a deny-only consistency check of service-writable timer rows. Registry absence is not ordering authority. Root intention authority resides in the retained inventory and fence; producer lifecycle remains incomplete. whole:Timer@609 179376ffb25fdc973418cfeae238cf1c69e3ec95495d6ac145ac6fa660f8a1c0 |
| +internal/host/rootinventory_timer.go ClaimRootHTTP.verifyInventoryTimer hint C,J1 PROPOSED E45 / R-E45-5: root executes a deny-only consistency check of service-writable timer rows. Registry absence is not ordering authority. Root intention authority resides in the retained inventory and fence; producer lifecycle remains incomplete. PROPOSED E46: R-E45-5: service-writable timer SQL is deny-only; root pending/inventory supplies ownership. Executing identity: root instance-control or root participation/update. registry:ClaimIncarnation@1006,registry:ClaimIncarnation@980,registry:ClaimOwner@1072,registry:ClaimOwner@299,registry:ClaimRowID@1107,registry:ClaimStamp@1032,registry:ClaimStamp@1052,registry:Host@936,registry:Name@958,registry:Registration@1144,whole:Timer@709 7af2d533a789e21c0306910c072aa186df0460cddb0d4b5490bbd26b0a19943d |
| +internal/host/rootinventory_timer.go ClaimRootHTTP.verifyNewTimerIntentions hint E2 PROPOSED E45 / R-E45-5: root executes a deny-only consistency check of service-writable timer rows. Registry absence is not ordering authority. Root intention authority resides in the retained inventory and fence; producer lifecycle remains incomplete. PROPOSED E46: R-E45-5: service-writable timer SQL is deny-only; root pending/inventory supplies ownership. Executing identity: root instance-control or root participation/update. whole:Timer@710 43a9e0925e79a484c680f7ca04c1ad19c13cb9863a5e9caf03104259bf9c3ec8 |
| internal/host/claimlaunch_execpermit.go ClaimLaunchRoot.execPermit authority D Root participation bounds the executable permit; TestPlan14ExecutionFinalLaunchUsesRootParticipation covers initial and final checks. PROPOSED E45: root uses retained reservation identities and live kernel containment checks; unrecognized processes still refuse. Current positive root acceptance requires rerun after proc-mount tightening. file:Read@157 cf0aa8db12ea1d451ce4e729572dc8c628ee22453bee25dae62c5ff13e85e769 |
| 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 |
| @@ -393,3 +393,39 @@ internal/host/rootreleaselock.go withClaimReleaseCensus authority E2 PROPOSED E4 |
| 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/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 |
| +internal/host/claimassembly.go ClaimAssembly.Build authority C,G Assembly supplies root registration client to moves alongside guard, enrollment and typed inventory. 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. file:Prepare@3854,file:Prepare@3871,file:Prepare@4583,file:Read@4513,whole:Timer@2044 bc18bc60ed228114abc612388455c146423ac5e2695b4a0397e0681df0ea7997 |
| +internal/host/rootinventory_timer_mutation.go ClaimRootHTTP.applyTimerMutation authority E2 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. registry:Host@248,registry:Name@262,whole:Timer@151,whole:Timer@250,whole:Timer@264,whole:Timer@472 971e82a0ca2d2505453d86618efa6b2e5acd1fa32d16d3fbe34a84cbdc17e68c |
| +internal/host/rootinventory_timer_mutation.go ClaimRootHTTP.verifyTimerMutation authority E2 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. registry:Host@232,registry:Name@253,whole:Timer@234,whole:Timer@255 581329ce2bbc4ebc643553ec65f1ed922999629bcd29bc7ddf30f6d1cd3dbdd6 |
| +internal/host/rootinventory_timer_mutation.go ClaimRootHTTP.verifyTimerMutationRow hint E2 PROPOSED E46: R-E45-5: service-writable timer SQL is deny-only; root pending/inventory supplies ownership. Executing identity: root instance-control or root participation/update. registry:ClaimIncarnation@1236,registry:ClaimIncarnation@1260,registry:ClaimIncarnation@367,registry:ClaimIncarnation@389,registry:ClaimOwner@1316,registry:ClaimOwner@1334,registry:ClaimOwner@451,registry:ClaimOwner@467,registry:ClaimOwner@612,registry:ClaimOwnership@339,registry:ClaimRowID@1350,registry:ClaimRowID@487,registry:ClaimStamp@1282,registry:ClaimStamp@1300,registry:ClaimStamp@415,registry:ClaimStamp@431,registry:Host@188,registry:Host@289,registry:Kind@1388,registry:Kind@1400,registry:Kind@825,registry:Kind@856,registry:Name@1055,registry:Name@205,registry:Name@327,registry:Path@951,registry:Registration@1368,registry:Registration@1457,registry:Registration@503,registry:Registration@521,registry:Registration@649,whole:ClaimOwnership@258,whole:ClaimOwnership@341,whole:Timer@1222,whole:Timer@1445,whole:Timer@1452,whole:Timer@190,whole:Timer@207,whole:Timer@279 a815c5cb716cb5dad1bddaa84600abcaf2fbef2dd8e8df3b24c313a90cdebf82 |
| +internal/host/rootinventory_timer_recovery.go ClaimRootHTTP.settleTimerInventory authority E2 PROPOSED E46: exact persisted root pending intent only; finish journal after effect matches, preserve denial, no new SQL effect or network claim authority. Executing identity: root instance-control or root participation/update. registry:Host@353,registry:Name@367,whole:Timer@355,whole:Timer@369,whole:Timer@512,whole:Timer@527,whole:Timer@536 88fa248f2431066f1b9ccca604351d3078a42ad8f244dcc38ea55cf405a3d3cd |
| +internal/hostbox/coldhandover.go ColdHandover.Run authority C,J2,J4 route-and-input 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@938,file:Lstat@10641,file:OpenRoot@10563,file:Read@1290,registry:Addr@3565,registry:Addrs@3583,registry:Backing@3602,registry:Canary@3659,registry:ClaimIncarnation@3443,registry:ClaimIncarnation@3945,registry:ClaimOwner@11391,registry:ClaimOwner@4027,registry:ClaimOwner@4047,registry:ClaimOwner@5922,registry:ClaimOwner@7014,registry:ClaimOwner@8242,registry:ClaimOwnership@10983,registry:ClaimOwnership@3413,registry:ClaimOwnership@4391,registry:ClaimOwnership@4609,registry:ClaimOwnership@4642,registry:ClaimRowID@11281,registry:ClaimRowID@11362,registry:ClaimRowID@4065,registry:ClaimRowID@4673,registry:ClaimRowID@6973,registry:ClaimRowID@8267,registry:ClaimStamp@3488,registry:ClaimStamp@3992,registry:Host@6850,registry:Host@8204,registry:Kind@3521,registry:Name@4727,registry:Name@6865,registry:Name@8223,registry:Platform@3643,registry:Registration@4091,registry:Registration@6930,registry:Registration@8292,registry:Replica@3673,registry:Route@10983,registry:Route@4391,registry:Route@4642,registry:Route@5173,registry:Route@5922,registry:Timers@4590,registry:Timers@4609,registry:Timers@4673,registry:Timers@4727,registry:Timers@6802,registry:Timers@8079,registry:Tunnel@3623,registry:Unit@3547,whole:ClaimOwnership@10997,whole:ClaimOwnership@3417,whole:ClaimOwnership@4403,whole:ClaimOwnership@4625,whole:ClaimOwnership@4654,whole:ColdRows@10985,whole:ColdRows@12097,whole:ColdRows@12107,whole:ColdRows@4888,whole:ColdRows@4896,whole:ColdRows@4904,whole:ColdRows@4911,whole:ColdRows@5175,whole:ColdRows@5924,whole:ColdRows@6804,whole:ColdRows@8081,whole:Route@10991,whole:Route@3398,whole:Route@3758,whole:Route@4190,whole:Route@4198,whole:Route@4249,whole:Route@4257,whole:Route@4397,whole:Route@4648,whole:Route@5182,whole:Route@5931,whole:Timer@3929,whole:Timer@4203,whole:Timer@4211,whole:Timer@4262,whole:Timer@4300,whole:Timer@4596,whole:Timer@4615,whole:Timer@4679,whole:Timer@4733,whole:Timer@6811,whole:Timer@8088 f93ed708bfa43eb371e597f32ca26f5e204bba0057663d38b64d65e2302a6bc1 |
| +internal/hostbox/inventory_expected.go TimerMutation.MarshalJSON hint E2 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. registry:Registration@151,whole:Timer@153 c7f03dad631ed3926e08bbdf5f733a544f631245b15f63ada0deed292abfa733 |
| +internal/hostbox/inventory_expected.go TimerMutation.UnmarshalJSON hint E2 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. registry:Registration@225,whole:Timer@227 a8e97d7bb426306519f06e0d4148af082d3d9ac907ce52779f5558bb5955eeca |
| +internal/hostbox/inventoryscope.go ValidateAgentInventory authority C,J2,J4 root-route 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. registry:ClaimIncarnation@189,registry:ClaimOwner@601,registry:ClaimOwnership@274,registry:ClaimRowID@825,registry:ClaimStamp@231,registry:DBPath@1271,registry:DBPath@1325,registry:DBPath@1383,registry:DBPath@1656,registry:DBPath@1794,registry:DBPath@2102,registry:Host@166,whole:ClaimOwnership@278,whole:Route@1186,whole:Route@151,whole:Route@572,whole:Route@794,whole:Route@845,whole:Route@950 ce8c02515e68984b648cb76e9a6c3be9737cbc29588ba08c438ceaed5ddcba91 |
| +internal/hostbox/registryrows.go RegistryRows.RetireRowsJournal authority C,J2,J4 root-route 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. registry:ClaimRowID@1166,registry:Host@706,registry:Host@805,registry:Name@903,registry:Ownership@1166,registry:Registration@1203,registry:Registration@955,whole:ClaimOwnership@1173,whole:ClaimOwnership@491,whole:ClaimRow@1288,whole:ClaimRow@1305,whole:ClaimRow@1314,whole:ClaimRow@1369,whole:ClaimRowArchive@1449 cfe9f08cd76b40a2e614966691b6c83b1bb1bab22d6111d00a8c53009e57bf4e |
| +internal/hostbox/timer_registration.go ReconcileAdapter.MutateTimer hint E2 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:Read@2136,file:Read@625,registry:ClaimIncarnation@1617,registry:ClaimOwner@2774,registry:ClaimOwner@3120,registry:ClaimOwnership@1588,registry:ClaimOwnership@1747,registry:ClaimOwnership@1767,registry:ClaimOwnership@2033,registry:ClaimOwnership@2052,registry:ClaimRowID@2244,registry:ClaimStamp@1666,registry:CreatedAt@1923,registry:CreatedAt@1971,registry:Host@209,registry:Host@2422,registry:Host@3086,registry:Kind@1434,registry:Kind@1446,registry:LastAt@1936,registry:LastAt@1986,registry:LastMs@1960,registry:LastMs@2014,registry:LastStatus@1946,registry:LastStatus@1998,registry:Name@1168,registry:Name@2433,registry:Name@3101,registry:Registration@1356,registry:Registration@1789,registry:Registration@1807,registry:Registration@1896,registry:Registration@2076,registry:Registration@2193,registry:Registration@2259,registry:Registration@2549,registry:Registration@3031,registry:Registration@3147,whole:ClaimOwnership@1591,whole:ClaimOwnership@1749,whole:ClaimOwnership@1771,whole:ClaimOwnership@2035,whole:ClaimOwnership@2055,whole:Route@1574,whole:Timer@1129,whole:Timer@1252,whole:Timer@1856,whole:Timer@1861,whole:Timer@1882,whole:Timer@2535,whole:Timer@2584,whole:Timer@2591,whole:Timer@2965,whole:Timer@3446,whole:Timer@3453,whole:Timer@3786 1548ec8fcc1f17d704492f4eae3af0caf0f71256a1a2851d30c89c746510d3d0 |
| +internal/hostbox/timer_registration.go selectedWork.MutateTimer hint E2 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. whole:Timer@221 137b8c9de9fa525eae8d7eec52e7308091864ad0c14111cab57173c76389dbc8 |
| +internal/hostbox/work.go type WorkSource diagnostic H3 schema-only 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:Prepare@184 4b92857846a32e3dc76f4e009402b4a4aa3d28de4b4eb9f1c7d6636320880ce2 |
| +internal/hostguard/rootinventory_settle.go Guard.SettleRootInventory authority E2 PROPOSED E46: exact persisted root pending intent only; finish journal after effect matches, preserve denial, no new SQL effect or network claim authority. Executing identity: root instance-control or root participation/update. c0bb1291f9675478887f5792d48d249fccc1163b17ad17a0f59b8f9ca24d2e41 |
| +internal/registry/claimrows.go Registry.RetireClaimRows hint E2 accepted-legacy;rev3=J1,J4;basis=route-and-input 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:QueryRowContext@1213,file:QueryRowContext@1754,file:QueryRowContext@2068,registry:AllowAbsent@2206,registry:AllowAbsent@3711,registry:AllowAbsent@747,registry:Archives@369,registry:ClaimIncarnation@3193,registry:ClaimOwner@3247,registry:ClaimOwnership@1923,registry:ClaimOwnership@2358,registry:ClaimRowID@1377,registry:ClaimRowID@3080,registry:ClaimRowID@3271,registry:ClaimStamp@3223,registry:Contents@1517,registry:Contents@3735,registry:Host@1093,registry:Host@1361,registry:Host@1827,registry:Host@2152,registry:Host@3064,registry:Host@3185,registry:Host@845,registry:Kind@1075,registry:Kind@1353,registry:Kind@1706,registry:Kind@3056,registry:Kind@3300,registry:Kind@764,registry:Kind@862,registry:Kind@883,registry:Kind@906,registry:Kind@971,registry:Name@1111,registry:Name@1369,registry:Name@2160,registry:Name@3072,registry:Name@3403,registry:Name@928,registry:Name@993,registry:Ownership@1377,registry:Ownership@1945,registry:Ownership@2380,registry:Ownership@3080,registry:Ownership@3193,registry:Ownership@3223,registry:Ownership@3247,registry:Ownership@3271,registry:Ownership@822,registry:Registration@1009,registry:Registration@2395,registry:Registration@2415,registry:Registration@2563,registry:Registration@3411,registry:Registration@944,registry:Row@1499,registry:remote@194,registry:remote@224,whole:ClaimOwnership@1379,whole:ClaimOwnership@1927,whole:ClaimOwnership@1947,whole:ClaimOwnership@2362,whole:ClaimOwnership@2382,whole:ClaimOwnership@3082,whole:ClaimOwnership@3195,whole:ClaimOwnership@3225,whole:ClaimOwnership@3249,whole:ClaimOwnership@3273,whole:ClaimOwnership@824,whole:ClaimRow@1501,whole:ClaimRow@1508,whole:ClaimRow@2668,whole:ClaimRow@2673,whole:ClaimRow@2796,whole:ClaimRow@302,whole:ClaimRow@311,whole:ClaimRow@428,whole:ClaimRow@637,whole:ClaimRow@676,whole:ClaimRow@732,whole:ClaimRowArchive@1486,whole:ClaimRowArchive@1573,whole:ClaimRowArchive@1591,whole:ClaimRowArchive@1601,whole:ClaimRowArchive@2624,whole:ClaimRowArchive@2642,whole:ClaimRowArchive@2737,whole:ClaimRowArchive@2866,whole:ClaimRowArchive@373,whole:ClaimRowArchive@3735,whole:Route@2024,whole:Timer@2558,whole:remoteClient@196,whole:remoteClient@226 c4941d6b434763e54789a825b83f5140f0b2bc7e6aea3ae1f220cf0d5e423b35 |
| +internal/schedule/claimtimers.go SelectedTimers.ReplaceTimers hint E2 accepted-legacy;rev3=J1;basis=root-route 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. PROPOSED E46: Held timer imports refuse missing or duplicate registration identities before any effect. registry:Host@311,registry:Name@499,registry:Name@562,registry:Name@720,registry:Name@852,registry:Name@898,registry:Registration@468,whole:Timer@1045,whole:Timer@265,whole:Timer@295,whole:Timer@451,whole:Timer@701,whole:Timer@762,whole:Timer@834 515e0b73a714a1a5c7f96bae02f1dd54036fb6b7c97c4014eb2af849036fa098 |
| +internal/schedule/claimtimers.go SelectedTimers.UpsertTimer hint E2 accepted-legacy;rev3=J1;basis=root-route 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. registry:Host@165,registry:Registration@63,whole:Timer@112,whole:Timer@215 775084e61380be13d3dadb74dc06bc717180be10a072c1beedaab013cba1713b |
| +internal/schedule/converge.go Converger.convergeHost hint E2 accepted-legacy;rev3=J1;basis=root-route 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. registry:Attempts@5259,registry:Backing@919,registry:DueAt@5248,registry:Host@1283,registry:Host@1573,registry:Host@160,registry:Host@1707,registry:Host@2003,registry:Host@214,registry:Host@255,registry:Host@3109,registry:Host@342,registry:Host@4166,registry:Host@4449,registry:Host@4589,registry:Host@4687,registry:Host@4854,registry:Host@5075,registry:Host@511,registry:Host@5588,registry:Host@5680,registry:Kind@687,registry:LastAt@730,registry:NextAt@5236,registry:NextAt@711,registry:Path@2417,registry:Registration@3337,registry:Registration@3737,registry:Registration@4297,registry:Registration@4607,registry:Registration@5143,registry:Registration@5316,registry:Registration@5337,registry:Spec@2389,whole:Timer@1292,whole:Timer@1677,whole:Timer@2326,whole:Timer@2374,whole:Timer@3308,whole:Timer@3366,whole:Timer@3424,whole:Timer@3445,whole:Timer@3472,whole:Timer@3523,whole:Timer@3672,whole:Timer@3707,whole:Timer@3783,whole:Timer@3830,whole:Timer@4250,whole:Timer@477,whole:Timer@4863,whole:Timer@5411,whole:Timer@639,whole:Timer@672,whole:Timer@798,whole:Timer@867,whole:Timer@881,whole:Timer@887,whole:Timer@898,whole:Timer@907 cf700422ad9c503d44201e0446d79fc936b741c119ba2877599ed8b6460b2364 |
| +internal/schedule/converge.go Converger.deleteSchedules hint E2 accepted-legacy;rev3=J1;basis=root-route 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. registry:Kind@358,registry:Name@391,whole:Timer@344,whole:Timer@463 4e027f5e81adba831f71668e0b2f9ed109fbcbb872e739561bfebbba3193f932 |
| +internal/schedule/runner.go Runner.fire hint E2 accepted-legacy;rev3=J1;basis=root-route 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. registry:Host@243,registry:Host@385,registry:Host@464,registry:Name@252,registry:Name@394,registry:Spec@148,registry:Spec@260,whole:Route@1131,whole:Route@1194,whole:Route@1362,whole:Route@506,whole:Route@632,whole:Route@829,whole:Timer@108,whole:Timer@1135,whole:Timer@1366,whole:Timer@317,whole:Timer@510,whole:Timer@636,whole:Timer@833,whole:Timer@933 554c7aa6a1cf471d3401f611849f4369d9a0b5c6ed50070588794ef08148f039 |
| +internal/schedule/runner.go Runner.prune hint E2 accepted-legacy;rev3=J1;basis=root-route 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. registry:Host@2191,registry:Host@2303,registry:Host@2546,registry:Host@2639,registry:Host@2904,registry:Host@634,registry:Kind@2704,registry:LastAt@2747,registry:Name@2914,registry:NextAt@2728,registry:Socket@655,registry:Socket@684,whole:Route@1118,whole:Route@2142,whole:Route@617,whole:Timer@2514,whole:Timer@2691,whole:Timer@2834 a587f00a66a0e31eeb415dfceddf33d66d0cc75737c5d29f7350459fe5718f4c |
| +internal/schedule/timer_mutation.go ClaimTimers.DeleteTimer hint E2 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. ec4c50e484b03a1fca06d488e63cf097f2b56db44afc882a15be595f36be475a |
| +internal/schedule/timer_mutation.go ClaimTimers.UpsertTimer hint E2 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. registry:Host@100,registry:Host@234,whole:Timer@255 b4756ef60ecc076d36719ca821d8bdcc07a857ad1125552d4687bd39efbd76d8 |
| +internal/schedule/timer_mutation.go SelectedTimers.mutate hint E2 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. registry:Host@185,registry:Host@386,registry:Kind@582,registry:Kind@594,registry:Name@394,registry:Registration@560,whole:Timer@358,whole:Timer@534,whole:Timer@677,whole:Timer@716,whole:Timer@721,whole:Timer@763 9a9465f9b9128fc285d71845e7b4a72f226fe4e3d7d3c98ae3b8b0b393d8ec14 |
| +internal/schedule/timer_mutation.go deleteSelectedTimer hint E2 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. registry:Host@273,registry:Name@281,registry:Registration@147,registry:Registration@289,whole:Timer@144 967c437238210b077ced61e68ef5dc556db666ebe731d55f313316f58575d0be |
| +internal/schedule/timer_mutation.go hasTimerMutator hint E2 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. de6d488d7b3256dd7100233ab844a38ab7ef9e073a1403a0fddb129331534a60 |
| +internal/schedule/timer_mutation.go mutateSelectedTimer hint E2 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. whole:Timer@353,whole:Timer@382 1e070561137393e7c793bdd221fcab59a71cc98dd7ddc5dbb9eebadb14a5ab57 |
| +internal/schedule/timer_mutation.go type ClaimTimers hint E2 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. 1a5f74880b0e4c846a2c9801779304987f23347a92f5ad4a6be988197ad10576 |
| +internal/schedule/timer_mutation.go type timerMutator hint E2 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. fea8083b860afff122572553c2c7928690079728c178b955aa0a2127adfa9d75 |
| +@coverage internal/provision 23d7dcec8048b5d76a4bfac0191994c10b873a8d554341f5638a54767118b1fe |
| +@coverage internal/schedule 0abec1591e88a5a54e8b2cf10da4c073285a91ba51f6712901a711359fb877ac |
| +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 |
diff --git a/internal/registry/claimrows.go b/internal/registry/claimrows.go| index 3051588b8..4b164549c 100644 |
| --- a/internal/registry/claimrows.go |
| +++ b/internal/registry/claimrows.go |
| @@ -24,6 +24,7 @@ func (o ClaimOwnership) Valid() bool { |
| } |
| type ClaimRow struct { |
| + AllowAbsent bool `json:",omitempty"` |
| Kind string |
| Host string |
| Name string |
| @@ -113,6 +114,9 @@ func (r *Registry) RetireClaimRows(ctx context.Context, journal string, targets |
| pending := make([]bool, len(targets)) |
| seen := map[string]bool{} |
| for i, t := range targets { |
| + if t.AllowAbsent && t.Kind != "timer" { |
| + return ErrClaimOwnership |
| + } |
| if !t.Ownership.Valid() || t.Host == "" || (t.Kind != "route" && t.Kind != "timer") || (t.Kind == "timer" && (t.Name == "" || t.Registration == "")) || (t.Kind == "route" && (t.Name != "" || t.Registration != "")) { |
| return ErrClaimOwnership |
| } |
| @@ -147,6 +151,10 @@ func (r *Registry) RetireClaimRows(ctx context.Context, journal string, targets |
| content, err = json.Marshal(row) |
| case "timer": |
| row, e := scanTimer(tx.QueryRowContext(ctx, `SELECT `+timerCols+` FROM timers WHERE host=? AND name=?`, t.Host, t.Name)) |
| + if errors.Is(e, sql.ErrNoRows) && t.AllowAbsent { |
| + content, err = []byte("null"), nil |
| + break |
| + } |
| if e != nil { |
| return fmt.Errorf("%w: %v", ErrClaimOwnership, e) |
| } |
| @@ -194,7 +202,7 @@ func (r *Registry) RetireClaimRows(ctx context.Context, journal string, targets |
| if e != nil { |
| return e |
| } |
| - if n != 1 { |
| + if n != 1 && !(n == 0 && t.AllowAbsent && string(archives[i].Contents) == "null") { |
| return ErrClaimOwnership |
| } |
| } |
diff --git a/internal/schedule/claimtimers.go b/internal/schedule/claimtimers.go| index abee98d44..b4f47eca5 100644 |
| --- a/internal/schedule/claimtimers.go |
| +++ b/internal/schedule/claimtimers.go |
| @@ -27,9 +27,16 @@ func (s SelectedTimers) write(host string, fn func() error) error { |
| return s.Work.With(s.Context, hostwrite.Identity{}, fn) |
| } |
| func (s SelectedTimers) UpsertTimer(t registry.Timer) error { |
| + t.Registration = "" |
| + if handled, err := s.mutate(t, false); handled { |
| + return err |
| + } |
| return s.write(t.Host, func() error { return s.Store.UpsertTimer(t) }) |
| } |
| func (s SelectedTimers) DeleteTimer(host, name string) error { |
| + if handled, err := s.mutate(registry.Timer{Host: host, Name: name}, true); handled { |
| + return err |
| + } |
| return s.write(host, func() error { return s.Store.DeleteTimer(host, name) }) |
| } |
| func (s SelectedTimers) GetTimer(host, name string) (*registry.Timer, error) { |
| @@ -77,5 +84,33 @@ func (s SelectedTimers) ReplaceTimers(host string, rows []registry.Timer) error |
| return errTimerHost |
| } |
| } |
| + if _, ok := s.Work.(timerMutator); ok { |
| + seen := map[string]bool{} |
| + for _, row := range copyRows { |
| + if row.Registration == "" || seen[row.Name] { |
| + return registry.ErrClaimOwnership |
| + } |
| + seen[row.Name] = true |
| + } |
| + old, err := s.TimersFor(host) |
| + if err != nil { |
| + return err |
| + } |
| + keep := map[string]bool{} |
| + for _, row := range copyRows { |
| + keep[row.Name] = true |
| + if _, err := s.mutate(row, false); err != nil { |
| + return err |
| + } |
| + } |
| + for _, row := range old { |
| + if !keep[row.Name] { |
| + if err := s.DeleteTimer(host, row.Name); err != nil { |
| + return err |
| + } |
| + } |
| + } |
| + return nil |
| + } |
| return s.write(host, func() error { return replacement.ReplaceTimers(host, copyRows) }) |
| } |
diff --git a/internal/schedule/converge.go b/internal/schedule/converge.go| index 534f1da0e..93cdd6c30 100644 |
| --- a/internal/schedule/converge.go |
| +++ b/internal/schedule/converge.go |
| @@ -263,9 +263,7 @@ func (c *Converger) convergeHost(ctx context.Context, rt registry.Route, decl De |
| kept := before[:0] |
| for _, row := range before { |
| if row.Kind == KindOnce && row.NextAt == 0 && row.LastAt < cutoff { |
| - if err := withTimerWork(ctx, func() error { |
| - return c.Reg.DeleteTimerRegistration(row.Host, row.Name, row.Registration) |
| - }); err != nil { |
| + if err := deleteSelectedTimer(ctx, c.Reg, row); err != nil { |
| return false, err |
| } |
| continue |
| @@ -280,11 +278,7 @@ func (c *Converger) convergeHost(ctx context.Context, rt registry.Route, decl De |
| // still carries from before the flip, so "no rows" stays true for |
| // every unit-backed host, not just ones that were always unit-backed. |
| var n int |
| - err := withTimerWork(ctx, func() error { |
| - var writeErr error |
| - n, writeErr = c.deleteSchedules(rt.Host, before, map[string]bool{}) |
| - return writeErr |
| - }) |
| + n, err := c.deleteSchedules(ctx, rt.Host, before, map[string]bool{}) |
| if err != nil { |
| return false, err |
| } |
| @@ -336,19 +330,34 @@ func (c *Converger) convergeHost(ctx context.Context, rt registry.Route, decl De |
| // A new occurrence is due whenever next moves (DueAt rides NextAt — |
| // UpsertTimer leaves both alone when NextAt is 0 and nothing changed). |
| var installed *registry.Timer |
| - err = withTimerWork(ctx, func() error { |
| - t := registry.Timer{Host: rt.Host, Name: sc.Name, Kind: KindSchedule, Spec: sp.String(), Path: sc.Path, NextAt: next, DueAt: next, CreatedAt: now.Unix()} |
| - if !c.HostWrites.Enabled { |
| - return c.Reg.UpsertTimer(t) |
| - } |
| + t := registry.Timer{Host: rt.Host, Name: sc.Name, Kind: KindSchedule, Spec: sp.String(), Path: sc.Path, NextAt: next, DueAt: next, CreatedAt: now.Unix()} |
| + handled := false |
| + if c.HostWrites.Enabled { |
| expected := "" |
| if prev != nil { |
| expected = prev.Registration |
| } |
| - var err error |
| - installed, err = c.Reg.UpsertTimerRegistration(t, expected) |
| - return err |
| - }) |
| + if prev != nil && !wouldChange && hasTimerMutator(ctx) { |
| + installed, handled = prev, true |
| + } else { |
| + installed, handled, err = mutateSelectedTimer(ctx, t, expected, false) |
| + } |
| + } |
| + if !handled { |
| + err = withTimerWork(ctx, func() error { |
| + if !c.HostWrites.Enabled { |
| + return c.Reg.UpsertTimer(t) |
| + } |
| + expected := "" |
| + if prev != nil { |
| + expected = prev.Registration |
| + } |
| + var err error |
| + installed, err = c.Reg.UpsertTimerRegistration(t, expected) |
| + return err |
| + }) |
| + } |
| + |
| if errors.Is(err, registry.ErrTimerKind) { |
| // Nothing was written — a collision must not pin `changed`, |
| // or a permanently-colliding name reports every single pass. |
| @@ -376,12 +385,7 @@ func (c *Converger) convergeHost(ctx context.Context, rt registry.Route, decl De |
| changed = true |
| } |
| } |
| - var n int |
| - err := withTimerWork(ctx, func() error { |
| - var writeErr error |
| - n, writeErr = c.deleteSchedules(rt.Host, before, keep) |
| - return writeErr |
| - }) |
| + n, err := c.deleteSchedules(ctx, rt.Host, before, keep) |
| if err != nil { |
| return changed, err |
| } |
| @@ -389,12 +393,30 @@ func (c *Converger) convergeHost(ctx context.Context, rt registry.Route, decl De |
| changed = true |
| } |
| if fireName != "" && keep[fireName] { |
| - if err := withTimerWork(ctx, func() error { |
| + fire := func() error { |
| if c.HostWrites.Enabled { |
| - return c.Reg.FireTimerNowRegistration(rt.Host, fireName, registrations[fireName], now.Unix()) |
| + row, err := c.Reg.GetTimer(rt.Host, fireName) |
| + if err != nil { |
| + return err |
| + } |
| + if row.Registration != registrations[fireName] { |
| + return registry.ErrTimerChanged |
| + } |
| + row.NextAt, row.DueAt, row.Attempts = now.Unix(), now.Unix(), 0 |
| + expected := row.Registration |
| + row.Registration = "" |
| + if _, handled, err := mutateSelectedTimer(ctx, *row, expected, false); handled { |
| + return err |
| + } |
| } |
| - return c.Reg.FireTimerNow(rt.Host, fireName, now.Unix()) |
| - }); err != nil { |
| + return withTimerWork(ctx, func() error { |
| + if c.HostWrites.Enabled { |
| + return c.Reg.FireTimerNowRegistration(rt.Host, fireName, registrations[fireName], now.Unix()) |
| + } |
| + return c.Reg.FireTimerNow(rt.Host, fireName, now.Unix()) |
| + }) |
| + } |
| + if err := fire(); err != nil { |
| return changed, err |
| } |
| changed = true |
| @@ -518,16 +540,19 @@ func holdsAwake(t registry.Timer, idle time.Duration, now time.Time) bool { |
| // deleteSchedules only removes rows observed by this pass. A same-name |
| // replacement keeps its registration and survives the stale deletion. |
| -func (c *Converger) deleteSchedules(host string, before []registry.Timer, keep map[string]bool) (int, error) { |
| +func (c *Converger) deleteSchedules(ctx context.Context, host string, before []registry.Timer, keep map[string]bool) (int, error) { |
| if !c.HostWrites.Enabled { |
| return c.Reg.DeleteTimersExcept(host, KindSchedule, keep) |
| } |
| + if err := withTimerWork(ctx, func() error { return nil }); err != nil { |
| + return 0, err |
| + } |
| n := 0 |
| for _, row := range before { |
| if row.Kind != KindSchedule || keep[row.Name] { |
| continue |
| } |
| - if err := c.Reg.DeleteTimerRegistration(host, row.Name, row.Registration); err != nil { |
| + if err := deleteSelectedTimer(ctx, c.Reg, row); err != nil { |
| return n, err |
| } |
| n++ |
diff --git a/internal/schedule/plan14_timer_producers_test.go b/internal/schedule/plan14_timer_producers_test.go| new file mode 100644 |
| index 000000000..17b765a1d |
| --- /dev/null |
| +++ b/internal/schedule/plan14_timer_producers_test.go |
| @@ -0,0 +1,126 @@ |
| +package schedule |
| + |
| +import ( |
| + "context" |
| + "fmt" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "testing" |
| + "time" |
| +) |
| + |
| +type protectedTimerWork struct { |
| + t *testing.T |
| + reg *registry.Registry |
| + held bool |
| + calls int |
| +} |
| + |
| +func (w *protectedTimerWork) With(_ context.Context, _ hostwrite.Identity, f func() error) error { |
| + if w.held { |
| + w.t.Fatal("recursive host exclusion") |
| + } |
| + w.held = true |
| + defer func() { w.held = false }() |
| + return f() |
| +} |
| +func (w *protectedTimerWork) Select(context.Context, string, string) (hostwrite.Work, error) { |
| + return w, nil |
| +} |
| +func (w *protectedTimerWork) MutateTimer(_ context.Context, row registry.Timer, expected string, remove bool) (*registry.Timer, error) { |
| + if w.held { |
| + w.t.Fatal("root timer RPC inside host exclusion") |
| + } |
| + w.calls++ |
| + if remove { |
| + return nil, w.reg.DeleteTimerOwnedRegistration(row) |
| + } |
| + rt, err := w.reg.Get(row.Host) |
| + if err != nil { |
| + return nil, err |
| + } |
| + row.ClaimOwnership = rt.ClaimOwnership |
| + row.Registration = fmt.Sprint("root-", w.calls) |
| + row.ClaimRowID = row.Registration |
| + return w.reg.MaterializeTimerRegistration(row, expected) |
| +} |
| +func TestPlan14TimerProducerTransactions(t *testing.T) { |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + reg, _ := convergeFixture(t) |
| + host := "a.bab.oncarlos.com" |
| + rt, err := reg.Get(host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + owner := registry.ClaimOwnership{ClaimIncarnation: "inc", ClaimStamp: 1, ClaimOwner: "owner", ClaimRowID: "route"} |
| + if err := reg.AdoptClaimRoute(t.Context(), *rt, owner); err != nil { |
| + t.Fatal(err) |
| + } |
| + rt.ClaimOwnership = owner |
| + w := &protectedTimerWork{t: t, reg: reg} |
| + writes := hostwrite.Config{Enabled: true, Selector: w} |
| + s := ClaimTimers{TimerStore: reg, Writes: writes, Context: t.Context()} |
| + row := registry.Timer{Host: host, Name: "once", Kind: KindOnce, Spec: "at 2000", Path: "/once", NextAt: 2000} |
| + if err := s.UpsertTimer(row); err != nil { |
| + t.Fatal(err) |
| + } |
| + first, _ := reg.GetTimer(host, row.Name) |
| + if err := s.UpsertTimer(row); err != nil { |
| + t.Fatal(err) |
| + } |
| + second, _ := reg.GetTimer(host, row.Name) |
| + if first.Registration == second.Registration { |
| + t.Fatal("once timer was not re-registered") |
| + } |
| + if err := s.DeleteTimer(host, row.Name); err != nil { |
| + t.Fatal(err) |
| + } |
| + ctx := context.WithValue(t.Context(), timerWorkKey{}, timerWork{ctx: t.Context(), work: w}) |
| + c := NewConverger(reg, nil, "box", nil) |
| + c.HostWrites = writes |
| + d := Declaration{Schedules: []Schedule{{Name: "tick", Every: "1h", Path: "/tick"}}} |
| + if _, err := c.convergeHost(ctx, *rt, d, "", time.Unix(1000, 0)); err != nil { |
| + t.Fatal(err) |
| + } |
| + original, _ := reg.GetTimer(host, "tick") |
| + if _, err := c.convergeHost(ctx, *rt, d, "tick", time.Unix(1100, 0)); err != nil { |
| + t.Fatal(err) |
| + } |
| + fired, _ := reg.GetTimer(host, "tick") |
| + if original.Registration == fired.Registration || fired.NextAt != 1100 { |
| + t.Fatal("run-now skipped root mutation", fired) |
| + } |
| + if _, err := c.convergeHost(ctx, *rt, Declaration{}, "", time.Unix(1200, 0)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if w.calls != 6 { |
| + t.Fatalf("producers used %d root mutations, want 6", w.calls) |
| + } |
| + selected := SelectedTimers{Store: reg, Work: w, Context: t.Context(), Host: host} |
| + if err := selected.ReplaceTimers(host, []registry.Timer{row}); err == nil || w.calls != 6 { |
| + t.Fatal("import minted missing registration", err, w.calls) |
| + } |
| + row.Registration = "snapshot" |
| + if err := selected.ReplaceTimers(host, []registry.Timer{row, row}); err == nil || w.calls != 6 { |
| + t.Fatal("duplicate import mutated rows", err, w.calls) |
| + } |
| + if err := selected.ReplaceTimers(host, []registry.Timer{row}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := selected.ReplaceTimers(host, nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + if w.calls != 8 { |
| + t.Fatal("landing bypassed root mutations", w.calls) |
| + } |
| + if err := s.UpsertTimer(row); err != nil { |
| + t.Fatal(err) |
| + } |
| + old, _ := reg.GetTimer(host, row.Name) |
| + if err := deleteSelectedTimer(ctx, reg, *old); err != nil { |
| + t.Fatal(err) |
| + } |
| + if w.calls != 10 { |
| + t.Fatal("runner prune bypassed root mutation", w.calls) |
| + } |
| +} |
diff --git a/internal/schedule/runner.go b/internal/schedule/runner.go| index 8f37ef98b..f340d5d24 100644 |
| --- a/internal/schedule/runner.go |
| +++ b/internal/schedule/runner.go |
| @@ -375,9 +375,7 @@ func (r *Runner) prune(ctx context.Context, now time.Time) { |
| if row.Kind != KindOnce || row.NextAt != 0 || row.LastAt >= cutoff { |
| continue |
| } |
| - if err := withTimerWork(hctx, func() error { |
| - return r.Reg.DeleteTimerRegistration(row.Host, row.Name, row.Registration) |
| - }); err != nil { |
| + if err := deleteSelectedTimer(hctx, r.Reg, row); err != nil { |
| log.Printf("schedule: runner prune %s/%s: %v", row.Host, row.Name, err) |
| } |
| } |
| @@ -495,7 +493,7 @@ func (r *Runner) fire(ctx context.Context, rt registry.Route, t registry.Timer) |
| sp, err := ParseSpec(t.Spec) |
| if err != nil { |
| log.Printf("schedule: %s/%s: bad spec %q: %v — dropping the row", rt.Host, t.Name, t.Spec, err) |
| - if err := withTimerWork(ctx, func() error { return r.Reg.DeleteTimerRegistration(rt.Host, t.Name, t.Registration) }); err != nil { |
| + if err := deleteSelectedTimer(ctx, r.Reg, t); err != nil { |
| log.Printf("schedule: %s/%s: drop bad row: %v", rt.Host, t.Name, err) |
| } |
| return |
diff --git a/internal/schedule/timer_mutation.go b/internal/schedule/timer_mutation.go| new file mode 100644 |
| index 000000000..e9fbd9b4c |
| --- /dev/null |
| +++ b/internal/schedule/timer_mutation.go |
| @@ -0,0 +1,100 @@ |
| +package schedule |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| + "github.com/carlosframework/platform/internal/registry" |
| + "os" |
| +) |
| + |
| +type timerMutator interface { |
| + MutateTimer(context.Context, registry.Timer, string, bool) (*registry.Timer, error) |
| +} |
| + |
| +func mutateSelectedTimer(ctx context.Context, t registry.Timer, expected string, remove bool) (*registry.Timer, bool, error) { |
| + selected, ok := ctx.Value(timerWorkKey{}).(timerWork) |
| + if !ok || selected.work == nil { |
| + return nil, false, nil |
| + } |
| + m, ok := selected.work.(timerMutator) |
| + if !ok { |
| + return nil, false, nil |
| + } |
| + row, err := m.MutateTimer(ctx, t, expected, remove) |
| + return row, true, err |
| +} |
| + |
| +func deleteSelectedTimer(ctx context.Context, reg *registry.Registry, t registry.Timer) error { |
| + if _, handled, err := mutateSelectedTimer(ctx, t, t.Registration, true); handled { |
| + return err |
| + } |
| + return withTimerWork(ctx, func() error { return reg.DeleteTimerRegistration(t.Host, t.Name, t.Registration) }) |
| +} |
| + |
| +type ClaimTimers struct { |
| + TimerStore |
| + Writes hostwrite.Config |
| + Context context.Context |
| +} |
| + |
| +func (s ClaimTimers) UpsertTimer(t registry.Timer) error { |
| + work, err := s.Writes.Select(s.Context, t.Host, "timers") |
| + if err != nil { |
| + return err |
| + } |
| + return (SelectedTimers{Store: s.TimerStore, Work: work, Context: s.Context, Host: t.Host}).UpsertTimer(t) |
| +} |
| +func (s ClaimTimers) DeleteTimer(host, name string) error { |
| + work, err := s.Writes.Select(s.Context, host, "timers") |
| + if err != nil { |
| + return err |
| + } |
| + return (SelectedTimers{Store: s.TimerStore, Work: work, Context: s.Context, Host: host}).DeleteTimer(host, name) |
| +} |
| + |
| +func (s SelectedTimers) mutate(t registry.Timer, remove bool) (bool, error) { |
| + m, ok := s.Work.(timerMutator) |
| + if !ok { |
| + return false, nil |
| + } |
| + if s.Store == nil || s.Context == nil || t.Host != s.Host { |
| + return true, hostwrite.ErrUnavailable |
| + } |
| + var old *registry.Timer |
| + err := s.Work.With(s.Context, hostwrite.Identity{}, func() error { |
| + var err error |
| + old, err = s.Store.GetTimer(t.Host, t.Name) |
| + if errors.Is(err, os.ErrNotExist) { |
| + return nil |
| + } |
| + return err |
| + }) |
| + if err != nil { |
| + return true, err |
| + } |
| + expected := "" |
| + if old != nil { |
| + expected = old.Registration |
| + if old.Kind != t.Kind && !remove { |
| + return true, registry.ErrTimerKind |
| + } |
| + } |
| + if remove { |
| + if old == nil { |
| + return true, nil |
| + } |
| + t = *old |
| + } |
| + _, err = m.MutateTimer(s.Context, t, expected, remove) |
| + return true, err |
| +} |
| + |
| +func hasTimerMutator(ctx context.Context) bool { |
| + s, ok := ctx.Value(timerWorkKey{}).(timerWork) |
| + if !ok { |
| + return false |
| + } |
| + _, ok = s.work.(timerMutator) |
| + return ok |
| +} |