carlos / platform Public

region HA 2b: a failed ListMod lists the status reports whole

listBoxKeys (ee21b94a) took any release.ModLister's ListMod error as the
read's error. The claim's boundary stores (hostops' mutation boundary and
conditional record store) always have a ListMod and refuse it with
ErrContract when the store they wrap has none, while their List reads.
Under such a wrap the console's live edges read failed, and the move
door answered "the fleet's edges files could not all be read" before its
own refusals (TestMoveDoorHostClaimRefusals/changed-under-the-request,
TestMoveDoorHostClaimFailedRecordStartReleasesTheClaim).

The horizon is only an optimisation, so a failed ListMod now falls back
to List, main's path, with main's errors. The census overlay's
internal/status coverage row moves; no unit or seal changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev da072ed517e92a45b378f40bcd445bf7564c322b parent 2ef8bd6
3 files changed, +27 −4
  • internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv +2 −1
  • internal/status/fleet.go +9 −3
  • internal/status/fleet_listing_test.go +16 −0

CI failed — run details

CI log
=== gate ===
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
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	319.637s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.768s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.303s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.712s
ok  	github.com/carlosframework/platform/internal/activator	250.756s
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.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.465s
ok  	github.com/carlosframework/platform/internal/apperr	0.093s
ok  	github.com/carlosframework/platform/internal/applog	0.161s
ok  	github.com/carlosframework/platform/internal/awsinv	0.003s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.959s
?   	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.012s
ok  	github.com/carlosframework/platform/internal/certwarm	0.003s
ok  	github.com/carlosframework/platform/internal/cgroup	0.704s
ok  	github.com/carlosframework/platform/internal/changelog	0.334s
ok  	github.com/carlosframework/platform/internal/claimactivation	0.031s
ok  	github.com/carlosframework/platform/internal/clamd	0.059s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	404.170s
?   	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.211s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.008s
ok  	github.com/carlosframework/platform/internal/dbverify	0.006s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.005s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.367s
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	45.113s
?   	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.148s
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.014s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.181s
ok  	github.com/carlosframework/platform/internal/host	178.722s
ok  	github.com/carlosframework/platform/internal/hostactors	5.656s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.014s
ok  	github.com/carlosframework/platform/internal/hostbox	2.753s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.575s
ok  	github.com/carlosframework/platform/internal/hostenv	0.014s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.014s
ok  	github.com/carlosframework/platform/internal/hostexec	5.146s
ok  	github.com/carlosframework/platform/internal/hostfence	0.037s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.672s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.014s
ok  	github.com/carlosframework/platform/internal/hostguard	1.031s
ok  	github.com/carlosframework/platform/internal/hostops	1.583s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.013s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.014s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.449s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.130s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.013s
ok  	github.com/carlosframework/platform/internal/infraview	0.015s
ok  	github.com/carlosframework/platform/internal/instancectl	5.266s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.740s
ok  	github.com/carlosframework/platform/internal/instancefs	0.018s
ok  	github.com/carlosframework/platform/internal/instancekey	0.015s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.022s
ok  	github.com/carlosframework/platform/internal/layout	1.534s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.018s
ok  	github.com/carlosframework/platform/internal/lease	9.819s
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.170s
ok  	github.com/carlosframework/platform/internal/maintenance	0.079s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.524s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.075s
ok  	github.com/carlosframework/platform/internal/metrics	0.004s
ok  	github.com/carlosframework/platform/internal/moves	21.190s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.789s
ok  	github.com/carlosframework/platform/internal/objstore	0.188s
ok  	github.com/carlosframework/platform/internal/ops	0.178s
ok  	github.com/carlosframework/platform/internal/park	0.211s
ok  	github.com/carlosframework/platform/internal/privatefile	0.003s
ok  	github.com/carlosframework/platform/internal/provision	37.392s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.012s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.566s
ok  	github.com/carlosframework/platform/internal/relay	7.239s
ok  	github.com/carlosframework/platform/internal/release	22.467s
ok  	github.com/carlosframework/platform/internal/rollout	3.902s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.057s
ok  	github.com/carlosframework/platform/internal/rootstate	0.026s
ok  	github.com/carlosframework/platform/internal/runs	0.576s
ok  	github.com/carlosframework/platform/internal/s3compat	0.004s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.507s
ok  	github.com/carlosframework/platform/internal/seeds	0.011s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.191s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.689s
ok  	github.com/carlosframework/platform/internal/status	0.072s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.279s
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.005s
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.590s
=== 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: 54132898 bytes (ceiling 52000000)
the linux/arm64 binary is 54132898 bytes, over the 52000000 ceiling.
look for an SDK client stored in an interface under html/template's
method retention (every exported method of the client is kept):
store method values instead. See discussion #275 and the comment in
.amadan/ci.d/20-cross@5.

amadan: step cross shard 2 of 5 failed: exit status 1
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64

amadan: job failed at step cross shard 2 of 5 — its output is above, not at the tail of this log
diff --git a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv
index 59786c211..20ac219b2 100644
--- a/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv
+++ b/internal/provision/testdata/plan14-root-reads-region-ha-2b.tsv
@@ -7,6 +7,7 @@
# Task 7: root's half of joining (internal/host joinroot.go: carlos system join, which runs guard-arm in process; units carlos-system-join.path and .service; bootstrap's enabledUnits arms the path). joinroot.go adds no root-read unit: RunSystemGuardArm, authority F,J4, is called and not changed. The two unit files are new root inputs, ruled authority F,J4 on the condition that the path unit's file is only a trigger: root's join reads nothing from it. The legacy seal stands.
# Task 8: joining mode and the gate (internal/host joining.go; RunAgent calls hostingGate before runAgent; ClaimWiring.startRegional sets the health answer's hosting condition from the gate's decision and clears it when the worker ends). runAgent, authority B,C,D,F,J4, is not changed. No root-read unit changed or was added. Envelope-only, and the seal stands. Before the gate, RunAgent has a box carrying the group marker resolve its group reference (agentEntry, through Task 6's resolveGroupBoxReference, with backoff), whatever the switches say, so runAgent's loops never keep an empty label; joining touches the join marker again once a minute while the box stays invited, so a failed carlos system join is retried.
# Task 9 (system.go): the install-channel, join and host-env verbs join systemVerb's list and runSystem's dispatch, and the usage gains their lines. The two seed-overlay authority declarations are re-digested, class, task, basis and (empty) kinds unchanged: no root read is added. Merged onto main 26065378, whose activation-tools overlay (census-ruling-activation-tools) re-ruled both declarations for the claims verb: the two rows carry that overlay's class, basis (B,C,D,F,G,J4) and text, re-digested over the merged code with both sets of verbs.
+# CI fix (internal/status fleet.go listBoxKeys): a store whose ListMod fails is listed whole, as one that is not a ModLister already was, so the claim's boundary stores (whose ListMod refuses when the store they wrap has none) no longer fail the move door's edges read. Only the internal/status coverage row moves; no root-read unit is added or changed, and the seal does not move.
@coverage . 9949422c526be724b50a4614fc79623b9823a1aa0960d1472fd9050f8000c3b0
@coverage internal/hostenv 6b65e7987026a4c81f410c0afcfaca13bc2b252d7382c45564f670e36a045bcb
internal/hostenv/cli.go runHostEnv authority A,J4 rev3=census-ruling-region-ha-2b;basis=carlos system host-env: write runs as root at a box's start-up and writes /etc/carlos/host.env, 0600, whole (temporary file and rename), from allowlisted parameters with safe values only, refusing a missing required value; its one read site is seed's --from file, an operator's host.env whose values go only to the parameter store; write's path is fixed and reached only through writeHostEnv file:ReadFile@1500 df4470176b229b311927ff0ac4f268d354c3a2357cac4300ac0992d2ebb90899
@@ -22,7 +23,7 @@ internal/host/installchannel_linux.go removeBeneath authority F,J4 rev3=census-r
@coverage internal/console e891098191a724430f78e9aa0c5d1307b9343283e0cb7506163383c4dee0df7c
@legacy 2f4ca2a51661d8aae1e0f38a41efdcc9df9f1dd75411ca6742d6ee3ae71f1756 internal/hostactors/autoenroll.go AutoEnroller.inputs legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input;census-ruling-region-ha-2b file:Read@102,file:Read@305 c0ae921ee9d8df9de403d488d9708c565d659a7bf880199313f50bdc7c8f8127
internal/hostactors/autoenroll.go AutoEnroller.inputs legacy accepted-legacy accepted-legacy;rev3=J4;basis=root-input;census-ruling-region-ha-2b file:Read@102,file:Read@305 c0ae921ee9d8df9de403d488d9708c565d659a7bf880199313f50bdc7c8f8127
-@coverage internal/status a1eee66fc906d6a7275017fd30c346b64aaafe4f88a501538248e99e9d95c33d
+@coverage internal/status e80d1b6bd86b860a75c7483fb51ab47161f0648a4aff2d0d1e92d5e56495b4b8
@legacy 30a3ee6b3365c202bf93b26087888a582c6b8d6df1a9f1fc6ba12e639ddd8684 internal/host/agent.go statusLoop legacy accepted-legacy accepted-legacy;rev3=census-ruling-region-ha-2b;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body with its tick through groupStatusTick, so a box whose host.env carries the group marker (CARLOS_BOX_REFERENCE=group) publishes its status report only under its group reference, resolved afresh each tick, and nothing until it resolves, never under status.Label; every other box, fleet boxes included, gets statusTick with today's label and key; read sites unchanged whole:Route@6154,whole:Route@6181,whole:Route@7423,whole:Route@9437 666ddbc4779442db4c868253cf5062c690e20ad643197688305e0742f6de9283
internal/host/agent.go statusLoop legacy accepted-legacy accepted-legacy;rev3=census-ruling-region-ha-2b;basis=the agent (/opt/carlos/carlos agent, carlos-edge.service, user carlos): main's body with its tick through groupStatusTick, so a box whose host.env carries the group marker (CARLOS_BOX_REFERENCE=group) publishes its status report only under its group reference, resolved afresh each tick, and nothing until it resolves, never under status.Label; every other box, fleet boxes included, gets statusTick with today's label and key; read sites unchanged whole:Route@6154,whole:Route@6181,whole:Route@7423,whole:Route@9437 666ddbc4779442db4c868253cf5062c690e20ad643197688305e0742f6de9283
@coverage internal/host/units ef87da22b7bc84e059c7bf64d936bc5854427ccde36c7bc0d0c264a43fb83d2e
diff --git a/internal/status/fleet.go b/internal/status/fleet.go
index 3d4920043..dc0d51031 100644
--- a/internal/status/fleet.go
+++ b/internal/status/fleet.go
@@ -245,8 +245,14 @@ func LoadBoxesComplete(ctx context.Context, s release.Store) (boxes []Box, compl
// an optimisation, never authority: every reader still judges each
// report's own GeneratedAt as before (BoxStale, the console's
// VerifyReport), and a day keeps every box an operator would still look
-// for as stale. A store that cannot say when an object was written
-// (release.ModLister) is listed whole, as before.
+// for as stale. A store that cannot say when an object was written is
+// listed whole, as before: one that is not a release.ModLister, and one
+// that is but whose ListMod fails. The second is the claim's boundary
+// stores (hostops' mutation boundary and conditional record store, the
+// mail claim gate): they always have a ListMod method and answer it with
+// their own refusal when the store they wrap has none, while their List
+// still reads. Because the horizon is only an optimisation, a failed
+// ListMod never fails the read: List answers, with main's errors.
const statusListingHorizon = 24 * time.Hour
// listBoxKeys lists status/boxes/, without the reports the horizon drops.
@@ -257,7 +263,7 @@ func listBoxKeys(ctx context.Context, s release.Store) ([]string, error) {
}
mods, err := ml.ListMod(ctx, "status/boxes/")
if err != nil {
- return nil, err
+ return s.List(ctx, "status/boxes/")
}
cut := time.Now().Add(-statusListingHorizon)
keys := make([]string, 0, len(mods))
diff --git a/internal/status/fleet_listing_test.go b/internal/status/fleet_listing_test.go
index 0f354ce23..71c7adf26 100644
--- a/internal/status/fleet_listing_test.go
+++ b/internal/status/fleet_listing_test.go
@@ -3,6 +3,7 @@ package status
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"os"
"path/filepath"
@@ -16,6 +17,14 @@ import (
// object was written does.
type listOnly struct{ release.Store }
+// refusesListMod has a ListMod that always refuses, as the claim's
+// boundary stores answer when the store they wrap has none.
+type refusesListMod struct{ release.Store }
+
+func (refusesListMod) ListMod(context.Context, string) ([]release.KeyMod, error) {
+ return nil, errors.New("required contract unavailable")
+}
+
// T2 (region HA plan 2b): 250 reports of boxes a group replaced long ago
// do not hide the two boxes running now. The old reports are dropped from
// the listing before any read and before the key cap, and the reading is
@@ -64,4 +73,11 @@ func TestStaleReportsDoNotHideLiveBoxes(t *testing.T) {
if _, complete, err := LoadBoxesComplete(ctx, listOnly{s}); err != nil || complete {
t.Fatalf("listed whole: complete %v, err %v", complete, err)
}
+ // A store whose ListMod refuses is listed whole too: the horizon is an
+ // optimisation, so its failure never fails the read (the move door read
+ // the fleet through the claim's boundary stores and answered 409 when
+ // it did).
+ if _, complete, err := LoadBoxesComplete(ctx, refusesListMod{s}); err != nil || complete {
+ t.Fatalf("refused ListMod listed whole: complete %v, err %v", complete, err)
+ }
}