host: the relocation install publishes the held release under main's held-stage lock and writes its receipt only after the reading under that lock proves it, checks every precondition before any write, and tests pin each write group's work and VerifyLocal's stamp and hold checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 files changed,
+372
−30
internal/host/claimcensus.go+6 −1internal/host/relocationassembly.go+9 −6internal/host/relocationbuild_test.go+7 −1internal/host/relocationheldstage_census_test.go+11 −2internal/host/relocationinstall.go+36 −15internal/host/relocationinstall_linux_test.go+247 −5internal/release/heldverify_test.go+38 −0internal/release/materializeheld.go+18 −0
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 298.867s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.994s
ok github.com/carlosframework/platform/internal/activator 135.833s
ok github.com/carlosframework/platform/internal/agentsock 0.028s
ok github.com/carlosframework/platform/internal/alerting 0.003s
ok github.com/carlosframework/platform/internal/analytics 0.025s
ok github.com/carlosframework/platform/internal/appconfig 2.137s
ok github.com/carlosframework/platform/internal/apperr 0.109s
ok github.com/carlosframework/platform/internal/applog 0.194s
ok github.com/carlosframework/platform/internal/awsinv 0.009s
ok github.com/carlosframework/platform/internal/boxchoice 0.011s
ok github.com/carlosframework/platform/internal/boxid 0.809s
? 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.015s
ok github.com/carlosframework/platform/internal/caps 0.022s
ok github.com/carlosframework/platform/internal/certwarm 0.004s
ok github.com/carlosframework/platform/internal/cgroup 0.207s
ok github.com/carlosframework/platform/internal/changelog 0.315s
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 315.967s
? 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.044s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.016s
ok github.com/carlosframework/platform/internal/dbverify 0.016s
ok github.com/carlosframework/platform/internal/dnsrec 0.016s
ok github.com/carlosframework/platform/internal/docsite 0.016s
ok github.com/carlosframework/platform/internal/domains 1.945s
ok github.com/carlosframework/platform/internal/doorkey 0.016s
ok github.com/carlosframework/platform/internal/economics 0.016s
ok github.com/carlosframework/platform/internal/edge 25.516s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.016s
ok github.com/carlosframework/platform/internal/fleetapi 0.126s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.019s
ok github.com/carlosframework/platform/internal/geoip 0.037s
ok github.com/carlosframework/platform/internal/host 47.975s
ok github.com/carlosframework/platform/internal/hostactors 0.454s
ok github.com/carlosframework/platform/internal/hostbox 1.361s
ok github.com/carlosframework/platform/internal/hostclaim 0.384s
ok github.com/carlosframework/platform/internal/hostevidence 0.020s
ok github.com/carlosframework/platform/internal/hostexec 0.323s
ok github.com/carlosframework/platform/internal/hostfence 0.057s
ok github.com/carlosframework/platform/internal/hostfiles 4.113s
ok github.com/carlosframework/platform/internal/hostfloor 0.024s
ok github.com/carlosframework/platform/internal/hostguard 0.130s
ok github.com/carlosframework/platform/internal/hostops 1.228s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.386s
ok github.com/carlosframework/platform/internal/hostresolve 0.145s
ok github.com/carlosframework/platform/internal/hostwrite 0.002s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 1.702s
ok github.com/carlosframework/platform/internal/instanceexec 2.697s
ok github.com/carlosframework/platform/internal/instancefs 0.003s
ok github.com/carlosframework/platform/internal/instancekey 0.002s
ok github.com/carlosframework/platform/internal/layout 1.301s
ok github.com/carlosframework/platform/internal/lease 9.434s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.009s
ok github.com/carlosframework/platform/internal/ledger 0.014s
ok github.com/carlosframework/platform/internal/mail 0.154s
ok github.com/carlosframework/platform/internal/maintenance 0.087s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.522s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.073s
ok github.com/carlosframework/platform/internal/metrics 0.004s
ok github.com/carlosframework/platform/internal/moves 21.078s
ok github.com/carlosframework/platform/internal/moves/archive 0.718s
ok github.com/carlosframework/platform/internal/objstore 0.158s
ok github.com/carlosframework/platform/internal/ops 0.155s
ok github.com/carlosframework/platform/internal/park 0.182s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
2026/10/11 22:48:15 provision: listing apps/bab/blog/instances/: list refused — its records are unseen this pass
2026/10/11 22:48:15 provision: listing apps/bab/: listdirs refused — its records are unseen this pass
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: listing apps/bac/tito/instances/: list refused — its records are unseen this pass
2026/10/11 22:48:15 provision: listing apps/bac/tito/instances/: list refused — its records are unseen this pass
2026/10/11 22:48:15 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:15 provision: remove retired instance route shop.example: provision: claim admission unavailable
2026/10/11 22:48:15 provision: remove retired instance route shop.example: provision: claim admission unavailable
2026/10/11 22:48:15 provision: remove retired sidecar route worker.sidecar.internal: provision: claim admission unavailable
2026/10/11 22:48:15 provision: remove retired sidecar route worker.sidecar.internal: provision: claim admission unavailable
2026/10/11 22:48:15 provision: claim install acme.go.tito.io: provision: claim admission unavailable
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/11 22:48:15 provision: claim install bot.eleven.bac.sidecar.internal: provision: claim admission unavailable
2026/10/11 22:48:15 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/11 22:48:15 provision: claim install bot.eleven.bac.sidecar.internal: registry: claim row ownership changed or unknown
2026/10/11 22:48:15 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/11 22:48:15 provision: acme.go.tito.io: not minting this pass: the box's layout intent could not be read: no sealed config
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 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/11 22:48:15 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/11 22:48:15 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/11 22:48:15 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/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 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/11 22:48:15 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/11 22:48:15 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/11 22:48:15 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/11 22:48:15 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/11 22:48:15 provision: claim install bot.eleven.bac.sidecar.internal: registry: claim row ownership changed or unknown
2026/10/11 22:48:15 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/11 22:48:15 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/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:15 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:16 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/11 22:48:16 provision: skipping record apps/bac/tito/instances/new.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/11 22:48:16 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/11 22:48:16 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/11 22:48:16 provision: skipping record apps/bac/tito/instances/new.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/11 22:48:16 provision: minted instance route ri.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rj.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rc.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rd.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route re.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rg.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rh.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rk.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rl.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route ra.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rb.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rf.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rd.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route ra.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rb.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route rc.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: skipping record apps/bac/tito/instances/rb.go.tito.io.json: read failed: read refused
2026/10/11 22:48:16 provision: skipping record apps/bac/tito/instances/rb.go.tito.io.json: read failed: read refused
2026/10/11 22:48:16 provision: minted instance route re.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.appa.example (app "appa" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.appb.example (app "appb" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.appe.example (app "appe" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.apph.example (app "apph" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.appi.example (app "appi" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.appj.example (app "appj" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.appc.example (app "appc" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.appd.example (app "appd" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.appf.example (app "appf" acct "bac")
2026/10/11 22:48:16 provision: minted instance route x.appg.example (app "appg" acct "bac")
2026/10/11 22:48:16 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/11 22:48:16 provision: minted instance route r0047.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0113.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0122.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0009.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0039.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0087.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0020.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0021.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0036.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0123.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0004.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0026.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0093.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0127.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0022.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0125.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0051.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0075.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0003.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0006.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0032.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0046.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0088.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0112.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0115.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0116.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0045.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0057.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0083.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0107.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0120.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0031.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0098.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0106.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0110.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0111.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0005.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0085.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0100.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0013.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0028.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0027.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0082.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0114.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0023.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0066.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0067.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0068.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0073.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0034.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0035.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0072.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0081.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0095.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0069.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0079.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0080.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0089.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0094.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0097.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0040.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0059.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0071.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0074.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0012.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0016.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0061.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0049.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0064.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0118.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0019.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0058.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0084.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0092.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0096.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0128.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0008.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0014.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0062.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0063.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0078.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0124.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0007.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0070.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0102.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0126.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0000.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0015.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0077.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0090.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0011.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0041.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0056.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0104.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0086.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0108.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0010.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0038.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0044.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0103.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0105.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0109.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0024.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0065.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0099.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0025.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0030.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0054.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0053.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0029.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0048.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0050.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0091.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0121.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0042.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0043.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0001.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0037.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0101.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0033.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0052.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0076.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0117.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0119.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0002.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0018.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0060.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0129.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0055.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route r0017.go.tito.io (app "tito" acct "bac")
--- FAIL: TestPlan9B8RecordWriteCensus (0.42s)
plan9_b8_census_test.go:371: record-write census rejected 6 site(s); update the explicit inventory only after reviewing the source:
changed site internal/hostops/operations.go:Controller.Attempt: source differs from the reviewed digest
changed site internal/provision/reconcile.go:Reconcile: source differs from the reviewed digest
changed site internal/provision/reconcile.go:mintHeld: source differs from the reviewed digest
unlisted site internal/host/relocationbuild.go:authorityStore.CopyVersion [def:CopyVersion,sel:CopyVersion]: review it, classify it, and update its explicit inventory entry
unlisted site internal/host/relocationbuild.go:authorityStore.PutIfAbsent [def:PutIfAbsent,sel:PutIfAbsent]: review it, classify it, and update its explicit inventory entry
unlisted site internal/host/relocationinstall.go:relocationRecord [key:RecordKey]: review it, classify it, and update its explicit inventory entry
2026/10/11 22:48:16 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 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/11 22:48:16 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted service route svc.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route whole.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route console.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted service route ha.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route shared.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:16 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: converged acme.go.tito.io channel stable -> edge
2026/10/11 22:48:17 provision: converged acme.go.tito.io channel edge -> canary/rehearsal
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: empty channel
2026/10/11 22:48:17 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: converged acme.go.tito.io environment "" -> "staging"
2026/10/11 22:48:17 provision: converged acme.go.tito.io environment "staging" -> ""
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: converged acme.go.tito.io steering "" -> "latency"
2026/10/11 22:48:17 provision: converged acme.go.tito.io steering "latency" -> ""
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: converged acme.go.tito.io hibernate false -> true
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: converged acme.go.tito.io hibernate true -> false (replication "daily" cleared: only a hibernating route's rung is read)
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: converged acme.go.tito.io status-named false -> true
2026/10/11 22:48:17 provision: converged acme.go.tito.io status-named true -> false
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted service route ha.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 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/11 22:48:17 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/11 22:48:17 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/11 22:48:17 provision: minted service route whole.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 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/11 22:48:17 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/11 22:48:17 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/11 22:48:17 provision: minted service route whole.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: skipping accounts/bac/account.json: malformed account manifest: invalid character 'o' in literal null (expecting 'u')
2026/10/11 22:48:17 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: manifest bac/tito: read failed: provision: manifest bac/tito: invalid character 'o' in literal null (expecting 'u')
2026/10/11 22:48:17 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/11 22:48:17 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/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 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/11 22:48:17 provision: not removing acme$evil.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/11 22:48:17 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/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 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/11 22:48:17 provision: not removing bad-service.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/11 22:48:17 provision: skipping record apps/bac/tito/instances/no-channel.go.tito.io.json: empty channel
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: skipping record apps/bac/tito/instances/no-channel.go.tito.io.json: empty channel
2026/10/11 22:48:17 provision: not removing no-channel.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/11 22:48:17 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/11 22:48:17 provision: removed retired instance route ghost.go.tito.io (record gone)
2026/10/11 22:48:17 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/11 22:48:17 provision: minted instance route dup.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/11 22:48:17 provision: removed retired instance route acme.go.tito.io (app in the trash)
2026/10/11 22:48:17 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/11 22:48:17 provision: removed retired static route site.go.tito.io (app in the trash)
2026/10/11 22:48:17 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/11 22:48:17 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/11 22:48:17 provision: removed retired instance route acme.go.tito.io (app in the trash)
2026/10/11 22:48:17 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/11 22:48:17 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/11 22:48:17 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/11 22:48:17 provision: converged acme.go.tito.io channel stable -> edge
2026/10/11 22:48:17 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/11 22:48:17 provision: converged acme.go.tito.io channel canary/rehearsal -> edge
2026/10/11 22:48:17 provision: converged acme.go.tito.io environment "staging" -> ""
2026/10/11 22:48:17 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/11 22:48:17 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/11 22:48:17 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/11 22:48:17 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/11 22:48:17 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/11 22:48:17 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/11 22:48:17 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/11 22:48:17 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/11 22:48:17 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/11 22:48:17 provision: dry-run: would mint instance route acme.go.tito.io (app "tito" acct "bac" channel "stable")
2026/10/11 22:48:17 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/11 22:48:17 provision: dry-run: would converge adopt.go.tito.io channel stable -> edge
2026/10/11 22:48:17 provision: dry-run: would converge adopt.go.tito.io environment "" -> "staging"
2026/10/11 22:48:17 provision: dry-run: would converge owned.go.tito.io channel stable -> edge
2026/10/11 22:48:17 provision: dry-run: would converge owned.go.tito.io environment "" -> "staging"
2026/10/11 22:48:17 provision: dry-run: would converge owned.go.tito.io hibernate false -> true
2026/10/11 22:48:17 provision: dry-run: would converge owned.go.tito.io seed
2026/10/11 22:48:17 provision: dry-run: would remove retired instance route gone.go.tito.io (record gone)
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: adopted existing route adopt.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/11 22:48:17 provision: converged adopt.go.tito.io channel stable -> edge
2026/10/11 22:48:17 provision: converged adopt.go.tito.io environment "" -> "staging"
2026/10/11 22:48:17 provision: converged owned.go.tito.io channel stable -> edge
2026/10/11 22:48:17 provision: converged owned.go.tito.io environment "" -> "staging"
2026/10/11 22:48:17 provision: converged owned.go.tito.io hibernate false -> true
2026/10/11 22:48:17 provision: removed retired instance route gone.go.tito.io (record gone)
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: adopted existing route story.carlosframework.com (app "story" acct "bad") — this record now owns its channel and environment
2026/10/11 22:48:17 provision: story.carlosframework.com exists but is not the route this record describes (kind "instance"/"static") — leaving it alone
2026/10/11 22:48:17 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted service route pub.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted service route pubs.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route exec.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted service route loop.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted service route named.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted service route priv.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route unit.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted static route pisite.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: removed retired static route site.go.tito.io (record gone)
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme-uk.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: converged acme.go.tito.io replicas "ie,uk" -> "uk"
2026/10/11 22:48:17 provision: converged acme.go.tito.io replicas "uk" -> ""
2026/10/11 22:48:17 provision: minted instance route acme-uk.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: converged acme-uk.go.tito.io replica mark true -> false
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 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/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/11 22:48:17 provision: acme.go.tito.io: not minting this pass: the box's layout intent could not be read: store down
2026/10/11 22:48:17 provision: router.go.tito.io: not minting this pass: the box's layout intent could not be read: store down
2026/10/11 22:48:17 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/11 22:48:17 provision: converged acme.go.tito.io channel stable -> edge
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
2026/10/11 22:48:17 provision: not removing acme.go.tito.io (record gone): participant of a running operation
2026/10/11 22:48:17 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/11 22:48:17 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
2026/10/11 22:48:17 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/11 22:48:18 provision: held acme.go.tito.io landing by mv_acme (was ready)
2026/10/11 22:48:18 provision: acme.go.tito.io: held restoring by rb_other, not by mv_acme; leaving it
2026/10/11 22:48:18 provision: acme.go.tito.io: the row is not this pass's (app "other" acct ""); not holding it for mv_acme
2026/10/11 22:48:18 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/11 22:48:18 provision: dry-run: would mint instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac" channel "stable")
2026/10/11 22:48:18 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/11 22:48:18 provision: held acme.go.tito.io landing by mv_acme (was ready)
2026/10/11 22:48:18 provision: removed retired sidecar route gone.eleven.bac.sidecar.internal (record gone)
2026/10/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 provision: converged sidecar bot.eleven.bac.sidecar.internal args ["sidecar","run"] -> ["worker","run"]
2026/10/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 provision: converged sidecar bot.eleven.bac.sidecar.internal ceilings 300/3600 -> 60/120
2026/10/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 provision: converged sidecar bot.eleven.bac.sidecar.internal channel stable -> canary/eleven
2026/10/11 22:48:18 provision: converged sidecar bot.eleven.bac.sidecar.internal environment "other" -> "relay"
2026/10/11 22:48:18 provision: converged sidecar bot.eleven.bac.sidecar.internal hibernate true -> false
2026/10/11 22:48:18 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: read failed: failStore: reads disabled under apps/bac/eleven/sidecars/
2026/10/11 22:48:18 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: malformed: invalid character 'n' looking for beginning of object key string
2026/10/11 22:48:18 provision: not removing sidecar bot.eleven.bac.sidecar.internal: its record exists but could not be read cleanly this pass
2026/10/11 22:48:18 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: empty channel
2026/10/11 22:48:18 provision: skipping sidecar record apps/bac/eleven/sidecars/held.json: empty channel
2026/10/11 22:48:18 provision: not removing sidecar held.eleven.bac.sidecar.internal: its record exists but could not be read cleanly this pass
2026/10/11 22:48:18 provision: removed retired sidecar route old.eleven.bac.sidecar.internal (record gone)
2026/10/11 22:48:18 provision: bac/eleven has sidecar records but no sidecars manifest — not minting
2026/10/11 22:48:18 provision: bac/eleven is in the trash — not minting sidecar routes for it
2026/10/11 22:48:18 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/11 22:48:18 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/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 provision: minted sidecar route hub.relay.bac.sidecar.internal (app "relay" acct "bac" args ["relay"])
2026/10/11 22:48:18 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/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 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/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 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/11 22:48:18 provision: removed retired sidecar route bot.eleven.bac.sidecar.internal (record gone)
2026/10/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 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/11 22:48:18 provision: skipping sidecar record apps/bac/relay/sidecars/hub.json: read failed: failStore: reads disabled under apps/bac/relay/sidecars/
2026/10/11 22:48:18 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/11 22:48:18 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/11 22:48:18 provision: dry-run: would mint sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" channel "stable" args ["sidecar","run"])
2026/10/11 22:48:18 provision: dry-run: would remove retired sidecar route gone.eleven.bac.sidecar.internal (record gone)
2026/10/11 22:48:18 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
FAIL
FAIL github.com/carlosframework/platform/internal/provision 2.988s
ok github.com/carlosframework/platform/internal/publicrelease 0.006s
ok github.com/carlosframework/platform/internal/registry 3.536s
ok github.com/carlosframework/platform/internal/relay 7.210s
ok github.com/carlosframework/platform/internal/release 17.964s
ok github.com/carlosframework/platform/internal/rollout 1.472s
ok github.com/carlosframework/platform/internal/rootaws 0.009s
ok github.com/carlosframework/platform/internal/s3compat 0.004s
ok github.com/carlosframework/platform/internal/safefile 0.003s
ok github.com/carlosframework/platform/internal/schedule 2.177s
ok github.com/carlosframework/platform/internal/sim/hostlife 2.295s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.555s
ok github.com/carlosframework/platform/internal/status 0.040s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.077s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.003s
ok github.com/carlosframework/platform/internal/termfence 0.003s
ok github.com/carlosframework/platform/internal/tunnelclient 0.003s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/updatewords 0.002s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.001s
ok github.com/carlosframework/platform/internal/vet 1.536s
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/host/claimcensus.go b/internal/host/claimcensus.go| index dbe24ae62..b5642fd6e 100644 |
| --- a/internal/host/claimcensus.go |
| +++ b/internal/host/claimcensus.go |
| @@ -56,7 +56,12 @@ func claimDataCensus(dataDir string) []string { |
| // request there (M3). |
| stage := claimCensusRel(dataDir, release.HeldStageDir(dataDir)) |
| staged := claimCensusRel(dataDir, release.HeldStagedDir(dataDir)) |
| - return []string{".hostclaims", ".hostexec", "hostguard", "relocate", stage, staged, hostbox.CensusFile(reg), hostbox.CensusFile(reg + "-wal"), hostbox.CensusFile(reg + "-shm")} |
| + // And its held-stage lock (Task 8c fix round 1, I1): every held |
| + // publication, root's relay and the relocation install's own, holds |
| + // .moves/held-<host>.lock (withHeldStageLock), which is never removed. |
| + // .moves is a shared tree, not opaque: regular files and directories |
| + // only, and anything naming the host being censused still refuses. |
| + return []string{".hostclaims", ".hostexec", "hostguard", "relocate", stage, staged, hostbox.CensusTree(movesDirName), hostbox.CensusFile(reg), hostbox.CensusFile(reg + "-wal"), hostbox.CensusFile(reg + "-shm")} |
| } |
| func claimOptCensus(optDir string) []string { |
diff --git a/internal/host/relocationassembly.go b/internal/host/relocationassembly.go| index dc710e43f..2278e3b0c 100644 |
| --- a/internal/host/relocationassembly.go |
| +++ b/internal/host/relocationassembly.go |
| @@ -72,17 +72,20 @@ type RelocationAssemblyConfig struct { |
| LaterProbeAttempts bool |
| Now func() time.Time |
| // ReleasePub and Ratchet are §4.1's evidence for the held stage |
| - // (release.MaterializeHeldUnder): the release signing key and the |
| - // agent's pointer digest ratchet, as its held-stage pass takes them |
| - // (heldStagePass). Task 10 passes the agent's. |
| + // (release.MaterializeHeldUnder): the agent's release key |
| + // (CARLOS_RELEASE_PUBKEY) and the edge's pointer digest ratchet, |
| + // pointerRatchet(false, optDir, dataDir, getenv), exactly as moves' T7 |
| + // takes them in the agent (movespass.go, heldReleaseReady). Never |
| + // root's: heldStagePass runs as root with pointerRatchet(true, ...). |
| ReleasePub ed25519.PublicKey |
| Ratchet *release.DigestRatchet |
| - // render and materialize are appconfig.RenderHeldUnder and |
| - // release.MaterializeHeldUnder (with ReleasePub and Ratchet bound); |
| - // nil is those. Tests replace them. |
| + // render, materialize and verifyLive are appconfig.RenderHeldUnder, |
| + // release.MaterializeHeldUnder (with ReleasePub and Ratchet bound) and |
| + // release.VerifyHeldLiveUnder; nil is those. Tests replace them. |
| render func(context.Context, release.Store, *ecdh.PrivateKey, string, registry.Route, string, string, func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) |
| materialize func(context.Context, release.Store, string, registry.Route, string, *release.ProofGate, hostwrite.Work, func(func() error) error) (*release.Manifest, error) |
| + verifyLive func(context.Context, release.Store, string, registry.Route, string, *release.ProofGate, func(func() error) error) error |
| } |
| // RelocationEffects binds every provider to this bound assembly. ack is |
diff --git a/internal/host/relocationbuild_test.go b/internal/host/relocationbuild_test.go| index 4535f42c5..98914ad44 100644 |
| --- a/internal/host/relocationbuild_test.go |
| +++ b/internal/host/relocationbuild_test.go |
| @@ -52,7 +52,10 @@ type buildFixture struct { |
| obs hostclaim.Observed |
| locals []hostbox.Selection |
| localErr error |
| - verifier PreparedFromInstallManifest |
| + // localHook, when set, is asked on every Local check that localErr |
| + // passes: a test loses the lease at one write group. |
| + localHook func(hostbox.Selection) error |
| + verifier PreparedFromInstallManifest |
| } |
| // hookedStore and hookedVersions run after, once a write has landed, so a |
| @@ -221,6 +224,9 @@ func newBuildFixtureWith(t *testing.T, o buildOptions) *buildFixture { |
| WorkDir: filepath.Join(data, "relocate", "build"), Now: func() time.Time { return created.Add(time.Hour) }, |
| Source: hostbox.WorkSource{Box: "dst", Region: "ie", Local: func(_ context.Context, c *hostclaim.Claim, s hostbox.Selection) error { |
| f.locals = append(f.locals, s) |
| + if f.localErr == nil && f.localHook != nil { |
| + return f.localHook(s) |
| + } |
| return f.localErr |
| }}} |
| f.verifier = PreparedFromInstallManifest{Claims: f.claims, Files: f.files, Data: f.bucket, Bucket: "data"} |
diff --git a/internal/host/relocationheldstage_census_test.go b/internal/host/relocationheldstage_census_test.go| index 6ada890e6..1c431c330 100644 |
| --- a/internal/host/relocationheldstage_census_test.go |
| +++ b/internal/host/relocationheldstage_census_test.go |
| @@ -2,12 +2,17 @@ |
| package host |
| -import "testing" |
| +import ( |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostbox" |
| +) |
| // K25, N11: a relocation target whose stage is blocked leaves moves' |
| // held-stage request, keyed by its host, and root's pass leaves an ack |
| // beside it. Another host's empty installation still initialises on the |
| -// box: the relay's two directories are census-opaque. |
| +// box: the relay's two directories are census-opaque, and moves' |
| +// held-stage lock directory a shared tree (fix round 1, I1). |
| func TestCensusInitializesWithAHeldStageRequest(t *testing.T) { |
| censusWithSharedFiles(t, "moves/held-stage", map[string]string{ |
| "moves/held-stage/other.example.json": `{"account":"acct","app":"app","host":"other.example","version":"v1","op":"op-1","box":"box"}`, |
| @@ -15,4 +20,8 @@ func TestCensusInitializesWithAHeldStageRequest(t *testing.T) { |
| censusWithSharedFiles(t, "moves/held-staged", map[string]string{ |
| "moves/held-staged/other.example.json": `{"version":"v1","op":"op-1","at":"2026-10-07T00:00:00Z"}`, |
| }) |
| + // The held-stage lock either side took for the publication. |
| + censusWithSharedFiles(t, hostbox.CensusTree(".moves"), map[string]string{ |
| + ".moves/held-other.example.lock": "", |
| + }) |
| } |
diff --git a/internal/host/relocationinstall.go b/internal/host/relocationinstall.go| index 8afcb5355..65a84fe29 100644 |
| --- a/internal/host/relocationinstall.go |
| +++ b/internal/host/relocationinstall.go |
| @@ -341,7 +341,10 @@ func relocationInstallSource(src hostbox.WorkSource, reg *registry.Registry) hos |
| // |
| // Idempotent: a pass that ends in ErrNotYet is repeated whole. |
| func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(context.Context, hostclaim.Observed) (hostclaim.LocalInstall, error) { |
| - render, materialize := cfg.render, cfg.materialize |
| + render, materialize, verifyLive := cfg.render, cfg.materialize, cfg.verifyLive |
| + if verifyLive == nil { |
| + verifyLive = release.VerifyHeldLiveUnder |
| + } |
| if render == nil { |
| render = appconfig.RenderHeldUnder |
| } |
| @@ -351,7 +354,10 @@ func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(cont |
| } |
| } |
| return func(ctx context.Context, o hostclaim.Observed) (hostclaim.LocalInstall, error) { |
| - if cfg.Data == nil || cfg.Registry == nil || cfg.OptDir == "" || a.Reconcile == nil || cfg.Source.Box == "" || o.Claim == nil || o.Claim.Holder == nil { |
| + // Every precondition before anything is written (a misconfigured |
| + // assembly leaves no row, file or claim write). |
| + if cfg.Data == nil || cfg.Registry == nil || cfg.OptDir == "" || cfg.DataDir == "" || a.Reconcile == nil || a.Wake == nil || a.Wake.Roles == nil || |
| + cfg.Source.Box == "" || cfg.Source.Local == nil || o.Claim == nil || o.Claim.Holder == nil { |
| return hostclaim.LocalInstall{}, ErrClaimStopUnavailable |
| } |
| c := o.Claim |
| @@ -434,9 +440,6 @@ func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(cont |
| // makes the directory, then records its device and inode |
| // materialized, under the install work; the probe's own |
| // PrepareState then finds it as its own. Never MkdirAll. |
| - if a.Wake == nil || a.Wake.Roles == nil { |
| - return hostclaim.LocalInstall{}, ErrClaimStopUnavailable |
| - } |
| installed, err := hostclaim.Read(ctx, a.Claims, o.Host) |
| if err != nil { |
| return hostclaim.LocalInstall{}, err |
| @@ -541,7 +544,15 @@ func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(cont |
| // version, through the claim-aware held stage (never MaterializeHeld, |
| // which refuses with CARLOS_HOST_CLAIM on). The edge unit cannot |
| // write the version store: ErrStageBlocked leaves root's relay |
| - // request (M3) and the receipt waits for live/<host>. |
| + // request (M3) and the receipt waits. |
| + // |
| + // As moves' T7 (heldReleaseReady): the publication holds the host's |
| + // held-stage lock (withHeldStageLock) across its swap, re-hash and |
| + // rollback, as root's relay does, and the receipt waits for the |
| + // reading under that same lock (release.VerifyHeldLiveUnder: the |
| + // link names the version and D3 accepts the bytes it names). A link |
| + // alone is never evidence: a publication may be about to roll it |
| + // back, and the bytes may have changed since. |
| version := c.Op.Journal["install_release"] |
| if version == "" { |
| return hostclaim.LocalInstall{}, fmt.Errorf("%w: no install_release journaled", hostrelocate.ErrRefused) |
| @@ -551,18 +562,28 @@ func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(cont |
| return hostclaim.LocalInstall{}, err |
| } |
| proof := release.NewProofGate(release.ProofPolicyPath, cfg.DataDir) |
| - _, err = materialize(ctx, a.Claims, cfg.OptDir, held, version, proof, heldIdentityWork{Work: released, ID: hostwrite.Identity{Release: version}}, nil) |
| - switch { |
| - case errors.Is(err, release.ErrStageBlocked): |
| - req := release.HeldStageRequest{Account: rec.Account, App: rec.App, Host: o.Host, Version: version, Op: c.Op.ID, Box: box} |
| - if err := writeHeldStageRequest(cfg.DataDir, req); err != nil { |
| + lock := func(fn func() error) error { return withHeldStageLock(cfg.DataDir, o.Host, fn) } |
| + err = verifyLive(ctx, a.Claims, cfg.OptDir, held, version, proof, lock) |
| + if errors.Is(err, release.ErrHeldNotLive) { |
| + _, err = materialize(ctx, a.Claims, cfg.OptDir, held, version, proof, heldIdentityWork{Work: released, ID: hostwrite.Identity{Release: version}}, lock) |
| + switch { |
| + case errors.Is(err, release.ErrStageBlocked): |
| + req := release.HeldStageRequest{Account: rec.Account, App: rec.App, Host: o.Host, Version: version, Op: c.Op.ID, Box: box} |
| + if err := writeHeldStageRequest(cfg.DataDir, req); err != nil { |
| + return hostclaim.LocalInstall{}, err |
| + } |
| + return hostclaim.LocalInstall{}, fmt.Errorf("%w: the pinned release %s waits for root's held stage", hostrelocate.ErrNotYet, version) |
| + case err != nil: |
| return hostclaim.LocalInstall{}, err |
| } |
| - case err != nil: |
| - return hostclaim.LocalInstall{}, err |
| + // Published by this call: the receipt waits for the same reading. |
| + err = verifyLive(ctx, a.Claims, cfg.OptDir, held, version, proof, lock) |
| + } |
| + if errors.Is(err, release.ErrHeldNotLive) { |
| + return hostclaim.LocalInstall{}, errors.Join(fmt.Errorf("%w: the pinned release %s is not live", hostrelocate.ErrNotYet, version), err) |
| } |
| - if live, err := release.LiveVersion(cfg.OptDir, o.Host); err != nil || live != version { |
| - return hostclaim.LocalInstall{}, errors.Join(fmt.Errorf("%w: the pinned release %s is not staged yet (live %q)", hostrelocate.ErrNotYet, version, live), err) |
| + if err != nil { |
| + return hostclaim.LocalInstall{}, err |
| } |
| journal.Steps["materialize"] = version |
| raw, err := json.Marshal(journal) |
diff --git a/internal/host/relocationinstall_linux_test.go b/internal/host/relocationinstall_linux_test.go| index d60b32868..d3901076c 100644 |
| --- a/internal/host/relocationinstall_linux_test.go |
| +++ b/internal/host/relocationinstall_linux_test.go |
| @@ -49,6 +49,12 @@ type installFixture struct { |
| stageErr error // the held stage's answer; nil points live/<host> at the version |
| renders int |
| configs map[string]string // the rendered config's variables |
| + // verifyErr is the held reading's answer once live/<host> names the |
| + // version (release.VerifyHeldLiveUnder's bytes check); lockProbe runs |
| + // inside the publication's guard and the reading's lock. |
| + verifyErr error |
| + lockProbe func(where string) |
| + published int |
| } |
| // installWake is the fixture's published wake state: two cron timers. |
| @@ -124,6 +130,7 @@ func newInstallFixtureWith(t *testing.T, opts buildOptions) *installFixture { |
| b.cfg.Source.Guard, b.cfg.Source.Now = guard, time.Now |
| b.cfg.render = f.render |
| b.cfg.materialize = f.materialize |
| + b.cfg.verifyLive = f.verifyLive |
| return f |
| } |
| @@ -154,13 +161,46 @@ func (f *installFixture) render(ctx context.Context, _ release.Store, _ *ecdh.Pr |
| } |
| // materialize stands for release.MaterializeHeldUnder: blocked (the edge |
| -// unit cannot write the version store) or, through its work, live/<host> |
| -// pointed at the pinned version. |
| -func (f *installFixture) materialize(ctx context.Context, _ release.Store, optDir string, rt registry.Route, version string, _ *release.ProofGate, work hostwrite.Work, _ func(func() error) error) (*release.Manifest, error) { |
| +// unit cannot write the version store) or, through its work and inside |
| +// guard (the caller's held-stage lock), live/<host> pointed at the pinned |
| +// version. |
| +func (f *installFixture) materialize(ctx context.Context, _ release.Store, optDir string, rt registry.Route, version string, _ *release.ProofGate, work hostwrite.Work, guard func(func() error) error) (*release.Manifest, error) { |
| if f.stageErr != nil { |
| return nil, f.stageErr |
| } |
| - return &release.Manifest{}, work.With(ctx, hostwrite.Identity{Release: version}, func() error { return f.publishLive(rt.Account, rt.App, version) }) |
| + if guard == nil { |
| + guard = func(publish func() error) error { return publish() } |
| + } |
| + return &release.Manifest{}, work.With(ctx, hostwrite.Identity{Release: version}, func() error { |
| + return guard(func() error { |
| + if f.lockProbe != nil { |
| + f.lockProbe("publish") |
| + } |
| + f.published++ |
| + return f.publishLive(rt.Account, rt.App, version) |
| + }) |
| + }) |
| +} |
| + |
| +// verifyLive stands for release.VerifyHeldLiveUnder: a peek at the link, |
| +// then, under lock, the link names the version and the bytes check answers |
| +// verifyErr. |
| +func (f *installFixture) verifyLive(_ context.Context, _ release.Store, optDir string, rt registry.Route, version string, proof *release.ProofGate, lock func(func() error) error) error { |
| + if lock == nil || proof == nil { |
| + return errors.New("a held reading without its lock or gate") |
| + } |
| + if live, err := release.LiveVersion(optDir, rt.Host); err != nil || live != version { |
| + return errors.Join(release.ErrHeldNotLive, err) |
| + } |
| + return lock(func() error { |
| + if f.lockProbe != nil { |
| + f.lockProbe("verify") |
| + } |
| + if live, err := release.LiveVersion(optDir, rt.Host); err != nil || live != version { |
| + return errors.Join(release.ErrHeldNotLive, err) |
| + } |
| + return f.verifyErr |
| + }) |
| } |
| func (f *installFixture) publishLive(acct, app, version string) error { |
| @@ -324,6 +364,7 @@ func TestRelocationInstallKeepsThePublishedEnvByteForByte(t *testing.T) { |
| // path is left as it was. |
| func TestRelocationInstallRefusesAManifestWithoutTheEnvItem(t *testing.T) { |
| f := newInstallFixture(t, map[string]string{hostfiles.WakeItem: installWake()}) |
| + before := f.read().Claim.Gen |
| p := f.paths() |
| if err := os.MkdirAll(filepath.Dir(p.EnvPath), 0o700); err != nil { |
| t.Fatal(err) |
| @@ -343,6 +384,9 @@ func TestRelocationInstallRefusesAManifestWithoutTheEnvItem(t *testing.T) { |
| if env, err := os.ReadFile(p.EnvPath); err != nil || string(env) != "STALE=1\n" { |
| t.Fatalf("the env path was touched: %q %v", env, err) |
| } |
| + if after := f.read().Claim.Gen; after != before { |
| + t.Fatalf("the claim was written: gen %d -> %d", before, after) |
| + } |
| if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { |
| t.Fatal("a refused install left a receipt") |
| } |
| @@ -448,7 +492,7 @@ func TestNoRelocationFileNamesTheHostOutsideItsInstallation(t *testing.T) { |
| } |
| inst := provision.InstanceDir(f.r.DataDir, buildHost) |
| allowed := []string{inst, filepath.Join(f.r.DataDir, "hostguard"), filepath.Join(f.r.DataDir, ".hostexec"), filepath.Join(f.r.DataDir, ".hostclaims"), |
| - release.HeldStageDir(f.r.DataDir), release.HeldStagedDir(f.r.DataDir)} |
| + release.HeldStageDir(f.r.DataDir), release.HeldStagedDir(f.r.DataDir), filepath.Join(f.r.DataDir, ".moves", heldStageLockFile(buildHost))} |
| err := filepath.WalkDir(f.r.DataDir, func(path string, d fs.DirEntry, err error) error { |
| if err != nil || !strings.Contains(path, buildHost) { |
| return err |
| @@ -721,3 +765,201 @@ func inodeOf(t *testing.T, info os.FileInfo) uint64 { |
| } |
| return st.Ino |
| } |
| + |
| +// Fix round 1, M1: every precondition is checked before anything is |
| +// written. An assembly without main's role assembly (the instance |
| +// directory's typed protocol) refuses with no row and no claim write. |
| +func TestRelocationInstallRefusesAMisconfiguredAssemblyBeforeAnyWrite(t *testing.T) { |
| + f := newInstallFixture(t, nil) |
| + before := f.read().Claim.Gen |
| + f.a.Wake = nil |
| + if _, err := f.install(); !errors.Is(err, ErrClaimStopUnavailable) { |
| + t.Fatalf("no role assembly: %v", err) |
| + } |
| + if row, err := f.reg.Get(buildHost); err != nil || row != nil { |
| + t.Fatalf("a row was minted: %+v %v", row, err) |
| + } |
| + if after := f.read().Claim.Gen; after != before { |
| + t.Fatalf("the claim was written: gen %d -> %d", before, after) |
| + } |
| +} |
| + |
| +// Fix round 1, I1: the held release is published under main's held-stage |
| +// lock, and read back under the same lock before the receipt, as moves' |
| +// T7 does (heldReleaseReady). Another holder of the lock is refused while |
| +// each runs. |
| +func TestRelocationInstallPublishesAndReadsUnderTheHeldStageLock(t *testing.T) { |
| + prev := heldLockWait |
| + t.Cleanup(func() { heldLockWait = prev }) |
| + heldLockWait = 50 * time.Millisecond |
| + f := newInstallFixture(t, nil) |
| + held := map[string]bool{} |
| + f.lockProbe = func(where string) { |
| + err := withHeldStageLock(f.r.DataDir, buildHost, func() error { return nil }) |
| + held[where] = errors.Is(err, errHeldLockBusy) |
| + } |
| + if _, err := f.install(); err != nil { |
| + t.Fatal(err) |
| + } |
| + if f.published != 1 || !held["publish"] || !held["verify"] { |
| + t.Fatalf("published %d, under the lock %v", f.published, held) |
| + } |
| +} |
| + |
| +// Fix round 1, I1: a link alone is never evidence. A link the reading does |
| +// not prove (the bytes it names, or the policy, refuse) leaves no receipt, |
| +// whether this install published it or root's relay did; once proven, the |
| +// install completes without publishing again. |
| +func TestRelocationInstallWritesNoReceiptOnAnUnprovenLink(t *testing.T) { |
| + for _, byRoot := range []bool{false, true} { |
| + f := newInstallFixture(t, nil) |
| + if byRoot { |
| + // The edge cannot write the version store: the install leaves |
| + // root's request, and root's relay publishes the link. |
| + f.stageErr = release.ErrStageBlocked |
| + if _, err := f.install(); !errors.Is(err, hostrelocate.ErrNotYet) { |
| + t.Fatalf("blocked: %v", err) |
| + } |
| + f.stageErr = nil |
| + if err := f.publishLive("acct", "app", "v1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + f.verifyErr = release.ErrHeldNotProven |
| + if _, err := f.install(); !errors.Is(err, release.ErrHeldNotProven) { |
| + t.Fatalf("root %v: an unproven link: %v", byRoot, err) |
| + } |
| + if live, err := release.LiveVersion(f.r.OptDir, buildHost); err != nil || live != "v1" { |
| + t.Fatalf("root %v: live %q %v", byRoot, live, err) |
| + } |
| + if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { |
| + t.Fatalf("root %v: a receipt on an unproven link", byRoot) |
| + } |
| + f.verifyErr = nil |
| + if _, err := f.install(); err != nil { |
| + t.Fatalf("root %v: proven: %v", byRoot, err) |
| + } |
| + if want := map[bool]int{false: 1, true: 0}[byRoot]; f.published != want { |
| + t.Fatalf("root %v: published %d times, want %d", byRoot, f.published, want) |
| + } |
| + } |
| +} |
| + |
| +// Fix round 1, I3: each of the install's write groups runs inside a With |
| +// of its selection, so main's Local (the execution lease's deadline and |
| +// loss latch) stops it. The lease is lost at one group at a time; that |
| +// group and everything after it write nothing. (The held config's group |
| +// is TestRelocationInstallStopsWhenTheLeaseIsLost.) |
| +func TestRelocationInstallStopsAtEachWriteGroupWhenTheLeaseIsLost(t *testing.T) { |
| + materializedDir := func(f *installFixture) bool { |
| + h := f.read().Claim.Holder |
| + in, err := f.inv.ReadInstallation(context.Background(), buildHost, h.Incarnation, h.Stamp) |
| + if err != nil { |
| + return false |
| + } |
| + rel, _ := filepath.Rel(f.r.DataDir, provision.InstanceDir(f.r.DataDir, buildHost)) |
| + for _, e := range in.Entries { |
| + if e.Kind == "path" && e.ManagedRoot == "data" && e.RelativePath == rel && e.Materialization == "materialized" { |
| + return true |
| + } |
| + } |
| + return false |
| + } |
| + live := func(f *installFixture) bool { |
| + v, err := release.LiveVersion(f.r.OptDir, buildHost) |
| + return err == nil && v == "v1" |
| + } |
| + for _, tc := range []struct { |
| + name string |
| + lose func(*installFixture, hostbox.Selection) bool |
| + row bool // the row was minted before the loss |
| + file bool // the files were restored before the loss |
| + link bool // the release was published before the loss |
| + }{ |
| + {"the row", func(*installFixture, hostbox.Selection) bool { return true }, false, false, false}, |
| + {"the files, env and wake state", func(f *installFixture, s hostbox.Selection) bool { |
| + return s.Action == relocationActionInstall && materializedDir(f) |
| + }, true, false, false}, |
| + {"the held release", func(_ *installFixture, s hostbox.Selection) bool { return s.Action == relocationActionHeldRelease }, true, true, false}, |
| + {"the journal", func(f *installFixture, s hostbox.Selection) bool { |
| + return s.Action == relocationActionInstall && live(f) |
| + }, true, true, true}, |
| + } { |
| + t.Run(tc.name, func(t *testing.T) { |
| + f := newInstallFixture(t, nil) |
| + f.localHook = func(s hostbox.Selection) error { |
| + if tc.lose(f, s) { |
| + return execlease.ErrLost |
| + } |
| + return nil |
| + } |
| + if _, err := f.install(); !errors.Is(err, execlease.ErrLost) { |
| + t.Fatalf("got %v", err) |
| + } |
| + row, err := f.reg.Get(buildHost) |
| + if err != nil || (row != nil) != tc.row { |
| + t.Fatalf("row %+v %v, want minted %v", row, err, tc.row) |
| + } |
| + p := f.paths() |
| + _, ferr := os.Stat(filepath.Join(p.FilesDir, "public", "index.html")) |
| + _, eerr := os.Stat(p.EnvPath) |
| + timers, _ := f.reg.TimersFor(buildHost) |
| + if (ferr == nil) != tc.file || (eerr == nil) != tc.file || (len(timers) == 2) != tc.file { |
| + t.Fatalf("files %v, env %v, timers %d; want restored %v", ferr, eerr, len(timers), tc.file) |
| + } |
| + if live(f) != tc.link { |
| + t.Fatalf("live/<host> published %v, want %v", live(f), tc.link) |
| + } |
| + if _, ok, _ := f.a.relocationReceipt(f.cfg)(t.Context(), f.read()); ok { |
| + t.Fatal("a journal was written") |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// Fix round 1, M2: VerifyLocal reads the guard record and the row back. A |
| +// stamp activated since the install, and a row whose hold was released or |
| +// taken by another op, are not the receipt's install. |
| +func TestRelocationVerifyLocalNeedsTheStampAndTheHold(t *testing.T) { |
| + ctx := t.Context() |
| + t.Run("the hold", func(t *testing.T) { |
| + f := newInstallFixture(t, nil) |
| + l, err := f.install() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + verify := f.a.relocationVerifyLocal(f.cfg) |
| + if changed, err := f.reg.ReleaseHold(buildHost, "rel-1"); err != nil || !changed { |
| + t.Fatalf("release: %v %v", changed, err) |
| + } |
| + if ok, err := verify(ctx, f.read(), l); err != nil || ok { |
| + t.Fatalf("a released hold verified: %v %v", ok, err) |
| + } |
| + if err := f.reg.SetHold(buildHost, "relocating", "other-op"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if ok, err := verify(ctx, f.read(), l); err != nil || ok { |
| + t.Fatalf("another op's hold verified: %v %v", ok, err) |
| + } |
| + }) |
| + t.Run("the stamp", func(t *testing.T) { |
| + f := newInstallFixture(t, nil) |
| + l, err := f.install() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + o, err := hostclaim.Update(ctx, f.claims, f.read(), func(c *hostclaim.Claim) error { c.Holder.Stamp++; return nil }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := f.a.Guard.Activate(ctx, buildHost, "dst", nil); err != nil { |
| + t.Fatal(err) |
| + } |
| + if c, err := hostguard.Capture(f.a.Guard.DataDir, buildHost); err != nil || c.Stamp == l.InstallStamp { |
| + t.Fatalf("not re-activated: %+v %v", c, err) |
| + } |
| + if ok, err := f.a.relocationVerifyLocal(f.cfg)(ctx, o, l); err != nil || ok { |
| + t.Fatalf("another stamp verified: %v %v", ok, err) |
| + } |
| + }) |
| +} |
diff --git a/internal/release/heldverify_test.go b/internal/release/heldverify_test.go| index 62d214438..c6bc1304e 100644 |
| --- a/internal/release/heldverify_test.go |
| +++ b/internal/release/heldverify_test.go |
| @@ -201,3 +201,41 @@ func TestVerifyHeldLiveRechecksAnExistingLink(t *testing.T) { |
| t.Fatal("a reading without a gate passed") |
| } |
| } |
| + |
| +// Region HA Task 8c: the claim path's reading. VerifyHeldLive refuses while |
| +// CARLOS_HOST_CLAIM is on, as MaterializeHeld does; VerifyHeldLiveUnder is |
| +// the same reading under the claim, and still refuses an unproven link, a |
| +// link on another version, and a reading without the lock. |
| +func TestVerifyHeldLiveUnderReadsUnderTheClaim(t *testing.T) { |
| + e := newHeldProofEnv(t) |
| + e.require(t, "console") |
| + e.prove(t, e.pinnedSHA) |
| + if _, err := MaterializeHeld(e.ctx, e.store, heldPub, nr(t), e.opt, heldRoute(), "v1", e.gate, openGuard); err != nil || e.live() != "v1" { |
| + t.Fatalf("publish v1: %v, live %q", err, e.live()) |
| + } |
| + t.Setenv("CARLOS_HOST_CLAIM", "1") |
| + verify := func(lock func(func() error) error) error { |
| + return VerifyHeldLiveUnder(e.ctx, e.store, e.opt, heldRoute(), "v1", NewProofGate(e.policy, e.bake), lock) |
| + } |
| + if err := VerifyHeldLive(e.ctx, e.store, e.opt, heldRoute(), "v1", NewProofGate(e.policy, e.bake), passLock); !errors.Is(err, ErrHeldClaimWired) { |
| + t.Fatalf("VerifyHeldLive under the claim: %v, want ErrHeldClaimWired", err) |
| + } |
| + if err := verify(passLock); err != nil { |
| + t.Fatalf("published and proven, under the claim: %v", err) |
| + } |
| + if err := verify(nil); err == nil { |
| + t.Fatal("a reading without the lock passed") |
| + } |
| + if err := StageArtifact(e.opt, "bab", "v1", "console", []byte("replaced later")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := verify(passLock); !errors.Is(err, ErrHeldNotProven) { |
| + t.Fatalf("replaced bytes: %v, want ErrHeldNotProven", err) |
| + } |
| + if err := PointLive(e.opt, heldTestHost, "bab", "v0", "console"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := verify(passLock); !errors.Is(err, ErrHeldNotLive) { |
| + t.Fatalf("on v0: %v, want ErrHeldNotLive", err) |
| + } |
| +} |
diff --git a/internal/release/materializeheld.go b/internal/release/materializeheld.go| index b8efb7454..ca8859a08 100644 |
| --- a/internal/release/materializeheld.go |
| +++ b/internal/release/materializeheld.go |
| @@ -185,6 +185,24 @@ func VerifyHeldLive(ctx context.Context, s Store, optDir string, rt registry.Rou |
| if err := heldStageable(rt, version); err != nil { |
| return err |
| } |
| + return verifyHeldLive(ctx, s, optDir, rt, version, proof, lock) |
| +} |
| + |
| +// VerifyHeldLiveUnder is VerifyHeldLive for a held row under the host |
| +// claim (region HA Task 8c), as MaterializeHeldUnder is MaterializeHeld's: |
| +// the same reading under the same lock, with heldShape in place of the |
| +// CARLOS_HOST_CLAIM refusal. It writes nothing; the claim's selection |
| +// authorized the publication it reads. |
| +func VerifyHeldLiveUnder(ctx context.Context, s Store, optDir string, rt registry.Route, version string, proof *ProofGate, lock func(read func() error) error) error { |
| + if err := heldShape(rt, version); err != nil { |
| + return err |
| + } |
| + return verifyHeldLive(ctx, s, optDir, rt, version, proof, lock) |
| +} |
| + |
| +// verifyHeldLive is VerifyHeldLive and VerifyHeldLiveUnder after their |
| +// shape checks. |
| +func verifyHeldLive(ctx context.Context, s Store, optDir string, rt registry.Route, version string, proof *ProofGate, lock func(read func() error) error) error { |
| if proof == nil { |
| return errHeldNoProofGate |
| } |