WIP claim Plan 14 track E checkpoint 32 (not for merge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 files changed,
+262
−10
internal/host/plan14_generation_requirements_root_linux_test.go+102 −1internal/host/plan14_generation_rows_test.go+23 −1internal/host/rootgeneration_inventory.go+5 −2internal/host/rootgeneration_rows.go+12 −0internal/provision/testdata/plan14-root-reads-e.tsv+7 −5internal/registry/artifacthosts.go+32 −0internal/registry/artifacthosts_test.go+69 −0internal/registry/readonly_test.go+8 −0internal/registry/remote.go+1 −1internal/registry/remote_dispatch.go+3 −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 361.868s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.866s
ok github.com/carlosframework/platform/cmd/moveprobe 92.213s
ok github.com/carlosframework/platform/cmd/zddprobe 0.702s
ok github.com/carlosframework/platform/internal/activator 234.095s
ok github.com/carlosframework/platform/internal/agentsock 0.016s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.021s
ok github.com/carlosframework/platform/internal/appconfig 2.275s
ok github.com/carlosframework/platform/internal/apperr 0.075s
ok github.com/carlosframework/platform/internal/applog 0.128s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.004s
ok github.com/carlosframework/platform/internal/boxid 0.658s
? 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.012s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.691s
ok github.com/carlosframework/platform/internal/changelog 0.305s
ok github.com/carlosframework/platform/internal/clamd 0.054s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 367.025s
? 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.248s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.008s
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.003s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.063s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.003s
ok github.com/carlosframework/platform/internal/edge 26.197s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.007s
ok github.com/carlosframework/platform/internal/fleetapi 0.120s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.015s
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 133.755s
ok github.com/carlosframework/platform/internal/hostactors 0.238s
ok github.com/carlosframework/platform/internal/hostbox 2.076s
ok github.com/carlosframework/platform/internal/hostclaim 0.495s
ok github.com/carlosframework/platform/internal/hostevidence 0.005s
ok github.com/carlosframework/platform/internal/hostexec 4.819s
ok github.com/carlosframework/platform/internal/hostfence 0.033s
ok github.com/carlosframework/platform/internal/hostfiles 10.529s
ok github.com/carlosframework/platform/internal/hostfloor 0.012s
ok github.com/carlosframework/platform/internal/hostguard 0.970s
ok github.com/carlosframework/platform/internal/hostops 1.398s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.002s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.449s
ok github.com/carlosframework/platform/internal/hostresolve 0.125s
ok github.com/carlosframework/platform/internal/hostwrite 0.002s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 1.812s
ok github.com/carlosframework/platform/internal/instanceexec 2.732s
ok github.com/carlosframework/platform/internal/instancefs 0.019s
ok github.com/carlosframework/platform/internal/instancekey 0.014s
ok github.com/carlosframework/platform/internal/instanceupdates 0.020s
ok github.com/carlosframework/platform/internal/layout 1.402s
ok github.com/carlosframework/platform/internal/lease 9.461s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.021s
ok github.com/carlosframework/platform/internal/ledger 0.011s
ok github.com/carlosframework/platform/internal/mail 0.158s
ok github.com/carlosframework/platform/internal/maintenance 0.088s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.514s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.090s
ok github.com/carlosframework/platform/internal/metrics 0.004s
ok github.com/carlosframework/platform/internal/moves 20.927s
ok github.com/carlosframework/platform/internal/moves/archive 0.733s
ok github.com/carlosframework/platform/internal/objstore 0.161s
ok github.com/carlosframework/platform/internal/ops 0.174s
ok github.com/carlosframework/platform/internal/park 0.199s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 13.497s
ok github.com/carlosframework/platform/internal/publicrelease 0.008s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.445s
ok github.com/carlosframework/platform/internal/relay 7.250s
ok github.com/carlosframework/platform/internal/release 21.807s
ok github.com/carlosframework/platform/internal/rollout 3.747s
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.025s
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.275s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.391s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.031s
ok github.com/carlosframework/platform/internal/status 0.038s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.211s
ok github.com/carlosframework/platform/internal/svcrole 0.001s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.003s
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.001s
ok github.com/carlosframework/platform/internal/vet 1.538s
=== 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
--- 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.027s
=== RUN TestT4StorageProbeMinio
2026/10/14 19:11:40 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DE7B2511B16B34, 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.021s
minio contract: PASS
diff --git a/internal/host/plan14_generation_requirements_root_linux_test.go b/internal/host/plan14_generation_requirements_root_linux_test.go| index 6ba8c5163..f9499b1e8 100644 |
| --- a/internal/host/plan14_generation_requirements_root_linux_test.go |
| +++ b/internal/host/plan14_generation_requirements_root_linux_test.go |
| @@ -116,6 +116,7 @@ func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) { |
| } |
| } |
| write(rootparticipation.Namespace, "census.json", rootparticipation.Census{Schema: 1, Box: "runner", Enrollment: "en", Migration: 1, Revision: 1, LaunchRevision: 1, Complete: true}) |
| + generationRunnerEmptyRegistry(t, dir, uid, gid) |
| rec := provision.Record{Schema: 1, Host: "retained.example", Account: "bab", App: "shop", Channel: "stable", CreatedAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)} |
| store, err := openDeployStore(t.Context()) |
| if err != nil { |
| @@ -247,7 +248,7 @@ func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) { |
| accept() |
| }) |
| } |
| - for _, attack := range []string{"route-missing", "route-owner", "timer-missing", "timer-registration", "extra-timer", "malformed-database"} { |
| + for _, attack := range []string{"unknown-route-host", "unknown-timer-host", "route-missing", "route-owner", "timer-missing", "timer-registration", "extra-timer", "malformed-database"} { |
| t.Run(attack, func(t *testing.T) { |
| initial := certificate() |
| rt, tm := route, timer |
| @@ -268,7 +269,36 @@ func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) { |
| extra.Name, extra.Registration, extra.ClaimRowID = "extra", "extra", "extra" |
| timers = append(timers, extra) |
| } |
| + cleanupUnknown := func() {} |
| rows(rt, timers) |
| + if attack == "unknown-route-host" || attack == "unknown-timer-host" { |
| + writer, err := openRegistryChild(filepath.Join(dataDir(), "registry.db"), uid, gid) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if attack == "unknown-route-host" { |
| + err = writer.Upsert(registry.Route{Host: "omitted.example"}) |
| + } else { |
| + err = writer.ReplaceTimers("omitted.example", []registry.Timer{{Host: "omitted.example", Name: "paused", Kind: "cron", NextAt: 0}}) |
| + } |
| + closeErr := writer.Close() |
| + if err != nil || closeErr != nil { |
| + t.Fatalf("unknown host setup: %v %v", err, closeErr) |
| + } |
| + cleanupUnknown = func() { |
| + writer, err := openRegistryChild(filepath.Join(dataDir(), "registry.db"), uid, gid) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer writer.Close() |
| + if err := writer.Delete("omitted.example"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := writer.ReplaceTimers("omitted.example", nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| path := filepath.Join(dataDir(), "registry.db") |
| if attack == "malformed-database" { |
| if err := os.Rename(path, path+".saved"); err != nil { |
| @@ -295,9 +325,80 @@ func TestPlan14GenerationRequiredArtifactsRootRunner(t *testing.T) { |
| t.Fatal(err) |
| } |
| } |
| + cleanupUnknown() |
| rows(route, []registry.Timer{timer}) |
| accept() |
| }) |
| } |
| } |
| + |
| +func generationRunnerEmptyRegistry(t *testing.T, opt string, uid, gid int) { |
| + t.Helper() |
| + journal, err := hostbox.OpenLaunchJournal("/var/lib/carlos-claims/journals.db") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := hostbox.NewJournalService(journal); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := journal.Close(); err != nil { |
| + 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 { |
| + pins, err := source.StagedPins(t.Context(), opt) |
| + if len(pins) != 0 { |
| + t.Fatalf("empty fixture has pins: %v", pins) |
| + } |
| + return err |
| + }) |
| + if (err != nil) != refuse { |
| + t.Fatalf("empty-root registry census: refuse=%v error=%v", refuse, err) |
| + } |
| + state, err := rootstate.Open(release.RootGenerationsDir, false, false) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer state.Close() |
| + c, err := (&release.RootGenerations{Store: state}).Completeness() |
| + if err != nil || c.Complete == refuse { |
| + t.Fatalf("empty-root certificate: %+v %v", c, err) |
| + } |
| + } |
| + check(t, false) |
| + for _, kind := range []string{"route", "timer"} { |
| + t.Run("empty-root-unknown-"+kind, func(t *testing.T) { |
| + writer, err := openRegistryChild(filepath.Join(dataDir(), "registry.db"), uid, gid) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if kind == "route" { |
| + err = writer.Upsert(registry.Route{Host: "omitted.example"}) |
| + } else { |
| + err = writer.ReplaceTimers("omitted.example", []registry.Timer{{Host: "omitted.example", Name: "paused", Kind: "cron", NextAt: 0}}) |
| + } |
| + closeErr := writer.Close() |
| + if err != nil || closeErr != nil { |
| + t.Fatalf("unknown host setup: %v %v", err, closeErr) |
| + } |
| + check(t, true) |
| + writer, err = openRegistryChild(filepath.Join(dataDir(), "registry.db"), uid, gid) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := writer.Delete("omitted.example"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := writer.ReplaceTimers("omitted.example", nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := writer.Close(); err != nil { |
| + t.Fatal(err) |
| + } |
| + check(t, false) |
| + }) |
| + } |
| +} |
diff --git a/internal/host/plan14_generation_rows_test.go b/internal/host/plan14_generation_rows_test.go| index d50bf29af..8fce06122 100644 |
| --- a/internal/host/plan14_generation_rows_test.go |
| +++ b/internal/host/plan14_generation_rows_test.go |
| @@ -17,7 +17,7 @@ import ( |
| ) |
| func TestPlan14GenerationRegistryRows(t *testing.T) { |
| - for _, attack := range []string{"none", "predecessor", "route-missing", "route-incarnation", "route-stamp", "route-owner", "route-id", "timer-missing", "timer-incarnation", "timer-stamp", "timer-owner", "timer-id", "timer-registration", "timer-extra", "route-entry-missing", "timer-entry-missing", "duplicate-entry", "timer-other-host", "wrong-root", "empty-installation", "unreadable", "changed-during-kernel", "row-next-pass", "timer-delivery-progress", "closed-registry"} { |
| + for _, attack := range []string{"none", "predecessor", "unknown-route-host", "unknown-timer-host", "unknown-host-during-kernel", "route-missing", "route-incarnation", "route-stamp", "route-owner", "route-id", "timer-missing", "timer-incarnation", "timer-stamp", "timer-owner", "timer-id", "timer-registration", "timer-extra", "route-entry-missing", "timer-entry-missing", "duplicate-entry", "timer-other-host", "wrong-root", "empty-installation", "unreadable", "changed-during-kernel", "row-next-pass", "timer-delivery-progress", "closed-registry"} { |
| t.Run(attack, func(t *testing.T) { |
| f, disk, _ := generationLaunchFixture(t) |
| if _, err := f.root.Serve(t.Context(), f.peer, f.req); err != nil { |
| @@ -76,6 +76,11 @@ func TestPlan14GenerationRegistryRows(t *testing.T) { |
| v := generationReconciler{read: func(ctx context.Context) (generationInventory, error) { |
| return generationRegistryInventory(ctx, reader, snapshot) |
| }, kernel: func(context.Context) (string, string, error) { |
| + if active && attack == "unknown-host-during-kernel" { |
| + if err := reg.ReplaceTimers("omitted.example", []registry.Timer{{Host: "omitted.example", Name: "paused", Kind: "cron", NextAt: 0}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| if active && attack == "changed-during-kernel" { |
| // The registry changes after the first read while root inventory stays fixed. |
| next := route |
| @@ -110,6 +115,14 @@ func TestPlan14GenerationRegistryRows(t *testing.T) { |
| } |
| rt, tm := route, timer |
| switch attack { |
| + case "unknown-route-host": |
| + if err := reg.Upsert(registry.Route{Host: "omitted.example"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "unknown-timer-host": |
| + if err := reg.ReplaceTimers("omitted.example", []registry.Timer{{Host: "omitted.example", Name: "paused", Kind: "cron", NextAt: 0}}); err != nil { |
| + t.Fatal(err) |
| + } |
| case "route-missing": |
| rt = registry.Route{} |
| case "route-incarnation": |
| @@ -196,6 +209,12 @@ func TestPlan14GenerationRegistryRows(t *testing.T) { |
| defer reg.Close() |
| } |
| active, reader = false, reg |
| + if err := reg.Delete("omitted.example"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg.ReplaceTimers("omitted.example", nil); err != nil { |
| + t.Fatal(err) |
| + } |
| if err := json.Unmarshal(saved, &snapshot); err != nil { |
| t.Fatal(err) |
| } |
| @@ -237,6 +256,9 @@ func TestPlan14GenerationRegistryProductionReader(t *testing.T) { |
| if _, err := productionGenerationRegistryInventory(t.Context(), snapshot); err != nil { |
| t.Fatal(err) |
| } |
| + if _, err := productionGenerationRegistryInventory(t.Context(), generationInventory{}); err == nil { |
| + t.Fatal("empty root census concealed an unknown registry host") |
| + } |
| snapshot.Payloads[0].Inventory.Entries = nil |
| if _, err := productionGenerationRegistryInventory(t.Context(), snapshot); err == nil { |
| t.Fatal("empty retained inventory erased a present row") |
diff --git a/internal/host/rootgeneration_inventory.go b/internal/host/rootgeneration_inventory.go| index 4fd3c9224..567158219 100644 |
| --- a/internal/host/rootgeneration_inventory.go |
| +++ b/internal/host/rootgeneration_inventory.go |
| @@ -231,8 +231,11 @@ func generationPayloadBindings(payloads []hostbox.GenerationPayload, installs ma |
| func productionGenerationInventory(ctx context.Context) (generationInventory, error) { |
| snapshot, err := generationInventoryAt(ctx, rootparticipation.Namespace, "/var/lib/carlos-claims/authority/fences", "/var/lib/carlos-claims/journals.db") |
| - if err != nil || len(snapshot.Installations) == 0 { |
| - return snapshot, err |
| + if err != nil { |
| + return generationInventory{}, err |
| + } |
| + if len(snapshot.Installations) == 0 { |
| + return productionGenerationRegistryInventory(ctx, snapshot) |
| } |
| store, err := openDeployStore(ctx) |
| if err != nil { |
diff --git a/internal/host/rootgeneration_rows.go b/internal/host/rootgeneration_rows.go| index 93996acd3..f42b69310 100644 |
| --- a/internal/host/rootgeneration_rows.go |
| +++ b/internal/host/rootgeneration_rows.go |
| @@ -58,6 +58,18 @@ func generationRegistryInventory(ctx context.Context, reg *registry.Registry, sn |
| } |
| } |
| } |
| + candidates, err := reg.ArtifactHosts() |
| + if err != nil { |
| + return zero, err |
| + } |
| + if len(candidates) > 8192 { |
| + return zero, os.ErrPermission |
| + } |
| + for _, host := range candidates { |
| + if !hosts[host] { |
| + return zero, os.ErrPermission |
| + } |
| + } |
| for host := range hosts { |
| if err := ctx.Err(); err != nil { |
| return zero, err |
diff --git a/internal/provision/testdata/plan14-root-reads-e.tsv b/internal/provision/testdata/plan14-root-reads-e.tsv| index 0b891395e..a81ea634b 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 31f9cc2f83dbad8a45e0bc453b80bb33d85f2e4187ce155c7a56afa69b7ce5cd |
| +@coverage internal/host fe2dff7143f2b673db504e54c2a75f141207c60855ead4b13bed85ec533450f7 |
| @coverage internal/release 2bb546a86c1ab49b02e424eb0b366b46776857a1249baa2d6fb61ead4209d6bd |
| @coverage internal/rootstate 6338f74a6b2059cf77cf478bd6111b37b441eb9d65a309464c378359d4bdac05 |
| 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 |
| @@ -303,7 +303,7 @@ 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 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. d928288910227cc14f158595e44d052436a23c407e89682dd0930ee1011d7e57 |
| internal/host/rootgeneration_inventory.go openGenerationLaunchInventory 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. 522445e6d8acbe083d9d8f001719c4dbd3af57c56429e6b73b05c55a3826cb8b |
| -internal/host/rootgeneration_inventory.go productionGenerationInventory authority E2 E31: census-only generation completeness compares retained route/timer obligations with actual ownership, row identity and timer registration through the existing read-only service-user parser on both source reads. Missing, replaced, extra or unreadable rows invalidate completeness without releasing pins. Row agreement does not establish omitted artifact classes or empty-installation authority. Fixtures: TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryReadOnly, TestPlan14GenerationRegistryProductionReader. Extended TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. 7eda05c4021b20a3c9aa102a291adac54ca2767484a3b21da7ef471d960e94ba |
| +internal/host/rootgeneration_inventory.go productionGenerationInventory 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. 2e0f4082ea9916f06b9dc50854a571274713cd53cc904da31ae5410eb1ee33f9 |
| internal/host/rootgeneration_inventory.go type generationInventory 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. 4de1dc2a2c9261b1e7bbd811d4d78a271d2fc5a37aeca196177f3e9527d0408e |
| 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 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. 593768e6ba937cf2cfa2635c8f796f767d6c2354737a72073cc351f514faed20 |
| @@ -317,6 +317,8 @@ internal/host/rootgeneration_payload_linux.go generationPayloadFiles authority E |
| 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 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. aaa70feaad4ed0331e7d45433bb0b6476601bbed68783294215cce16eaa5a3eb |
| 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 E31: census-only generation completeness compares retained route/timer obligations with actual ownership, row identity and timer registration through the existing read-only service-user parser on both source reads. Missing, replaced, extra or unreadable rows invalidate completeness without releasing pins. Row agreement does not establish omitted artifact classes or empty-installation authority. Fixtures: TestPlan14GenerationRegistryRows, TestPlan14GenerationRegistryReadOnly, TestPlan14GenerationRegistryProductionReader. Extended TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. registry:ClaimOwnership@1738,registry:ClaimOwnership@2117,registry:ClaimRowID@2170,registry:Host@1716,registry:Host@2095,registry:Name@2036,registry:Registration@2148,whole:ClaimOwnership@1252,whole:ClaimOwnership@1316,whole:ClaimOwnership@1330,whole:ClaimOwnership@1344,whole:ClaimOwnership@1615,whole:ClaimOwnership@1744,whole:ClaimOwnership@1762,whole:ClaimOwnership@1817,whole:ClaimOwnership@2066,whole:ClaimOwnership@2123,whole:ClaimOwnership@2141,whole:ClaimOwnership@2236,whole:ClaimOwnership@2264,whole:Route@1633,whole:Timer@1922,whole:Timer@2000 4f33a3cfa22218a9faecf3ad06b86ad7832a4b1f98c484cff79d948e8c7eb7b4 |
| -@coverage internal/registry c5d195f3b4e018a076aee1d2a139c407a9bdcbc43896f95660d3437667764fee |
| -internal/registry/remote.go serveRemote authority E2 E31 mixed declaration: E2 adds only TimersFor to the existing read-only child allowlist so root can compare actual timer identities with retained obligations without parsing SQLite as root. All mutations remain refused. Generic writable dispatch remains unchanged legacy behavior. Pre-flight 80, 82, 83 and 90 justify promotion while retaining the original immutable legacy fingerprint. Fixtures: TestRemoteReadOnlyWALRegistry; production credential-drop witness TestPlan14GenerationRequiredArtifactsRootRunner is runner-pending. file:Open@332,registry:CallbackError@1681,registry:CallbackFailed@1638,registry:Deadline@1264,registry:Deadline@1338,registry:Method@1022,registry:Method@1043,registry:Method@1573,whole:remoteRequest@1395,whole:remoteRequest@835,whole:remoteRequest@922,whole:remoteResponse@1117,whole:remoteResponse@1163,whole:remoteResponse@1457,whole:remoteResponse@1755,whole:remoteResponse@1791,whole:remoteResponse@392,whole:remoteResponse@431 0d083f04f3363b5df2aa9df4e05fe80014b2d07ec77c88fd449807eb170deb93 |
| +internal/host/rootgeneration_rows.go generationRegistryInventory 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. registry:ClaimOwnership@1976,registry:ClaimOwnership@2355,registry:ClaimRowID@2408,registry:Host@1954,registry:Host@2333,registry:Name@2274,registry:Registration@2386,whole:ClaimOwnership@1252,whole:ClaimOwnership@1316,whole:ClaimOwnership@1330,whole:ClaimOwnership@1344,whole:ClaimOwnership@1853,whole:ClaimOwnership@1982,whole:ClaimOwnership@2000,whole:ClaimOwnership@2055,whole:ClaimOwnership@2304,whole:ClaimOwnership@2361,whole:ClaimOwnership@2379,whole:ClaimOwnership@2474,whole:ClaimOwnership@2502,whole:Route@1871,whole:Timer@2160,whole:Timer@2238 ec488db0d8eae89879f0b0d628d20c74bcd35907599b32067a17db72707b69a3 |
| +@coverage internal/registry b8bc7442621fce6ae121cf8557f44289803ec85971ee30581bd4c85ffad480bd |
| +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 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. registry:A@6070,registry:Account@3533,registry:Accounts@2616,registry:Addr@4110,registry:Addrs@4118,registry:Alias@6118,registry:Alias@6182,registry:Alias@6270,registry:Aliases@5992,registry:Alive@7812,registry:Args@5694,registry:At@5135,registry:At@6201,registry:At@6279,registry:At@7560,registry:Attempts@7582,registry:B@2001,registry:Backing@3929,registry:BuildState@4336,registry:Changed@1809,registry:Changed@2902,registry:Changed@2989,registry:Changed@3078,registry:Changed@3149,registry:Changed@3231,registry:Changed@7492,registry:Channel@3593,registry:Channel@4576,registry:Channel@4980,registry:CheckedAt@5061,registry:Class@4048,registry:DoneBefore@7821,registry:Due@5464,registry:Due@5528,registry:Due@7604,registry:Env@4644,registry:Env@4891,registry:Env@4991,registry:ExcludeHosts@7287,registry:ExcludeHosts@7382,registry:ExpectGood@5260,registry:ExpectOp@2957,registry:Expected@1063,registry:Expected@289,registry:Expected@752,registry:F@1781,registry:F@1842,registry:Fleet@1867,registry:Fleet@2059,registry:Fleet@2141,registry:Fleet@2222,registry:Fleet@2315,registry:FleetBoxes@2276,registry:Fleets@1925,registry:From@3704,registry:From@3765,registry:FromDB@3840,registry:FromOp@3045,registry:FromSock@3850,registry:Hash@4516,registry:Health@4127,registry:Host@272,registry:Host@2730,registry:Host@2771,registry:Host@2857,registry:Host@2933,registry:Host@3019,registry:Host@3111,registry:Host@3183,registry:Host@3275,registry:Host@3525,registry:Host@3585,registry:Host@3643,registry:Host@3696,registry:Host@3757,registry:Host@3832,registry:Host@3921,registry:Host@3975,registry:Host@4040,registry:Host@4102,registry:Host@4180,registry:Host@4255,registry:Host@4328,registry:Host@4393,registry:Host@4454,registry:Host@4508,registry:Host@4636,registry:Host@4690,registry:Host@4751,registry:Host@4813,registry:Host@5043,registry:Host@5127,registry:Host@5184,registry:Host@5252,registry:Host@5341,registry:Host@5401,registry:Host@5456,registry:Host@5520,registry:Host@5635,registry:Host@5686,registry:Host@5753,registry:Host@5828,registry:Host@5891,registry:Host@6488,registry:Host@6635,registry:Host@6923,registry:Host@6987,registry:Host@7046,registry:Host@7129,registry:Host@7212,registry:Host@7443,registry:Host@7528,registry:Host@7660,registry:Host@7741,registry:Host@7904,registry:Host@837,registry:Host@948,registry:Hosts@187,registry:Hosts@4567,registry:Hosts@4882,registry:Hosts@4971,registry:Id@1057,registry:Id@1368,registry:Id@1467,registry:Id@1547,registry:Id@1698,registry:IdValue@2435,registry:Idle@5761,registry:Journal@1075,registry:Journal@1374,registry:Journal@1553,registry:Journal@442,registry:Keep@7920,registry:Kind@7749,registry:Kind@7912,registry:Label@2068,registry:Label@2150,registry:Label@2231,registry:Label@4759,registry:Limit@7278,registry:Limit@7373,registry:MaxRun@5769,registry:Method@151,registry:Ms@7576,registry:Name@1895,registry:Name@2454,registry:Name@6995,registry:Name@7054,registry:Name@7137,registry:Name@7451,registry:Name@7536,registry:Name@7668,registry:Name@845,registry:Name@956,registry:Next@301,registry:NextAt@7594,registry:NextGood@5274,registry:NextValue@1627,registry:NextValue@1725,registry:Now@5962,registry:Now@7271,registry:Now@7366,registry:Now@7459,registry:Now@980,registry:Number64@2542,registry:Number@7709,registry:Number@7780,registry:Number@7865,registry:Observed@365,registry:ObservedValue@643,registry:Old@1620,registry:Old@1718,registry:On@5409,registry:On@5836,registry:On@5899,registry:Op@2875,registry:Op@2951,registry:Op@3119,registry:Op@3191,registry:Op@3283,registry:Owner@280,registry:Owner@377,registry:Owner@660,registry:P@6589,registry:Path@6820,registry:Paths@6517,registry:Prefix@6643,registry:Prev@5199,registry:Registration@7145,registry:Registration@7544,registry:Registration@853,registry:Registration@964,registry:Replica@4462,registry:Replicas@4401,registry:Retirement@1704,registry:Route@2707,registry:Routes@2665,registry:Routes@3309,registry:Routes@5562,registry:Routes@5929,registry:Rows@6931,registry:Seed@5643,registry:Sqid@2446,registry:Sqid@2509,registry:Sqid@2588,registry:Status@2240,registry:Status@2865,registry:Status@2941,registry:Status@3027,registry:Status@3651,registry:Status@5051,registry:Status@6191,registry:Status@7566,registry:Status@7676,registry:Status@869,registry:Steering@4188,registry:StringMap@2352,registry:T@6871,registry:T@747,registry:TValue@2812,registry:Tags@2159,registry:Target@4821,registry:Targets@453,registry:Text@1273,registry:Text@1420,registry:Text@2556,registry:Text@6676,registry:Text@6739,registry:Timer@6959,registry:Timer@704,registry:Timers@7182,registry:Timers@7241,registry:Timers@7330,registry:Times@7954,registry:To@3712,registry:To@3773,registry:ToDB@3862,registry:ToOp@3037,registry:ToSock@3870,registry:TokenHash@2077,registry:Tunnel@4698,registry:Unit@3983,registry:UpdateSettings@4263,registry:Ver@5192,registry:Ver@5349,registry:W@6422,registry:Wildcards@6316,whole:Account@2620,whole:ClaimOwnership@282,whole:ClaimOwnership@379,whole:ClaimOwnership@662,whole:ClaimRow@455,whole:ClaimRowArchive@561,whole:ClaimRowArchive@571,whole:ColdRows@1622,whole:ColdRows@1629,whole:ColdRows@1720,whole:ColdRows@1727,whole:Fleet@1783,whole:Fleet@1844,whole:Fleet@1871,whole:Fleet@1929,whole:FleetBox@2003,whole:FleetBox@2280,whole:GoodBuild@5262,whole:GoodBuild@5276,whole:Route@2669,whole:Route@2711,whole:Route@2814,whole:Route@3313,whole:Route@3468,whole:Route@3474,whole:Route@367,whole:Route@5566,whole:Route@5933,whole:RouteAlias@5996,whole:RouteAlias@6072,whole:RoutePath@6521,whole:RoutePath@6591,whole:Timer@645,whole:Timer@6873,whole:Timer@6933,whole:Timer@6963,whole:Timer@708,whole:Timer@7186,whole:Timer@7245,whole:Timer@7334,whole:Timer@749,whole:WildcardDomain@6320,whole:WildcardDomain@6424 df83a3282714a65c50c067415dfe51c0be4a7e8e384b6b0041875c365cc7f264 |
diff --git a/internal/registry/artifacthosts.go b/internal/registry/artifacthosts.go| new file mode 100644 |
| index 000000000..0c0bc22d8 |
| --- /dev/null |
| +++ b/internal/registry/artifacthosts.go |
| @@ -0,0 +1,32 @@ |
| +package registry |
| + |
| +import "os" |
| + |
| +// ArtifactHosts discovers candidates; it supplies no installation authority. |
| +// Include orphan and inactive timers. Due-time filtering would hide obligations. |
| +func (r *Registry) ArtifactHosts() ([]string, error) { |
| + if r.remote != nil { |
| + out, err := r.remote.call(nil, remoteRequest{Method: "ArtifactHosts"}, nil) |
| + return out.Hosts, err |
| + } |
| + rows, err := r.sql.Query(`SELECT host FROM routes UNION SELECT host FROM timers ORDER BY host LIMIT 8193`) |
| + if err != nil { |
| + return nil, err |
| + } |
| + defer rows.Close() |
| + var hosts []string |
| + for rows.Next() { |
| + var host string |
| + if err := rows.Scan(&host); err != nil { |
| + return nil, err |
| + } |
| + if len(hosts) == 8192 { |
| + return nil, os.ErrPermission |
| + } |
| + hosts = append(hosts, host) |
| + } |
| + if err := rows.Err(); err != nil { |
| + return nil, err |
| + } |
| + return hosts, nil |
| +} |
diff --git a/internal/registry/artifacthosts_test.go b/internal/registry/artifacthosts_test.go| new file mode 100644 |
| index 000000000..8328ef63f |
| --- /dev/null |
| +++ b/internal/registry/artifacthosts_test.go |
| @@ -0,0 +1,69 @@ |
| +package registry |
| + |
| +import ( |
| + "fmt" |
| + "reflect" |
| + "testing" |
| +) |
| + |
| +func TestArtifactHosts(t *testing.T) { |
| + for _, mode := range []string{"empty", "union", "missing-timers", "closed", "limit", "overflow"} { |
| + t.Run(mode, func(t *testing.T) { |
| + r := openTestRegistry(t) |
| + defer r.Close() |
| + var want []string |
| + switch mode { |
| + case "union": |
| + for _, host := range []string{"route-only", "both"} { |
| + if err := r.Upsert(Route{Host: host}); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + for _, host := range []string{"orphan", "both"} { |
| + if err := r.ReplaceTimers(host, []Timer{{Host: host, Name: "paused", Kind: "cron", NextAt: 0}, {Host: host, Name: "done", Kind: "once", NextAt: -1}}); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + want = []string{"both", "orphan", "route-only"} |
| + case "missing-timers": |
| + if _, err := r.sql.Exec("DROP TABLE timers"); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "closed": |
| + if err := r.Close(); err != nil { |
| + t.Fatal(err) |
| + } |
| + case "limit", "overflow": |
| + n := 8192 |
| + if mode == "overflow" { |
| + n++ |
| + } |
| + tx, err := r.sql.Begin() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + defer tx.Rollback() |
| + for i := 0; i < n; i++ { |
| + host := fmt.Sprintf("host-%05d", i) |
| + if _, err := tx.Exec("INSERT INTO timers (host,name,kind,spec,path,next_at,created_at) VALUES (?, 'paused', 'cron', '', '', 0, 1)", host); err != nil { |
| + t.Fatal(err) |
| + } |
| + want = append(want, host) |
| + } |
| + if err := tx.Commit(); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + got, err := r.ArtifactHosts() |
| + if mode == "closed" || mode == "missing-timers" || mode == "overflow" { |
| + if err == nil || got != nil { |
| + t.Fatalf("incomplete discovery: count=%d error=%v", len(got), err) |
| + } |
| + return |
| + } |
| + if err != nil || !reflect.DeepEqual(got, want) { |
| + t.Fatalf("host discovery: count=%d want=%d error=%v", len(got), len(want), err) |
| + } |
| + }) |
| + } |
| +} |
diff --git a/internal/registry/readonly_test.go b/internal/registry/readonly_test.go| index c0fd0477b..f0b0e7ad2 100644 |
| --- a/internal/registry/readonly_test.go |
| +++ b/internal/registry/readonly_test.go |
| @@ -4,6 +4,7 @@ import ( |
| "io" |
| "os" |
| "path/filepath" |
| + "reflect" |
| "testing" |
| ) |
| @@ -27,6 +28,9 @@ func TestRemoteReadOnlyWALRegistry(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| + if err := writer.ReplaceTimers("orphan", []Timer{{Host: "orphan", Name: "paused", Kind: "cron", NextAt: 0}}); err != nil { |
| + t.Fatal(err) |
| + } |
| for _, suffix := range []string{"", "-wal", "-shm"} { |
| if err := os.Chmod(path+suffix, 0444); err != nil { |
| t.Fatal(err) |
| @@ -55,6 +59,10 @@ func TestRemoteReadOnlyWALRegistry(t *testing.T) { |
| t.Fatal(err) |
| } |
| defer reader.Close() |
| + hosts, err := reader.ArtifactHosts() |
| + if err != nil || !reflect.DeepEqual(hosts, []string{"orphan", "readonly"}) { |
| + t.Fatalf("readonly artifact hosts: %v %v", hosts, err) |
| + } |
| row, err := reader.Get("readonly") |
| if err != nil || row == nil || row.Account != "acct" || row.App != "app" { |
| t.Fatalf("readonly WAL read: %+v %v", row, err) |
diff --git a/internal/registry/remote.go b/internal/registry/remote.go| index ea5197302..2dd340353 100644 |
| --- a/internal/registry/remote.go |
| +++ b/internal/registry/remote.go |
| @@ -275,7 +275,7 @@ func serveRemote(path string, input io.Reader, output io.Writer, readOnly bool) |
| } |
| }() |
| for q := range requests { |
| - if readOnly && q.Method != "Get" && q.Method != "TimersFor" { |
| + if readOnly && q.Method != "Get" && q.Method != "TimersFor" && q.Method != "ArtifactHosts" { |
| out := remoteResponse{} |
| setResponseError(&out, errRemoteMethod) |
| if err := enc.Encode(out); err != nil { |
diff --git a/internal/registry/remote_dispatch.go b/internal/registry/remote_dispatch.go| index 42534b1a0..511217da0 100644 |
| --- a/internal/registry/remote_dispatch.go |
| +++ b/internal/registry/remote_dispatch.go |
| @@ -95,6 +95,7 @@ type remoteRequest struct { |
| } |
| type remoteResponse struct { |
| + Hosts []string |
| Failed bool |
| Error string |
| ErrorKind int |
| @@ -122,6 +123,8 @@ type remoteResponse struct { |
| func (r *Registry) dispatchRemote(ctx context.Context, q remoteRequest, callback func(remoteResponse) error) (out remoteResponse, err error) { |
| switch q.Method { |
| + case "ArtifactHosts": |
| + out.Hosts, err = r.ArtifactHosts() |
| case "SetClaimHold": |
| err = r.SetClaimHold(ctx, q.Host, q.Owner, q.Expected, q.Next) |
| case "AdoptClaimRoute": |