Merge origin/main (fc70bc75) into fix-shutdown-lease-292
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
12 files changed,
+1059
−12
README.md+18 −1auth_e2e_test.go+62 −10docs/site/changelog-links.json+24 −0docs/site/changelog.json+366 −0docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md+7 −0internal/console/materialize.go+9 −0internal/console/reserved_sqids_test.go+140 −0internal/domains/mint.go+8 −1internal/domains/reserved.go+76 −0internal/domains/reserved_test.go+340 −0internal/provision/plan14_root_reads_test.go+6 −0internal/provision/testdata/plan14-root-reads-reserved285.tsv+3 −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 339.485s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.943s
ok github.com/carlosframework/platform/cmd/moveprobe 93.149s
ok github.com/carlosframework/platform/cmd/zddprobe 0.706s
ok github.com/carlosframework/platform/internal/activator 237.929s
ok github.com/carlosframework/platform/internal/agentsock 0.022s
ok github.com/carlosframework/platform/internal/alerting 0.007s
ok github.com/carlosframework/platform/internal/analytics 0.026s
ok github.com/carlosframework/platform/internal/appconfig 2.323s
ok github.com/carlosframework/platform/internal/apperr 0.093s
ok github.com/carlosframework/platform/internal/applog 0.151s
ok github.com/carlosframework/platform/internal/awsinv 0.008s
ok github.com/carlosframework/platform/internal/boxchoice 0.009s
ok github.com/carlosframework/platform/internal/boxid 0.972s
? 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.016s
ok github.com/carlosframework/platform/internal/caps 0.020s
ok github.com/carlosframework/platform/internal/certwarm 0.010s
ok github.com/carlosframework/platform/internal/cgroup 0.721s
ok github.com/carlosframework/platform/internal/changelog 0.307s
ok github.com/carlosframework/platform/internal/clamd 0.066s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 362.919s
? 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.075s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.009s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.003s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.171s
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.145s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.007s
ok github.com/carlosframework/platform/internal/fleetapi 0.122s
ok github.com/carlosframework/platform/internal/gate 1.655s
ok github.com/carlosframework/platform/internal/genselect 0.014s
ok github.com/carlosframework/platform/internal/geo 0.018s
ok github.com/carlosframework/platform/internal/geoip 0.029s
ok github.com/carlosframework/platform/internal/host 124.705s
ok github.com/carlosframework/platform/internal/hostactors 0.390s
ok github.com/carlosframework/platform/internal/hostbox 2.078s
ok github.com/carlosframework/platform/internal/hostclaim 0.494s
ok github.com/carlosframework/platform/internal/hostevidence 0.008s
ok github.com/carlosframework/platform/internal/hostexec 4.899s
ok github.com/carlosframework/platform/internal/hostfence 0.034s
ok github.com/carlosframework/platform/internal/hostfiles 10.582s
ok github.com/carlosframework/platform/internal/hostfloor 0.012s
ok github.com/carlosframework/platform/internal/hostguard 1.006s
ok github.com/carlosframework/platform/internal/hostops 1.446s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.003s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostrelocate 0.438s
ok github.com/carlosframework/platform/internal/hostresolve 0.131s
ok github.com/carlosframework/platform/internal/hostwrite 0.001s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 1.818s
ok github.com/carlosframework/platform/internal/instanceexec 2.727s
ok github.com/carlosframework/platform/internal/instancefs 0.018s
ok github.com/carlosframework/platform/internal/instancekey 0.016s
ok github.com/carlosframework/platform/internal/instanceupdates 0.021s
ok github.com/carlosframework/platform/internal/layout 1.440s
ok github.com/carlosframework/platform/internal/lease 9.451s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.020s
ok github.com/carlosframework/platform/internal/ledger 0.011s
ok github.com/carlosframework/platform/internal/mail 0.162s
ok github.com/carlosframework/platform/internal/maintenance 0.086s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.527s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.065s
ok github.com/carlosframework/platform/internal/metrics 0.004s
ok github.com/carlosframework/platform/internal/moves 21.112s
ok github.com/carlosframework/platform/internal/moves/archive 0.782s
ok github.com/carlosframework/platform/internal/objstore 0.159s
ok github.com/carlosframework/platform/internal/ops 0.168s
ok github.com/carlosframework/platform/internal/park 0.195s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
2026/10/14 15:16:10 provision: listing apps/bab/blog/instances/: list refused — its records are unseen this pass
2026/10/14 15:16:10 provision: listing apps/bab/: listdirs refused — its records are unseen this pass
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: listing apps/bac/tito/instances/: list refused — its records are unseen this pass
2026/10/14 15:16:10 provision: listing apps/bac/tito/instances/: list refused — its records are unseen this pass
2026/10/14 15:16:10 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:10 provision: remove retired instance route shop.example: provision: claim admission unavailable
2026/10/14 15:16:10 provision: remove retired instance route shop.example: provision: claim admission unavailable
2026/10/14 15:16:10 provision: remove retired sidecar route worker.sidecar.internal: provision: claim admission unavailable
2026/10/14 15:16:10 provision: remove retired sidecar route worker.sidecar.internal: provision: claim admission unavailable
2026/10/14 15:16:10 provision: claim install acme.go.tito.io: provision: claim admission unavailable
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/14 15:16:10 provision: claim install bot.eleven.bac.sidecar.internal: provision: claim admission unavailable
2026/10/14 15:16:10 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/14 15:16:10 provision: claim install bot.eleven.bac.sidecar.internal: registry: claim row ownership changed or unknown
2026/10/14 15:16:10 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/14 15:16:10 provision: acme.go.tito.io: not minting this pass: the box's layout intent could not be read: no sealed config
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 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/14 15:16:10 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/14 15:16:10 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/14 15:16:10 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/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 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/14 15:16:10 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/14 15:16:10 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/14 15:16:10 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/14 15:16:10 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/14 15:16:10 provision: claim install bot.eleven.bac.sidecar.internal: registry: claim row ownership changed or unknown
2026/10/14 15:16:10 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/14 15:16:10 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/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:10 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/14 15:16:10 provision: skipping record apps/bac/tito/instances/new.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/14 15:16:10 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/14 15:16:10 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/14 15:16:10 provision: skipping record apps/bac/tito/instances/new.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/14 15:16:10 provision: minted instance route ra.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rc.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rd.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route re.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rf.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rh.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rj.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rk.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rb.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rg.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route ri.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rl.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rd.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route ra.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rb.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route rc.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: skipping record apps/bac/tito/instances/rb.go.tito.io.json: read failed: read refused
2026/10/14 15:16:10 provision: skipping record apps/bac/tito/instances/rb.go.tito.io.json: read failed: read refused
2026/10/14 15:16:10 provision: minted instance route re.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.appi.example (app "appi" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.appa.example (app "appa" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.appb.example (app "appb" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.appd.example (app "appd" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.appe.example (app "appe" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.apph.example (app "apph" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.appj.example (app "appj" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.appc.example (app "appc" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.appf.example (app "appf" acct "bac")
2026/10/14 15:16:10 provision: minted instance route x.appg.example (app "appg" acct "bac")
2026/10/14 15:16:10 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/14 15:16:11 provision: minted instance route r0034.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0053.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0124.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0028.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0033.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0045.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0051.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0117.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0061.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0085.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0101.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0125.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0023.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0050.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0080.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0126.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0088.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0091.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0014.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0041.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0042.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0081.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0102.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0060.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0094.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0104.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0113.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0029.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0032.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0043.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0071.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0074.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0062.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0064.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0067.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0073.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0109.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0005.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0031.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0038.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0072.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0108.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0000.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0022.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0030.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0046.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0066.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0076.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0121.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0070.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0100.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0129.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0040.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0054.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0118.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0107.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0111.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0017.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0048.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0065.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0068.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0079.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0082.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0077.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0096.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0122.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0008.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0112.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0016.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0024.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0056.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0075.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0086.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0097.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0003.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0039.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0004.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0057.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0084.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0087.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0092.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0037.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0049.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0063.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0116.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0044.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0115.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0025.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0058.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0090.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0110.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0015.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0035.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0089.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0095.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0099.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0010.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0018.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0069.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0098.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0019.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0026.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0083.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0123.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0007.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0078.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0093.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0036.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0103.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0105.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0106.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0012.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0055.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0127.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0128.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0001.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0047.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0059.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0119.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0120.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0027.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0013.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0002.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0006.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0011.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0020.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0052.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0114.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0009.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:11 provision: minted instance route r0021.go.tito.io (app "tito" acct "bac")
--- FAIL: TestPlan14RootReadCensus (9.77s)
plan14_root_reads_test.go:680: root read census: 21 discrepancies
changed root read internal/activator/activator.go:Service.BeginSidecarRequest; review class, task, sites and digest
changed root read internal/activator/activator.go:Service.begin; review class, task, sites and digest
changed root read internal/activator/activator.go:Service.doWake; review class, task, sites and digest
changed root read internal/activator/activator.go:Service.sweepOnce; review class, task, sites and digest
changed root read internal/activator/activator.go:Service.wake; review class, task, sites and digest
changed root read internal/activator/backend_exec.go:ExecBackend.spawnUnitFrom; review class, task, sites and digest
changed root read internal/activator/maintenance.go:Service.cleanupFailedWake; review class, task, sites and digest
changed root read internal/activator/opset.go:Service.reapUnprovenWake; review class, task, sites and digest
changed root source envelope internal/activator
changed root source envelope internal/lease
unclassified root read internal/activator/backend_exec.go:ExecBackend.LegacyRunGone registry:Host@174,registry:Host@225,registry:Host@268,registry:Host@623
unclassified root read internal/activator/backend_unit.go:SplitBackend.LegacyRunGone file:Exec@240,registry:Backing@89,registry:Host@200,registry:Host@305,whole:Route@364
unclassified root read internal/activator/containment.go:Service.legacyRunGone whole:Route@257
changed legacy declaration internal/activator/activator.go:Service.BeginSidecarRequest
changed legacy declaration internal/activator/activator.go:Service.begin
changed legacy declaration internal/activator/activator.go:Service.doWake
changed legacy declaration internal/activator/activator.go:Service.sweepOnce
changed legacy declaration internal/activator/activator.go:Service.wake
changed legacy declaration internal/activator/backend_exec.go:ExecBackend.spawnUnitFrom
changed legacy declaration internal/activator/maintenance.go:Service.cleanupFailedWake
changed legacy declaration internal/activator/opset.go:Service.reapUnprovenWake
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 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/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route whole.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route console.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted service route ha.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted service route svc.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route shared.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: converged acme.go.tito.io channel stable -> edge
2026/10/14 15:16:21 provision: converged acme.go.tito.io channel edge -> canary/rehearsal
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: empty channel
2026/10/14 15:16:21 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: converged acme.go.tito.io environment "" -> "staging"
2026/10/14 15:16:21 provision: converged acme.go.tito.io environment "staging" -> ""
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: converged acme.go.tito.io steering "" -> "latency"
2026/10/14 15:16:21 provision: converged acme.go.tito.io steering "latency" -> ""
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: converged acme.go.tito.io hibernate false -> true
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: converged acme.go.tito.io hibernate true -> false (replication "daily" cleared: only a hibernating route's rung is read)
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: converged acme.go.tito.io status-named false -> true
2026/10/14 15:16:21 provision: converged acme.go.tito.io status-named true -> false
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted service route ha.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 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/14 15:16:21 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/14 15:16:21 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/14 15:16:21 provision: minted service route whole.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 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/14 15:16:21 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/14 15:16:21 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/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: minted service route whole.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: skipping accounts/bac/account.json: malformed account manifest: invalid character 'o' in literal null (expecting 'u')
2026/10/14 15:16:21 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: manifest bac/tito: read failed: provision: manifest bac/tito: invalid character 'o' in literal null (expecting 'u')
2026/10/14 15:16:21 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/14 15:16:21 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/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 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/14 15:16:21 provision: not removing acme$evil.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/14 15:16:21 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/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 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/14 15:16:21 provision: not removing bad-service.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/14 15:16:21 provision: skipping record apps/bac/tito/instances/no-channel.go.tito.io.json: empty channel
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: skipping record apps/bac/tito/instances/no-channel.go.tito.io.json: empty channel
2026/10/14 15:16:21 provision: not removing no-channel.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/14 15:16:21 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/14 15:16:21 provision: removed retired instance route ghost.go.tito.io (record gone)
2026/10/14 15:16:21 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/14 15:16:21 provision: minted instance route dup.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/14 15:16:21 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/14 15:16:21 provision: removed retired instance route acme.go.tito.io (app in the trash)
2026/10/14 15:16:21 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:21 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/14 15:16:21 provision: removed retired static route site.go.tito.io (app in the trash)
2026/10/14 15:16:21 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/14 15:16:22 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/14 15:16:22 provision: removed retired instance route acme.go.tito.io (app in the trash)
2026/10/14 15:16:22 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/14 15:16:22 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/14 15:16:22 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/14 15:16:22 provision: converged acme.go.tito.io channel stable -> edge
2026/10/14 15:16:22 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/14 15:16:22 provision: converged acme.go.tito.io channel canary/rehearsal -> edge
2026/10/14 15:16:22 provision: converged acme.go.tito.io environment "staging" -> ""
2026/10/14 15:16:22 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/14 15:16:22 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/14 15:16:22 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/14 15:16:22 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/14 15:16:22 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/14 15:16:22 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/14 15:16:22 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/14 15:16:22 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/14 15:16:22 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/14 15:16:22 provision: dry-run: would mint instance route acme.go.tito.io (app "tito" acct "bac" channel "stable")
2026/10/14 15:16:22 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/14 15:16:22 provision: dry-run: would converge adopt.go.tito.io channel stable -> edge
2026/10/14 15:16:22 provision: dry-run: would converge adopt.go.tito.io environment "" -> "staging"
2026/10/14 15:16:22 provision: dry-run: would converge owned.go.tito.io channel stable -> edge
2026/10/14 15:16:22 provision: dry-run: would converge owned.go.tito.io environment "" -> "staging"
2026/10/14 15:16:22 provision: dry-run: would converge owned.go.tito.io hibernate false -> true
2026/10/14 15:16:22 provision: dry-run: would converge owned.go.tito.io seed
2026/10/14 15:16:22 provision: dry-run: would remove retired instance route gone.go.tito.io (record gone)
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: adopted existing route adopt.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/14 15:16:22 provision: converged adopt.go.tito.io channel stable -> edge
2026/10/14 15:16:22 provision: converged adopt.go.tito.io environment "" -> "staging"
2026/10/14 15:16:22 provision: converged owned.go.tito.io channel stable -> edge
2026/10/14 15:16:22 provision: converged owned.go.tito.io environment "" -> "staging"
2026/10/14 15:16:22 provision: converged owned.go.tito.io hibernate false -> true
2026/10/14 15:16:22 provision: removed retired instance route gone.go.tito.io (record gone)
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: adopted existing route story.carlosframework.com (app "story" acct "bad") — this record now owns its channel and environment
2026/10/14 15:16:22 provision: story.carlosframework.com exists but is not the route this record describes (kind "instance"/"static") — leaving it alone
2026/10/14 15:16:22 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted service route pub.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted service route pubs.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted service route loop.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted service route named.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted service route priv.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route unit.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route exec.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted static route pisite.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: removed retired static route site.go.tito.io (record gone)
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route acme-uk.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: converged acme.go.tito.io replicas "ie,uk" -> "uk"
2026/10/14 15:16:22 provision: converged acme.go.tito.io replicas "uk" -> ""
2026/10/14 15:16:22 provision: minted instance route acme-uk.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: converged acme-uk.go.tito.io replica mark true -> false
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 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/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:22 provision: acme.go.tito.io: not minting this pass: the box's layout intent could not be read: store down
2026/10/14 15:16:22 provision: router.go.tito.io: not minting this pass: the box's layout intent could not be read: store down
2026/10/14 15:16:22 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/14 15:16:22 provision: converged acme.go.tito.io channel stable -> edge
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
2026/10/14 15:16:22 provision: not removing acme.go.tito.io (record gone): participant of a running operation
2026/10/14 15:16:22 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/14 15:16:22 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
2026/10/14 15:16:22 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/14 15:16:22 provision: held acme.go.tito.io landing by mv_acme (was ready)
2026/10/14 15:16:22 provision: acme.go.tito.io: held restoring by rb_other, not by mv_acme; leaving it
2026/10/14 15:16:22 provision: acme.go.tito.io: the row is not this pass's (app "other" acct ""); not holding it for mv_acme
2026/10/14 15:16:22 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/14 15:16:22 provision: dry-run: would mint instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac" channel "stable")
2026/10/14 15:16:22 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/14 15:16:22 provision: held acme.go.tito.io landing by mv_acme (was ready)
2026/10/14 15:16:22 provision: removed retired sidecar route gone.eleven.bac.sidecar.internal (record gone)
2026/10/14 15:16:22 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:22 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:22 provision: converged sidecar bot.eleven.bac.sidecar.internal args ["sidecar","run"] -> ["worker","run"]
2026/10/14 15:16:22 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:22 provision: converged sidecar bot.eleven.bac.sidecar.internal ceilings 300/3600 -> 60/120
2026/10/14 15:16:22 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:22 provision: converged sidecar bot.eleven.bac.sidecar.internal channel stable -> canary/eleven
2026/10/14 15:16:22 provision: converged sidecar bot.eleven.bac.sidecar.internal environment "other" -> "relay"
2026/10/14 15:16:22 provision: converged sidecar bot.eleven.bac.sidecar.internal hibernate true -> false
2026/10/14 15:16:22 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: read failed: failStore: reads disabled under apps/bac/eleven/sidecars/
2026/10/14 15:16:22 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: malformed: invalid character 'n' looking for beginning of object key string
2026/10/14 15:16:22 provision: not removing sidecar bot.eleven.bac.sidecar.internal: its record exists but could not be read cleanly this pass
2026/10/14 15:16:22 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: empty channel
2026/10/14 15:16:22 provision: skipping sidecar record apps/bac/eleven/sidecars/held.json: empty channel
2026/10/14 15:16:22 provision: not removing sidecar held.eleven.bac.sidecar.internal: its record exists but could not be read cleanly this pass
2026/10/14 15:16:22 provision: removed retired sidecar route old.eleven.bac.sidecar.internal (record gone)
2026/10/14 15:16:23 provision: bac/eleven has sidecar records but no sidecars manifest — not minting
2026/10/14 15:16:23 provision: bac/eleven is in the trash — not minting sidecar routes for it
2026/10/14 15:16:23 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/14 15:16:23 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/14 15:16:23 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:23 provision: minted sidecar route hub.relay.bac.sidecar.internal (app "relay" acct "bac" args ["relay"])
2026/10/14 15:16:23 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/14 15:16:23 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:23 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/14 15:16:23 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:23 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:23 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/14 15:16:23 provision: removed retired sidecar route bot.eleven.bac.sidecar.internal (record gone)
2026/10/14 15:16:23 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:23 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/14 15:16:23 provision: skipping sidecar record apps/bac/relay/sidecars/hub.json: read failed: failStore: reads disabled under apps/bac/relay/sidecars/
2026/10/14 15:16:23 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/14 15:16:23 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/14 15:16:23 provision: dry-run: would mint sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" channel "stable" args ["sidecar","run"])
2026/10/14 15:16:23 provision: dry-run: would remove retired sidecar route gone.eleven.bac.sidecar.internal (record gone)
2026/10/14 15:16:23 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/14 15:16:23 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:23 provision: converged acme.go.tito.io update settings "{\"updates\":\"manual\",\"channel\":\"stable\"}" -> ""
2026/10/14 15:16:23 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:23 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:23 provision: manifest bac/tito: read failed: provision: manifest bac/tito: unexpected end of JSON input
2026/10/14 15:16:23 provision: manifest bac/tito: read failed: provision: manifest bac/tito: unexpected end of JSON input
2026/10/14 15:16:23 provision: acme.go.tito.io: not minted this pass: the app's update policy could not be read
2026/10/14 15:16:23 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/14 15:16:23 provision: manifest bac/tito: read failed: provision: manifest bac/tito: unexpected end of JSON input
2026/10/14 15:16:23 provision: acme.go.tito.io: not minted this pass: the app's update policy could not be read
2026/10/14 15:16:23 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.116s
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.044s
ok github.com/carlosframework/platform/internal/relay 7.235s
ok github.com/carlosframework/platform/internal/release 19.621s
ok github.com/carlosframework/platform/internal/rollout 3.732s
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.007s
ok github.com/carlosframework/platform/internal/rootstate 0.025s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.327s
ok github.com/carlosframework/platform/internal/sim/hostlife 9.637s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.817s
ok github.com/carlosframework/platform/internal/status 0.037s
ok github.com/carlosframework/platform/internal/steering 0.006s
ok github.com/carlosframework/platform/internal/stopevidence 0.182s
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.004s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.567s
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/README.md b/README.md| index 0a92a4838..492ee8361 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -269,7 +269,24 @@ curl -sS https://apt.carlosframework.com/latest.txt # prints $v |
| carlos system update --console <console> --version $v # then: carlos system status |
| ``` |
| -Last, roll it: `carlos system update --console <console> --version vX.Y.Z`, then `carlos system status`. Every box should read `current` within the spread. A box that reads `rolled-back` or `refused` says why on the same line; fix that box, and do not re-cut the release. |
| +Last, roll it, and roll dev before flagship. The order is fixed: dev boxes, then the dev console, then check dev end to end, then flagship boxes, then the flagship console. Do not start on flagship until dev has been exercised on the new build. |
| + |
| +`carlos system update --console <console> --version vX.Y.Z`, then `carlos system status`. Every box should read `current` within the spread. A box that reads `rolled-back` or `refused` says why on the same line; fix that box, and do not re-cut the release. |
| + |
| +**Checking dev.** The check is `wakecheck`, a standing hibernating app on dev. Each request writes one SQLite row and replies with its build, the box's host and the row count (`hits=`). It has no region, so both dev boxes serve it: `carlos-dev-ie`, which is the console box, and `carlos-dev-ie-b`. Once dev is rolled: |
| + |
| +1. Confirm it is asleep. `carlos logs --app wakecheck --console https://platform.dev.carlosframework.com --since 30m` should end in "instance exited". Do not use `instances list --health` for this, because it always reads Asleep (#287). Idle is 15 minutes and bots can keep it awake, so wait it out. |
| +2. Wake it on `carlos-dev-ie` through DNS: `curl -sS https://wakecheck.dev.carlosframework.com/`. A pass is a 200 whose `host=` is that box, in about 0.6 s to first byte. |
| +3. Wake it on `carlos-dev-ie-b` by pinning its address: `curl -sS --resolve wakecheck.dev.carlosframework.com:443:18.200.150.210 https://wakecheck.dev.carlosframework.com/`. A pass is a 200 whose `host=` is that box. It takes about 1.6 s, because it restores from S3. |
| +4. `carlos status --layout` shows `wakecheck` as `nested` on both boxes. |
| + |
| +On dev, `hits=` going backwards from one box to the other is not a failure. With the claim off, a wake that switches box can serve a stale local DB. That is the known hazard in #286, and it stays until the claim ruling lands. Judge the check on the 200s and the `host=` values, not on the count. |
| + |
| +Three things catch people out on the roll: |
| + |
| +- **`system update` rolls a whole channel.** It rolls every box on the channel within the spread. It cannot canary one box. To put a release on one box first, roll that box by hand with `scripts/update-platform-binary.sh` in platform-infrastructure. |
| +- **Old updaters need `carlos-instance-control` restarted.** A box whose updater predates v0.26.0 does not restart `carlos-instance-control` when it rolls. The moment the new edge serves, restart it by hand. Until then the new agent refuses isolated wakes as "running an older build". Use a one-off watcher to do the restart, not memory. |
| +- **The dev console needs its release key first.** Before dev can be rolled with `system update`, its console must hold the release key. That is `/etc/carlos/release.env` (root:root, 0600) and a `console.service.d/release-key.conf` drop-in that loads it. |
| - Refresh the embedded timezone tables when a new IANA tzdata release is out: |
| `scripts/update-tzgeo.sh <version>`, then `go test ./internal/geo`. |
diff --git a/auth_e2e_test.go b/auth_e2e_test.go| index 38bc15301..22ee1dfb7 100644 |
| --- a/auth_e2e_test.go |
| +++ b/auth_e2e_test.go |
| @@ -48,10 +48,14 @@ func TestAuthCLI(t *testing.T) { |
| fc := newFakeConsole(t) |
| home := t.TempDir() |
| creds := filepath.Join(home, ".carlos", "credentials") |
| - env := append(os.Environ(), |
| + // carlosTestEnv, never os.Environ(): openStore prefers an inherited |
| + // CARLOS_DEPLOYMENT_BUCKET over any dir store, so blank it (and the AWS |
| + // profile) so no child can reach a real bucket. |
| + env := carlosTestEnv(t, |
| + "CARLOS_DEPLOYMENT_BUCKET=", |
| + "AWS_PROFILE=", |
| "CARLOS_CREDENTIALS="+creds, |
| "HOME="+home, |
| - "CARLOS_CONSOLE=", |
| ) |
| run := func(args ...string) (string, error) { |
| @@ -128,12 +132,13 @@ func TestAuthCLI(t *testing.T) { |
| func TestAccountsCreateNeedsAnOwner(t *testing.T) { |
| bin := buildCarlos(t) |
| tmp := t.TempDir() |
| - env := append(os.Environ(), |
| + env := carlosTestEnv(t, |
| + "CARLOS_DEPLOYMENT_BUCKET=", |
| + "AWS_PROFILE=", |
| "CARLOS_DEPLOYMENT_DIR="+filepath.Join(tmp, "store"), |
| "CARLOS_CREDENTIALS="+filepath.Join(tmp, "credentials"), |
| "CARLOS_DATA_DIR="+tmp, |
| "HOME="+tmp, |
| - "CARLOS_CONSOLE=", |
| ) |
| cmd := exec.Command(bin, "accounts", "create", "-name", "orphan") |
| cmd.Env = env |
| @@ -191,12 +196,13 @@ func TestAccountsCreateWithOwnerFlag(t *testing.T) { |
| tmp := t.TempDir() |
| storeDir := filepath.Join(tmp, "store") |
| cmd := exec.Command(bin, "accounts", "create", "-name", "kass", "-owner", "paul@keymail.dev") |
| - cmd.Env = append(os.Environ(), |
| + cmd.Env = carlosTestEnv(t, |
| + "CARLOS_DEPLOYMENT_BUCKET=", |
| + "AWS_PROFILE=", |
| "CARLOS_DEPLOYMENT_DIR="+storeDir, |
| "CARLOS_CREDENTIALS="+filepath.Join(tmp, "credentials"), |
| "CARLOS_DATA_DIR="+tmp, |
| "HOME="+tmp, |
| - "CARLOS_CONSOLE=", |
| ) |
| // pick() walks cwd ancestors for .carlos/config, so the subprocess must not run from the real checkout. |
| cmd.Dir = tmp |
| @@ -225,6 +231,47 @@ func TestAccountsCreateWithOwnerFlag(t *testing.T) { |
| } |
| } |
| +// The headless path mints through the same allocator as the console, so it |
| +// steps over a reserved sqid too (#285): with 1167 the highest account in the |
| +// bucket, the next account is 1169 ("dew"), never 1168 ("dev"). |
| +func TestAccountsCreateSkipsAReservedSqid(t *testing.T) { |
| + bin := buildCarlos(t) |
| + tmp := t.TempDir() |
| + storeDir := filepath.Join(tmp, "store") |
| + store := release.NewDirStore(storeDir) |
| + if _, err := store.Put(t.Context(), domains.AccountManifestKey(domains.Sqid(1167)), |
| + []byte(`{"id":1167,"sqid":"`+domains.Sqid(1167)+`","name":"neighbour","created_at":"2026-08-02T09:00:00Z"}`), ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + cmd := exec.Command(bin, "accounts", "create", "-name", "shop", "-owner", "owner@example.com") |
| + // openStore prefers CARLOS_DEPLOYMENT_BUCKET over CARLOS_DEPLOYMENT_DIR, so |
| + // an inherited bucket (plus AWS credentials) would mint a real account. |
| + // Blank both, so the child can only reach the dir store. |
| + cmd.Env = carlosTestEnv(t, |
| + "CARLOS_DEPLOYMENT_BUCKET=", |
| + "AWS_PROFILE=", |
| + "CARLOS_DEPLOYMENT_DIR="+storeDir, |
| + "CARLOS_DATA_DIR="+tmp, |
| + ) |
| + // pick() walks cwd ancestors for .carlos/config, so the subprocess must not run from the real checkout. |
| + cmd.Dir = tmp |
| + out, err := cmd.CombinedOutput() |
| + if err != nil { |
| + t.Fatalf("carlos accounts create --owner: %v\n%s", err, out) |
| + } |
| + if _, _, err := store.Get(t.Context(), domains.AccountManifestKey("dev")); err == nil { |
| + t.Fatalf("accounts create minted the reserved sqid dev:\n%s", out) |
| + } |
| + body, _, err := store.Get(t.Context(), domains.AccountManifestKey("dew")) |
| + if err != nil { |
| + t.Fatalf("no manifest at accounts/dew/ (%v):\n%s", err, out) |
| + } |
| + var m domains.AccountManifest |
| + if err := json.Unmarshal(body, &m); err != nil || m.ID != 1169 || m.Name != "shop" { |
| + t.Fatalf("manifest = %+v (%v), want shop at id 1169", m, err) |
| + } |
| +} |
| + |
| // A padded name is trimmed BEFORE it is validated or minted — the gate and |
| // the write must agree on what "the name" is. Before this fix, -name " |
| // kass " passed ValidAccountName on the trimmed copy but handed the |
| @@ -236,6 +283,8 @@ func TestAccountsCreateTrimsTheNameBeforeMinting(t *testing.T) { |
| storeDir := filepath.Join(tmp, "store") |
| cmd := exec.Command(bin, "accounts", "create", "-name", " kass ", "-owner", "paul@keymail.dev") |
| cmd.Env = carlosTestEnv(t, |
| + "CARLOS_DEPLOYMENT_BUCKET=", |
| + "AWS_PROFILE=", |
| "CARLOS_DEPLOYMENT_DIR="+storeDir, |
| "CARLOS_DATA_DIR="+tmp, |
| ) |
| @@ -282,13 +331,16 @@ func TestAccountsCreateUsesTheLoggedInIdentity(t *testing.T) { |
| t.Fatal(err) |
| } |
| cmd := exec.Command(bin, "accounts", "create", "-name", "kass") |
| - cmd.Env = append(os.Environ(), |
| + cmd.Env = carlosTestEnv(t, |
| + // No deployment store of any kind: the console owns the bucket write |
| + // on this path, and a laptop has none configured. Blanked rather than |
| + // left out, so an inherited bucket (or dir) can never be reached. |
| + "CARLOS_DEPLOYMENT_BUCKET=", |
| + "CARLOS_DEPLOYMENT_DIR=", |
| + "AWS_PROFILE=", |
| "CARLOS_CREDENTIALS="+creds, |
| "CARLOS_DATA_DIR="+tmp, |
| "HOME="+tmp, |
| - "CARLOS_CONSOLE=", |
| - // Deliberately no CARLOS_DEPLOYMENT_DIR: the console owns the bucket |
| - // write on this path, and a laptop has no deployment store configured. |
| ) |
| // pick() walks cwd ancestors for .carlos/config, so the subprocess must not run from the real checkout. |
| cmd.Dir = tmp |
diff --git a/docs/site/changelog-links.json b/docs/site/changelog-links.json| index c74a15d3e..cfadf491c 100644 |
| --- a/docs/site/changelog-links.json |
| +++ b/docs/site/changelog-links.json |
| @@ -1,5 +1,29 @@ |
| { |
| "//": "For the changelog: each release-note bullet's bold lead, mapped to the branches behind it. Kept by hand, one release at a time, as step 10 of the release recipe in the README. go run ./cmd/changelog fails on a lead no bullet has, or a branch that did not land in that release.", |
| + "v0.27.2": { |
| + "Two builds at once keeps each build's socket name.": [ |
| + "zdd-gen-socket-names" |
| + ], |
| + "A long host name keeps stop-then-start updates.": [ |
| + "zdd-gen-socket-names" |
| + ] |
| + }, |
| + "v0.27.1": { |
| + "A box with many instances no longer fails its update while it starts.": [ |
| + "hotfix-startup-sweep-281" |
| + ] |
| + }, |
| + "v0.27.0": { |
| + "Root keeps its own record of each box's releases.": [ |
| + "feat/claim-root-trusted-state" |
| + ], |
| + "`carlos __cgexec` can appear in `ps`.": [ |
| + "zdd-flag-readiness-3" |
| + ], |
| + "After the roll, check placement on each box.": [ |
| + "zdd-flag-readiness-3" |
| + ] |
| + }, |
| "v0.26.0": { |
| "Two builds at once, off until your deployment turns it on.": [ |
| "zdd-1-generations", |
diff --git a/docs/site/changelog.json b/docs/site/changelog.json| index 8b0e6ad14..b3987ca74 100644 |
| --- a/docs/site/changelog.json |
| +++ b/docs/site/changelog.json |
| @@ -2,6 +2,372 @@ |
| "//": "Generated by go run ./cmd/changelog from git, the published release notes and Amadan. Do not edit; regenerate.", |
| "repo": "https://amadan.net/carlos/platform", |
| "releases": [ |
| + { |
| + "tag": "v0.27.2", |
| + "date": "2026-10-14", |
| + "sha": "ebddcfc6f4c55a6239d07faa6b46d3773186327b", |
| + "published": true, |
| + "notes_url": "https://github.com/carlosframework/releases/releases/tag/v0.27.2", |
| + "notes": { |
| + "form": "bullets", |
| + "sections": [ |
| + { |
| + "title": "", |
| + "bullets": [ |
| + { |
| + "md": "**Two builds at once keeps each build's socket name.** Where a deployment turns on two builds at once, each build's socket now has the same file name it has with the switch off, in a directory of its own. An app that works out its own address from its socket's name now starts the same way either way. Before, such an app saw a different name for the new build and refused to start. Nothing changes where the switch is off, which is every deployment by default.", |
| + "branches": [ |
| + "zdd-gen-socket-names" |
| + ] |
| + }, |
| + { |
| + "md": "**Nothing changes for people using the CLI.**" |
| + } |
| + ] |
| + }, |
| + { |
| + "title": "For operators", |
| + "bullets": [ |
| + { |
| + "md": "**Roll the boxes first, then the console.**" |
| + }, |
| + { |
| + "md": "**Coming from v0.26.x, read the [v0.27.0](https://github.com/carlosframework/releases/releases/tag/v0.27.0) and [v0.27.1](https://github.com/carlosframework/releases/releases/tag/v0.27.1) operator notes first.** They all still apply." |
| + }, |
| + { |
| + "md": "**A long host name keeps stop-then-start updates.** With two builds at once on, a host on a flat box whose name is longer than 59 bytes is updated the old way: stopped, then started on the new build. Its socket path would otherwise be too long.", |
| + "branches": [ |
| + "zdd-gen-socket-names" |
| + ] |
| + }, |
| + { |
| + "md": "**Every dormant switch stays off.** Leave `CARLOS_SWAP`, `CARLOS_MOVES_PHASE1`, `CARLOS_HOST_CLAIM`, `CARLOS_HOST_CLAIM_ORIGINS`, `CARLOS_HOST_CLAIM_RETIRED_LOCATIONS`, `CARLOS_CONSOLE_ID`, `CARLOS_REGION_HA`, `CONSOLE_APP_REGIONS`, `CARLOS_TEMPORARY_APPS` and `CARLOS_REQUIRE_POINTER_DIGEST` unset, and don't run `carlos channels digests on`. With `CARLOS_HOST_CLAIM` set, instance control now refuses to start until a later release.", |
| + "docs": [ |
| + "/docs/cli#carlos-channels-digests" |
| + ] |
| + } |
| + ] |
| + } |
| + ] |
| + }, |
| + "changes": [ |
| + { |
| + "sha": "ebddcfc6f4c55a6239d07faa6b46d3773186327b", |
| + "date": "2026-10-14", |
| + "subject": "zdd rev 19 amendment: generation sockets keep the flag-off file name", |
| + "branch": "zdd-gen-socket-names", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/zdd-gen-socket-names", |
| + "commits": 5, |
| + "discussions": [ |
| + { |
| + "id": 272, |
| + "title": "Zero-downtime deploys: owed before CARLOS_SWAP can go on", |
| + "state": "open" |
| + } |
| + ], |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "claude" |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "sha": "d2f58fd330726e461b1022edfc9e4c4688696dd1", |
| + "date": "2026-10-13", |
| + "subject": "startup-largebox-test-281", |
| + "branch": "startup-largebox-test-281", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/startup-largebox-test-281", |
| + "commits": 1, |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "claude" |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "sha": "21d91fd6087aa1ef7a21f35fc9188b263e3d0529", |
| + "date": "2026-10-13", |
| + "subject": "host: rootinventory_test.go is linux-only", |
| + "branch": "fix-cross-test-compile", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/fix-cross-test-compile", |
| + "commits": 1, |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "claude" |
| + ] |
| + } |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "tag": "v0.27.1", |
| + "date": "2026-10-13", |
| + "sha": "6dfbff103985e1e81899f2e611b1afff0ddb3769", |
| + "published": true, |
| + "notes_url": "https://github.com/carlosframework/releases/releases/tag/v0.27.1", |
| + "notes": { |
| + "form": "bullets", |
| + "sections": [ |
| + { |
| + "title": "", |
| + "bullets": [ |
| + { |
| + "md": "**A box with many instances no longer fails its update while it starts.** On a box with over a hundred instances, v0.27.0's update could take longer than the 20 seconds it allows, because the edge waited for the agent to check every instance first. The box then rolled back after about 45 seconds of failed requests. The edge now starts serving straight away. Requests for an instance wait up to 30 seconds for that check to finish, then get the usual answer that the instance is waking.", |
| + "branches": [ |
| + "hotfix-startup-sweep-281" |
| + ] |
| + }, |
| + { |
| + "md": "**Nothing changes for people using the CLI.**" |
| + } |
| + ] |
| + }, |
| + { |
| + "title": "For operators", |
| + "bullets": [ |
| + { |
| + "md": "**Roll the boxes first, then the console.**" |
| + }, |
| + { |
| + "md": "**Coming from v0.26.x, read the [v0.27.0 operator notes](https://github.com/carlosframework/releases/releases/tag/v0.27.0) first.** They all still apply." |
| + }, |
| + { |
| + "md": "**After each box updates, check that it finished starting.** The update's 20-second check now passes once the edge serves, before the agent has checked every instance. So also check the box's journal for `agent: hibernation activator armed`, and that one request to a sleeping instance succeeds." |
| + }, |
| + { |
| + "md": "**Every dormant switch stays off.** Leave `CARLOS_SWAP`, `CARLOS_MOVES_PHASE1`, `CARLOS_HOST_CLAIM`, `CARLOS_HOST_CLAIM_ORIGINS`, `CARLOS_HOST_CLAIM_RETIRED_LOCATIONS`, `CARLOS_CONSOLE_ID`, `CARLOS_REGION_HA`, `CONSOLE_APP_REGIONS`, `CARLOS_TEMPORARY_APPS` and `CARLOS_REQUIRE_POINTER_DIGEST` unset, and don't run `carlos channels digests on`. With `CARLOS_HOST_CLAIM` set, instance control now refuses to start until a later release.", |
| + "docs": [ |
| + "/docs/cli#carlos-channels-digests" |
| + ] |
| + } |
| + ] |
| + } |
| + ] |
| + }, |
| + "changes": [ |
| + { |
| + "sha": "6dfbff103985e1e81899f2e611b1afff0ddb3769", |
| + "date": "2026-10-13", |
| + "subject": "hotfix #281: the edge no longer waits on the startup sweep and recovery", |
| + "branch": "hotfix-startup-sweep-281", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/hotfix-startup-sweep-281", |
| + "commits": 1, |
| + "discussions": [ |
| + { |
| + "id": 281, |
| + "title": "Agent startup is not bounded under the 20s update gate: sweep and recovery still run before the edge listens", |
| + "state": "open" |
| + } |
| + ], |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell" |
| + } |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "tag": "v0.27.0", |
| + "date": "2026-10-13", |
| + "sha": "f4cf27dd0c48eac32d7b4beaec77686752ca6cff", |
| + "published": true, |
| + "notes_url": "https://github.com/carlosframework/releases/releases/tag/v0.27.0", |
| + "notes": { |
| + "form": "bullets", |
| + "sections": [ |
| + { |
| + "title": "", |
| + "bullets": [ |
| + { |
| + "md": "**Root keeps its own record of each box's releases.** A box's root process now keeps the builds it has seen run healthy, and the build each service ran before its current one, in directories only root can write. Before, it read them from places the platform's own service could write. Restarts, rollbacks and recovering from a bad build work exactly as they did.", |
| + "branches": [ |
| + "feat/claim-root-trusted-state" |
| + ] |
| + }, |
| + { |
| + "md": "**Nothing changes for people using the CLI.**" |
| + } |
| + ] |
| + }, |
| + { |
| + "title": "For operators", |
| + "bullets": [ |
| + { |
| + "md": "**Roll the boxes first, then the console.**" |
| + }, |
| + { |
| + "md": "**Coming from v0.25.x, read the [v0.26.0 operator notes](https://github.com/carlosframework/releases/releases/tag/v0.26.0) and the [v0.26.1 notes](https://github.com/carlosframework/releases/releases/tag/v0.26.1) first.** They all still apply." |
| + }, |
| + { |
| + "md": "**Before rolling, check two things on each box.** `/`, `/var` and `/var/lib` must be owned by root and not writable by group or others, and `/var/lib/carlos-claims` must not exist. If either check fails, root can't update a service that has its own unit, such as the console, on that box, and logs `repoint failed: permission denied` on every pass." |
| + }, |
| + { |
| + "md": "**Health history starts empty.** Root no longer reads the health history the service kept before this release. Don't add a `require` or `prove` entry for a service to `/etc/carlos/proof-policy.json` until both its current build and the one before it have been seen healthy since the roll. Otherwise rolling back to the earlier build waits for a proof." |
| + }, |
| + { |
| + "md": "**One earlier build per service is kept.** `/opt/carlos/previous/<host>` points at the build each service ran before its current one, and the box keeps that build when it prunes old versions. Disk use can grow by up to one build per service." |
| + }, |
| + { |
| + "md": "**`carlos __cgexec` can appear in `ps`.** Each new instance process now starts through a short-lived helper that places it in its cgroup before it runs. For a few milliseconds `ps` can show `carlos __cgexec` as a child of the agent. Monitoring, or a `pkill` that matches \"carlos\" in a command line, can catch it.", |
| + "branches": [ |
| + "zdd-flag-readiness-3" |
| + ] |
| + }, |
| + { |
| + "md": "**After the roll, check placement on each box.** `journalctl -u carlos-edge --since '<roll time>' | grep 'placement before exec failed'` should print nothing. A line there means a process was moved into its cgroup after it started instead. That affects resource accounting, not serving.", |
| + "branches": [ |
| + "zdd-flag-readiness-3" |
| + ] |
| + }, |
| + { |
| + "md": "**Every dormant switch stays off.** Leave `CARLOS_SWAP`, `CARLOS_MOVES_PHASE1`, `CARLOS_HOST_CLAIM`, `CARLOS_HOST_CLAIM_ORIGINS`, `CARLOS_HOST_CLAIM_RETIRED_LOCATIONS`, `CARLOS_CONSOLE_ID`, `CARLOS_REGION_HA`, `CONSOLE_APP_REGIONS`, `CARLOS_TEMPORARY_APPS` and `CARLOS_REQUIRE_POINTER_DIGEST` unset, and don't run `carlos channels digests on`. With `CARLOS_HOST_CLAIM` set, instance control now refuses to start until a later release.", |
| + "docs": [ |
| + "/docs/cli#carlos-channels-digests" |
| + ] |
| + } |
| + ] |
| + } |
| + ] |
| + }, |
| + "changes": [ |
| + { |
| + "sha": "f4cf27dd0c48eac32d7b4beaec77686752ca6cff", |
| + "date": "2026-10-13", |
| + "subject": "changelog: v0.26.1 (release step 10)", |
| + "branch": "changelog-v0.26.1", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/changelog-v0.26.1", |
| + "commits": 1, |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "claude" |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "sha": "2e00b17e18f49cd391b2c6890f8abd68bbdeba23", |
| + "date": "2026-10-13", |
| + "subject": "plan: zdd flag readiness round 3 (draft)", |
| + "branch": "zdd-flag-readiness-3", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/zdd-flag-readiness-3", |
| + "commits": 23, |
| + "discussions": [ |
| + { |
| + "id": 272, |
| + "title": "Zero-downtime deploys: owed before CARLOS_SWAP can go on", |
| + "state": "open" |
| + } |
| + ], |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "claude" |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "sha": "645a7d0541cc77bbf52523f88a901f720ceaf64a", |
| + "date": "2026-10-13", |
| + "subject": "claim Plan 14 Part 1 (#280): root-trusted state foundations, families B, C and D", |
| + "branch": "feat/claim-root-trusted-state", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/feat%2Fclaim-root-trusted-state", |
| + "commits": 1, |
| + "discussions": [ |
| + { |
| + "id": 280, |
| + "title": "Root trusts service-writable hostguard and health records under /data (claim root paths)", |
| + "state": "open" |
| + } |
| + ], |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "codex" |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "sha": "34a4328f7d29cfed7b5404cec39cef616e115a26", |
| + "date": "2026-10-13", |
| + "subject": "hotfix-instance-key-gate", |
| + "branch": "hotfix-instance-key-gate", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/hotfix-instance-key-gate", |
| + "commits": 1, |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "claude" |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "sha": "ba52d5efe55a4a8a78acaa0af9d500e803aeb4c0", |
| + "date": "2026-10-13", |
| + "subject": "plan: zero-downtime flag readiness, round 2 (draft for Astra's review)", |
| + "branch": "zdd-flag-readiness-2", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/zdd-flag-readiness-2", |
| + "commits": 48, |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "claude" |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "sha": "67698c9c016e1f34fbb4a337ebc833380d023937", |
| + "date": "2026-10-13", |
| + "subject": "changelog-nested-bullets", |
| + "branch": "changelog-nested-bullets", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/changelog-nested-bullets", |
| + "commits": 1, |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "claude" |
| + ] |
| + } |
| + ] |
| + }, |
| + { |
| + "sha": "729e85080ff0891fc8c622367dc52a5d27823c8a", |
| + "date": "2026-10-13", |
| + "subject": "changelog-v0.26.0", |
| + "branch": "changelog-v0.26.0", |
| + "branch_url": "https://amadan.net/carlos/platform/-/branch/changelog-v0.26.0", |
| + "commits": 1, |
| + "credit": [ |
| + { |
| + "person": "Paul Campbell", |
| + "agents": [ |
| + "claude" |
| + ] |
| + } |
| + ] |
| + } |
| + ] |
| + }, |
| { |
| "tag": "v0.26.1", |
| "date": "2026-10-13", |
diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md b/docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md| index cb583fd00..8ca66882b 100644 |
| --- a/docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md |
| +++ b/docs/superpowers/plans/2026-10-06-moves-phase1-rehearsal.md |
| @@ -242,6 +242,13 @@ deployment before its first run. |
| - [ ] **Each deployment's home region recorded:** `CONSOLE_HOME_REGION` in |
| the console's environment. A box with no `CARLOS_REGION` in its |
| `host.env` publishes no `placement` in its edges file: it is a home box. |
| +- [ ] **Dev's home region has two boxes: wake rehearsal apps on one.** While |
| + dev's claim is off, both home boxes (`carlos-dev-ie` and |
| + `carlos-dev-ie-b`) take every unregioned record. A wake that switches box |
| + can serve a stale local DB (amadan #286; `activator.go`, "only while a |
| + region has one box"). So rehearsal apps in dev's home region are woken on |
| + one box only, or the run waits for dev's claim. #286 ruled this a |
| + dev-only hazard, accepted. |
| - [ ] **Rehearsal boxes keep instances flat.** Moves refuse a nested |
| instance. Set `CARLOS_LAYOUT_DEFAULT=flat` in each rehearsal box's |
| `host.env`. Pre-flight check, before any request: `carlos status |
diff --git a/internal/console/materialize.go b/internal/console/materialize.go| index 295a052e2..dd3020ca7 100644 |
| --- a/internal/console/materialize.go |
| +++ b/internal/console/materialize.go |
| @@ -80,6 +80,15 @@ func MaterializeAccounts(ctx context.Context, s release.Store, st *Store) (int, |
| "not a new account", key, sqid, m.Sqid, m.ID, domains.Sqid(m.ID)) |
| continue |
| } |
| + // A live account holding a reserved sqid (#285). The allocator never |
| + // mints one, so this fires only if the list grows over an existing |
| + // account or a manifest was written by hand. The account is left alone, |
| + // as the ruling says; this line is what makes it visible on every |
| + // console start instead of surfacing as broken DNS. |
| + if !m.Deleted() && domains.IsReservedSqid(m.Sqid) { |
| + log.Printf("console: account %s (id %d) has a reserved sqid; its hostnames under the apps domain "+ |
| + "collide with platform infrastructure (#285)", m.Sqid, m.ID) |
| + } |
| // Everything the row and the manifest can say to each other. Only the |
| // last case — never seen here, and live — falls through to birth. |
| switch { |
diff --git a/internal/console/reserved_sqids_test.go b/internal/console/reserved_sqids_test.go| new file mode 100644 |
| index 000000000..357d8f677 |
| --- /dev/null |
| +++ b/internal/console/reserved_sqids_test.go |
| @@ -0,0 +1,140 @@ |
| +package console |
| + |
| +import ( |
| + "bytes" |
| + "log" |
| + "net/http" |
| + "net/url" |
| + "os" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/domains" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// devAccountID is the id whose sqid is "dev" (#285). |
| +const devAccountID = int64(1168) |
| + |
| +// Every console door that creates an account (landing's personal-account |
| +// mint, POST /accounts/new, POST /auth/cli/accounts) mints through |
| +// domains.MintAccount, so none of them can hand out the reserved "dev". Each |
| +// door is driven against a bucket whose highest account is 1167, so the next |
| +// plain id would be 1168. |
| +func TestConsoleCreationDoorsSkipReservedSqids(t *testing.T) { |
| + doors := map[string]func(t *testing.T, fx *consoleFixture) (owner string){ |
| + "landing": func(t *testing.T, fx *consoleFixture) string { |
| + fx.setUser("newbie@keymail.dev") |
| + defer fx.setUser("paul@keymail.dev") |
| + c := newBrowser(t) |
| + signUp(t, fx, c, "code-new") |
| + if st, loc, _ := get(t, c, fx.ts.URL+"/"); st != http.StatusFound || !strings.HasPrefix(loc, "/a/") { |
| + t.Fatalf("landing = %d -> %q, want 302 into an account", st, loc) |
| + } |
| + return "newbie@keymail.dev" |
| + }, |
| + "POST /accounts/new": func(t *testing.T, fx *consoleFixture) string { |
| + c := signInPaul(t, fx) |
| + csrf := sessionToken(t, fx, c) |
| + if st, loc, _ := postForm(t, c, fx.ts.URL+"/accounts/new", |
| + url.Values{"csrf": {csrf}, "name": {"shop"}}); st != http.StatusSeeOther { |
| + t.Fatalf("create = %d -> %q, want 303", st, loc) |
| + } |
| + return "paul@keymail.dev" |
| + }, |
| + "POST /auth/cli/accounts": func(t *testing.T, fx *consoleFixture) string { |
| + token := cliToken(t, fx, "paul@keymail.dev", "code-acct") |
| + if st, out := postJSON(t, fx.ts.URL+"/auth/cli/accounts", map[string]string{"name": "shop"}, token); st != http.StatusOK { |
| + t.Fatalf("accounts create = %d %v", st, out) |
| + } |
| + return "paul@keymail.dev" |
| + }, |
| + } |
| + for name, door := range doors { |
| + t.Run(name, func(t *testing.T) { |
| + fx := newConsoleFixture(t) |
| + ctx := t.Context() |
| + if err := WriteAccountManifest(ctx, fx.rel, devAccountID-1, domains.Sqid(devAccountID-1), |
| + "neighbour", time.Unix(1700000000, 0)); err != nil { |
| + t.Fatal(err) |
| + } |
| + owner := door(t, fx) |
| + |
| + accts, err := fx.st.AccountsFor(owner) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var made *Account |
| + for i := range accts { |
| + if accts[i].ID > devAccountID-1 { |
| + made = &accts[i] |
| + } |
| + } |
| + if made == nil { |
| + t.Fatalf("no new account for %s in %+v", owner, accts) |
| + } |
| + if made.ID != devAccountID+1 || made.Sqid != "dew" { |
| + t.Fatalf("minted (%d, %q), want (%d, \"dew\"): the door skipped the reserved-sqid rule", |
| + made.ID, made.Sqid, devAccountID+1) |
| + } |
| + if _, _, err := fx.rel.Get(ctx, AccountManifestKey("dev")); err == nil { |
| + t.Fatal("a manifest landed at accounts/dev/") |
| + } |
| + if a, err := fx.st.AccountBySqid("dev"); err != nil || a != nil { |
| + t.Fatalf("AccountBySqid(dev) = %+v %v, want none", a, err) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +// The ruling leaves existing accounts alone, so an account that already holds |
| +// a reserved sqid (the list grew over it, or a manifest was written by hand) |
| +// is materialized as usual and named in the log, once per pass. A tombstoned |
| +// one is not named: it serves nothing. |
| +func TestMaterializeFlagsALiveReservedSqid(t *testing.T) { |
| + var logged bytes.Buffer |
| + log.SetOutput(&logged) |
| + t.Cleanup(func() { log.SetOutput(os.Stderr) }) |
| + |
| + st := openTestStore(t) |
| + rel := release.NewDirStore(t.TempDir()) |
| + ctx := t.Context() |
| + created := time.Unix(1700000000, 0).UTC() |
| + edgeID := int64(49868) // Sqid "edge" |
| + for _, a := range []struct { |
| + id int64 |
| + name string |
| + }{{devAccountID, "held"}, {devAccountID + 1, "plain"}, {edgeID, "retired"}} { |
| + if err := WriteAccountManifest(ctx, rel, a.id, domains.Sqid(a.id), a.name, created); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := domains.TombstoneAccount(ctx, rel, "edge", created); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + if _, err := MaterializeAccounts(ctx, rel, st); err != nil { |
| + t.Fatal(err) |
| + } |
| + if a, err := st.AccountBySqid("dev"); err != nil || a == nil || a.ID != devAccountID { |
| + t.Fatalf("AccountBySqid(dev) = %+v %v: an existing account must be left as it is", a, err) |
| + } |
| + out := logged.String() |
| + if !strings.Contains(out, "account dev (id 1168) has a reserved sqid") { |
| + t.Errorf("no reserved-sqid line for the live dev account:\n%s", out) |
| + } |
| + flagged := 0 |
| + for _, line := range strings.Split(out, "\n") { |
| + if !strings.Contains(line, "reserved sqid") { |
| + continue |
| + } |
| + flagged++ |
| + if strings.Contains(line, "—") { |
| + t.Errorf("the reserved-sqid log line carries an em dash: %s", line) |
| + } |
| + } |
| + if flagged != 1 { |
| + t.Errorf("%d reserved-sqid lines, want exactly one (dev; not the plain dew, not the retired edge):\n%s", flagged, out) |
| + } |
| +} |
diff --git a/internal/domains/mint.go b/internal/domains/mint.go| index be905f63e..1fa33bfe1 100644 |
| --- a/internal/domains/mint.go |
| +++ b/internal/domains/mint.go |
| @@ -130,6 +130,13 @@ const mintAttempts = 5 |
| // materialization, which skips it): handing that id to somebody else would put |
| // two objects on one identity, which is the single thing this scheme exists to |
| // prevent. |
| +// |
| +// An id whose sqid is reserved (IsReservedSqid, #285) is stepped over: the |
| +// answer is the first id past the highest claimed one whose sqid is free. No |
| +// manifest or tombstone is written for the skipped id, and none is needed. |
| +// The skip is a pure function of the id, so every minter computes the same |
| +// answer, a restart changes nothing, and once a later id is minted the |
| +// skipped one sits below the highest claim, where nothing allocates. |
| func NextAccountID(ctx context.Context, s release.Store) (int64, error) { |
| keys, err := s.List(ctx, "accounts/") |
| if err != nil { |
| @@ -155,7 +162,7 @@ func NextAccountID(ctx context.Context, s release.Store) (int64, error) { |
| max = m.ID |
| } |
| } |
| - return max + 1, nil |
| + return skipReservedIDs(max + 1), nil |
| } |
| // MintAccount allocates the next free account id and publishes its manifest, |
diff --git a/internal/domains/reserved.go b/internal/domains/reserved.go| new file mode 100644 |
| index 000000000..c07c41f44 |
| --- /dev/null |
| +++ b/internal/domains/reserved.go |
| @@ -0,0 +1,76 @@ |
| +package domains |
| + |
| +// reservedSqids are the labels no account may ever be given as its sqid |
| +// (#285, Paul's ruling 2026-10-14). |
| +// |
| +// Why: an account's apps live at <app>.<sqid>.<apps-domain>, and the |
| +// deployment's own infrastructure also hangs labels directly under the apps |
| +// domain. When the two meet, DNS answers for the infrastructure, not the |
| +// account. On 2026-10-13 flagship gained `*.dev.oncarlos.com` pointing at the |
| +// dev deployment's box, and Sqid(1168) is "dev": every app of account 1168 |
| +// would have resolved to dev's edge, and dev's edge would have tried to |
| +// issue certificates for them. |
| +// |
| +// The alphabet and the offset stay frozen. Instead, NextAccountID steps over |
| +// any id whose sqid is listed here, so the id is simply never handed out. |
| +// Existing accounts are untouched: console.MaterializeAccounts logs any live |
| +// account that holds a listed sqid, so adding a label that some account |
| +// already has shows up on the next console start rather than silently. |
| +// |
| +// Only list labels that Sqid can actually produce. TestReservedSqidsAreProducible |
| +// fails on any other entry, so dead entries cannot creep in. A label cannot |
| +// be produced when it: |
| +// - contains i, l or o (not in the frozen alphabet): relay, platform, |
| +// console, mail, tunnel, static; |
| +// - has fewer than 3 letters (sqidOffset's floor): us, uk, au, eu; |
| +// - starts with "a" (base 23 has no leading zero digit): acme, app, api, |
| +// admin, assets, auth; |
| +// - contains anything but lowercase letters: eu-west-1, _acme-challenge. |
| +// |
| +// Extending: add the label with a comment naming where it comes from. The |
| +// skip only matters for ids that have not been minted yet, so adding a label |
| +// is always safe. Removing one is safe too, but once the allocator has |
| +// passed its id that id stays unused for good (allocation is "one past the |
| +// highest id in the bucket", so nothing goes back for it). |
| +var reservedSqids = []string{ |
| + // dev.oncarlos.com: the dev deployment's apps domain. Flagship's Route53 |
| + // zone has `*.dev.oncarlos.com A <dev edge>` (carloku-infrastructure |
| + // dns53.tf, oncarlos_dev_wildcard, #9). Sqid(1168). |
| + "dev", |
| + // edge.<apps-domain>: the steering hostname the converger publishes and |
| + // health-checks on every armed edge (steering.ConvergeDeployment, |
| + // edge steeringprobe, console reservedDomains). Sqid(49868). |
| + "edge", |
| + // www.<apps-domain>: not served today, but it is the label anyone would |
| + // add next for the apps domain's own apex site, and a tenant holding it |
| + // would look like the platform's own website. Held back pre-emptively; |
| + // Sqid(9978). |
| + "www", |
| +} |
| + |
| +// ReservedSqids returns a copy of the labels no account may be given as its |
| +// sqid. A copy, so no caller can change what the allocator skips. |
| +func ReservedSqids() []string { |
| + return append([]string(nil), reservedSqids...) |
| +} |
| + |
| +// IsReservedSqid reports whether s is a label no account may be given. |
| +func IsReservedSqid(s string) bool { |
| + for _, r := range reservedSqids { |
| + if s == r { |
| + return true |
| + } |
| + } |
| + return false |
| +} |
| + |
| +// skipReservedIDs returns id, or the first id after it whose sqid is not |
| +// reserved. A pure function of the id and the list, which is what makes the |
| +// skip safe without any stored state: every minter, on every box and after |
| +// every restart, steps over exactly the same ids. |
| +func skipReservedIDs(id int64) int64 { |
| + for IsReservedSqid(Sqid(id)) { |
| + id++ |
| + } |
| + return id |
| +} |
diff --git a/internal/domains/reserved_test.go b/internal/domains/reserved_test.go| new file mode 100644 |
| index 000000000..34f9e97db |
| --- /dev/null |
| +++ b/internal/domains/reserved_test.go |
| @@ -0,0 +1,340 @@ |
| +package domains_test |
| + |
| +import ( |
| + "encoding/json" |
| + "math" |
| + "os" |
| + "path/filepath" |
| + "runtime" |
| + "slices" |
| + "strings" |
| + "sync" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/domains" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// devID is the account id whose sqid is "dev". Pinned here as well as |
| +// derived, because #285 was filed against the wrong id (1169, which is |
| +// "dew"): a test that only ever derived it would not have caught that. |
| +const devID = 1168 |
| + |
| +// sqidID inverts Sqid by plain base-23 decoding, the way the encoder builds |
| +// its digits. ok is false for a string Sqid can never emit for a positive id. |
| +// It lives in the test on purpose: nothing in production decodes a sqid, and |
| +// the producibility check should not trust the encoder to vouch for itself. |
| +func sqidID(s string) (int64, bool) { |
| + const alphabet = "abcdefghjkmnpqrstuvwxyz" |
| + if len(s) < 3 || len(s) > 16 { |
| + return 0, false |
| + } |
| + var n int64 |
| + for i := 0; i < len(s); i++ { |
| + d := strings.IndexByte(alphabet, s[i]) |
| + if d < 0 { |
| + return 0, false |
| + } |
| + if n > (math.MaxInt64-int64(d))/23 { |
| + return 0, false // past every int64 id: Sqid never gets here |
| + } |
| + n = n*23 + int64(d) |
| + } |
| + id := n - 23*23 |
| + if id < 1 || domains.Sqid(id) != s { |
| + return 0, false // a leading "a", or "baa" (id 0) |
| + } |
| + return id, true |
| +} |
| + |
| +func TestDevIsReservedAndIsSqid1168(t *testing.T) { |
| + if got := domains.Sqid(devID); got != "dev" { |
| + t.Fatalf("Sqid(%d) = %q, want \"dev\"", devID, got) |
| + } |
| + if !domains.IsReservedSqid("dev") { |
| + t.Fatal(`"dev" is not reserved: account 1168's apps would resolve to the dev deployment's edge`) |
| + } |
| + if domains.Sqid(1169) != "dew" || domains.IsReservedSqid("dew") { |
| + t.Fatalf("Sqid(1169) = %q (reserved %v): 1169 is the id the allocator should land on", |
| + domains.Sqid(1169), domains.IsReservedSqid(domains.Sqid(1169))) |
| + } |
| +} |
| + |
| +// Every listed label must be one Sqid can actually emit. A label with i, l |
| +// or o in it, under 3 letters, or starting with "a" can never be an account's |
| +// sqid, and listing it would only suggest a protection that is not needed. |
| +func TestReservedSqidsAreProducible(t *testing.T) { |
| + list := domains.ReservedSqids() |
| + if len(list) == 0 { |
| + t.Fatal("no reserved sqids") |
| + } |
| + seen := map[string]bool{} |
| + for _, label := range list { |
| + if seen[label] { |
| + t.Errorf("%q is listed twice", label) |
| + } |
| + seen[label] = true |
| + id, ok := sqidID(label) |
| + if !ok { |
| + t.Errorf("%q is listed as reserved but Sqid can never produce it; drop it", label) |
| + continue |
| + } |
| + if !domains.ValidSqid(label) { |
| + t.Errorf("%q decodes to id %d but ValidSqid rejects it", label, id) |
| + } |
| + t.Logf("reserved %q = Sqid(%d)", label, id) |
| + } |
| + // The decoder itself, against the frozen golden values and the cases it |
| + // must refuse. |
| + for s, want := range map[string]int64{"bab": 1, "cxn": 1000, "dev": devID} { |
| + if got, ok := sqidID(s); !ok || got != want { |
| + t.Errorf("sqidID(%q) = %d %v, want %d", s, got, ok, want) |
| + } |
| + } |
| + for _, s := range []string{"app", "acme", "baa", "relay", "us", "ops"} { |
| + if _, ok := sqidID(s); ok { |
| + t.Errorf("sqidID(%q) decoded, but Sqid never emits it", s) |
| + } |
| + } |
| +} |
| + |
| +// ReservedSqids hands out a copy: a caller editing its slice must not change |
| +// what the allocator skips. |
| +func TestReservedSqidsIsACopy(t *testing.T) { |
| + list := domains.ReservedSqids() |
| + list[0] = "zzz" |
| + if domains.IsReservedSqid("zzz") || !slices.Contains(domains.ReservedSqids(), "dev") { |
| + t.Fatal("editing ReservedSqids' result changed the reserved set") |
| + } |
| +} |
| + |
| +// The allocator steps over every reserved id, seeded one below it with a |
| +// single manifest rather than a thousand real rows. |
| +func TestNextAccountIDSkipsReservedSqids(t *testing.T) { |
| + ctx := t.Context() |
| + for _, label := range domains.ReservedSqids() { |
| + id, ok := sqidID(label) |
| + if !ok { |
| + t.Fatalf("%q is not producible", label) |
| + } |
| + s := release.NewDirStore(t.TempDir()) |
| + writeManifest(t, s, id-1, domains.Sqid(id-1)) |
| + got, err := domains.NextAccountID(ctx, s) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if got <= id { |
| + t.Errorf("after id %d, NextAccountID = %d (%q); want past %d, whose sqid is the reserved %q", |
| + id-1, got, domains.Sqid(got), id, label) |
| + } |
| + if domains.IsReservedSqid(domains.Sqid(got)) { |
| + t.Errorf("NextAccountID = %d, whose sqid %q is reserved", got, domains.Sqid(got)) |
| + } |
| + } |
| + |
| + // And the plain case is untouched: an id whose sqid is free is the |
| + // answer, exactly as before. |
| + s := release.NewDirStore(t.TempDir()) |
| + writeManifest(t, s, devID-2, domains.Sqid(devID-2)) |
| + if got, err := domains.NextAccountID(ctx, s); err != nil || got != devID-1 { |
| + t.Errorf("NextAccountID after %d = %d (%v), want %d", devID-2, got, err, devID-1) |
| + } |
| +} |
| + |
| +// MintAccount, the one allocator every creation path uses, lands on 1169 |
| +// across the skip, and writes nothing at accounts/dev/. |
| +func TestMintAccountNeverMintsDev(t *testing.T) { |
| + ctx := t.Context() |
| + s := release.NewDirStore(t.TempDir()) |
| + writeManifest(t, s, devID-1, domains.Sqid(devID-1)) |
| + id, sqid, err := domains.MintAccount(ctx, s, "shop", "owner@example.com", time.Unix(1700000000, 0)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if id != devID+1 || sqid != "dew" { |
| + t.Fatalf("minted (%d, %q), want (%d, \"dew\")", id, sqid, devID+1) |
| + } |
| + if _, _, err := s.Get(ctx, domains.AccountManifestKey("dev")); err == nil { |
| + t.Fatal("a manifest landed at accounts/dev/") |
| + } |
| +} |
| + |
| +// The race at the skip: another minter takes 1169 between our listing and our |
| +// write. We must re-list and take 1170, never fall back to 1168. |
| +func TestMintAccountLosesTheRaceAtTheSkip(t *testing.T) { |
| + ctx := t.Context() |
| + inner := release.NewDirStore(t.TempDir()) |
| + writeManifest(t, inner, devID-1, domains.Sqid(devID-1)) |
| + planted := false |
| + s := &racingStore{Store: inner, beforePut: func(key string) { |
| + if planted { |
| + return |
| + } |
| + planted = true |
| + if key != domains.AccountManifestKey("dew") { |
| + t.Errorf("first create-only write went to %s, want accounts/dew/", key) |
| + } |
| + writeManifest(t, inner, devID+1, "dew") |
| + }} |
| + id, sqid, err := domains.MintAccount(ctx, s, "shop", "", time.Now()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if id != devID+2 || sqid != domains.Sqid(devID+2) { |
| + t.Fatalf("minted (%d, %q), want (%d, %q)", id, sqid, devID+2, domains.Sqid(devID+2)) |
| + } |
| +} |
| + |
| +// Concurrent minters around the skip: every one gets a distinct id, none gets |
| +// 1168, and together they fill 1169.. with no gap but the reserved one. |
| +func TestConcurrentMintsAroundTheSkip(t *testing.T) { |
| + ctx := t.Context() |
| + s := release.NewDirStore(t.TempDir()) |
| + writeManifest(t, s, devID-1, domains.Sqid(devID-1)) |
| + |
| + // Five at once: a minter loses at most once to each of the others, and |
| + // MintAccount allows five attempts. |
| + const n = 5 |
| + ids := make([]int64, n) |
| + errs := make([]error, n) |
| + var wg sync.WaitGroup |
| + start := make(chan struct{}) |
| + for i := range n { |
| + wg.Add(1) |
| + go func() { |
| + defer wg.Done() |
| + <-start |
| + ids[i], _, errs[i] = domains.MintAccount(ctx, s, "shop", "", time.Now()) |
| + }() |
| + } |
| + close(start) |
| + wg.Wait() |
| + for i, err := range errs { |
| + if err != nil { |
| + t.Fatalf("minter %d: %v", i, err) |
| + } |
| + } |
| + slices.Sort(ids) |
| + for i, id := range ids { |
| + if want := int64(devID + 1 + i); id != want { |
| + t.Fatalf("minted ids %v, want %d..%d", ids, devID+1, devID+n) |
| + } |
| + } |
| + if _, _, err := s.Get(ctx, domains.AccountManifestKey("dev")); err == nil { |
| + t.Fatal("a concurrent mint landed at accounts/dev/") |
| + } |
| +} |
| + |
| +// The skip holds across restarts: a fresh store over the same bucket (a new |
| +// process, another box) computes the same answer, before and after the id |
| +// past the skip is minted. Nothing is stored for the skipped id, so there is |
| +// nothing for a restart to lose. |
| +func TestReservedSkipSurvivesRestart(t *testing.T) { |
| + ctx := t.Context() |
| + dir := t.TempDir() |
| + writeManifest(t, release.NewDirStore(dir), devID-1, domains.Sqid(devID-1)) |
| + |
| + for range 2 { |
| + got, err := domains.NextAccountID(ctx, release.NewDirStore(dir)) |
| + if err != nil || got != devID+1 { |
| + t.Fatalf("NextAccountID on a fresh store = %d (%v), want %d", got, err, devID+1) |
| + } |
| + } |
| + if _, _, err := domains.MintAccount(ctx, release.NewDirStore(dir), "shop", "", time.Now()); err != nil { |
| + t.Fatal(err) |
| + } |
| + got, err := domains.NextAccountID(ctx, release.NewDirStore(dir)) |
| + if err != nil || got != devID+2 { |
| + t.Fatalf("NextAccountID after minting %d = %d (%v), want %d", devID+1, got, err, devID+2) |
| + } |
| + keys, err := release.NewDirStore(dir).List(ctx, "accounts/") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, k := range keys { |
| + body, _, err := release.NewDirStore(dir).Get(ctx, k) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var m domains.AccountManifest |
| + if err := json.Unmarshal(body, &m); err != nil { |
| + t.Fatal(err) |
| + } |
| + if domains.IsReservedSqid(m.Sqid) { |
| + t.Fatalf("%s holds the reserved sqid %q", k, m.Sqid) |
| + } |
| + } |
| +} |
| + |
| +// Every creation path goes through the one rule. The rule lives in |
| +// NextAccountID, MintAccount is its only caller, and the account rows the |
| +// console and the registry write must take their ids from MintAccount. This |
| +// census pins the callers, so a new creation path has to be added here on |
| +// purpose, with someone checking that it mints through MintAccount. |
| +// |
| +// The materialization writers (console EnsureAccount, registry PutAccount in |
| +// domains.Reconcile and the CLI) copy an id the bucket already holds; they |
| +// never choose one, so they are not creation paths. |
| +func TestAccountCreationPathsCensus(t *testing.T) { |
| + _, here, _, ok := runtime.Caller(0) |
| + if !ok { |
| + t.Fatal("no caller") |
| + } |
| + root := filepath.Join(filepath.Dir(here), "..", "..") |
| + want := map[string][]string{ |
| + "domains.NextAccountID(": nil, // only MintAccount, inside this package |
| + "NextAccountID(ctx": {"internal/domains/mint.go"}, |
| + "domains.MintAccount(": { |
| + "accounts.go", // carlos accounts create --owner |
| + "internal/console/server.go", // Server.mintAccount: landing, /accounts/new, /auth/cli/accounts |
| + }, |
| + "s.mintAccount(": { |
| + "internal/console/authcli.go", // POST /auth/cli/accounts |
| + "internal/console/server.go", // landing mint and POST /accounts/new |
| + }, |
| + ".CreateAccount(": {"internal/console/server.go"}, // inside Server.mintAccount only |
| + } |
| + got := map[string][]string{} |
| + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { |
| + if err != nil { |
| + return err |
| + } |
| + if d.IsDir() { |
| + switch d.Name() { |
| + case ".git", "node_modules", "vendor", "testdata": |
| + return filepath.SkipDir |
| + } |
| + if strings.HasPrefix(d.Name(), ".") && path != root { |
| + return filepath.SkipDir |
| + } |
| + return nil |
| + } |
| + if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { |
| + return nil |
| + } |
| + b, err := os.ReadFile(path) |
| + if err != nil { |
| + return err |
| + } |
| + rel, _ := filepath.Rel(root, path) |
| + rel = filepath.ToSlash(rel) |
| + for needle := range want { |
| + if strings.Contains(string(b), needle) && !slices.Contains(got[needle], rel) { |
| + got[needle] = append(got[needle], rel) |
| + } |
| + } |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for needle, files := range want { |
| + slices.Sort(files) |
| + slices.Sort(got[needle]) |
| + if !slices.Equal(files, got[needle]) { |
| + t.Errorf("%q is called from %v, want %v. A new account creation path must mint through "+ |
| + "domains.MintAccount so it skips reserved sqids (#285); then add it here", needle, got[needle], files) |
| + } |
| + } |
| +} |
diff --git a/internal/provision/plan14_root_reads_test.go b/internal/provision/plan14_root_reads_test.go| index fe8dbe6b7..d1c2bb971 100644 |
| --- a/internal/provision/plan14_root_reads_test.go |
| +++ b/internal/provision/plan14_root_reads_test.go |
| @@ -668,6 +668,12 @@ func TestPlan14RootReadCensus(t *testing.T) { |
| // activator rows and four new ones (generation socket directories) add |
| // fourteen frozen declarations. |
| seal = "e736a5e2652cc546db02c5a385ae7da9fa8a4313d3064f6b431642431974d65d" |
| + // Census overlay reserved285 (#285): envelope-only, for the reserved-sqid |
| + // skip in the account allocator and its materialization log line. No |
| + // root-read unit changes, so the legacy seal above stands. |
| + if err = loadRootReadInventory(filepath.Join(root, "internal/provision/testdata/plan14-root-reads-reserved285.tsv"), &inv, true); err != nil { |
| + t.Fatal(err) |
| + } |
| errs := append(compareRootReads(got, inv), checkRootReadLegacy(inv, seal)...) |
| errs = append(errs, checkRootReadDeclarations(got, inv)...) |
| if len(errs) > 0 { |
diff --git a/internal/provision/testdata/plan14-root-reads-reserved285.tsv b/internal/provision/testdata/plan14-root-reads-reserved285.tsv| new file mode 100644 |
| index 000000000..031735ddb |
| --- /dev/null |
| +++ b/internal/provision/testdata/plan14-root-reads-reserved285.tsv |
| @@ -0,0 +1,3 @@ |
| +# Plan 14 census overlay reserved285 (amadan #285, reserved account sqids): the account allocator (internal/domains NextAccountID) steps over ids whose sqid is a reserved label, and the console's account materialization logs a live account that holds one. Envelope-only: no root-read unit changed or was added, nothing runs as root that did not before, and no registry, /data, release or claim read was added. Earlier overlays are immutable. |
| +@coverage internal/domains 5d2afacc1669ea25966a274801f709a1f896c4c24c946766bb4bf1cf29915ea1 |
| +@coverage internal/console 8a423453c4f4e86c0e6ba1548333bb36159487adaed9c8e97a1a63f63f031444 |