system status: bound the bootstrap comparison to 10s (#316, Astra P2); census overlay
The comparison reads the whole bootstrap binary inside the status read, which is also carlos system update's preview; on slow storage it could outrun the CLI's 30-second timeout and fail both. Past bootstrapReadBudget the row reads unreadable and the rest of the response stands. A ctx that ended during the read is reported even by a store that read on regardless. Census: envelope-only overlay bootstrap-carlos-follows-316 for . and internal/console, proposed on #280. The seal stands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+31
−1
internal/console/apisystembootstrap_test.go+8 −0internal/console/apisystemupdate.go+13 −1internal/provision/plan14_root_reads_test.go+7 −0internal/provision/testdata/plan14-root-reads-bootstrap-carlos-follows-316.tsv+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.012s
? 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 324.948s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.911s
ok github.com/carlosframework/platform/cmd/moveprobe 93.329s
ok github.com/carlosframework/platform/cmd/zddprobe 0.711s
ok github.com/carlosframework/platform/internal/activator 250.779s
ok github.com/carlosframework/platform/internal/agentsock 0.019s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.023s
ok github.com/carlosframework/platform/internal/appconfig 2.524s
ok github.com/carlosframework/platform/internal/apperr 0.085s
ok github.com/carlosframework/platform/internal/applog 0.140s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.005s
ok github.com/carlosframework/platform/internal/boxid 0.510s
? 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.009s
ok github.com/carlosframework/platform/internal/caps 0.014s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.712s
ok github.com/carlosframework/platform/internal/changelog 0.327s
ok github.com/carlosframework/platform/internal/claimactivation 0.856s
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 407.710s
? 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.149s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.017s
ok github.com/carlosframework/platform/internal/dbverify 0.010s
ok github.com/carlosframework/platform/internal/dnsrec 0.007s
ok github.com/carlosframework/platform/internal/docsite 0.013s
ok github.com/carlosframework/platform/internal/domains 2.430s
ok github.com/carlosframework/platform/internal/doorkey 0.015s
ok github.com/carlosframework/platform/internal/economics 0.012s
ok github.com/carlosframework/platform/internal/edge 45.133s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.016s
ok github.com/carlosframework/platform/internal/fleetapi 0.140s
ok github.com/carlosframework/platform/internal/gate 1.661s
ok github.com/carlosframework/platform/internal/genselect 0.021s
ok github.com/carlosframework/platform/internal/geo 0.021s
ok github.com/carlosframework/platform/internal/geoip 0.035s
ok github.com/carlosframework/platform/internal/historyreadback 0.166s
ok github.com/carlosframework/platform/internal/host 184.652s
ok github.com/carlosframework/platform/internal/hostactors 5.484s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.013s
ok github.com/carlosframework/platform/internal/hostbox 2.880s
ok github.com/carlosframework/platform/internal/hostclaim 0.643s
ok github.com/carlosframework/platform/internal/hostenv 0.013s
ok github.com/carlosframework/platform/internal/hostevidence 0.012s
ok github.com/carlosframework/platform/internal/hostexec 5.173s
ok github.com/carlosframework/platform/internal/hostfence 0.042s
ok github.com/carlosframework/platform/internal/hostfiles 10.467s
ok github.com/carlosframework/platform/internal/hostfloor 0.015s
ok github.com/carlosframework/platform/internal/hostguard 1.047s
ok github.com/carlosframework/platform/internal/hostops 1.677s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.012s
ok github.com/carlosframework/platform/internal/hostquarantine 0.012s
ok github.com/carlosframework/platform/internal/hostrelocate 0.547s
ok github.com/carlosframework/platform/internal/hostresolve 0.150s
ok github.com/carlosframework/platform/internal/hostwrite 0.014s
ok github.com/carlosframework/platform/internal/infraview 0.014s
ok github.com/carlosframework/platform/internal/instancectl 5.227s
ok github.com/carlosframework/platform/internal/instanceexec 2.757s
ok github.com/carlosframework/platform/internal/instancefs 0.018s
ok github.com/carlosframework/platform/internal/instancekey 0.016s
ok github.com/carlosframework/platform/internal/instanceupdates 0.018s
ok github.com/carlosframework/platform/internal/layout 1.614s
ok github.com/carlosframework/platform/internal/lbhealth 0.015s
ok github.com/carlosframework/platform/internal/lease 9.814s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.007s
ok github.com/carlosframework/platform/internal/ledger 0.014s
ok github.com/carlosframework/platform/internal/mail 0.189s
ok github.com/carlosframework/platform/internal/maintenance 0.098s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.583s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.075s
ok github.com/carlosframework/platform/internal/metrics 0.008s
ok github.com/carlosframework/platform/internal/moves 21.310s
ok github.com/carlosframework/platform/internal/moves/archive 0.824s
ok github.com/carlosframework/platform/internal/objstore 0.220s
ok github.com/carlosframework/platform/internal/ops 0.192s
ok github.com/carlosframework/platform/internal/park 0.227s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 39.623s
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 5.650s
ok github.com/carlosframework/platform/internal/relay 7.251s
ok github.com/carlosframework/platform/internal/release 24.537s
ok github.com/carlosframework/platform/internal/rollout 3.894s
ok github.com/carlosframework/platform/internal/rolloutwords 0.005s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.061s
ok github.com/carlosframework/platform/internal/rootstate 0.027s
ok github.com/carlosframework/platform/internal/runs 0.582s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.005s
ok github.com/carlosframework/platform/internal/schedule 2.522s
ok github.com/carlosframework/platform/internal/seeds 0.013s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.583s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.462s
ok github.com/carlosframework/platform/internal/status 0.069s
ok github.com/carlosframework/platform/internal/steering 0.007s
ok github.com/carlosframework/platform/internal/stopevidence 0.287s
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.007s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.005s
ok github.com/carlosframework/platform/internal/usage 0.005s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.599s
=== 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: 49414306 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: downloading https://github.com/minio/minio/releases/download/RELEASE.2025-09-07T16-13-09Z/minio.linux-arm64.RELEASE.2025-09-07T16-13-09Z
minio: downloaded, digest verified
mc: downloading https://github.com/minio/mc/releases/download/RELEASE.2025-08-13T08-35-41Z/mc.linux-arm64.RELEASE.2025-08-13T08-35-41Z
mc: downloaded, 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.023s
=== RUN TestT4StorageProbeMinio
2026/10/19 23:59:32 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E013C15AF714BB, 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/console/apisystembootstrap_test.go b/internal/console/apisystembootstrap_test.go| index d051e1574..4221b8f25 100644 |
| --- a/internal/console/apisystembootstrap_test.go |
| +++ b/internal/console/apisystembootstrap_test.go |
| @@ -59,4 +59,12 @@ func TestSystemStatusReportsWhetherBootstrapIsStablesBuild(t *testing.T) { |
| if row, _ := bootstrap().(map[string]any); row["state"] != "current" { |
| t.Fatalf("matching bootstrap: row = %v, want current", row) |
| } |
| + // Past its budget the comparison reads unreadable, and the status read |
| + // still answers (Astra review: a slow store must not time the CLI out). |
| + was := bootstrapReadBudget |
| + bootstrapReadBudget = 0 |
| + t.Cleanup(func() { bootstrapReadBudget = was }) |
| + if row, _ := bootstrap().(map[string]any); row["state"] != "unreadable" || row["error"] == "" { |
| + t.Fatalf("over budget: row = %v, want unreadable with the error", row) |
| + } |
| } |
diff --git a/internal/console/apisystemupdate.go b/internal/console/apisystemupdate.go| index e6a58a2d4..f3f3dd9b5 100644 |
| --- a/internal/console/apisystemupdate.go |
| +++ b/internal/console/apisystemupdate.go |
| @@ -615,7 +615,8 @@ func (s *Server) systemArtifactMatches(ctx context.Context, key, sha string) (bo |
| } |
| // objectSHA256 streams the object at key through a hasher; absent is |
| -// os.ErrNotExist. |
| +// os.ErrNotExist. A ctx that ended during the read is its error, even on a |
| +// store that read on regardless. |
| func (s *Server) objectSHA256(ctx context.Context, key string) (string, error) { |
| rc, _, err := release.OpenObject(ctx, s.cfg.Releases, key) |
| if err != nil { |
| @@ -626,6 +627,9 @@ func (s *Server) objectSHA256(ctx context.Context, key string) (string, error) { |
| if _, err := io.Copy(h, rc); err != nil { |
| return "", fmt.Errorf("%s: reading the object to compare it: %w", key, err) |
| } |
| + if err := ctx.Err(); err != nil { |
| + return "", fmt.Errorf("%s: reading the object to compare it: %w", key, err) |
| + } |
| return hex.EncodeToString(h.Sum(nil)), nil |
| } |
| @@ -638,6 +642,12 @@ const bootstrapKey = "bootstrap/carlos" |
| // bootstrapPlatform is the one platform bootstrap/carlos serves. |
| const bootstrapPlatform = "linux-arm64" |
| +// bootstrapReadBudget bounds the comparison. It reads the whole binary, and |
| +// the status read it rides (also `carlos system update`'s preview) must |
| +// answer inside the CLI's 30-second timeout on slow storage: past the budget |
| +// the row reads unreadable and the rest of the response stands. |
| +var bootstrapReadBudget = 10 * time.Second |
| + |
| // bootstrapState is the status row for bootstrapKey against stable's signed |
| // linux-arm64 artifact, or nil when there is nothing to compare: no verified |
| // stable pointer, no linux-arm64 artifact in it, or no bootstrap object (a |
| @@ -652,6 +662,8 @@ func (s *Server) bootstrapState(ctx context.Context, stable *release.SystemPoint |
| return nil |
| } |
| row := map[string]any{"version": stable.Current.Version, "key": art.Key} |
| + ctx, cancel := context.WithTimeout(ctx, bootstrapReadBudget) |
| + defer cancel() |
| got, err := s.objectSHA256(ctx, bootstrapKey) |
| switch { |
| case errors.Is(err, os.ErrNotExist): |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index 8e68b7dbb..c9c8e9b54 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -933,6 +933,13 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-claim-guard-umask.tsv"), &inv, true); err != nil { |
| t.Fatal(err) |
| } |
| + // Census overlay bootstrap-carlos-follows-316 (amadan #316: system status |
| + // compares bootstrap/carlos with stable's build), loaded last. Envelope |
| + // only, proposed; ruling asked of the census owner on #280. The seal |
| + // stands. |
| + if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-bootstrap-carlos-follows-316.tsv"), &inv, true); err != nil { |
| + t.Fatal(err) |
| + } |
| seal = "53b3cbb6f46d657ce32f49dad84dae79316c80da949ccf37273833eef5c5b2d7" |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
diff --git a/internal/provision/testdata/plan14-root-reads-bootstrap-carlos-follows-316.tsv b/internal/provision/testdata/plan14-root-reads-bootstrap-carlos-follows-316.tsv| new file mode 100644 |
| index 000000000..d92e2784f |
| --- /dev/null |
| +++ b/internal/provision/testdata/plan14-root-reads-bootstrap-carlos-follows-316.tsv |
| @@ -0,0 +1,3 @@ |
| +# Census overlay bootstrap-carlos-follows-316 (amadan #316: carlos system status compares bootstrap/carlos with stable's signed linux-arm64 artifact), loaded LAST. Envelope only: no root-read unit is added or changed; the console's status door gains a store read of bootstrap/carlos and the CLI prints the result. Proposed; ruling asked of the census owner on #280. The seal stands. |
| +@coverage internal/console f697865970fd453298b73b42dc31b7c3b4ae2fd97e27a4837522eb8bc9b604b0 |
| +@coverage . ea902ca81c5eff7e3eb4b6b36afe1e0557101e8b27e27b6821f7b2073534c0a3 |