carlos / platform Public

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>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6286ad951d8849892d6b8d8754c72f21c13bfe10 parent a84ca97
40 files changed, +1694 −91
  • internal/host/agent.go +4 −0
  • internal/host/claimassembly.go +4 −0
  • internal/host/plan14_generation_creation_test.go +9 −0
  • internal/host/plan14_generation_registry_root_linux_test.go +18 −0
  • internal/host/plan14_generation_requirements_linux_test.go +15 −0
  • internal/host/plan14_generation_requirements_root_linux_test.go +32 −1
  • internal/host/plan14_generation_retirement_test.go +38 −0
  • internal/host/plan14_generation_timer_retirement_test.go +25 −1
  • internal/host/plan14_installed_launch_root_linux_test.go +390 −0
  • internal/host/plan14_timer_intention_test.go +183 −3
  • internal/host/rootgeneration_inventory.go +16 −1
  • internal/host/rootgeneration_parked.go +12 −2
  • internal/host/rootgeneration_requirements.go +36 −10
  • internal/host/rootgeneration_rows.go +3 −0
  • internal/host/rootinventory.go +15 −2
  • internal/host/rootinventory_timer.go +6 −3
  • internal/host/rootinventory_timer_mutation.go +114 −0
  • internal/host/rootinventory_timer_recovery.go +51 −0
  • internal/host/rootlaunchverifier.go +1 −1
  • internal/hostbox/coldhandover.go +19 −0
  • internal/hostbox/coldhandover_test.go +14 −1
  • internal/hostbox/inventory_expected.go +36 −1
  • internal/hostbox/inventoryscope.go +3 −0
  • internal/hostbox/journalservice.go +1 −0
  • internal/hostbox/reconcile_adapter.go +2 −2
  • internal/hostbox/registryrows.go +1 −1
  • internal/hostbox/stopinventory.go +1 −0
  • internal/hostbox/timer_registration.go +134 −0
  • internal/hostbox/typedretire.go +12 −8
  • internal/hostbox/work.go +2 −0
  • internal/hostguard/rootinventory_settle.go +34 −0
  • internal/hostguard/rootinventory_settle_test.go +71 −0
  • internal/provision/plan14_e_write_census_test.go +14 −4
  • internal/provision/testdata/plan14-root-reads-e.tsv +52 −16
  • internal/registry/claimrows.go +9 −1
  • internal/schedule/claimtimers.go +35 −0
  • internal/schedule/converge.go +54 −29
  • internal/schedule/plan14_timer_producers_test.go +126 −0
  • internal/schedule/runner.go +2 −4
  • internal/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
+}