carlos / platform Public

Merge remote-tracking branch 'origin/main' into feat/claim-root-trusted-state

claude agent, operated by paul@keymail.dev 345b9044bfa457c2934759035a2ef0b1df458c99 parent 6291ebaparent 34a4328
6 files changed, +372 −43
  • internal/activator/instancekey_test.go +65 −0
  • internal/console/layoutguard_test.go +7 −4
  • internal/host/agent.go +7 −3
  • internal/host/instancekeys.go +72 −11
  • internal/host/instancekeys_test.go +185 −11
  • internal/host/layoutleg.go +36 −14

CI failed — 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	354.854s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.215s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.260s
ok  	github.com/carlosframework/platform/internal/activator	227.109s
ok  	github.com/carlosframework/platform/internal/agentsock	0.015s
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.397s
ok  	github.com/carlosframework/platform/internal/apperr	0.074s
ok  	github.com/carlosframework/platform/internal/applog	0.131s
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.649s
?   	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.201s
ok  	github.com/carlosframework/platform/internal/changelog	0.304s
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.867s
?   	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	5.774s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.005s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.004s
ok  	github.com/carlosframework/platform/internal/domains	2.217s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	26.089s
?   	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.126s
ok  	github.com/carlosframework/platform/internal/gate	1.657s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.016s
ok  	github.com/carlosframework/platform/internal/geoip	0.029s
ok  	github.com/carlosframework/platform/internal/host	90.942s
ok  	github.com/carlosframework/platform/internal/hostactors	0.552s
ok  	github.com/carlosframework/platform/internal/hostbox	2.218s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.516s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.015s
ok  	github.com/carlosframework/platform/internal/hostexec	4.972s
ok  	github.com/carlosframework/platform/internal/hostfence	0.034s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.490s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.014s
ok  	github.com/carlosframework/platform/internal/hostguard	0.870s
ok  	github.com/carlosframework/platform/internal/hostops	1.509s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.014s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.014s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.453s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.139s
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	1.730s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.726s
ok  	github.com/carlosframework/platform/internal/instancefs	0.018s
ok  	github.com/carlosframework/platform/internal/instancekey	0.017s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.030s
ok  	github.com/carlosframework/platform/internal/layout	1.445s
ok  	github.com/carlosframework/platform/internal/lease	9.473s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.007s
ok  	github.com/carlosframework/platform/internal/ledger	0.017s
ok  	github.com/carlosframework/platform/internal/mail	0.168s
ok  	github.com/carlosframework/platform/internal/maintenance	0.089s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.564s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.081s
ok  	github.com/carlosframework/platform/internal/metrics	0.006s
ok  	github.com/carlosframework/platform/internal/moves	21.165s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.769s
ok  	github.com/carlosframework/platform/internal/objstore	0.173s
ok  	github.com/carlosframework/platform/internal/ops	0.167s
ok  	github.com/carlosframework/platform/internal/park	0.204s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
2026/10/13 14:38:50 provision: listing apps/bab/blog/instances/: list refused — its records are unseen this pass
2026/10/13 14:38:50 provision: listing apps/bab/: listdirs refused — its records are unseen this pass
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: listing apps/bac/tito/instances/: list refused — its records are unseen this pass
2026/10/13 14:38:50 provision: listing apps/bac/tito/instances/: list refused — its records are unseen this pass
2026/10/13 14:38:50 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:38:50 provision: remove retired instance route shop.example: provision: claim admission unavailable
2026/10/13 14:38:50 provision: remove retired instance route shop.example: provision: claim admission unavailable
2026/10/13 14:38:50 provision: remove retired sidecar route worker.sidecar.internal: provision: claim admission unavailable
2026/10/13 14:38:50 provision: remove retired sidecar route worker.sidecar.internal: provision: claim admission unavailable
2026/10/13 14:38:50 provision: claim install acme.go.tito.io: provision: claim admission unavailable
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/13 14:38:50 provision: claim install bot.eleven.bac.sidecar.internal: provision: claim admission unavailable
2026/10/13 14:38:50 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/13 14:38:50 provision: claim install bot.eleven.bac.sidecar.internal: registry: claim row ownership changed or unknown
2026/10/13 14:38:50 provision: refusing to mint route acme.go.tito.io (app "tito" acct "bac"): this box declares CARLOS_INBOUND=none (no-inbound) and cannot serve routed instance traffic
2026/10/13 14:38:50 provision: acme.go.tito.io: not minting this pass: the box's layout intent could not be read: no sealed config
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: REFUSING to mint sidecar bot.eleven.bac.sidecar.internal: bot.eleven.bac.sidecar.internal-sidecar.env is staged on this box — the legacy file co-spawn and a native sidecar row are two mechanisms for one host; remove the file to migrate this host onto sidecar records
2026/10/13 14:38:50 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/13 14:38:50 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/13 14:38:50 provision: refusing to mint route acme.go.tito.io (app "tito" acct "bac"): this box declares CARLOS_INBOUND=none (no-inbound) and cannot serve routed instance traffic
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: REFUSING to mint sidecar bot.eleven.bac.sidecar.internal: bot.eleven.bac.sidecar.internal-sidecar.env is staged on this box — the legacy file co-spawn and a native sidecar row are two mechanisms for one host; remove the file to migrate this host onto sidecar records
2026/10/13 14:38:50 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/13 14:38:50 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/13 14:38:50 provision: bot.eleven.bac.sidecar.internal exists but is not a sidecar row this pass owns (kind "sidecar" provisioned false app "eleven" acct "bac") — leaving it alone
2026/10/13 14:38:50 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/13 14:38:50 provision: claim install bot.eleven.bac.sidecar.internal: registry: claim row ownership changed or unknown
2026/10/13 14:38:50 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/13 14:38:50 provision: bot.eleven.bac.sidecar.internal exists but is not a sidecar row this pass owns (kind "sidecar" provisioned false app "eleven" acct "bac") — leaving it alone
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:38:50 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/13 14:38:50 provision: skipping record apps/bac/tito/instances/new.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/13 14:38:50 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/13 14:38:50 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/13 14:38:50 provision: skipping record apps/bac/tito/instances/new.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/13 14:38:50 provision: minted instance route rk.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route rl.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route ra.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route rb.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route re.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route rh.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route ri.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route rj.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route rc.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route rd.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route rf.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:50 provision: minted instance route rg.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route rc.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route rd.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route ra.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route rb.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: skipping record apps/bac/tito/instances/rb.go.tito.io.json: read failed: read refused
2026/10/13 14:38:51 provision: skipping record apps/bac/tito/instances/rb.go.tito.io.json: read failed: read refused
2026/10/13 14:38:51 provision: minted instance route re.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.appe.example (app "appe" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.appf.example (app "appf" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.appg.example (app "appg" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.appi.example (app "appi" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.appj.example (app "appj" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.appc.example (app "appc" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.apph.example (app "apph" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.appa.example (app "appa" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.appb.example (app "appb" acct "bac")
2026/10/13 14:38:51 provision: minted instance route x.appd.example (app "appd" acct "bac")
2026/10/13 14:38:51 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/13 14:38:51 provision: minted instance route r0122.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0123.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0054.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0010.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0013.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0075.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0106.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0114.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0126.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0022.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0049.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0060.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0061.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0065.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0069.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0099.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0100.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0014.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0031.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0059.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0118.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0008.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0032.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0056.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0003.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0109.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0079.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0037.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0066.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0067.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0103.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0001.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0028.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0046.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0124.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0050.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0085.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0090.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0097.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0002.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0025.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0077.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0004.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0023.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0095.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0108.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0007.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0030.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0078.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0094.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0105.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0112.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0017.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0084.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0107.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0129.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0021.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0044.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0055.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0064.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0113.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0125.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0063.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0104.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0026.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0051.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0119.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0011.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0027.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0048.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0080.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0091.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0116.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0012.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0040.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0045.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0072.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0087.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0115.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0120.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0020.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0029.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0034.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0041.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0058.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0070.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0053.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0071.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0074.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0086.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0088.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0110.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0117.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0019.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0024.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0062.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0082.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0089.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0127.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0052.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0000.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0006.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0018.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0035.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0043.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0073.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0093.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0047.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0057.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0068.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0096.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0111.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0101.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0015.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0033.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0092.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0016.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0036.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0042.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0076.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0098.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0102.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0121.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0038.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0039.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0128.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0005.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0009.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0081.go.tito.io (app "tito" acct "bac")
2026/10/13 14:38:51 provision: minted instance route r0083.go.tito.io (app "tito" acct "bac")
--- FAIL: TestPlan14RootReadCensus (10.32s)
    plan14_root_reads_test.go:627: root read census: 7 discrepancies
        changed root read internal/host/agent.go:runAgent; review class, task, sites and digest
        changed root read internal/host/instancekeys.go:backfillInstanceKeys; review class, task, sites and digest
        changed root read internal/host/layoutleg.go:activatorStart.Run; review class, task, sites and digest
        changed root source envelope internal/host
        unclassified root read internal/host/instancekeys.go:keyBackfillInSlot.BackfillInstanceKey registry:Host@199,registry:Host@258,whole:Route@351,whole:Route@386,whole:Route@550
        changed legacy declaration internal/host/instancekeys.go:backfillInstanceKeys
        changed legacy declaration internal/host/layoutleg.go:activatorStart.Run
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: refusing to mint route acme.go.tito.io (app "tito" acct "bac"): this box declares CARLOS_INBOUND=none (no-inbound) and cannot serve routed instance traffic
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route console.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted service route ha.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted service route svc.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route whole.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route shared.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: converged acme.go.tito.io channel stable -> edge
2026/10/13 14:39:02 provision: converged acme.go.tito.io channel edge -> canary/rehearsal
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: empty channel
2026/10/13 14:39:02 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: converged acme.go.tito.io environment "" -> "staging"
2026/10/13 14:39:02 provision: converged acme.go.tito.io environment "staging" -> ""
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: converged acme.go.tito.io steering "" -> "latency"
2026/10/13 14:39:02 provision: converged acme.go.tito.io steering "latency" -> ""
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: converged acme.go.tito.io hibernate false -> true
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: converged acme.go.tito.io hibernate true -> false (replication "daily" cleared: only a hibernating route's rung is read)
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: converged acme.go.tito.io status-named false -> true
2026/10/13 14:39:02 provision: converged acme.go.tito.io status-named true -> false
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted service route ha.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: converged ha.go.tito.io upstreams addr "" -> "", addrs "127.0.0.1:7001,127.0.0.1:7002" -> "127.0.0.1:7001,127.0.0.1:7003" (probe "{\"path\":\"/\",\"status\":200,\"interval_ms\":2000,\"fail_after\":3,\"rise_after\":2}" -> "{\"path\":\"/\",\"status\":200,\"interval_ms\":2000,\"fail_after\":3,\"rise_after\":2}")
2026/10/13 14:39:02 provision: converged ha.go.tito.io upstreams addr "" -> "", addrs "127.0.0.1:7001,127.0.0.1:7003" -> "127.0.0.1:7003,127.0.0.1:7001" (probe "{\"path\":\"/\",\"status\":200,\"interval_ms\":2000,\"fail_after\":3,\"rise_after\":2}" -> "{\"path\":\"/\",\"status\":200,\"interval_ms\":2000,\"fail_after\":3,\"rise_after\":2}")
2026/10/13 14:39:02 provision: converged ha.go.tito.io upstreams addr "" -> "", addrs "127.0.0.1:7003,127.0.0.1:7001" -> "127.0.0.1:7003,127.0.0.1:7001" (probe "{\"path\":\"/\",\"status\":200,\"interval_ms\":2000,\"fail_after\":3,\"rise_after\":2}" -> "{\"path\":\"/healthz\",\"status\":200,\"interval_ms\":2000,\"fail_after\":3,\"rise_after\":2}")
2026/10/13 14:39:02 provision: minted service route whole.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: converged whole.go.tito.io upstreams addr "127.0.0.1:9443" -> "", addrs "" -> "127.0.0.1:9443,127.0.0.1:9444" (probe "" -> "{\"path\":\"/\",\"status\":200,\"interval_ms\":2000,\"fail_after\":3,\"rise_after\":2}")
2026/10/13 14:39:02 provision: converged whole.go.tito.io upstreams addr "" -> "", addrs "127.0.0.1:9443,127.0.0.1:9444" -> "" (probe "{\"path\":\"/\",\"status\":200,\"interval_ms\":2000,\"fail_after\":3,\"rise_after\":2}" -> "")
2026/10/13 14:39:02 provision: whole.go.tito.io: record describes a different route SHAPE than the row (kind "instance"/"service" unit ""/"" socket "/run/carlos/whole.go.tito.io.sock"/"" db "/data/whole.go.tito.io.db"/"") — not converged in place; `carlos remove whole.go.tito.io` purges this route's capability grants (registry.Delete's contract, not a bug) before the next pass re-mints it, so treat that as a last resort, not a routine fix
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: minted service route whole.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: skipping accounts/bac/account.json: malformed account manifest: invalid character 'o' in literal null (expecting 'u')
2026/10/13 14:39:02 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: manifest bac/tito: read failed: provision: manifest bac/tito: invalid character 'o' in literal null (expecting 'u')
2026/10/13 14:39:02 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: disagrees with its own path (host="wrong-host.go.tito.io" account="bac" app="tito")
2026/10/13 14:39:02 provision: skipping record apps/bac/tito/instances/acme$evil.go.tito.io.json: host "acme$evil.go.tito.io" is not a hostname
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: skipping record apps/bac/tito/instances/acme$evil.go.tito.io.json: host "acme$evil.go.tito.io" is not a hostname
2026/10/13 14:39:02 provision: not removing acme$evil.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/13 14:39:02 provision: skipping record apps/bac/tito/instances/bad-service.go.tito.io.json: invalid shape: kind service with no addr: the platform only proxies to a service, so the record must name the upstream it proxies to
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: skipping record apps/bac/tito/instances/bad-service.go.tito.io.json: invalid shape: kind service with no addr: the platform only proxies to a service, so the record must name the upstream it proxies to
2026/10/13 14:39:02 provision: not removing bad-service.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/13 14:39:02 provision: skipping record apps/bac/tito/instances/no-channel.go.tito.io.json: empty channel
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: skipping record apps/bac/tito/instances/no-channel.go.tito.io.json: empty channel
2026/10/13 14:39:02 provision: not removing no-channel.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/13 14:39:02 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/13 14:39:02 provision: removed retired instance route ghost.go.tito.io (record gone)
2026/10/13 14:39:02 provision: duplicate host dup.go.tito.io: keeping apps/bac/tito/instances/dup.go.tito.io.json, skipping apps/bad/tito/instances/dup.go.tito.io.json
2026/10/13 14:39:02 provision: minted instance route dup.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/13 14:39:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/13 14:39:02 provision: removed retired instance route acme.go.tito.io (app in the trash)
2026/10/13 14:39:02 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:02 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/13 14:39:02 provision: removed retired static route site.go.tito.io (app in the trash)
2026/10/13 14:39:02 provision: trash marker for bac/tito unreadable: release: malformed trash marker for bac/tito: invalid character 'n' looking for beginning of object key string — not minting for this app
2026/10/13 14:39:02 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/13 14:39:02 provision: removed retired instance route acme.go.tito.io (app in the trash)
2026/10/13 14:39:02 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/13 14:39:02 provision: trash marker for bac/tito unreadable: release: malformed trash marker for bac/tito: invalid character 'n' looking for beginning of object key string — not minting for this app
2026/10/13 14:39:02 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/13 14:39:02 provision: converged acme.go.tito.io channel stable -> edge
2026/10/13 14:39:02 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/13 14:39:02 provision: converged acme.go.tito.io channel canary/rehearsal -> edge
2026/10/13 14:39:02 provision: converged acme.go.tito.io environment "staging" -> ""
2026/10/13 14:39:02 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/13 14:39:02 provision: acme.go.tito.io exists but is not the route this record describes (backing "unit" (box-scoped, not expressible in a record)) — leaving it alone
2026/10/13 14:39:02 provision: acme.go.tito.io exists but is not the route this record describes (tunnel "pi-fleet" (box-scoped, not expressible in a record)) — leaving it alone
2026/10/13 14:39:02 provision: acme.go.tito.io exists but is not the route this record describes (caps "port:25" (box-scoped, not expressible in a record)) — leaving it alone
2026/10/13 14:39:02 provision: acme.go.tito.io exists but is not the route this record describes (platform (owned by the domains reconcile pass)) — leaving it alone
2026/10/13 14:39:02 provision: acme.go.tito.io exists but is not the route this record describes (canary (owned by the domains reconcile pass)) — leaving it alone
2026/10/13 14:39:02 provision: acme.go.tito.io exists but is not the route this record describes (db "/data/other.db"/"/data/acme.go.tito.io.db") — leaving it alone
2026/10/13 14:39:02 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/13 14:39:02 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/13 14:39:02 provision: dry-run: would mint instance route acme.go.tito.io (app "tito" acct "bac" channel "stable")
2026/10/13 14:39:02 provision: dry-run: would adopt existing route adopt.go.tito.io (app "tito" acct "bac") — this record would own its channel and environment
2026/10/13 14:39:02 provision: dry-run: would converge adopt.go.tito.io channel stable -> edge
2026/10/13 14:39:02 provision: dry-run: would converge adopt.go.tito.io environment "" -> "staging"
2026/10/13 14:39:02 provision: dry-run: would converge owned.go.tito.io channel stable -> edge
2026/10/13 14:39:02 provision: dry-run: would converge owned.go.tito.io environment "" -> "staging"
2026/10/13 14:39:02 provision: dry-run: would converge owned.go.tito.io hibernate false -> true
2026/10/13 14:39:02 provision: dry-run: would converge owned.go.tito.io seed
2026/10/13 14:39:02 provision: dry-run: would remove retired instance route gone.go.tito.io (record gone)
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: adopted existing route adopt.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/13 14:39:03 provision: converged adopt.go.tito.io channel stable -> edge
2026/10/13 14:39:03 provision: converged adopt.go.tito.io environment "" -> "staging"
2026/10/13 14:39:03 provision: converged owned.go.tito.io channel stable -> edge
2026/10/13 14:39:03 provision: converged owned.go.tito.io environment "" -> "staging"
2026/10/13 14:39:03 provision: converged owned.go.tito.io hibernate false -> true
2026/10/13 14:39:03 provision: removed retired instance route gone.go.tito.io (record gone)
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: adopted existing route story.carlosframework.com (app "story" acct "bad") — this record now owns its channel and environment
2026/10/13 14:39:03 provision: story.carlosframework.com exists but is not the route this record describes (kind "instance"/"static") — leaving it alone
2026/10/13 14:39:03 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted service route pub.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted service route pubs.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route exec.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted service route loop.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted service route named.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted service route priv.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route unit.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted static route pisite.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: removed retired static route site.go.tito.io (record gone)
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route acme-uk.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: converged acme.go.tito.io replicas "ie,uk" -> "uk"
2026/10/13 14:39:03 provision: converged acme.go.tito.io replicas "uk" -> ""
2026/10/13 14:39:03 provision: minted instance route acme-uk.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: converged acme-uk.go.tito.io replica mark true -> false
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: acme.go.tito.io: row is on the nested layout (db "/data/instances/acme.go.tito.io/acme.go.tito.io.db" socket "/run/carlos/instances/acme.go.tito.io/app.sock"); the record's shared paths are not drift
2026/10/13 14:39:03 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: acme.go.tito.io: not minting this pass: the box's layout intent could not be read: store down
2026/10/13 14:39:03 provision: router.go.tito.io: not minting this pass: the box's layout intent could not be read: store down
2026/10/13 14:39:03 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/13 14:39:03 provision: converged acme.go.tito.io channel stable -> edge
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
2026/10/13 14:39:03 provision: not removing acme.go.tito.io (record gone): participant of a running operation
2026/10/13 14:39:03 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
2026/10/13 14:39:03 provision: refusing to mint held route acme.go.tito.io (app "tito" acct "bac"): this box declares CARLOS_INBOUND=none (no-inbound) and cannot serve routed instance traffic
2026/10/13 14:39:03 provision: held acme.go.tito.io landing by mv_acme (was ready)
2026/10/13 14:39:03 provision: acme.go.tito.io: held restoring by rb_other, not by mv_acme; leaving it
2026/10/13 14:39:03 provision: acme.go.tito.io: the row is not this pass's (app "other" acct ""); not holding it for mv_acme
2026/10/13 14:39:03 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/13 14:39:03 provision: dry-run: would mint instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac" channel "stable")
2026/10/13 14:39:03 provision: acme.go.tito.io: not held landing by mv_acme: the row's hold changed under this pass; the next pass judges it afresh
2026/10/13 14:39:03 provision: held acme.go.tito.io landing by mv_acme (was ready)
2026/10/13 14:39:03 provision: removed retired sidecar route gone.eleven.bac.sidecar.internal (record gone)
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: converged sidecar bot.eleven.bac.sidecar.internal args ["sidecar","run"] -> ["worker","run"]
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: converged sidecar bot.eleven.bac.sidecar.internal ceilings 300/3600 -> 60/120
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: converged sidecar bot.eleven.bac.sidecar.internal channel stable -> canary/eleven
2026/10/13 14:39:03 provision: converged sidecar bot.eleven.bac.sidecar.internal environment "other" -> "relay"
2026/10/13 14:39:03 provision: converged sidecar bot.eleven.bac.sidecar.internal hibernate true -> false
2026/10/13 14:39:03 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: read failed: failStore: reads disabled under apps/bac/eleven/sidecars/
2026/10/13 14:39:03 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: malformed: invalid character 'n' looking for beginning of object key string
2026/10/13 14:39:03 provision: not removing sidecar bot.eleven.bac.sidecar.internal: its record exists but could not be read cleanly this pass
2026/10/13 14:39:03 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: empty channel
2026/10/13 14:39:03 provision: skipping sidecar record apps/bac/eleven/sidecars/held.json: empty channel
2026/10/13 14:39:03 provision: not removing sidecar held.eleven.bac.sidecar.internal: its record exists but could not be read cleanly this pass
2026/10/13 14:39:03 provision: removed retired sidecar route old.eleven.bac.sidecar.internal (record gone)
2026/10/13 14:39:03 provision: bac/eleven has sidecar records but no sidecars manifest — not minting
2026/10/13 14:39:03 provision: bac/eleven is in the trash — not minting sidecar routes for it
2026/10/13 14:39:03 provision: sidecar bot.eleven.bac.sidecar.internal is not placeable here: it asks for "no-inbound" placement and this box accepts inbound work (CARLOS_INBOUND="")
2026/10/13 14:39:03 provision: sidecar hub.relay.bac.sidecar.internal is not placeable here: it asks for "no-inbound" placement and this box accepts inbound work (CARLOS_INBOUND="")
2026/10/13 14:39:03 provision: minted sidecar route hub.relay.bac.sidecar.internal (app "relay" acct "bac" args ["relay"])
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: sidecar bot.eleven.bac.sidecar.internal is not placeable here: it asks for "no-inbound" placement and this box accepts inbound work (CARLOS_INBOUND="")
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: sidecar bot.eleven.bac.sidecar.internal is not placeable here: it asks for "no-inbound" placement and this box accepts inbound work (CARLOS_INBOUND="")
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: sidecar bot.eleven.bac.sidecar.internal is not placeable here: it asks for "no-inbound" placement and this box accepts inbound work (CARLOS_INBOUND="")
2026/10/13 14:39:03 provision: removed retired sidecar route bot.eleven.bac.sidecar.internal (record gone)
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: sidecar bot.eleven.bac.sidecar.internal is not placeable here: it asks for "no-inbound" placement and this box accepts inbound work (CARLOS_INBOUND="")
2026/10/13 14:39:03 provision: skipping sidecar record apps/bac/relay/sidecars/hub.json: read failed: failStore: reads disabled under apps/bac/relay/sidecars/
2026/10/13 14:39:03 provision: REFUSING to mint sidecar bot.eleven.bac.sidecar.internal: bot.eleven.bac.sidecar.internal-sidecar.env is staged on this box — the legacy file co-spawn and a native sidecar row are two mechanisms for one host; remove the file to migrate this host onto sidecar records
2026/10/13 14:39:03 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/13 14:39:03 provision: dry-run: would mint sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" channel "stable" args ["sidecar","run"])
2026/10/13 14:39:03 provision: dry-run: would remove retired sidecar route gone.eleven.bac.sidecar.internal (record gone)
2026/10/13 14:39:03 provision: bot.eleven.bac.sidecar.internal exists but is not a sidecar row this pass owns (kind "sidecar" provisioned false app "eleven" acct "bac") — leaving it alone
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:03 provision: converged acme.go.tito.io update settings "{\"updates\":\"manual\",\"channel\":\"stable\"}" -> ""
2026/10/13 14:39:03 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:04 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:04 provision: manifest bac/tito: read failed: provision: manifest bac/tito: unexpected end of JSON input
2026/10/13 14:39:04 provision: manifest bac/tito: read failed: provision: manifest bac/tito: unexpected end of JSON input
2026/10/13 14:39:04 provision: acme.go.tito.io: not minted this pass: the app's update policy could not be read
2026/10/13 14:39:04 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/13 14:39:04 provision: manifest bac/tito: read failed: provision: manifest bac/tito: unexpected end of JSON input
2026/10/13 14:39:04 provision: acme.go.tito.io: not minted this pass: the app's update policy could not be read
2026/10/13 14:39:04 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
FAIL
FAIL	github.com/carlosframework/platform/internal/provision	13.691s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.084s
ok  	github.com/carlosframework/platform/internal/relay	7.252s
ok  	github.com/carlosframework/platform/internal/release	20.370s
ok  	github.com/carlosframework/platform/internal/rollout	3.785s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.007s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.008s
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.326s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.061s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.370s
ok  	github.com/carlosframework/platform/internal/status	0.039s
ok  	github.com/carlosframework/platform/internal/steering	0.006s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.132s
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.004s
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.005s
ok  	github.com/carlosframework/platform/internal/usage	0.005s
ok  	github.com/carlosframework/platform/internal/version	0.001s
ok  	github.com/carlosframework/platform/internal/vet	1.563s
FAIL
make: *** [Makefile:18: test] Error 1

amadan: step gate failed: exit status 2

amadan: job failed at step gate — its output is above, not at the tail of this log
diff --git a/internal/activator/instancekey_test.go b/internal/activator/instancekey_test.go
index 5bc223dfe..c632af017 100644
--- a/internal/activator/instancekey_test.go
+++ b/internal/activator/instancekey_test.go
@@ -1002,3 +1002,68 @@ func TestBackfillInstanceKeyRefusesFencedAndConflictRows(t *testing.T) {
})
}
}
+
+// v0.26.1 hotfix: the agent's backfill now runs beside the activator
+// rather than before it, so a backfill and a start's own mint can reach
+// the same host at once. Both read the files and ask a slow bucket outside
+// adminTokenMu, and both are held there until each has done so, so each
+// decides from a first read that found no key. They still decide and
+// append under the lock, create-only, so the host ends with exactly one
+// key, appended whole after the bytes that were there, and exactly one of
+// the two reports it minted.
+func TestBackfillAndAStartsMintRacingOnOneHostWriteOneKey(t *testing.T) {
+ const rounds = 40
+ tok := "CARLOS_ADMIN_TOKEN=" + strings.Repeat("b", 64) + "\n"
+ for i := range rounds {
+ b := &ExecBackend{DataDir: t.TempDir()}
+ b.ConfiguredKey = func(ctx context.Context, _ registry.Route) (string, error) {
+ select {
+ case <-time.After(5 * time.Millisecond): // a slow bucket that sets no key
+ return "", nil
+ case <-ctx.Done():
+ return "", ctx.Err()
+ }
+ }
+ rt := registry.Route{Host: "shop.example.com", Kind: "instance", Status: "ready", Socket: "/run/carlos/shop.sock"}
+ own := filepath.Join(b.DataDir, rt.Host+".env")
+ if err := os.WriteFile(own, []byte(tok), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ // Both callers pass phase one before either takes the lock.
+ var phase sync.WaitGroup
+ phase.Add(2)
+ instanceKeyHooks.betweenPhases = func(string) { phase.Done(); phase.Wait() }
+ var wg sync.WaitGroup
+ var backfilled, started InstanceKeyOutcome
+ var berr, serr error
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ backfilled, berr = b.BackfillInstanceKey(t.Context(), rt)
+ }()
+ go func() {
+ defer wg.Done()
+ started, serr = b.ensureInstanceKeyFor(t.Context(), rt, b.pathsFor(rt))
+ }()
+ wg.Wait()
+ instanceKeyHooks.betweenPhases = nil
+ if berr != nil || serr != nil {
+ t.Fatalf("round %d: backfill %v, %v; start %v, %v", i, backfilled, berr, started, serr)
+ }
+ minted := 0
+ for _, o := range []InstanceKeyOutcome{backfilled, started} {
+ switch o {
+ case InstanceKeyMinted:
+ minted++
+ case InstanceKeyPresent:
+ default:
+ t.Fatalf("round %d: outcome %v, want minted or present", i, o)
+ }
+ }
+ raw, _ := os.ReadFile(own)
+ keys := keyLines(raw)
+ if minted != 1 || len(keys) != 1 || !wellFormedInstanceKey(keys[0]) || string(raw) != tok+InstanceKeyVar+"="+keys[0]+"\n" {
+ t.Fatalf("round %d: %d minted, %d keys, file %d bytes; want one whole key appended once", i, minted, len(keys), len(raw))
+ }
+ }
+}
diff --git a/internal/console/layoutguard_test.go b/internal/console/layoutguard_test.go
index 2154ddcb3..b05056134 100644
--- a/internal/console/layoutguard_test.go
+++ b/internal/console/layoutguard_test.go
@@ -106,10 +106,13 @@ func TestSystemDoorsRefuseAPreLayoutMoveWhileABoxIsNested(t *testing.T) {
t.Fatalf("update to v0.19.0 with a nested row = %d %v, want 409", st, out)
}
// A version at the floor or above is not the guard's business; the
- // request goes on to the job (and fails there, on this fixture's
- // absent feed, which is not what this test is about).
- if st, _ := postJSON(t, fx.ts.URL+"/api/cli/system/update", map[string]any{"channel": "canary", "version": "v0.26.0"}, owner); st == http.StatusConflict {
- t.Error("update to v0.26.0 was refused by the layout guard")
+ // request goes on to the job (and fails there, which is not what this
+ // test is about). The fixture has no SystemFeed, so the job asks the
+ // vendor feed: the version must be one that can never be published, or
+ // the job succeeds once it is (v0.26.0 was, on 2026-10-13), moves the
+ // pointer, and the rollback below lands on the wrong version.
+ if st, _ := postJSON(t, fx.ts.URL+"/api/cli/system/update", map[string]any{"channel": "canary", "version": "v99.0.0"}, owner); st == http.StatusConflict {
+ t.Error("update to v99.0.0 was refused by the layout guard")
}
// Every row flat: the rollback goes through.
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 50343c9e7..a817678d8 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -548,9 +548,11 @@ func runAgent(args []string, buildClaims ClaimFactory) {
// worse off than before this existed (a leftover unit that keeps
// running is exactly what an agent restart left before this pass).
//
- // Then, on a legacy box, layout recovery and the instance-key
- // backfill, before Start can wake anything; on a claim box, Start
- // alone. The order and the branch are activatorStart's.
+ // Then, on a legacy box, layout recovery before Start can wake
+ // anything, and the instance-key backfill in the background after
+ // it, which this does not wait for (the edge below must listen
+ // within the update gate, whatever the box's size); on a claim
+ // box, Start alone. The order and the branch are activatorStart's.
activatorStart{
Claims: claims.Writes.Enabled,
Sweep: func(ctx context.Context) error {
@@ -569,7 +571,9 @@ func runAgent(args []string, buildClaims ClaimFactory) {
return pass
},
Routes: reg.List,
+ Route: reg.Get,
Keys: eb,
+ Slot: svc.PrepareScheduledToken,
Publish: armedLayoutPass.Store,
Start: svc.Start,
}.Run(context.Background())
diff --git a/internal/host/instancekeys.go b/internal/host/instancekeys.go
index d79ea90f4..74b19c6c0 100644
--- a/internal/host/instancekeys.go
+++ b/internal/host/instancekeys.go
@@ -3,6 +3,7 @@ package host
import (
"context"
"errors"
+ "fmt"
"log"
"time"
@@ -13,9 +14,11 @@ import (
// The instance-key backfill (L21, plan 2026-10-10-instance-key-provision.md
// ruling 7). Every launch path mints CARLOS_INSTANCE_KEY before it reads the
// env, so no process is ever started without one. The backfill gives every
-// instance already on a box its key now, at agent start, rather than at its
-// next start, so the rollout can be checked box by box before any app adopts
-// the key: a host that the backfill names "present" or "minted" is ready.
+// instance already on a box its key now, just after agent start, rather
+// than at its next start, so the rollout can be checked box by box before
+// any app adopts the key: a host that the backfill names "present" or
+// "minted" is ready. It runs in the background once the activator has
+// started (activatorStart.Run), never on the path the edge waits for.
//
// It runs only where the legacy activator writes <host>.env itself. With
// host claims on, every write to a host's installation runs under the
@@ -31,12 +34,64 @@ type instanceKeyBackfiller interface {
}
// instanceKeyBackfillBudget bounds one pass. Each keyless row costs one
-// read of the bucket's configuration before its key is minted, and the
-// pass runs before the activator starts, so with the bucket unreachable
-// the rows after the budget fail fast rather than each waiting out its
-// own read; every one of them mints at its next start.
+// read of the bucket's configuration before its key is minted. The pass
+// runs in the background after the activator starts (activatorStart.Run),
+// so it never holds the edge up, but with the bucket unreachable the rows
+// after the budget still fail fast rather than each waiting out its own
+// read; every one of them mints at its next start.
const instanceKeyBackfillBudget = time.Minute
+// hostPreparationSlot runs prepare in host's preparation slot, ordered
+// with the host's maintenance fence: (*activator.Service).
+// PrepareScheduledToken. It answers activator.ErrFenced without running
+// prepare when a fence is held for host, and a fence asked for while
+// prepare runs is installed only after it returns. Wakes do not wait on
+// the slot.
+type hostPreparationSlot func(host string, prepare func() (string, error)) (string, error)
+
+// errInstanceKeyRowSkipped is a row the backfill listed and, when its turn
+// came, did not write for: an operation held its host fenced, or its row
+// had changed (a move, a layout flip, a removal) so it is no longer one
+// the backfill selects. Counted with the skipped rows; its next start
+// mints the key if it still needs one.
+var errInstanceKeyRowSkipped = errors.New("skipped")
+
+// keyBackfillInSlot is the backfill's step for one row once the activator
+// is serving (the v0.26.1 hotfix moved the pass off the startup path). The
+// layout pass and every move fence a host before they move its files, and
+// install that fence only after any preparation already in the slot has
+// returned; the slot refuses a host already fenced. So inside the slot no
+// file of the host moves, and the row is read again there, so the write
+// goes to the paths the host has now, not those of the list taken before.
+// A start's own mint is not ordered by the slot: it and the backfill both
+// decide and append under the activator's adminTokenMu, create-only, so
+// whichever is second finds the key present.
+type keyBackfillInSlot struct {
+ keys instanceKeyBackfiller
+ slot hostPreparationSlot
+ route func(string) (*registry.Route, error)
+}
+
+func (g keyBackfillInSlot) BackfillInstanceKey(ctx context.Context, rt registry.Route) (activator.InstanceKeyOutcome, error) {
+ got, err := activator.InstanceKeyFailed, error(nil)
+ _, serr := g.slot(rt.Host, func() (string, error) {
+ fresh, rerr := g.route(rt.Host)
+ switch {
+ case rerr != nil:
+ err = fmt.Errorf("read its row: %w", rerr)
+ case fresh == nil || !instanceKeyRoute(*fresh):
+ err = fmt.Errorf("%w: its row changed after the backfill listed it", errInstanceKeyRowSkipped)
+ default:
+ got, err = g.keys.BackfillInstanceKey(ctx, *fresh)
+ }
+ return "", nil
+ })
+ if serr != nil {
+ return activator.InstanceKeyFailed, fmt.Errorf("%w: an operation holds it (%v)", errInstanceKeyRowSkipped, serr)
+ }
+ return got, err
+}
+
// instanceKeyRoute selects the rows the backfill writes for: ready,
// activator-spawned instances, the rows execSyncable delivers config to,
// without sidecars and without replicas (a replica is never minted a key,
@@ -58,10 +113,12 @@ type instanceKeyBackfill struct {
}
// backfillInstanceKeys runs the backfill over routes, within
-// instanceKeyBackfillBudget. It never fails the agent: a host it cannot
-// write is logged and counted, and that host's next start mints its key (or
-// refuses, loudly, if the same fault is still there). Nothing logged names
-// a key.
+// instanceKeyBackfillBudget, and logs one count line when it ends,
+// "agent: instance keys: ...": on a legacy box that is after the activator
+// has started, within the budget plus one row's read. It never fails the
+// agent: a host it cannot write is logged and counted, and that host's
+// next start mints its key (or refuses, loudly, if the same fault is
+// still there). Nothing logged names a key.
//
// The error is read before the outcome: a refused or failed row answers
// InstanceKeyFailed (the zero outcome) beside its error, and counting an
@@ -77,6 +134,10 @@ func backfillInstanceKeys(ctx context.Context, routes []registry.Route, b instan
}
got, err := b.BackfillInstanceKey(ctx, rt)
switch {
+ case errors.Is(err, errInstanceKeyRowSkipped):
+ st.Skipped++
+ log.Printf("agent: instance key for %s: %v (its next start mints it)", rt.Host, err)
+ continue
case errors.Is(err, activator.ErrInstanceKeyDeferred):
st.Deferred++
log.Printf("agent: instance key for %s: %v", rt.Host, err)
diff --git a/internal/host/instancekeys_test.go b/internal/host/instancekeys_test.go
index 1b6c63339..f09c8ac12 100644
--- a/internal/host/instancekeys_test.go
+++ b/internal/host/instancekeys_test.go
@@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"reflect"
+ "sync"
+ "sync/atomic"
"testing"
"time"
@@ -121,14 +123,29 @@ func (f keyBackfillFunc) BackfillInstanceKey(ctx context.Context, rt registry.Ro
return f(ctx, rt)
}
-// Task 6 ruling (K1): the start sequence is activatorStart's, so the two
-// guards runAgent cannot be tested for are pinned here. A claim box starts
-// and nothing else: no sweep, no recovery, no backfill (ruling 7). A legacy
-// box sweeps, recovers, backfills, publishes the pass and then starts, in
-// that order: the backfill after every journal is settled and before the
-// pass is published or anything wakes. A route list that fails skips the
-// backfill only; the pass is still published and the service still starts.
-func TestActivatorStartBackfillsOnlyALegacyBoxBetweenRecoveryAndPublish(t *testing.T) {
+// openSlot is a preparation slot with no fence held: it runs prepare.
+func openSlot(_ string, prepare func() (string, error)) (string, error) { return prepare() }
+
+// routeOf answers the registry's Get from a fixed list.
+func routeOf(routes ...registry.Route) func(string) (*registry.Route, error) {
+ return func(host string) (*registry.Route, error) {
+ for _, rt := range routes {
+ if rt.Host == host {
+ return &rt, nil
+ }
+ }
+ return nil, nil
+ }
+}
+
+// Task 6 ruling (K1), as the v0.26.1 hotfix changed it: the start sequence
+// is activatorStart's, so the two guards runAgent cannot be tested for are
+// pinned here. A claim box starts and nothing else: no sweep, no recovery,
+// no backfill (ruling 7). A legacy box sweeps, recovers, publishes the
+// pass and starts, in that order, and the backfill runs after Start, once
+// every journal is settled. A route list that fails skips the backfill
+// only.
+func TestActivatorStartBackfillsOnlyALegacyBoxAfterRecoveryAndStart(t *testing.T) {
ready := registry.Route{Host: "shop.example.com", Kind: "instance", Status: "ready", Socket: "/run/carlos/shop.example.com.sock"}
for _, tc := range []struct {
name string
@@ -136,28 +153,185 @@ func TestActivatorStartBackfillsOnlyALegacyBoxBetweenRecoveryAndPublish(t *testi
listErr error
want []string
}{
- {"legacy box", false, nil, []string{"sweep", "recover", "backfill", "publish", "start"}},
+ {"legacy box", false, nil, []string{"sweep", "recover", "publish", "start", "backfill"}},
{"legacy box, routes unreadable", false, errors.New("db locked"), []string{"sweep", "recover", "publish", "start"}},
{"claim box", true, nil, []string{"start"}},
} {
t.Run(tc.name, func(t *testing.T) {
+ var mu sync.Mutex
var events []string
- note := func(e string) { events = append(events, e) }
- activatorStart{
+ note := func(e string) { mu.Lock(); events = append(events, e); mu.Unlock() }
+ done := activatorStart{
Claims: tc.claims,
Sweep: func(context.Context) error { note("sweep"); return nil },
Recover: func(context.Context, bool) *layout.Pass { note("recover"); return nil },
Routes: func() ([]registry.Route, error) { return []registry.Route{ready}, tc.listErr },
+ Route: routeOf(ready),
Keys: keyBackfillFunc(func(context.Context, registry.Route) (activator.InstanceKeyOutcome, error) {
note("backfill")
return activator.InstanceKeyMinted, nil
}),
+ Slot: openSlot,
Publish: func(*layout.Pass) { note("publish") },
Start: func() { note("start") },
}.Run(context.Background())
+ select {
+ case <-done:
+ case <-time.After(10 * time.Second):
+ t.Fatal("the backfill never finished")
+ }
+ mu.Lock()
+ defer mu.Unlock()
if !reflect.DeepEqual(events, tc.want) {
t.Fatalf("events = %v, want %v", events, tc.want)
}
})
}
}
+
+// fenceCtx is a context with the deadline FenceAndStop requires.
+func fenceCtx(t *testing.T, d time.Duration) context.Context {
+ ctx, cancel := context.WithTimeout(t.Context(), d)
+ t.Cleanup(cancel)
+ return ctx
+}
+
+// Off the startup path, the backfill shares the box with the layout pass
+// and moves, which fence a host before they move its files. A host that
+// is fenced when its turn comes is skipped and nothing is asked of it, and
+// a row whose status changed after the list (it began a move) is skipped
+// the same way; its next start mints the key. The rest are written from
+// the row as it stands, not as it was listed.
+func TestBackfillInSlotSkipsAFencedHostAndReadsTheRowAgain(t *testing.T) {
+ ready := func(host string) registry.Route {
+ return registry.Route{Host: host, Kind: "instance", Status: "ready", Socket: "/run/carlos/" + host + ".sock"}
+ }
+ fenced, moved, shop := ready("fenced.example.com"), ready("moved.example.com"), ready("shop.example.com")
+ svc := &activator.Service{}
+ if _, err := svc.FenceAndStop(fenceCtx(t, time.Second), fenced.Host, 1); err != nil && !errors.Is(err, activator.ErrStopUnproven) {
+ t.Fatalf("fence: %v", err)
+ }
+ nowMoving := moved
+ nowMoving.Status = "moving"
+ nowShop := shop
+ nowShop.Socket = "/run/carlos/nested/shop.example.com.sock"
+ f := &fakeKeyBackfiller{got: map[string]activator.InstanceKeyOutcome{shop.Host: activator.InstanceKeyMinted}}
+ var sockets []string
+ g := keyBackfillInSlot{
+ keys: keyBackfillFunc(func(ctx context.Context, rt registry.Route) (activator.InstanceKeyOutcome, error) {
+ sockets = append(sockets, rt.Socket)
+ return f.BackfillInstanceKey(ctx, rt)
+ }),
+ slot: svc.PrepareScheduledToken,
+ route: routeOf(fenced, nowMoving, nowShop),
+ }
+ st := backfillInstanceKeys(t.Context(), []registry.Route{fenced, moved, shop}, g)
+ if !reflect.DeepEqual(f.asked, []string{shop.Host}) {
+ t.Fatalf("asked %v, want only %s", f.asked, shop.Host)
+ }
+ if !reflect.DeepEqual(sockets, []string{nowShop.Socket}) {
+ t.Fatalf("written from %v, want the row as it stands now", sockets)
+ }
+ if st != (instanceKeyBackfill{Minted: 1, Skipped: 2}) {
+ t.Fatalf("stats = %+v, want one minted and two skipped", st)
+ }
+}
+
+// The other half of "nothing moves a file under it": a fence asked for
+// while a row is being written (the layout pass or a move about to move
+// the host's files) is installed only once the write has returned.
+func TestBackfillInSlotHoldsOffAFenceUntilTheWriteReturns(t *testing.T) {
+ rt := registry.Route{Host: "shop.example.com", Kind: "instance", Status: "ready", Socket: "/run/carlos/shop.example.com.sock"}
+ svc := &activator.Service{}
+ writing, release := make(chan struct{}), make(chan struct{})
+ var wrote atomic.Bool
+ g := keyBackfillInSlot{
+ keys: keyBackfillFunc(func(context.Context, registry.Route) (activator.InstanceKeyOutcome, error) {
+ close(writing)
+ <-release
+ wrote.Store(true)
+ return activator.InstanceKeyMinted, nil
+ }),
+ slot: svc.PrepareScheduledToken,
+ route: routeOf(rt),
+ }
+ done := make(chan instanceKeyBackfill)
+ go func() { done <- backfillInstanceKeys(t.Context(), []registry.Route{rt}, g) }()
+ <-writing
+ fenced := make(chan bool)
+ go func() {
+ _, err := svc.FenceAndStopIfUnfenced(fenceCtx(t, 10*time.Second), rt.Host, 1)
+ if err != nil && !errors.Is(err, activator.ErrStopUnproven) {
+ t.Errorf("fence: %v", err)
+ }
+ fenced <- wrote.Load()
+ }()
+ select {
+ case <-fenced:
+ t.Fatal("the fence was installed while the backfill was writing the host")
+ case <-time.After(200 * time.Millisecond):
+ }
+ close(release)
+ if !<-fenced {
+ t.Fatal("the fence was installed before the write returned")
+ }
+ if st := <-done; st != (instanceKeyBackfill{Minted: 1}) {
+ t.Fatalf("stats = %+v", st)
+ }
+}
+
+// v0.26.1 hotfix: the v0.26.0 roll to a 127-instance box failed its update
+// gate because the backfill ran serially before Start, one bucket read per
+// keyless row, and the edge never listened inside the gate's 20 seconds.
+// On a box with hundreds of keyless rows and a slow bucket, Start is
+// called, and Run returns (runAgent goes on to the agent socket and the
+// edge), without waiting for the backfill.
+func TestActivatorStartDoesNotWaitForTheBackfillOnALargeBox(t *testing.T) {
+ const rows, perRow, bound = 300, 30 * time.Millisecond, 2 * time.Second
+ var routes []registry.Route
+ for i := range rows {
+ host := fmt.Sprintf("shop%03d.example.com", i)
+ routes = append(routes, registry.Route{Host: host, Kind: "instance", Status: "ready", Socket: "/run/carlos/" + host + ".sock"})
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ slowBucket := keyBackfillFunc(func(ctx context.Context, rt registry.Route) (activator.InstanceKeyOutcome, error) {
+ select {
+ case <-time.After(perRow):
+ return activator.InstanceKeyMinted, nil
+ case <-ctx.Done():
+ return activator.InstanceKeyFailed, fmt.Errorf("%w: %w", activator.ErrInstanceKeyDeferred, ctx.Err())
+ }
+ })
+ began := time.Now()
+ var startedAfter time.Duration
+ done := activatorStart{
+ Sweep: func(context.Context) error { return nil },
+ Recover: func(context.Context, bool) *layout.Pass { return nil },
+ Routes: func() ([]registry.Route, error) { return routes, nil },
+ Route: routeOf(routes...),
+ Keys: slowBucket,
+ Slot: (&activator.Service{}).PrepareScheduledToken,
+ Publish: func(*layout.Pass) {},
+ Start: func() { startedAfter = time.Since(began) },
+ }.Run(ctx)
+ returned := time.Since(began)
+ if startedAfter == 0 || startedAfter > bound {
+ t.Errorf("Start was called %v after Run began, want within %v (the backfill must not hold it)", startedAfter, bound)
+ }
+ if returned > bound {
+ t.Errorf("Run returned after %v, want within %v: the edge waits for Run", returned, bound)
+ }
+ select {
+ case <-done:
+ t.Fatal("the backfill had finished when Run returned; it should still be running")
+ default:
+ }
+ // Agent shutdown is not this test's: cancel the pass and let it end.
+ cancel()
+ select {
+ case <-done:
+ case <-time.After(30 * time.Second):
+ t.Fatal("the backfill did not end once its context was cancelled")
+ }
+}
diff --git a/internal/host/layoutleg.go b/internal/host/layoutleg.go
index 2abec0341..fabd8861d 100644
--- a/internal/host/layoutleg.go
+++ b/internal/host/layoutleg.go
@@ -354,36 +354,58 @@ func startActivator(ctx context.Context, sweep func(context.Context) error, reco
//
// On a legacy box it is the sweep, then layout recovery (design §8.2,
// "Recovery is the journal": every journal a previous agent left is
-// completed and the host converged to the fleet's intent), then the
-// instance-key backfill (L21), then the pass's publication, then Start. The
-// backfill runs after recovery has settled every journal and before the
-// pass is published or the activator wakes anything, so nothing moves a
-// file under it; the pass is published only after recovery, so the serial
-// leg never runs beside it.
+// completed and the host converged to the fleet's intent), then the pass's
+// publication, then Start. The pass is published only after recovery, so
+// the serial leg never runs beside it.
+//
+// The instance-key backfill (L21) runs after Start, in the background, and
+// Run does not wait for it (the v0.26.1 hotfix). Before, it ran between
+// recovery and the publication, one bucket read per keyless row, and on a
+// box with 127 instances the edge, which comes up after Run returns, did
+// not listen within the update gate's 20 seconds. The backfill still runs
+// after recovery has settled every journal. What kept anything from moving
+// a file under it while it ran first, nothing else running yet, is now
+// the activator's per-host preparation slot (keyBackfillInSlot): each row
+// is read again and written inside it; a host that an operation holds
+// fenced (a layout pass, a move) is skipped, and a fence asked for while
+// the row is written waits until the write is done. A start's own mint
+// beside the backfill is serialized by the activator's adminTokenMu, and
+// both are create-only, so the host ends with one key.
type activatorStart struct {
Claims bool // claims.Writes.Enabled: a claim box
Sweep func(context.Context) error // the instance-unit sweep
Recover func(context.Context, bool) *layout.Pass // builds the pass and recovers under it; the bool is whether the sweep succeeded
Routes func() ([]registry.Route, error) // the registry's List
+ Route func(string) (*registry.Route, error) // the registry's Get: each row is read again in its host's slot
Keys instanceKeyBackfiller // the exec backend
+ Slot hostPreparationSlot // the service's PrepareScheduledToken
Publish func(*layout.Pass) // armedLayoutPass.Store
Start func() // the service's Start
}
-func (s activatorStart) Run(ctx context.Context) {
+// Run starts the activator and returns. The channel closes when the
+// instance-key backfill has finished; on a claim box it is closed already.
+// runAgent does not wait on it.
+func (s activatorStart) Run(ctx context.Context) <-chan struct{} {
+ done := make(chan struct{})
if s.Claims {
s.Start()
- return
+ close(done)
+ return done
}
startActivator(ctx, s.Sweep, func(ctx context.Context, swept bool) {
- pass := s.Recover(ctx, swept)
- if routes, err := s.Routes(); err != nil {
+ s.Publish(s.Recover(ctx, swept))
+ }, s.Start)
+ go func() {
+ defer close(done)
+ routes, err := s.Routes()
+ if err != nil {
log.Printf("agent: instance keys: list routes: %v (each host's next start mints its key)", err)
- } else {
- backfillInstanceKeys(ctx, routes, s.Keys)
+ return
}
- s.Publish(pass)
- }, s.Start)
+ backfillInstanceKeys(ctx, routes, keyBackfillInSlot{keys: s.Keys, slot: s.Slot, route: s.Route})
+ }()
+ return done
}
// layoutIntentTimeout bounds the one intent read at startup, which the