carlos / platform Public

Static sites honour a _redirects file

A static site can now ship a `_redirects` file at the top of its tree, in
the format Netlify introduced and Cloudflare Pages adopted: one
`source destination [status]` rule per line. Sources are paths or
http(s)://host URLs (a host rule matches only that host, for www -> apex);
`:name` captures a segment and a final `*` the rest, as `:splat`. 301
(default), 302, 303, 307 and 308 redirect; 200 serves another file of the
tree at the request's URL (SPA fallback) and 404 serves one as the
not-found page. A file at the path wins unless the status ends in `!`.

`carlos ship`/`deploy` refuse a file with a line the edge cannot use
(PackDir), naming every such line; Netlify conditions and proxying to
other hosts are not supported. The edge parses the file once per version
swap and keeps the rules with that version's tree, never serves the file,
skips a redirect to the URL being requested, and cleans every Location so
no capture can make it protocol-relative. A site without the file is
served exactly as before. Documented under carlos deploy in
docs/site/cli.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by james@keymail.dev pushed by james@keymail.dev 3feb2d6085d8394be61f2c3bc060d4d696b95cf1 parent 5f341f3
4 files changed, +911 −21
  • docs/site/cli.md +38 −0
  • internal/edge/redirects.go +418 −0
  • internal/edge/redirects_test.go +319 −0
  • internal/edge/static.go +136 −21

CI failed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (1 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.019s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (38 s)
=== gofmt (2 s)
gofmt -l .: clean
=== rest (203 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	5.814s
ok  	github.com/carlosframework/platform/cmd/moveprobe	12.855s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.798s
ok  	github.com/carlosframework/platform/internal/activator	63.266s
ok  	github.com/carlosframework/platform/internal/agentsock	0.031s
ok  	github.com/carlosframework/platform/internal/alerting	0.006s
ok  	github.com/carlosframework/platform/internal/analytics	0.051s
ok  	github.com/carlosframework/platform/internal/appconfig	3.899s
ok  	github.com/carlosframework/platform/internal/apperr	0.266s
ok  	github.com/carlosframework/platform/internal/applog	0.562s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.021s
ok  	github.com/carlosframework/platform/internal/boxid	1.766s
?   	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.087s
ok  	github.com/carlosframework/platform/internal/caps	0.044s
ok  	github.com/carlosframework/platform/internal/certwarm	0.004s
ok  	github.com/carlosframework/platform/internal/cgroup	0.823s
ok  	github.com/carlosframework/platform/internal/changelog	0.668s
ok  	github.com/carlosframework/platform/internal/claimactivation	3.069s
ok  	github.com/carlosframework/platform/internal/clamd	0.064s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.920s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.023s
ok  	github.com/carlosframework/platform/internal/dbverify	0.013s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.016s
ok  	github.com/carlosframework/platform/internal/docsite	0.022s
ok  	github.com/carlosframework/platform/internal/domains	5.428s
ok  	github.com/carlosframework/platform/internal/doorkey	0.011s
ok  	github.com/carlosframework/platform/internal/economics	0.010s
ok  	github.com/carlosframework/platform/internal/edge	51.023s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.009s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.166s
ok  	github.com/carlosframework/platform/internal/gate	1.673s
ok  	github.com/carlosframework/platform/internal/genselect	0.046s
ok  	github.com/carlosframework/platform/internal/geo	0.016s
ok  	github.com/carlosframework/platform/internal/geoip	0.051s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.241s
ok  	github.com/carlosframework/platform/internal/host	149.165s
ok  	github.com/carlosframework/platform/internal/hostactors	9.164s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.019s
ok  	github.com/carlosframework/platform/internal/hostbox	5.402s
ok  	github.com/carlosframework/platform/internal/hostclaim	2.084s
ok  	github.com/carlosframework/platform/internal/hostenv	0.019s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.009s
ok  	github.com/carlosframework/platform/internal/hostexec	9.661s
ok  	github.com/carlosframework/platform/internal/hostfence	0.187s
ok  	github.com/carlosframework/platform/internal/hostfiles	12.494s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.147s
ok  	github.com/carlosframework/platform/internal/hostguard	1.301s
ok  	github.com/carlosframework/platform/internal/hostops	4.102s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.005s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.004s
ok  	github.com/carlosframework/platform/internal/hostrelocate	1.414s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.600s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.002s
ok  	github.com/carlosframework/platform/internal/infraview	0.004s
ok  	github.com/carlosframework/platform/internal/instancectl	5.356s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.981s
ok  	github.com/carlosframework/platform/internal/instancefs	0.008s
ok  	github.com/carlosframework/platform/internal/instancekey	0.026s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.034s
ok  	github.com/carlosframework/platform/internal/layout	3.063s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.008s
ok  	github.com/carlosframework/platform/internal/lease	10.444s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	1.306s
ok  	github.com/carlosframework/platform/internal/ledger	0.178s
ok  	github.com/carlosframework/platform/internal/mail	0.607s
ok  	github.com/carlosframework/platform/internal/maintenance	0.133s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	6.168s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.271s
ok  	github.com/carlosframework/platform/internal/metrics	0.017s
ok  	github.com/carlosframework/platform/internal/moves	35.804s
ok  	github.com/carlosframework/platform/internal/moves/archive	2.231s
ok  	github.com/carlosframework/platform/internal/objstore	0.538s
ok  	github.com/carlosframework/platform/internal/ops	0.416s
ok  	github.com/carlosframework/platform/internal/park	0.908s
ok  	github.com/carlosframework/platform/internal/plans	0.042s
ok  	github.com/carlosframework/platform/internal/privatefile	0.011s
2026/10/22 15:34:55 provision: listing apps/bab/blog/instances/: list refused — its records are unseen this pass
2026/10/22 15:34:55 provision: listing apps/bab/: listdirs refused — its records are unseen this pass
2026/10/22 15:34:55 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:55 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:55 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:55 provision: listing apps/bac/tito/instances/: list refused — its records are unseen this pass
2026/10/22 15:34:55 provision: listing apps/bac/tito/instances/: list refused — its records are unseen this pass
2026/10/22 15:34:56 provision: skipping record apps/bac/tito/instances/one.example.com.json: unreadable: read refused
2026/10/22 15:34:56 provision: listing apps/bac/tito/instances/: list refused — its records are unseen this pass
2026/10/22 15:34:56 provision: an app's records could not be listed: they are skipped
2026/10/22 15:34:56 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:34:56 provision: remove retired instance route shop.example: provision: claim admission unavailable
2026/10/22 15:34:56 provision: remove retired instance route shop.example: provision: claim admission unavailable
2026/10/22 15:34:56 provision: remove retired sidecar route worker.sidecar.internal: provision: claim admission unavailable
2026/10/22 15:34:56 provision: remove retired sidecar route worker.sidecar.internal: provision: claim admission unavailable
2026/10/22 15:34:56 provision: claim install acme.go.tito.io: provision: claim admission unavailable
2026/10/22 15:34:56 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:56 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/22 15:34:56 provision: claim install bot.eleven.bac.sidecar.internal: provision: claim admission unavailable
2026/10/22 15:34:56 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/22 15:34:56 provision: claim install bot.eleven.bac.sidecar.internal: registry: claim row ownership changed or unknown
2026/10/22 15:34:56 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/22 15:34:56 provision: acme.go.tito.io: not minting this pass: the box's layout intent could not be read: no sealed config
2026/10/22 15:34:56 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:56 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:56 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/22 15:34:56 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/22 15:34:56 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/22 15:34:56 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/22 15:34:56 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:56 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/22 15:34:56 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/22 15:34:56 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/22 15:34:56 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:56 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/22 15:34:56 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:56 provision: claim install acme.go.tito.io: registry: claim row ownership changed or unknown
2026/10/22 15:34:56 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/22 15:34:56 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["run"])
2026/10/22 15:34:56 provision: claim install bot.eleven.bac.sidecar.internal: registry: claim row ownership changed or unknown
2026/10/22 15:34:56 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/22 15:34:56 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:56 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/22 15:34:56 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/22 15:34:57 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:34:57 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:34:57 provision: skipping record apps/bac/tito/instances/new.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:34:57 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/22 15:34:57 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:34:57 provision: skipping record apps/bac/tito/instances/new.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:34:57 provision: minted instance route rc.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route re.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rg.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rj.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rd.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rf.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rh.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route ri.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rk.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rl.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route ra.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rb.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rb.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rc.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route rd.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route ra.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: skipping record apps/bac/tito/instances/rb.go.tito.io.json: read failed: read refused
2026/10/22 15:34:57 provision: skipping record apps/bac/tito/instances/rb.go.tito.io.json: read failed: read refused
2026/10/22 15:34:57 provision: minted instance route re.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.appb.example (app "appb" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.appc.example (app "appc" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.appf.example (app "appf" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.appg.example (app "appg" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.appi.example (app "appi" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.appj.example (app "appj" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.appd.example (app "appd" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.appe.example (app "appe" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.apph.example (app "apph" acct "bac")
2026/10/22 15:34:57 provision: minted instance route x.appa.example (app "appa" acct "bac")
2026/10/22 15:34:58 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/22 15:34:58 provision: minted instance route r0030.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0079.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0053.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0025.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0036.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0040.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0051.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0059.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0081.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0100.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0055.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0009.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0122.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0128.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0005.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0008.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0029.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0033.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0038.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0047.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0084.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0103.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0044.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0068.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0070.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0110.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0048.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0096.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0026.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0035.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0058.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0061.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0080.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0078.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0109.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0001.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0039.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0065.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0106.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0002.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0075.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0102.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0004.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0011.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0014.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0069.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0093.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0024.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0077.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0010.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0027.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0031.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0043.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0067.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0088.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0091.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0105.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0006.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0056.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0098.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0113.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0117.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0120.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0125.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0003.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0071.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0095.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0097.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0115.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0129.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0028.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0034.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0112.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0083.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0015.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0017.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0022.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0082.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0013.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0023.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0064.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0066.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0118.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0123.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0020.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0016.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0089.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0119.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0000.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0007.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0074.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0032.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0037.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0057.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0092.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0104.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0042.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0046.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0063.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0090.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0126.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0073.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0041.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0045.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0050.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0087.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0054.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0072.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0085.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0086.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0018.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0094.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0107.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0111.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0076.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0121.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0012.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0062.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0116.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0124.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0049.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0052.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0060.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0099.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0101.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0108.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0114.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0127.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0019.go.tito.io (app "tito" acct "bac")
2026/10/22 15:34:58 provision: minted instance route r0021.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 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/22 15:35:00 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted instance route console.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted service route ha.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted service route svc.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted instance route whole.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted instance route shared.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: converged acme.go.tito.io channel stable -> edge
2026/10/22 15:35:00 provision: converged acme.go.tito.io channel edge -> canary/rehearsal
2026/10/22 15:35:00 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:00 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: empty channel
2026/10/22 15:35:00 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: converged acme.go.tito.io environment "" -> "staging"
2026/10/22 15:35:01 provision: converged acme.go.tito.io environment "staging" -> ""
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: converged acme.go.tito.io steering "" -> "latency"
2026/10/22 15:35:01 provision: converged acme.go.tito.io steering "latency" -> ""
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: converged acme.go.tito.io hibernate false -> true
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: converged acme.go.tito.io hibernate true -> false (replication "daily" cleared: only a hibernating route's rung is read)
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: converged acme.go.tito.io status-named false -> true
2026/10/22 15:35:01 provision: converged acme.go.tito.io status-named true -> false
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted service route ha.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 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/22 15:35:01 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/22 15:35:01 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/22 15:35:01 provision: minted service route whole.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 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/22 15:35:01 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/22 15:35:01 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/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted service route whole.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: skipping accounts/bac/account.json: malformed account manifest: invalid character 'o' in literal null (expecting 'u')
2026/10/22 15:35:01 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: manifest bac/tito: read failed: provision: manifest bac/tito: invalid character 'o' in literal null (expecting 'u')
2026/10/22 15:35:01 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/22 15:35:01 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/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 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/22 15:35:01 provision: not removing acme$evil.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/22 15:35:01 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/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 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/22 15:35:01 provision: not removing bad-service.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/22 15:35:01 provision: skipping record apps/bac/tito/instances/no-channel.go.tito.io.json: empty channel
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: skipping record apps/bac/tito/instances/no-channel.go.tito.io.json: empty channel
2026/10/22 15:35:01 provision: not removing no-channel.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: skipping record apps/bac/tito/instances/acme.go.tito.io.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:35:01 provision: not removing acme.go.tito.io: its record exists but could not be read cleanly this pass
2026/10/22 15:35:01 provision: removed retired instance route ghost.go.tito.io (record gone)
2026/10/22 15:35:01 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/22 15:35:01 provision: minted instance route dup.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/22 15:35:01 provision: removed retired instance route acme.go.tito.io (app in the trash)
2026/10/22 15:35:01 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/22 15:35:01 provision: removed retired static route site.go.tito.io (app in the trash)
2026/10/22 15:35:01 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/22 15:35:01 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/22 15:35:01 provision: removed retired instance route acme.go.tito.io (app in the trash)
2026/10/22 15:35:01 provision: bac/tito is in the trash — not minting instance routes for it
2026/10/22 15:35:01 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/22 15:35:01 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/22 15:35:01 provision: converged acme.go.tito.io channel stable -> edge
2026/10/22 15:35:01 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/22 15:35:01 provision: converged acme.go.tito.io channel canary/rehearsal -> edge
2026/10/22 15:35:01 provision: converged acme.go.tito.io environment "staging" -> ""
2026/10/22 15:35:01 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/22 15:35:01 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/22 15:35:01 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/22 15:35:01 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/22 15:35:01 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/22 15:35:01 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/22 15:35:01 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/22 15:35:01 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/22 15:35:01 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/22 15:35:01 provision: dry-run: would mint instance route acme.go.tito.io (app "tito" acct "bac" channel "stable")
2026/10/22 15:35:01 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/22 15:35:01 provision: dry-run: would converge adopt.go.tito.io channel stable -> edge
2026/10/22 15:35:01 provision: dry-run: would converge adopt.go.tito.io environment "" -> "staging"
2026/10/22 15:35:01 provision: dry-run: would converge owned.go.tito.io channel stable -> edge
2026/10/22 15:35:01 provision: dry-run: would converge owned.go.tito.io environment "" -> "staging"
2026/10/22 15:35:01 provision: dry-run: would converge owned.go.tito.io hibernate false -> true
2026/10/22 15:35:01 provision: dry-run: would converge owned.go.tito.io seed
2026/10/22 15:35:01 provision: dry-run: would remove retired instance route gone.go.tito.io (record gone)
2026/10/22 15:35:01 provision: converged owned.go.tito.io channel stable -> edge
2026/10/22 15:35:01 provision: converged owned.go.tito.io environment "" -> "staging"
2026/10/22 15:35:01 provision: converged owned.go.tito.io hibernate false -> true
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: adopted existing route adopt.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/22 15:35:01 provision: converged adopt.go.tito.io channel stable -> edge
2026/10/22 15:35:01 provision: converged adopt.go.tito.io environment "" -> "staging"
2026/10/22 15:35:01 provision: removed retired instance route gone.go.tito.io (record gone)
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: adopted existing route story.carlosframework.com (app "story" acct "bad") — this record now owns its channel and environment
2026/10/22 15:35:01 provision: story.carlosframework.com exists but is not the route this record describes (kind "instance"/"static") — leaving it alone
2026/10/22 15:35:01 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted service route pub.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted service route pubs.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted instance route exec.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted service route loop.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted service route named.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted service route priv.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted instance route unit.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted static route pisite.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: minted static route site.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:01 provision: removed retired static route site.go.tito.io (record gone)
2026/10/22 15:35:01 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route acme-uk.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: converged acme.go.tito.io replicas "ie,uk" -> "uk"
2026/10/22 15:35:02 provision: converged acme.go.tito.io replicas "uk" -> ""
2026/10/22 15:35:02 provision: minted instance route acme-uk.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: converged acme-uk.go.tito.io replica mark true -> false
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 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/22 15:35:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: minted instance route router.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:02 provision: acme.go.tito.io: not minting this pass: the box's layout intent could not be read: store down
2026/10/22 15:35:02 provision: router.go.tito.io: not minting this pass: the box's layout intent could not be read: store down
2026/10/22 15:35:02 provision: adopted existing route acme.go.tito.io (app "tito" acct "bac") — this record now owns its channel and environment
2026/10/22 15:35:02 provision: converged acme.go.tito.io channel stable -> edge
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
2026/10/22 15:35:02 provision: not removing acme.go.tito.io (record gone): participant of a running operation
2026/10/22 15:35:02 provision: removed retired instance route acme.go.tito.io (record gone)
2026/10/22 15:35:02 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
2026/10/22 15:35:02 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/22 15:35:02 provision: held acme.go.tito.io landing by mv_acme (was ready)
2026/10/22 15:35:02 provision: acme.go.tito.io: held restoring by rb_other, not by mv_acme; leaving it
2026/10/22 15:35:02 provision: acme.go.tito.io: the row is not this pass's (app "other" acct ""); not holding it for mv_acme
2026/10/22 15:35:02 provision: acme.go.tito.io exists but is not the route this record describes (app "other"/"tito") — leaving it alone
2026/10/22 15:35:02 provision: dry-run: would mint instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac" channel "stable")
2026/10/22 15:35:02 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/22 15:35:02 provision: held acme.go.tito.io landing by mv_acme (was ready)
2026/10/22 15:35:02 provision: minted instance route acme.example.com (app "shop" acct "bac")
2026/10/22 15:35:02 provision: minted instance route plain.example.com (app "shop" acct "bac")
2026/10/22 15:35:02 provision: skipping record apps/bac/shop/instances/garbled.shop.example.com.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:35:02 provision: skipping record apps/bac/shop/instances/liar.shop.example.com.json: disagrees with its own path (host="other.shop.example.com" account="bac" app="shop")
2026/10/22 15:35:02 provision: skipping record apps/bac/shop/instances/garbled.shop.example.com.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:35:02 provision: skipping record apps/bac/shop/instances/liar.shop.example.com.json: disagrees with its own path (host="other.shop.example.com" account="bac" app="shop")
2026/10/22 15:35:02 provision: skipping record apps/bac/shop/instances/garbled.shop.example.com.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:35:02 provision: skipping record apps/bac/shop/instances/liar.shop.example.com.json: disagrees with its own path (host="other.shop.example.com" account="bac" app="shop")
2026/10/22 15:35:02 provision: skipping record apps/bac/shop/instances/garbled.shop.example.com.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:35:02 provision: listing apps/bac/shop/instances/: list refused — its records are unseen this pass
2026/10/22 15:35:03 provision: a.shop.example.com: swapped for import sd_vwcp3dowkr27ml7bnbdmzv5ooy (the original route is journaled)
2026/10/22 15:35:03 provision: a.shop.example.com: import sd_vwcp3dowkr27ml7bnbdmzv5ooy committed: its gate is cleared and its journal row dropped
2026/10/22 15:35:03 provision: a.shop.example.com: row is on the nested layout (db "/data/instances/a.shop.example.com/a.shop.example.com.db" socket "/run/carlos/instances/a.shop.example.com/app.sock"); the record's shared paths are not drift
2026/10/22 15:35:03 provision: a.shop.example.com: import sd_kabb2zs7wx5jibgzfuaktedska rolled back to its original route
2026/10/22 15:35:03 provision: a.shop.example.com: import sd_kabb2zs7wx5jibgzfuaktedska: the host's replica is left alone: the record apps/bac/shop/instances/a.shop.example.com.json holds the host
2026/10/22 15:35:03 provision: a.shop.example.com: import sd_kabb2zs7wx5jibgzfuaktedska is cleaned up and its journal row retired
2026/10/22 15:35:03 provision: a.shop.example.com: import sd_4udvvhhibymblqhbm7occostry rolled back to its original route
2026/10/22 15:35:03 provision: a.shop.example.com: import sd_4udvvhhibymblqhbm7occostry: the host's replica is left alone: the record apps/bac/shop/instances/a.shop.example.com.json holds the host
2026/10/22 15:35:03 provision: a.shop.example.com: import sd_4udvvhhibymblqhbm7occostry is cleaned up and its journal row retired
2026/10/22 15:35:03 provision: dry-run: would redo a.shop.example.com's swap record CAS for import sd_grninxhb5aafl5ipjcv36xnb6e
2026/10/22 15:35:03 provision: dry-run: would clear a.shop.example.com's import gate
2026/10/22 15:35:03 provision: dry-run: would converge a.shop.example.com seed source
2026/10/22 15:35:03 provision: a.shop.example.com: row is on the nested layout (db "/data/instances/a.shop.example.com/a.shop.example.com.db" socket "/run/carlos/instances/a.shop.example.com/app.sock"); the record's shared paths are not drift
2026/10/22 15:35:03 provision: a.shop.example.com: the import's record apps/bac/shop/instances/a.shop.example.com.json is not one this pass can use (connection reset)
2026/10/22 15:35:03 provision: a.shop.example.com: the import's record apps/bac/shop/instances/a.shop.example.com.json is not one this pass can use (provision: invalid maintenance projection or protected fields)
2026/10/22 15:35:03 provision: a.shop.example.com: the import's record apps/bac/shop/instances/a.shop.example.com.json is not one this pass can use (the record under apps/bac/shop/instances/a.shop.example.com.json is apps/bac/shop/instances/b.shop.example.com.json's)
2026/10/22 15:35:03 provision: a.shop.example.com: import sd_yqwaga75po5il7voal5jsfciru cannot roll back on this box: the activator is not wired; it stays journaled and gated
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_l6oxnvse7jwf4wg2wobd762gze cannot roll back on this box: the activator is not wired; it stays journaled and gated
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_4luoo46s4tzaydh4krw3lv2b6i is journaled, but this box cannot act on imports; its row is left alone
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_r5o3nx3tx2n2rmkqzvhy3qgfmu is journaled, but this box cannot act on imports; its row is left alone
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_ermv5equmaw2u6slyb6pwxgum4 is journaled, but this box cannot act on imports; its row is left alone
2026/10/22 15:35:04 provision: the import journal could not be read: registry: import journal a.shop.example.com: original row: unexpected end of JSON input (no import is acted on and no route is removed this pass)
2026/10/22 15:35:04 provision: b.shop.example.com: import sd_plsrvzoszfyk4mil3ojiclsmau left alone this pass: the import journal could not be read
2026/10/22 15:35:04 provision: c.shop.example.com: import sd_vow6g3vpr3cj52evbecfyerz7i left alone this pass: the import journal could not be read
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_da73vepkxvmgokn3u4lwg2kvsy committed: its gate is cleared and its journal row dropped
2026/10/22 15:35:04 provision: a.shop.example.com: not converging import sd_slalmng27pwqxtrocpeyhiccgi: it is swapped but not committed, and the row on this box has no import gate
2026/10/22 15:35:04 provision: a.shop.example.com: not converging import sd_gxigpkrff7iwddhlq6mc7kxytq: it is swapped but not committed, and the row on this box has no import gate
2026/10/22 15:35:04 provision: a.shop.example.com: not converging import sd_r4ook5dngmc6fxdlvi36ob2h5q: it is placed but not committed, and the row on this box has no import gate
2026/10/22 15:35:04 provision: a.shop.example.com: not converging import sd_tdjkumnht4dxm5xwawhjnitfcu: it is placed but not committed, and the row on this box has no import gate
2026/10/22 15:35:04 provision: a.shop.example.com: row is on the nested layout (db "/data/instances/a.shop.example.com/a.shop.example.com.db" socket "/run/carlos/instances/a.shop.example.com/app.sock"); the record's shared paths are not drift
2026/10/22 15:35:04 provision: converge seed source on a.shop.example.com: constraint failed: seed_source is read-only here (1811)
2026/10/22 15:35:04 provision: a.shop.example.com: row is on the nested layout (db "/data/instances/a.shop.example.com/a.shop.example.com.db" socket "/run/carlos/instances/a.shop.example.com/app.sock"); the record's shared paths are not drift
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_2gwpvxk3ewvxmsplh4yi7cfgfy committed: its gate is cleared and its journal row dropped
2026/10/22 15:35:04 provision: a.shop.example.com: row is on the nested layout (db "/data/instances/a.shop.example.com/a.shop.example.com.db" socket "/run/carlos/instances/a.shop.example.com/app.sock"); the record's shared paths are not drift
2026/10/22 15:35:04 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:04 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/22 15:35:04 provision: acme.go.tito.io: not minting a seeded instance: this box does not mint nested (<nil>)
2026/10/22 15:35:04 provision: acme.go.tito.io: not minting a seeded instance: claim control is on for this box
2026/10/22 15:35:04 provision: acme.go.tito.io: aborted seed sd_f3skzjpebckx3ke7kg2dvjouia: this box cannot read seed markers, and places no seed; nothing local is removed
2026/10/22 15:35:04 provision: acme.go.tito.io: aborted seed sd_f3skzjpebckx3ke7kg2dvjouia rolled back (no box had swapped it)
2026/10/22 15:35:04 provision: acme.go.tito.io: not minting a seeded instance: its record does not describe a hibernating instance
2026/10/22 15:35:04 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:04 provision: removed instance route acme.go.tito.io (its seed sd_56jgj3g5ppmjamvjscjecildv4 was aborted); the next pass rolls it back
2026/10/22 15:35:04 provision: acme.go.tito.io: aborted seed sd_56jgj3g5ppmjamvjscjecildv4: this box cannot read seed markers, and places no seed; nothing local is removed
2026/10/22 15:35:04 provision: acme.go.tito.io: aborted seed sd_56jgj3g5ppmjamvjscjecildv4 rolled back (no box had swapped it)
2026/10/22 15:35:04 provision: acme.go.tito.io: aborted seed sd_bnezk2mf2qhw4h66uabccprjyq has a row this pass may not remove; leaving the seed aborted
2026/10/22 15:35:04 provision: acme.go.tito.io: aborted seed sd_oee6px5voxoccd2i6mpwq56vgu rolled back (no box had swapped it)
2026/10/22 15:35:04 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:04 provision: removed instance route acme.go.tito.io (its seed sd_3c6tlanifqhnv36pjlvskdj2hq was aborted); the next pass rolls it back
2026/10/22 15:35:04 provision: acme.go.tito.io: aborted seed sd_3c6tlanifqhnv36pjlvskdj2hq has no route on this box: its local data is removed
2026/10/22 15:35:04 provision: acme.go.tito.io: aborted seed sd_3c6tlanifqhnv36pjlvskdj2hq rolled back (no box had swapped it)
2026/10/22 15:35:04 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_7luguv32sbiuidsnov6nv4tyae (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_7luguv32sbiuidsnov6nv4tyae rolled back to its original route
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_7luguv32sbiuidsnov6nv4tyae is cleaned up and its journal row retired
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_cl5g44a7fbsv424li5dp4iiu6u (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_cl5g44a7fbsv424li5dp4iiu6u rolled back to its original route
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_cl5g44a7fbsv424li5dp4iiu6u is cleaned up and its journal row retired
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_m7kfd3bda5tvxbbf4ff5fnv4sq (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_m7kfd3bda5tvxbbf4ff5fnv4sq rolled back to its original route
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_m7kfd3bda5tvxbbf4ff5fnv4sq: the host's replica is left alone: its record no longer names this seed
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_m7kfd3bda5tvxbbf4ff5fnv4sq is cleaned up and its journal row retired
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_hrwoojr5pvmyv5faxibc2vfk2m (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_hrwoojr5pvmyv5faxibc2vfk2m rollback deferred: the host is not fenced and stopped yet (<nil>)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_hrwoojr5pvmyv5faxibc2vfk2m rolled back to its original route
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_hrwoojr5pvmyv5faxibc2vfk2m is cleaned up and its journal row retired
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_lpi66rvtuw5h7ciymnvwm6qnsi (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_lpi66rvtuw5h7ciymnvwm6qnsi rollback deferred: the host is not fenced and stopped yet (stop unproven)
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_c7nkqpotsegopdnxqzdld3mts4 (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_c7nkqpotsegopdnxqzdld3mts4 is cleaned up and its journal row retired
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_jfptklxhh7kyodtgpkznsm3ch4 (the original route is journaled)
2026/10/22 15:35:04 provision: skipping record apps/bac/shop/instances/a.shop.example.com.json: malformed: provision: invalid maintenance projection or protected fields
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_jfptklxhh7kyodtgpkznsm3ch4 left alone this pass: its record could not be read
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_g5c3j7ce6fuinetpxsibmidlaa (the original route is journaled)
2026/10/22 15:35:04 provision: skipping record apps/bac/shop/instances/a.shop.example.com.json: read failed: connection reset
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_g5c3j7ce6fuinetpxsibmidlaa left alone this pass: its record could not be read
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_uzf4p3rq57mx5jaw35nbakonri (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_uzf4p3rq57mx5jaw35nbakonri: close the import gate: constraint failed: injected (1811)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_uzf4p3rq57mx5jaw35nbakonri rolled back to its original route
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_uzf4p3rq57mx5jaw35nbakonri is cleaned up and its journal row retired
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_duq2so3mnbxrgy7c3v3u4lsu2y (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_duq2so3mnbxrgy7c3v3u4lsu2y: close the import gate: constraint failed: injected (1811)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_duq2so3mnbxrgy7c3v3u4lsu2y rolled back to its original route
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_duq2so3mnbxrgy7c3v3u4lsu2y is cleaned up and its journal row retired
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_crld6gavcbsbxkxlhm7xtjrmsy (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_crld6gavcbsbxkxlhm7xtjrmsy rollback deferred: the host is not fenced and stopped yet (injected failure at fence)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_crld6gavcbsbxkxlhm7xtjrmsy rolled back to its original route
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_crld6gavcbsbxkxlhm7xtjrmsy is cleaned up and its journal row retired
2026/10/22 15:35:04 provision: a.shop.example.com: swapped for import sd_ieidibljeeowjw7ztve6o7w6ta (the original route is journaled)
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_ieidibljeeowjw7ztve6o7w6ta rolled back to its original route
2026/10/22 15:35:04 provision: a.shop.example.com: import sd_ieidibljeeowjw7ztve6o7w6ta is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_smqkip5f3ub3m26immwba4xliu (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_smqkip5f3ub3m26immwba4xliu: retire its instance key: injected failure at retire-key
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_smqkip5f3ub3m26immwba4xliu rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_smqkip5f3ub3m26immwba4xliu is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_akij3impi345fqbflhtowjwn6i (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_akij3impi345fqbflhtowjwn6i rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_akij3impi345fqbflhtowjwn6i is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_cavrm6zodornwxpxyjisk4ahve (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_cavrm6zodornwxpxyjisk4ahve: remove its local data: injected failure at remove-local
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_cavrm6zodornwxpxyjisk4ahve rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_cavrm6zodornwxpxyjisk4ahve is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_xidwakjwschusvtieguk5b4fji (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_xidwakjwschusvtieguk5b4fji rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_xidwakjwschusvtieguk5b4fji is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_h5um4jcdnz4q3oucszykbgukua (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_h5um4jcdnz4q3oucszykbgukua: remove its local data: injected failure at remove-local-done
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_h5um4jcdnz4q3oucszykbgukua rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_h5um4jcdnz4q3oucszykbgukua is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_xiqeahfw5e5uvwva4eqmxnyhoi (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_xiqeahfw5e5uvwva4eqmxnyhoi rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_xiqeahfw5e5uvwva4eqmxnyhoi is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_t5fws6kzobxlpgqaavhf2o2qdm (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_t5fws6kzobxlpgqaavhf2o2qdm: restore the original route: constraint failed: injected (1811)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_t5fws6kzobxlpgqaavhf2o2qdm rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_t5fws6kzobxlpgqaavhf2o2qdm is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_tggqop7dy7c7f7yjryyerowv7u (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_tggqop7dy7c7f7yjryyerowv7u: restore the original route: constraint failed: injected (1811)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_tggqop7dy7c7f7yjryyerowv7u rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_tggqop7dy7c7f7yjryyerowv7u is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_hj7vysebbyzlxsmfurmhswtvga (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_hj7vysebbyzlxsmfurmhswtvga is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_tqemxpzzixth7rpdwj5zric5lu (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_tqemxpzzixth7rpdwj5zric5lu rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_tqemxpzzixth7rpdwj5zric5lu: delete its snapshot: injected failure at delete-snapshot
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_tqemxpzzixth7rpdwj5zric5lu is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_256sstrfkiv72d7gtpng6il4ue (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_256sstrfkiv72d7gtpng6il4ue rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_256sstrfkiv72d7gtpng6il4ue is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_zidfudyhfj37qxcciyo4mt76km (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_zidfudyhfj37qxcciyo4mt76km rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_zidfudyhfj37qxcciyo4mt76km: delete the host's replica: injected failure at delete-replica
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_zidfudyhfj37qxcciyo4mt76km is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_uyxkdqhozkifzvabfnfuytrzs4 (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_uyxkdqhozkifzvabfnfuytrzs4 rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_uyxkdqhozkifzvabfnfuytrzs4 is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_73ndhthuaa535d5ft3yfppfnre (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_73ndhthuaa535d5ft3yfppfnre rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_73ndhthuaa535d5ft3yfppfnre: retire its journal row: constraint failed: injected (1811)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_73ndhthuaa535d5ft3yfppfnre is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_6sqs52rs6r6dinribi6w2qxh6y (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_6sqs52rs6r6dinribi6w2qxh6y rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_6sqs52rs6r6dinribi6w2qxh6y: retire its journal row: constraint failed: injected (1811)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_6sqs52rs6r6dinribi6w2qxh6y is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_q7z3mthl3mhv6lgujfz73pig2y (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_q7z3mthl3mhv6lgujfz73pig2y rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_q7z3mthl3mhv6lgujfz73pig2y is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: mark import sd_q7z3mthl3mhv6lgujfz73pig2y rolled back: injected failure at cas
2026/10/22 15:35:05 provision: a.shop.example.com: aborted seed sd_q7z3mthl3mhv6lgujfz73pig2y rolled back (no box had swapped it)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_t4g2alkmc7xai6qxcpvhptvlem (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_t4g2alkmc7xai6qxcpvhptvlem rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_t4g2alkmc7xai6qxcpvhptvlem is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: aborted seed sd_t4g2alkmc7xai6qxcpvhptvlem rolled back (no box had swapped it)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_2uw4ved5aj5fovrxgc6mmewpem (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_2uw4ved5aj5fovrxgc6mmewpem rollback deferred: the host is not fenced and stopped yet (<nil>)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_2uw4ved5aj5fovrxgc6mmewpem rollback deferred: the host is not fenced and stopped yet (<nil>)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_2uw4ved5aj5fovrxgc6mmewpem rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_2uw4ved5aj5fovrxgc6mmewpem is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_rp3jdqfchncme64clze5irnrku (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_rp3jdqfchncme64clze5irnrku rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_rp3jdqfchncme64clze5irnrku is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_dlerwytubdlecren45jzxddkye (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_dlerwytubdlecren45jzxddkye rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_dlerwytubdlecren45jzxddkye is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: minted instance route a.shop.example.com (app "shop" acct "bac")
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_b47d4ohv4jo2e54ebylwnxyo4i (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_b47d4ohv4jo2e54ebylwnxyo4i rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_b47d4ohv4jo2e54ebylwnxyo4i is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: minted instance route a.shop.example.com (app "shop" acct "bac")
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_punnklypnkw3dv2fu6hwjdjoym (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_punnklypnkw3dv2fu6hwjdjoym rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_punnklypnkw3dv2fu6hwjdjoym is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: minted instance route a.shop.example.com (app "shop" acct "bac")
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_2frgqgatlgdoifka4fo7n34ply (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_2frgqgatlgdoifka4fo7n34ply rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_2frgqgatlgdoifka4fo7n34ply is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: minted instance route a.shop.example.com (app "other" acct "bac")
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_u3zrawofdc2ew2hhfue56wq5pe (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_u3zrawofdc2ew2hhfue56wq5pe rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_u3zrawofdc2ew2hhfue56wq5pe is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: minted instance route a.shop.example.com (app "other" acct "bac")
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_p7nibtlf57vv3inkwyk5gp5wsi (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_p7nibtlf57vv3inkwyk5gp5wsi rolled back to its original route
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_p7nibtlf57vv3inkwyk5gp5wsi is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: minted instance route a.shop.example.com (app "other" acct "bac")
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_7img36ltjysy2eboelqezrh7vu (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_7img36ltjysy2eboelqezrh7vu has no route on this box: its local data is removed and no route is put back
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_7img36ltjysy2eboelqezrh7vu is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_liqpkwr776zrh5luo443fg3aq4 (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_ufo3xjvjnyiduqz6hw4c2c34o4 (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_6toa5oub6wmnlorrrdvxibhkre (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_6toa5oub6wmnlorrrdvxibhkre cannot roll back on this box: the activator is not wired; it stays journaled and gated
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_3yledjahib3dhx4ojgld4qjkce (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_3yledjahib3dhx4ojgld4qjkce is not rolled back: the route changed under the fence, so nothing there is touched
2026/10/22 15:35:05 provision: a.shop.example.com: import "../not-a-seed" names no snapshot and no replica; only its journal row is cleaned up
2026/10/22 15:35:05 provision: a.shop.example.com: import ../not-a-seed is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_wgi6qkjqjfzgcpql5raynkadxu (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_diotxgrp4m63ifndckzhqf5fjq (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_63ms6zlv5wgrjaxiordsszp3ra (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_ghghd6bca2zr222efuompbfgp4 (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_ky4hcz4vso7sy4xbc7he2g2fhq (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_ky4hcz4vso7sy4xbc7he2g2fhq rollback deferred: the host is not fenced and stopped yet (stop evidence unreadable)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_ylikabktyznkchhkvnlqav3efu (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_ylikabktyznkchhkvnlqav3efu has no route on this box: its local data is removed and no route is put back
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_ylikabktyznkchhkvnlqav3efu is cleaned up and its journal row retired
2026/10/22 15:35:05 provision: minted instance route a.shop.example.com (app "shop" acct "bac")
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_bjez4sljwz457mk3fic5pd5hnm (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_bjez4sljwz457mk3fic5pd5hnm: remove its local data: injected failure at remove-local
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_bjez4sljwz457mk3fic5pd5hnm is not rolled back: the route at the host is not the import's, so nothing there is touched
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_owu77ydiz6txnudaa3xb465bva (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_owu77ydiz6txnudaa3xb465bva: a route appeared at the host, so its local database is left and the rollback waits
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_g4nrux7h7qlslrw3dhp3u3divm (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_g4nrux7h7qlslrw3dhp3u3divm rollback deferred: no fence generation: registry busy
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_ozjxt4p3lguyq5l5v4vqhxoemy (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: import sd_ozjxt4p3lguyq5l5v4vqhxoemy rollback deferred: no fence generation: the generation source answered 0
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_5qzd3giileiqohjwwlqyan277y (the original route is journaled)
2026/10/22 15:35:05 provision: a.shop.example.com: swapped for import sd_u3sxmiiujxlbomgs6q6xntirla (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_7yrrlpaeymkhpp4galp7zgnsye (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: import deferred: this box cannot read the instance's env file
2026/10/22 15:35:06 provision: a.shop.example.com: import deferred: the instance's env file could not be read: a symlink
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_rtriikcytu5ez6yanikd377odi (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_rtriikcytu5ez6yanikd377odi rolled back to its original route
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_rtriikcytu5ez6yanikd377odi is cleaned up and its journal row retired
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_qpwqceq6gmf7f7sh7k775yec6i (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_qpwqceq6gmf7f7sh7k775yec6i rolled back to its original route
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_qpwqceq6gmf7f7sh7k775yec6i is cleaned up and its journal row retired
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_7hbuzcxwqxeruuh2w43jqyxxuu (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_7hbuzcxwqxeruuh2w43jqyxxuu rolled back to its original route
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_7hbuzcxwqxeruuh2w43jqyxxuu is cleaned up and its journal row retired
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_o7buj6qeul4ouktzrqhqygrh5y (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_o7buj6qeul4ouktzrqhqygrh5y: retire its instance key: the env file is a symlink
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_o7buj6qeul4ouktzrqhqygrh5y rolled back to its original route
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_o7buj6qeul4ouktzrqhqygrh5y is cleaned up and its journal row retired
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_jclwka5tw6jtycay2vgvsdptam (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: row is on the nested layout (db "/data/instances/a.shop.example.com/a.shop.example.com.db" socket "/run/carlos/instances/a.shop.example.com/app.sock"); the record's shared paths are not drift
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_zojipsruev2iebrl7ojhhywtkm (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_hlntcq27bjp5xq7yco5kz6i6na refused: there is no route at a.shop.example.com to replace
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_odb5aeo3behiajut6sa2zvqcq4 refused: the route's address is 127.0.0.1:9, not 127.0.0.1:8401
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_6r2pvzkpf4f6zmnw5sntpdd2py refused: the route's address is 127.0.0.1:8401,127.0.0.1:8402, not 127.0.0.1:8401
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_qapujl2lepukpnej7nyvoxgldm refused: the route's address is 127.0.0.1:9, not 127.0.0.1:8401
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_qkzqwfdjwwms7tbvepjrcrfkde refused: the route belongs to app other
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_qqdr6wgpmpf6t2q3ezl6duggz4 refused: the route is filed under another account
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_qgspf3rx2j433ikag5dfswtbau refused: the route is filed under the operator account, and no assignment maps shop to this app
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_7u6427pck2ze5rsqih3yodqan4 refused: the route is filed under the operator account, and no assignment maps shop to this app
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_ivyw2xolxozhdjp5cu2sdka5we refused: the route is filed under the operator account, and no assignment maps shop to this app
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_h5cjtanhd5kojlqn5evncuh34a refused: the route is filed under the operator account, and no assignment maps shop to this app
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_z2akbgbkbimhwrm2r4dcndn4je refused: the route is not an address route this import can take over (socket)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_waxcgeldh6pmk43ezo3hjfobk4 refused: the route is not an address route this import can take over (no address)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_l2ohdafvbpxkrhhvwbxwhrcp7q refused: the route is not an address route this import can take over (provisioned)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_b5vgebyhgdv5zmu2aybsf55b5i refused: the route is not an address route this import can take over (kind static)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_qvttm7jgki2l7vbpgkzvounama refused: the route is not an address route this import can take over (backing)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_jottrylbsgzus7ajh7nwjury3q refused: the route is not an address route this import can take over (tunnel)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_mjnlfryzu7g4lupcdmorlqkg64 refused: the route is not an address route this import can take over (caps)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_jw4r5sdqftm52wlgh242l33so4 refused: the route is not an address route this import can take over (platform)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_6cgs6o2ci3gxtv76mw2nmyb5gi refused: the route is not an address route this import can take over (canary)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_4jrpcv6tqeotzy5bg2xsgw7agq refused: the route is not an address route this import can take over (kind sidecar)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_hcrizdsrijhrl2ng3hgsct4fji refused: the route is not an address route this import can take over (region)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_dqc6kf7eerymx44ily6eabyu4a refused: the route is not an address route this import can take over (fleet_box)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_nbxosfrjkuiq65jc25o4opwliy refused: the route is not an address route this import can take over (hibernate)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_ifuhuqbigzthyvlujj3qiebvim refused: the route is not an address route this import can take over (claim_owner)
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_copxantjgzjty3gtnq6rqg6seu refused: this box does not run instances nested
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_refpldzqjcbpxk7xtvkxc4yrje refused: this box does not run instances nested
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_4keesmqnegx6qef6u6nyzci2sm refused: claim control is on for this box
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_nbtnpakvuptq3pri6nuw3nlckm (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_x3p2yeqns6qoloaiwwbthpxwba (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: import deferred: this box cannot read the legacy route assignments
2026/10/22 15:35:06 provision: a.shop.example.com: import deferred: the legacy route assignments could not be read: store down
2026/10/22 15:35:06 provision: a.shop.example.com: not swapping for import sd_fz5hjyoatosefxpqwj6htw46ha: its verify hash is not a sha256
2026/10/22 15:35:06 provision: a.shop.example.com: not swapping for import sd_omk6oyjlbtwr7xki46u6wpcr5u: its verify hash is not a sha256
2026/10/22 15:35:06 provision: a.shop.example.com: not swapping for import sd_uct5bimv6fpi56kvgjrj6xtovy: its verify hash is not a sha256
2026/10/22 15:35:06 provision: a.shop.example.com: not swapping for import sd_../../x: its host or seed id is malformed (seeds: not a seed id)
2026/10/22 15:35:06 provision: A.shop.example.com/..: not swapping for import sd_uipshjz42ntfsmjpc2jqlq3tn4: its host or seed id is malformed (seeds: not a host)
2026/10/22 15:35:06 provision: a.shop.example.com: not swapping for import sd_cedaufl4l3es23j2fryolznrsy: its record does not describe a nested, hibernating instance
2026/10/22 15:35:06 provision: a.shop.example.com: not swapping for import sd_26xajfnon242hbsd4u7bfnvrpu: its record does not describe a nested, hibernating instance
2026/10/22 15:35:06 provision: a.shop.example.com: not swapping for import sd_63t3xaftciwgmep227tsvvmghu: its record does not describe a nested, hibernating instance
2026/10/22 15:35:06 provision: a.shop.example.com: not swapping for import sd_vpkpvglx6jttk7qaq76avracsu: its record does not describe a nested, hibernating instance
2026/10/22 15:35:06 provision: a.shop.example.com: import deferred: the box's layout intent could not be read: store down
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_bl4us6ficku27w5a7oac645kn4 (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: import deferred: the box's nested probe did not answer: ready probe timed out
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_jkuiar2iqhrhw7fjxv4brgsrtm (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: swapped for import sd_bckau6mxkos6qmhurdb7vblbua (the original route is journaled)
2026/10/22 15:35:06 provision: a.shop.example.com: import deferred: the route is held by operation op_1
2026/10/22 15:35:06 provision: dry-run: would consider swapping a.shop.example.com for import sd_piodmzvncsqlxjwn35a2aajinu
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_zrpvgcawynoxtgt3tw2uhmj65u rolled back to its original route
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_zrpvgcawynoxtgt3tw2uhmj65u: the host's replica is left alone: the record apps/bac/shop/instances/a.shop.example.com.json holds the host
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_zrpvgcawynoxtgt3tw2uhmj65u is cleaned up and its journal row retired
2026/10/22 15:35:06 provision: a.shop.example.com: import sd_kobncdxhxdm4zlj3rnriqevdhy left alone: the import journal holds seed sd_uw3qgculbhesh5pszthyvxstry at swapped
2026/10/22 15:35:07 provision: a.shop.example.com: not minting import sd_7ze6ncezijkruyrl77ucmjimky: it is swapped but not committed, and only its swap makes its row
2026/10/22 15:35:07 provision: a.shop.example.com: not minting import sd_dcopvzbyx3jeuamcyllw7x4mxq: it is placed but not committed, and only its swap makes its row
2026/10/22 15:35:07 provision: a.shop.example.com: swapped for import sd_fvualjdgollmy774uoyyxtygiu (the original route is journaled)
2026/10/22 15:35:07 provision: a.shop.example.com: not minting import sd_fvualjdgollmy774uoyyxtygiu: it is swapped but not committed, and only its swap makes its row
2026/10/22 15:35:07 provision: a.shop.example.com: not minting import sd_fvualjdgollmy774uoyyxtygiu: it is placed but not committed, and only its swap makes its row
2026/10/22 15:35:07 provision: minted instance route a.shop.example.com (app "shop" acct "bac")
2026/10/22 15:35:07 provision: removed retired sidecar route gone.eleven.bac.sidecar.internal (record gone)
2026/10/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 provision: converged sidecar bot.eleven.bac.sidecar.internal args ["sidecar","run"] -> ["worker","run"]
2026/10/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 provision: converged sidecar bot.eleven.bac.sidecar.internal ceilings 300/3600 -> 60/120
2026/10/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 provision: converged sidecar bot.eleven.bac.sidecar.internal channel stable -> canary/eleven
2026/10/22 15:35:07 provision: converged sidecar bot.eleven.bac.sidecar.internal environment "other" -> "relay"
2026/10/22 15:35:07 provision: converged sidecar bot.eleven.bac.sidecar.internal hibernate true -> false
2026/10/22 15:35:07 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: read failed: failStore: reads disabled under apps/bac/eleven/sidecars/
2026/10/22 15:35:07 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: malformed: invalid character 'n' looking for beginning of object key string
2026/10/22 15:35:07 provision: not removing sidecar bot.eleven.bac.sidecar.internal: its record exists but could not be read cleanly this pass
2026/10/22 15:35:07 provision: skipping sidecar record apps/bac/eleven/sidecars/bot.json: empty channel
2026/10/22 15:35:07 provision: skipping sidecar record apps/bac/eleven/sidecars/held.json: empty channel
2026/10/22 15:35:07 provision: not removing sidecar held.eleven.bac.sidecar.internal: its record exists but could not be read cleanly this pass
2026/10/22 15:35:07 provision: removed retired sidecar route old.eleven.bac.sidecar.internal (record gone)
2026/10/22 15:35:07 provision: bac/eleven has sidecar records but no sidecars manifest — not minting
2026/10/22 15:35:07 provision: bac/eleven is in the trash — not minting sidecar routes for it
2026/10/22 15:35:07 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/22 15:35:07 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/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 provision: minted sidecar route hub.relay.bac.sidecar.internal (app "relay" acct "bac" args ["relay"])
2026/10/22 15:35:07 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/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 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/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 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/22 15:35:07 provision: removed retired sidecar route bot.eleven.bac.sidecar.internal (record gone)
2026/10/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 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/22 15:35:07 provision: skipping sidecar record apps/bac/relay/sidecars/hub.json: read failed: failStore: reads disabled under apps/bac/relay/sidecars/
2026/10/22 15:35:07 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/22 15:35:07 provision: minted sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" args ["sidecar","run"])
2026/10/22 15:35:07 provision: dry-run: would mint sidecar route bot.eleven.bac.sidecar.internal (app "eleven" acct "bac" channel "stable" args ["sidecar","run"])
2026/10/22 15:35:07 provision: dry-run: would remove retired sidecar route gone.eleven.bac.sidecar.internal (record gone)
2026/10/22 15:35:07 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/22 15:35:07 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:07 provision: converged acme.go.tito.io update settings "{\"updates\":\"manual\",\"channel\":\"stable\"}" -> ""
2026/10/22 15:35:07 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:07 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:07 provision: manifest bac/tito: read failed: provision: manifest bac/tito: unexpected end of JSON input
2026/10/22 15:35:07 provision: manifest bac/tito: read failed: provision: manifest bac/tito: unexpected end of JSON input
2026/10/22 15:35:07 provision: acme.go.tito.io: not minted this pass: the app's update policy could not be read
2026/10/22 15:35:07 provision: minted instance route acme.go.tito.io (app "tito" acct "bac")
2026/10/22 15:35:07 provision: manifest bac/tito: read failed: provision: manifest bac/tito: unexpected end of JSON input
2026/10/22 15:35:07 provision: acme.go.tito.io: not minted this pass: the app's update policy could not be read
2026/10/22 15:35:07 provision: minted instance route acme.go.tito.io held landing by mv_acme (app "tito" acct "bac")
--- FAIL: TestPlan14RootReadCensus (31.17s)
    plan14_root_reads_test.go:1329: root read census: 9 discrepancies
        changed root read internal/edge/static.go:PackDir; review class, task, sites and digest
        changed root source envelope internal/edge
        stale root read internal/edge/static.go:staticSite.serveTree
        unclassified root read internal/edge/static.go:lookupTree file:Stat@188,file:Stat@282
        unclassified root read internal/edge/static.go:staticSite.loadRedirects file:ReadFile@87
        unclassified root read internal/edge/static.go:staticSite.serveFile file:Open@114,file:Stat@859
        unclassified root read internal/edge/static.go:staticSite.serveRule file:Open@509
        changed legacy declaration internal/edge/static.go:PackDir
        changed legacy declaration internal/edge/static.go:staticSite.serveTree
FAIL
FAIL	github.com/carlosframework/platform/internal/provision	42.880s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.008s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	7.587s
ok  	github.com/carlosframework/platform/internal/relay	7.344s
ok  	github.com/carlosframework/platform/internal/release	37.483s
ok  	github.com/carlosframework/platform/internal/rollout	5.628s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.011s
ok  	github.com/carlosframework/platform/internal/rootaws	0.009s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.071s
ok  	github.com/carlosframework/platform/internal/rootstate	0.041s
ok  	github.com/carlosframework/platform/internal/runs	0.640s
ok  	github.com/carlosframework/platform/internal/s3compat	0.008s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	4.075s
ok  	github.com/carlosframework/platform/internal/seeds	0.013s
ok  	github.com/carlosframework/platform/internal/sim/engines	6.973s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	31.790s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.217s
ok  	github.com/carlosframework/platform/internal/status	0.133s
ok  	github.com/carlosframework/platform/internal/steering	0.017s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.448s
ok  	github.com/carlosframework/platform/internal/svcrole	0.006s
ok  	github.com/carlosframework/platform/internal/telemetry	0.006s
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.005s
ok  	github.com/carlosframework/platform/internal/updatewords	0.007s
ok  	github.com/carlosframework/platform/internal/usage	0.024s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	2.037s
FAIL
=== internal_console@0 (91 s)
ok  	github.com/carlosframework/platform/internal/console	88.467s
=== internal_console@1 (49 s)
ok  	github.com/carlosframework/platform/internal/console	47.043s
=== internal_console@2 (54 s)
ok  	github.com/carlosframework/platform/internal/console	52.372s
=== internal_console@3 (47 s)
ok  	github.com/carlosframework/platform/internal/console	45.429s
=== internal_console@4 (53 s)
ok  	github.com/carlosframework/platform/internal/console	50.510s
=== internal_console@5 (51 s)
ok  	github.com/carlosframework/platform/internal/console	49.493s
=== internal_console@6 (56 s)
ok  	github.com/carlosframework/platform/internal/console	54.199s
=== internal_console@7 (55 s)
ok  	github.com/carlosframework/platform/internal/console	52.735s
=== root@0 (51 s)
ok  	github.com/carlosframework/platform	48.434s
=== root@1 (54 s)
ok  	github.com/carlosframework/platform	51.856s
=== root@2 (54 s)
ok  	github.com/carlosframework/platform	51.987s

FAILED: rest
make: *** [Makefile:19: 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/docs/site/cli.md b/docs/site/cli.md
index a964ca5ab..7b80cbc29 100644
--- a/docs/site/cli.md
+++ b/docs/site/cli.md
@@ -330,6 +330,44 @@ app is served only at its own platform address, so `--host` and
carlos deploy --temporary --ttl 3d ./dist
```
+#### Redirects for a static site
+
+Put a file named `_redirects` at the top of the site directory. It uses the
+format Netlify and Cloudflare Pages use: one rule per line, with a source, a
+destination and an optional status.
+
+```text
+# source destination status
+/old-page /new-page 301
+/blog/* /posts/:splat 301
+/docs/:slug /guide/:slug 302
+/app/* /app/index.html 200
+https://www.example.com/* https://example.com/:splat 301!
+/* /404.html 404
+```
+
+- A source is a path, or a URL with a host. A URL source applies only to
+ requests for that host, so the last-but-one rule sends `www` to the apex.
+- `:name` matches one path segment. A `*` at the end matches the rest of the
+ path, and the destination gets it as `:splat`.
+- The status is 301 when you leave it out. 302, 303, 307 and 308 are also
+ redirects. 200 serves the destination's file at the requested URL, which
+ is how a single-page app serves its client-side routes. 404 serves the
+ destination's file as the not-found page.
+- A 200 or 404 rule takes a path in the same site. A redirect can also go to
+ another host.
+- A request's query string is kept unless the destination has one of its
+ own.
+- When a file exists at the requested path, the file wins. Add `!` to the
+ status (`301!`) to make the rule win.
+- Rules apply in order, and the first rule that matches is used. A line
+ starting with `#` is a comment.
+
+`carlos ship` and `carlos deploy` refuse a `_redirects` file with a line they
+cannot use, and name the line. Netlify's conditions, such as `Country=`, and
+proxying to another host are not supported. A file can have up to 2,000
+rules. The edge does not serve the `_redirects` file itself.
+
#### Two builds at once
When a new build reaches an awake instance, it starts beside the old one,
diff --git a/internal/edge/redirects.go b/internal/edge/redirects.go
new file mode 100644
index 000000000..f38a1d416
--- /dev/null
+++ b/internal/edge/redirects.go
@@ -0,0 +1,418 @@
+// Redirect and rewrite rules for static sites: a `_redirects` file at the root
+// of a shipped tree, in the format Netlify introduced and Cloudflare Pages
+// adopted. One rule per line, "source destination [status]":
+//
+// /old /new 301
+// /blog/* /posts/:splat
+// /docs/:slug /guide/:slug 302
+// /app/* /app/index.html 200
+// https://www.example.com/* https://example.com/:splat 301!
+//
+// The subset served here:
+//
+// - A source is a path, or an http(s) URL whose host must match the
+// request's (the scheme is not compared: the edge serves https only).
+// A segment ":name" captures one path segment; a final "*" captures the
+// rest of the path, "" included, so "/blog/*" also matches "/blog"; a
+// non-empty splat keeps the request's trailing slash.
+// Matching is case-sensitive and ignores a trailing slash.
+// - A destination is a path, or for a redirect an http(s) URL. ":name" and
+// ":splat" are replaced with what the source captured; placeholders in a
+// URL's scheme or host are not. A request's query string is carried over
+// when the destination names none of its own.
+// - The status is 301 (the default), 302, 303, 307 or 308 for a redirect;
+// 200 serves the destination's file at the request's URL (a rewrite) and
+// 404 serves it with a 404 (a custom not-found page). A rewrite's
+// destination is always a file in the same tree, never another host.
+// - A file at the request path wins over every rule (Netlify's
+// "shadowing"), unless the status carries a trailing "!".
+// - The first matching rule applies. A line whose first field starts with
+// "#" is a comment, and so is everything from a field starting with "#".
+//
+// Netlify's conditions (Country=, Language=, Role=...), proxying to another
+// host and the netlify.toml form are not supported. `carlos ship` refuses a
+// tree whose _redirects has a line it cannot use (PackDir), so a site never
+// ships rules the edge would ignore; the edge itself skips such a line and
+// logs it, which only a tree packed by something else can reach.
+package edge
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "path"
+ "strconv"
+ "strings"
+)
+
+// redirectsFile is the rules file's name at the root of a static tree. The
+// file is configuration, not content: the edge never serves it.
+const redirectsFile = "_redirects"
+
+// Limits on one _redirects file. Matching is a scan of every rule on each
+// request the tree cannot answer, so the count is bounded the way Cloudflare
+// Pages bounds it.
+const (
+ redirectsMaxBytes = 256 << 10
+ redirectsMaxRules = 2000
+ redirectsMaxLine = 2048
+)
+
+// redirectRule is one parsed line of a _redirects file.
+type redirectRule struct {
+ line int // 1-based line number, for logs
+ host string // lowercased; "" matches any host
+ segs []string // source path segments; ":name" captures one
+ splat bool // the source ends in "*"
+ prefix string // a URL destination's "scheme://host", never substituted
+ to string // the destination template after prefix
+ status int
+ force bool // "!": applies even when a file exists at the request path
+}
+
+// redirectRules is a parsed _redirects file, immutable once built.
+type redirectRules struct {
+ rules []redirectRule
+}
+
+// isRedirect reports whether status is one a redirect rule may answer with.
+func isRedirect(status int) bool {
+ switch status {
+ case 301, 302, 303, 307, 308:
+ return true
+ }
+ return false
+}
+
+// parseRedirects parses a _redirects file. It returns the rules it could use
+// and an error naming every line it could not; callers decide which matters.
+// Ship refuses on any error, and the edge serves the usable rules.
+func parseRedirects(data []byte) (*redirectRules, error) {
+ if len(data) > redirectsMaxBytes {
+ return nil, fmt.Errorf("%s is %d bytes; the limit is %d", redirectsFile, len(data), redirectsMaxBytes)
+ }
+ rs := &redirectRules{}
+ var errs []error
+ // A byte-order mark is what some editors save at the front; it is not
+ // part of the first rule.
+ text := strings.TrimPrefix(string(data), "\ufeff")
+ for i, line := range strings.Split(text, "\n") {
+ n := i + 1
+ line = strings.TrimSpace(strings.TrimSuffix(line, "\r"))
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+ if len(line) > redirectsMaxLine {
+ errs = append(errs, fmt.Errorf("line %d: longer than %d characters", n, redirectsMaxLine))
+ continue
+ }
+ rule, err := parseRedirectLine(line)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("line %d: %v", n, err))
+ continue
+ }
+ if len(rs.rules) == redirectsMaxRules {
+ errs = append(errs, fmt.Errorf("line %d: more than %d rules", n, redirectsMaxRules))
+ continue
+ }
+ rule.line = n
+ rs.rules = append(rs.rules, rule)
+ }
+ return rs, errors.Join(errs...)
+}
+
+func parseRedirectLine(line string) (redirectRule, error) {
+ var fields []string
+ for _, f := range strings.Fields(line) {
+ if strings.HasPrefix(f, "#") {
+ break // the rest of the line is a comment
+ }
+ fields = append(fields, f)
+ }
+ var rule redirectRule
+ switch {
+ case len(fields) < 2:
+ return rule, errors.New("want a source and a destination")
+ case len(fields) > 3:
+ if strings.Contains(fields[3], "=") {
+ return rule, fmt.Errorf("conditions such as %q are not supported", fields[3])
+ }
+ return rule, fmt.Errorf("unexpected %q after the status", fields[3])
+ }
+
+ rule.status = 301
+ if len(fields) == 3 {
+ st := fields[2]
+ if s, ok := strings.CutSuffix(st, "!"); ok {
+ rule.force, st = true, s
+ }
+ code, err := strconv.Atoi(st)
+ if err != nil || !(isRedirect(code) || code == 200 || code == 404) {
+ return rule, fmt.Errorf("status %q: want 301, 302, 303, 307, 308, 200 or 404, optionally followed by !", fields[2])
+ }
+ rule.status = code
+ }
+
+ src := fields[0]
+ if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
+ u, err := url.Parse(src)
+ if err != nil || u.Host == "" || u.User != nil || u.Port() != "" || u.RawQuery != "" || u.Fragment != "" {
+ return rule, fmt.Errorf("source %q: want a URL with only a host and a path", src)
+ }
+ rule.host = serverName(u.Hostname()) // as requestHost normalizes the request's
+ src = u.Path
+ if src == "" {
+ src = "/"
+ }
+ }
+ if !strings.HasPrefix(src, "/") {
+ return rule, fmt.Errorf("source %q: want a path starting with / or an http(s) URL", fields[0])
+ }
+ if strings.ContainsAny(src, "?#") {
+ return rule, fmt.Errorf("source %q: query strings and fragments are not matched", fields[0])
+ }
+ names := map[string]bool{}
+ trimmed := strings.Trim(src, "/")
+ for _, seg := range strings.Split(trimmed, "/") {
+ switch {
+ case seg == "":
+ if trimmed != "" {
+ return rule, fmt.Errorf("source %q: empty path segment", fields[0])
+ }
+ case rule.splat:
+ return rule, fmt.Errorf("source %q: * must be the last segment", fields[0])
+ case seg == "*":
+ rule.splat = true
+ case strings.Contains(seg, "*"):
+ return rule, fmt.Errorf("source %q: * must be a whole segment", fields[0])
+ case strings.HasPrefix(seg, ":"):
+ name := seg[1:]
+ if !validPlaceholder(name) || name == "splat" {
+ return rule, fmt.Errorf("source %q: bad placeholder %q", fields[0], seg)
+ }
+ if names[name] {
+ return rule, fmt.Errorf("source %q: placeholder %q appears twice", fields[0], seg)
+ }
+ names[name] = true
+ rule.segs = append(rule.segs, seg)
+ default:
+ // Requests match decoded, as files are found, so a literal
+ // segment is decoded here to meet them: "/caf%C3%A9" matches a
+ // request for /café. A decoded "/" would be two segments, and
+ // no request path has one inside a segment.
+ dec, err := url.PathUnescape(seg)
+ if err != nil || strings.Contains(dec, "/") {
+ return rule, fmt.Errorf("source %q: bad percent-encoding in %q", fields[0], seg)
+ }
+ if strings.HasPrefix(dec, ":") {
+ return rule, fmt.Errorf("source %q: %q would read as a placeholder", fields[0], seg)
+ }
+ if dec == "." || dec == ".." {
+ return rule, fmt.Errorf("source %q: dot segments never match", fields[0])
+ }
+ rule.segs = append(rule.segs, dec)
+ }
+ }
+
+ dst := fields[1]
+ switch {
+ case strings.HasPrefix(dst, "http://") || strings.HasPrefix(dst, "https://"):
+ if !isRedirect(rule.status) {
+ return rule, fmt.Errorf("destination %q: a %d serves a file from this site, so it takes a path, not a URL", dst, rule.status)
+ }
+ u, err := url.Parse(dst)
+ if err != nil || u.Host == "" || u.User != nil {
+ return rule, fmt.Errorf("destination %q: not a usable URL", dst)
+ }
+ i := len(u.Scheme) + len("://") + len(u.Host)
+ if !strings.EqualFold(dst[:i], u.Scheme+"://"+u.Host) {
+ return rule, fmt.Errorf("destination %q: not a usable URL", dst)
+ }
+ rule.prefix, rule.to = dst[:i], dst[i:]
+ if rule.to != "" && !strings.ContainsAny(rule.to[:1], "/?#") {
+ return rule, fmt.Errorf("destination %q: not a usable URL", dst)
+ }
+ case strings.HasPrefix(dst, "//") || strings.HasPrefix(dst, "/\\"):
+ return rule, fmt.Errorf("destination %q: name another host with https://", dst)
+ case strings.HasPrefix(dst, "/"):
+ rule.to = dst
+ default:
+ return rule, fmt.Errorf("destination %q: want a path starting with / or an http(s) URL", dst)
+ }
+ return rule, nil
+}
+
+// validPlaceholder: a placeholder name is letters, digits and underscores.
+func validPlaceholder(name string) bool {
+ if name == "" {
+ return false
+ }
+ for _, c := range name {
+ if !(c == '_' || c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
+ return false
+ }
+ }
+ return true
+}
+
+// match reports whether the rule applies to a request for host whose cleaned
+// path splits into reqSegs, and returns what the source captured.
+func (rule *redirectRule) match(host string, reqSegs []string) (map[string]string, bool) {
+ if rule.host != "" && rule.host != host {
+ return nil, false
+ }
+ if len(reqSegs) < len(rule.segs) || !rule.splat && len(reqSegs) != len(rule.segs) {
+ return nil, false
+ }
+ var caps map[string]string
+ for i, seg := range rule.segs {
+ if strings.HasPrefix(seg, ":") {
+ if caps == nil {
+ caps = map[string]string{}
+ }
+ caps[seg[1:]] = reqSegs[i]
+ } else if seg != reqSegs[i] {
+ return nil, false
+ }
+ }
+ if rule.splat {
+ if caps == nil {
+ caps = map[string]string{}
+ }
+ caps["splat"] = strings.Join(reqSegs[len(rule.segs):], "/")
+ }
+ return caps, true
+}
+
+// expand substitutes the captures into the destination template. With escape
+// set, values are percent-encoded for a Location header: per segment in the
+// path, as a query value after "?". Without it they go in raw, for a rewrite's
+// file lookup. A ":name" the source did not capture stays as written.
+func expand(t string, caps map[string]string, escape bool) string {
+ var b strings.Builder
+ inQuery := false
+ for i := 0; i < len(t); {
+ c := t[i]
+ if c == '?' || c == '#' {
+ inQuery = true
+ }
+ if c != ':' {
+ b.WriteByte(c)
+ i++
+ continue
+ }
+ j := i + 1
+ for j < len(t) && validPlaceholder(t[j:j+1]) {
+ j++
+ }
+ v, ok := caps[t[i+1:j]]
+ if !ok {
+ b.WriteString(t[i:j])
+ i = j
+ continue
+ }
+ switch {
+ case !escape:
+ b.WriteString(v)
+ case inQuery:
+ b.WriteString(url.QueryEscape(v))
+ default:
+ parts := strings.Split(v, "/")
+ for k, p := range parts {
+ parts[k] = url.PathEscape(p)
+ }
+ b.WriteString(strings.Join(parts, "/"))
+ }
+ i = j
+ }
+ return b.String()
+}
+
+// redirectAction is what a matched rule asks the handler to do.
+type redirectAction struct {
+ status int
+ location string // a redirect's Location
+ file string // a rewrite's cleaned, rooted path within the tree
+}
+
+// resolve finds the first rule that applies to r's host and the cleaned path
+// p. exists is whether the tree has a file at p, which shadows every rule not
+// forced. rawPath and rawQuery are the request's own, used to refuse a
+// redirect to the URL being requested, which would loop.
+func (rs *redirectRules) resolve(host, p, rawPath, rawQuery string, exists bool) (redirectAction, bool) {
+ if rs == nil || len(rs.rules) == 0 {
+ return redirectAction{}, false
+ }
+ var reqSegs []string
+ if p != "/" {
+ reqSegs = strings.Split(p[1:], "/")
+ }
+ for i := range rs.rules {
+ rule := &rs.rules[i]
+ if exists && !rule.force {
+ continue
+ }
+ caps, ok := rule.match(host, reqSegs)
+ if !ok {
+ continue
+ }
+ if sp := caps["splat"]; sp != "" && strings.HasSuffix(rawPath, "/") {
+ // The cleaned path lost it, and a page moved from /about/ to
+ // /about resolves its relative links against the wrong directory.
+ caps["splat"] = sp + "/"
+ }
+ if !isRedirect(rule.status) {
+ // The template's own query and fragment are cut BEFORE the
+ // captures go in: a decoded capture can hold "?" or "#", and it
+ // is part of the file's name, not the end of it.
+ to := rule.to
+ if k := strings.IndexAny(to, "?#"); k >= 0 {
+ to = to[:k]
+ }
+ return redirectAction{status: rule.status, file: path.Clean("/" + expand(to, caps, false))}, true
+ }
+ // loc is the destination after any scheme://host: path, query.
+ loc := expand(rule.to, caps, true)
+ frag := ""
+ if k := strings.IndexByte(loc, '#'); k >= 0 {
+ loc, frag = loc[:k], loc[k:]
+ }
+ if rule.prefix == "" {
+ // Cleaned here, as http.Redirect would clean it, so the loop
+ // check below sees the Location that is actually sent. Clean
+ // also folds the "//" an empty capture can leave at the front,
+ // which a browser would read as another host.
+ pth, q, hasQ := strings.Cut(loc, "?")
+ trailing := strings.HasSuffix(pth, "/")
+ pth = path.Clean("/" + pth)
+ if trailing && pth != "/" {
+ pth += "/"
+ }
+ loc = pth
+ if hasQ {
+ loc += "?" + q
+ }
+ }
+ if !strings.Contains(loc, "?") && rawQuery != "" {
+ loc += "?" + rawQuery
+ }
+ if rule.prefix == "" || hostOf(rule.prefix) == host {
+ pth, q, _ := strings.Cut(loc, "?")
+ if pth == "" {
+ pth = "/"
+ }
+ if u, err := url.Parse(pth); err == nil && u.Path == rawPath && q == rawQuery {
+ continue // a redirect to this very URL
+ }
+ }
+ return redirectAction{status: rule.status, location: rule.prefix + loc + frag}, true
+ }
+ return redirectAction{}, false
+}
+
+// hostOf is the host of a "scheme://host[:port]" prefix, normalized as
+// requestHost normalizes a request's.
+func hostOf(prefix string) string {
+ _, h, _ := strings.Cut(prefix, "://")
+ return requestHost(h)
+}
diff --git a/internal/edge/redirects_test.go b/internal/edge/redirects_test.go
new file mode 100644
index 000000000..8495a3537
--- /dev/null
+++ b/internal/edge/redirects_test.go
@@ -0,0 +1,319 @@
+package edge
+
+import (
+ "context"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestParseRedirectsAcceptsTheSupportedForms(t *testing.T) {
+ rs, err := parseRedirects([]byte(`# a comment
+
+/old /new
+/blog/* /posts/:splat 302
+/docs/:slug /guide/:slug 307 # trailing comment
+/app/* /app/index.html 200
+/* /404.html 404
+/forced /elsewhere 301!
+https://www.example.com/* https://example.com/:splat 301!
+/ext https://example.org/x?y=1 308
+` + "/crlf /there 303\r\n"))
+ if err != nil {
+ t.Fatalf("parseRedirects: %v", err)
+ }
+ if _, err := parseRedirects([]byte("\ufeff/bom /ok\n")); err != nil {
+ t.Fatalf("a file starting with a byte-order mark: %v", err)
+ }
+ if got := len(rs.rules); got != 9 {
+ t.Fatalf("parsed %d rules, want 9", got)
+ }
+ if r := rs.rules[0]; r.status != 301 || r.force || r.line != 3 {
+ t.Fatalf("rule 0 = %+v, want a 301 from line 3, not forced", r)
+ }
+ if r := rs.rules[6]; r.host != "www.example.com" || !r.force || !r.splat || r.prefix != "https://example.com" {
+ t.Fatalf("host rule = %+v", r)
+ }
+}
+
+func TestParseRedirectsRefusesWhatItCannotServe(t *testing.T) {
+ for _, line := range []string{
+ "/only-a-source",
+ "/a /b 301 Country=us",
+ "/a /b 301 extra",
+ "/a /b 418",
+ "/a /b 200x",
+ "a /b",
+ "/caf%ZZ /c",
+ "/a%2Fb /c",
+ "/%3Aname /c",
+ "/a b",
+ "/a //evil.example",
+ `/a /\evil.example`,
+ "/a/*/b /c",
+ "/a* /c",
+ "/:splat /c",
+ "/:a/:a /c",
+ "/:a-b /c",
+ "/a//b /c",
+ "/a?x=1 /c",
+ "/a/../b /c",
+ "https://www.example.com:8443/ /c",
+ "/a https://example.org/ 200",
+ "/a https://example.org/ 404",
+ "/a ftp://example.org/",
+ "/a https:///nohost",
+ } {
+ rs, err := parseRedirects([]byte(line + "\n"))
+ if err == nil {
+ t.Errorf("parseRedirects(%q) = %+v, want an error", line, rs.rules)
+ continue
+ }
+ if !strings.Contains(err.Error(), "line 1:") {
+ t.Errorf("parseRedirects(%q) error %q does not name the line", line, err)
+ }
+ if len(rs.rules) != 0 {
+ t.Errorf("parseRedirects(%q) kept %d rules", line, len(rs.rules))
+ }
+ }
+}
+
+func TestParseRedirectsKeepsTheGoodLinesAroundABadOne(t *testing.T) {
+ rs, err := parseRedirects([]byte("/a /b\n/c /d 999\n/e /f\n"))
+ if err == nil || !strings.Contains(err.Error(), "line 2:") {
+ t.Fatalf("err = %v, want one naming line 2", err)
+ }
+ if len(rs.rules) != 2 || rs.rules[1].line != 3 {
+ t.Fatalf("rules = %+v, want lines 1 and 3", rs.rules)
+ }
+}
+
+func TestParseRedirectsLimits(t *testing.T) {
+ if _, err := parseRedirects([]byte(strings.Repeat("#", redirectsMaxBytes+1))); err == nil {
+ t.Fatal("an oversized file parsed")
+ }
+ var b strings.Builder
+ for range redirectsMaxRules + 1 {
+ b.WriteString("/a /b\n")
+ }
+ rs, err := parseRedirects([]byte(b.String()))
+ if err == nil || len(rs.rules) != redirectsMaxRules {
+ t.Fatalf("%d rules, err %v: want %d rules and an error", len(rs.rules), err, redirectsMaxRules)
+ }
+}
+
+func TestPackDirRefusesABadRedirectsFile(t *testing.T) {
+ dir := writeTree(t, map[string]string{
+ "index.html": "home",
+ "_redirects": "/ok /fine\n/a /b 418\n",
+ })
+ _, err := PackDir(dir)
+ if err == nil || !strings.Contains(err.Error(), "_redirects") || !strings.Contains(err.Error(), "line 2:") {
+ t.Fatalf("PackDir = %v, want a refusal naming _redirects line 2", err)
+ }
+ // Only the root file is the rules file; one deeper is content.
+ if _, err := PackDir(writeTree(t, map[string]string{"sub/_redirects": "not rules"})); err != nil {
+ t.Fatalf("PackDir with a nested _redirects: %v", err)
+ }
+}
+
+// redirectFixture ships a site with a _redirects file behind a static
+// handler, and returns a client that does not follow redirects.
+func redirectFixture(t *testing.T, rules string) (*httptest.Server, *http.Client) {
+ t.Helper()
+ store := release.NewDirStore(filepath.Join(t.TempDir(), "store"))
+ priv, pub := testKeys(t)
+ shipSite(t, store, "site", "v1", map[string]string{
+ "index.html": "home",
+ "old.html": "old page",
+ "forced.html": "forced page",
+ "style.css": "body{}",
+ "about/index.html": "about",
+ "app/index.html": "the app",
+ "docs/page.html": "a doc",
+ "404.html": "<!doctype html><p>custom not found",
+ "_redirects": rules,
+ })
+ if err := release.Promote(context.Background(), store, priv, release.OpsAccount, "site", "canary/x", "v1"); err != nil {
+ t.Fatalf("Promote: %v", err)
+ }
+ rt := registry.Route{Host: "site.example.com", Kind: "static", App: "site", Channel: "canary/x", Status: "ready"}
+ h := newStaticHandler(rt, store, pub, filepath.Join(t.TempDir(), "cache"))
+ srv := httptest.NewServer(h)
+ t.Cleanup(srv.Close)
+ client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
+ return srv, client
+}
+
+type redirectResult struct {
+ code int
+ location string
+ body string
+ resp *http.Response
+}
+
+func fetch(t *testing.T, c *http.Client, method, base, host, path string) redirectResult {
+ t.Helper()
+ req, err := http.NewRequest(method, base+path, nil)
+ if err != nil {
+ t.Fatalf("new request: %v", err)
+ }
+ req.Host = host
+ resp, err := c.Do(req)
+ if err != nil {
+ t.Fatalf("%s %s: %v", method, path, err)
+ }
+ defer resp.Body.Close()
+ b, _ := io.ReadAll(resp.Body)
+ return redirectResult{resp.StatusCode, resp.Header.Get("Location"), string(b), resp}
+}
+
+const siteRules = `
+/old.html /new.html 301
+/forced.html /elsewhere 302!
+/blog/* /posts/:splat 301
+/docs/:slug /guide/:slug?from=:slug 302
+/app/* /app/index.html 200
+https://www.example.com/* https://site.example.com/:splat 301!
+/loop/ /loop 301
+/ext https://example.org/x?y=1 307
+/empty/* /:splat/tail 301
+/frag /target#section 302
+/slash/* /slash/:splat/ 301!
+/caf%C3%A9 /cafe 301
+/files/:n /docs/:n.html 200
+/missing-rewrite /no/such/file 200
+/* /404.html 404
+`
+
+func TestStaticRedirects(t *testing.T) {
+ srv, c := redirectFixture(t, siteRules)
+ const host = "site.example.com"
+ for _, tc := range []struct {
+ host, path string
+ code int
+ location string // checked when code is a redirect
+ body string // checked when code is not
+ }{
+ // A file at the path shadows a rule; "!" overrides that.
+ {host, "/old.html", 200, "", "old page"},
+ {host, "/forced.html", 302, "/elsewhere", ""},
+ // Splats, placeholders, query strings.
+ {host, "/blog/2024/hello", 301, "/posts/2024/hello", ""},
+ {host, "/blog/2024/hello?x=1", 301, "/posts/2024/hello?x=1", ""},
+ {host, "/blog/2024/", 301, "/posts/2024/", ""},
+ {host, "/blog", 301, "/posts/", ""},
+ {host, "/docs/a%20b", 302, "/guide/a%20b?from=a+b", ""},
+ // Paths match decoded, as files are found: %2F separates segments.
+ {host, "/docs/a%2Fb", 404, "", "<!doctype html><p>custom not found"},
+ {host, "/ext?a=b", 307, "https://example.org/x?y=1", ""},
+ {host, "/frag?q=1", 302, "/target?q=1#section", ""},
+ // An existing file outside the rules is untouched.
+ {host, "/docs/page.html", 200, "", "a doc"},
+ // A directory with an index exists; a path under it is rewritten.
+ {host, "/app/", 200, "", "the app"},
+ {host, "/app/settings/profile", 200, "", "the app"},
+ // A redirect to the URL requested is skipped, not looped.
+ {host, "/loop/", 301, "/loop", ""},
+ {host, "/loop", 404, "", "<!doctype html><p>custom not found"},
+ // An empty capture never yields a protocol-relative Location.
+ {host, "/empty/", 301, "/tail", ""},
+ // A trailing-slash rule stops once the slash is there.
+ {host, "/slash/x", 301, "/slash/x/", ""},
+ {host, "/slash/x/", 404, "", "<!doctype html><p>custom not found"},
+ // A percent-encoded source matches the decoded request.
+ {host, "/caf%C3%A9", 301, "/cafe", ""},
+ // A "#" in a capture is part of the file name, not its end.
+ {host, "/files/page", 200, "", "a doc"},
+ {host, "/files/page.html%23", 404, "", "404 page not found\n"},
+ // The host rule matches its host only, and keeps the trailing slash.
+ {"www.example.com", "/about/", 301, "https://site.example.com/about/", ""},
+ {"WWW.Example.com.", "/style.css", 301, "https://site.example.com/style.css", ""},
+ {host, "/about/", 200, "", "about"},
+ // A rewrite to nothing is a plain 404; everything else hits /*.
+ {host, "/missing-rewrite", 404, "", "404 page not found\n"},
+ {host, "/nope", 404, "", "<!doctype html><p>custom not found"},
+ // The rules file is never served.
+ {host, "/_redirects", 404, "", "<!doctype html><p>custom not found"},
+ } {
+ got := fetch(t, c, "GET", srv.URL, tc.host, tc.path)
+ if got.code != tc.code {
+ t.Errorf("GET %s%s = %d (Location %q, body %q), want %d", tc.host, tc.path, got.code, got.location, got.body, tc.code)
+ continue
+ }
+ if tc.code >= 300 && tc.code < 400 {
+ if got.location != tc.location {
+ t.Errorf("GET %s%s Location = %q, want %q", tc.host, tc.path, got.location, tc.location)
+ }
+ } else if got.body != tc.body {
+ t.Errorf("GET %s%s body = %q, want %q", tc.host, tc.path, got.body, tc.body)
+ }
+ if v := got.resp.Header.Get("X-Carlos-Version"); v != "v1" {
+ t.Errorf("GET %s%s X-Carlos-Version = %q, want v1", tc.host, tc.path, v)
+ }
+ if cc := got.resp.Header.Get("Cache-Control"); cc != "no-cache" && tc.code != 404 {
+ t.Errorf("GET %s%s Cache-Control = %q, want no-cache", tc.host, tc.path, cc)
+ }
+ }
+
+ // The not-found page is typed and carries no validator.
+ got := fetch(t, c, "GET", srv.URL, host, "/nope")
+ if ct := got.resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
+ t.Errorf("404 page Content-Type = %q, want text/html", ct)
+ }
+ if et := got.resp.Header.Get("ETag"); et != "" {
+ t.Errorf("404 page ETag = %q, want none", et)
+ }
+ if head := fetch(t, c, "HEAD", srv.URL, host, "/nope"); head.code != 404 || head.body != "" {
+ t.Errorf("HEAD /nope = %d %q, want 404 with no body", head.code, head.body)
+ }
+}
+
+func TestStaticSPAFallbackLeavesAssetsAlone(t *testing.T) {
+ srv, c := redirectFixture(t, "/* /index.html 200\n")
+ const host = "site.example.com"
+ for path, want := range map[string]string{
+ "/style.css": "body{}",
+ "/about/": "about",
+ "/some/client/route": "home",
+ "/_redirects": "home",
+ } {
+ if got := fetch(t, c, "GET", srv.URL, host, path); got.code != 200 || got.body != want {
+ t.Errorf("GET %s = %d %q, want 200 %q", path, got.code, got.body, want)
+ }
+ }
+ // A rewritten page revalidates like the file it serves.
+ first := fetch(t, c, "GET", srv.URL, host, "/some/route")
+ tag := first.resp.Header.Get("ETag")
+ if tag == "" {
+ t.Fatal("rewritten page has no ETag")
+ }
+ req, _ := http.NewRequest("GET", srv.URL+"/other/route", nil)
+ req.Host = host
+ req.Header.Set("If-None-Match", tag)
+ resp, err := c.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resp.Body.Close()
+ if resp.StatusCode != http.StatusNotModified {
+ t.Fatalf("conditional GET of a rewritten page = %d, want 304", resp.StatusCode)
+ }
+}
+
+func TestStaticWithoutRedirectsIsUnchanged(t *testing.T) {
+ _, _, _, srv := staticFixture(t)
+ c := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
+ if got := fetch(t, c, "GET", srv.URL, "site.example.com", "/missing"); got.code != 404 || got.body != "404 page not found\n" {
+ t.Fatalf("GET /missing = %d %q, want the plain 404", got.code, got.body)
+ }
+ if got := fetch(t, c, "GET", srv.URL, "site.example.com", "/docs/page.html/index.html"); got.code != 404 {
+ t.Fatalf("GET under a file = %d, want 404", got.code)
+ }
+}
diff --git a/internal/edge/static.go b/internal/edge/static.go
index a61779f77..b1aa0ae07 100644
--- a/internal/edge/static.go
+++ b/internal/edge/static.go
@@ -28,6 +28,7 @@ import (
"path"
"path/filepath"
"sort"
+ "strconv"
"strings"
"sync"
"sync/atomic"
@@ -96,6 +97,14 @@ func PackDir(dir string) ([]byte, error) {
if _, err := tw.Write(b); err != nil {
return nil, err
}
+ if name == redirectsFile {
+ // Refused here rather than skipped at the edge, so a rule the
+ // edge cannot use is an error the shipper sees, not a redirect
+ // that silently never happens.
+ if _, err := parseRedirects(b); err != nil {
+ return nil, fmt.Errorf("pack %s: %s: %w", dir, name, err)
+ }
+ }
}
if err := tw.Close(); err != nil {
return nil, err
@@ -123,10 +132,11 @@ type staticSite struct {
recheck time.Duration
mu sync.Mutex
- version string // currently served version; "" until first resolve
- checkedAt time.Time // when the pointer was last read
- digest string // the served tree's manifest digest
- name string // the served tree's directory: siteDirName(version, digest)
+ version string // currently served version; "" until first resolve
+ checkedAt time.Time // when the pointer was last read
+ digest string // the served tree's manifest digest
+ name string // the served tree's directory: siteDirName(version, digest)
+ redirects *redirectRules // the served tree's _redirects; nil when it has none
// ratchet is #253's per-box ratchet for this process's pointer reads
// (Edge.staticRatchet); nil admits every verified pointer.
@@ -152,7 +162,7 @@ func newStaticHandler(rt registry.Route, store release.Store, pub ed25519.Public
}
func (s *staticSite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- dir, version, err := s.currentDir(r.Context())
+ tree, err := s.currentDir(r.Context())
if err != nil {
// Nothing has ever resolved here, so there is no version to name:
// this 502 stays unstamped rather than claiming a build it isn't
@@ -172,7 +182,7 @@ func (s *staticSite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// stamps what is actually served, never the pointer's unreachable head.
// Set before any body is written, so it lands on the tree's own 404s too
// (the handler serving that tree IS a version).
- w.Header().Set("X-Carlos-Version", version)
+ w.Header().Set("X-Carlos-Version", tree.version)
// A page out of the tree is revalidated before it is reused, and so is
// the tree's own 404. With Last-Modified alone a browser GUESSES a
// freshness lifetime — typically a tenth of the file's age — and reuses
@@ -185,7 +195,15 @@ func (s *staticSite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// from the errors it writes itself, a 416 or a 400: neither is a status a
// browser caches on a guess.)
w.Header().Set("Cache-Control", "no-cache")
- s.serveTree(dir, w, r)
+ s.serveTree(tree, w, r)
+}
+
+// servedTree is what one request is answered from: an extracted tree, the
+// version it holds, and the rules its _redirects declared. They travel
+// together so a request never pairs one version's files with another's rules.
+type servedTree struct {
+ dir, version string
+ redirects *redirectRules
}
// dir is where the tree named name lives once extracted.
@@ -216,7 +234,8 @@ func (s *staticSite) appDir() string {
// afterwards so ServeHTTP can stamp X-Carlos-Version without re-taking mu (and
// without the window in between, where a concurrent resolve could swap
// s.version and make the header describe a different tree than the one served).
-func (s *staticSite) currentDir(ctx context.Context) (dir, version string, err error) {
+// The tree's redirect rules come back with it for the same reason.
+func (s *staticSite) currentDir(ctx context.Context) (servedTree, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.version != "" && s.recheck > 0 && time.Since(s.checkedAt) < s.recheck {
@@ -224,7 +243,7 @@ func (s *staticSite) currentDir(ctx context.Context) (dir, version string, err e
// version, and stamping that version is honest by construction — the
// header names what the bytes are, not what the pointer may have
// advanced to since the last check.
- return s.dir(s.name), s.version, nil
+ return s.served(), nil
}
cp, err := release.Channel(ctx, s.store, s.pub, s.acct, s.app, s.channel)
if err != nil {
@@ -242,22 +261,48 @@ func (s *staticSite) currentDir(ctx context.Context) (dir, version string, err e
return s.stale(fmt.Errorf("swap current -> %s: %w", name, err))
}
s.version, s.digest, s.name = cp.Version, digest, name
+ s.redirects = s.loadRedirects(s.dir(name), cp.Version)
}
s.checkedAt = time.Now()
- return s.dir(s.name), s.version, nil
+ return s.served(), nil
+}
+
+// served is the tree currently being served. The caller holds mu.
+func (s *staticSite) served() servedTree {
+ return servedTree{dir: s.dir(s.name), version: s.version, redirects: s.redirects}
+}
+
+// loadRedirects parses the _redirects at the root of an extracted tree, once
+// per version swap. A tree without one has no rules. A line that does not
+// parse is logged and skipped: ship refuses such a tree (PackDir), so only a
+// tree packed some other way gets here, and its remaining rules still apply.
+func (s *staticSite) loadRedirects(dir, version string) *redirectRules {
+ data, err := os.ReadFile(filepath.Join(dir, redirectsFile))
+ if errors.Is(err, fs.ErrNotExist) {
+ return nil
+ }
+ if err != nil {
+ log.Printf("edge: static %s/%s: %s %s: %v — serving without its rules", s.app, s.channel, version, redirectsFile, err)
+ return nil
+ }
+ rules, err := parseRedirects(data)
+ if err != nil {
+ log.Printf("edge: static %s/%s: %s %s: %s", s.app, s.channel, version, redirectsFile, strings.ReplaceAll(err.Error(), "\n", "; "))
+ }
+ return rules
}
// stale is the degraded path: keep serving the version we already verified
// and extracted — and name it, so the response header describes the CACHED
// tree rather than the head this handler could not reach — or fail if there
// has never been one.
-func (s *staticSite) stale(err error) (string, string, error) {
+func (s *staticSite) stale(err error) (servedTree, error) {
if s.version == "" {
- return "", "", err
+ return servedTree{}, err
}
log.Printf("edge: static %s/%s: %v — still serving cached %s", s.app, s.channel, err, s.version)
s.checkedAt = time.Now() // back off: retry next recheck window, not next request
- return s.dir(s.name), s.version, nil
+ return s.served(), nil
}
// ensureExtracted makes the version's verified tree exist and returns its
@@ -398,7 +443,9 @@ func extractSiteTar(data []byte, dst string) error {
// serveTree answers one request from an extracted version dir:
// http.FileServer semantics with index.html for directories and a clean 404
-// (never a directory listing) when neither the file nor an index exists.
+// (never a directory listing) when neither the file nor an index exists —
+// after the tree's _redirects (redirects.go) has had its say. A rule applies
+// only where no file answers the path, unless it is forced.
//
// It serves the file itself rather than through http.ServeFile, because
// ServeFile offers the file's mtime as Last-Modified and answers
@@ -412,7 +459,7 @@ func extractSiteTar(data []byte, dst string) error {
// The validator is the file's content (tagCache). What ServeFile did besides
// is kept by hand: the refusal of a ".." element, and the redirect off a
// trailing /index.html onto its directory.
-func (s *staticSite) serveTree(dir string, w http.ResponseWriter, r *http.Request) {
+func (s *staticSite) serveTree(tree servedTree, w http.ResponseWriter, r *http.Request) {
if containsDotDot(r.URL.Path) {
// The Clean below would resolve it harmlessly inside the tree; this
// keeps the answer ServeFile always gave such a path.
@@ -421,13 +468,12 @@ func (s *staticSite) serveTree(dir string, w http.ResponseWriter, r *http.Reques
}
// Clean rooted at "/" so no request path can climb out of the tree.
p := path.Clean("/" + r.URL.Path)
- fp := filepath.Join(dir, filepath.FromSlash(p))
- fi, err := os.Stat(fp)
- if err == nil && fi.IsDir() {
- fp = filepath.Join(fp, "index.html")
- fi, err = os.Stat(fp)
+ fp, fi, err := lookupTree(tree.dir, p)
+ if act, ok := tree.redirects.resolve(requestHost(r.Host), p, r.URL.Path, r.URL.RawQuery, err == nil); ok {
+ s.serveRule(tree.dir, act, w, r)
+ return
}
- if err != nil || fi.IsDir() {
+ if err != nil {
http.NotFound(w, r)
return
}
@@ -442,6 +488,75 @@ func (s *staticSite) serveTree(dir string, w http.ResponseWriter, r *http.Reques
w.WriteHeader(http.StatusMovedPermanently)
return
}
+ s.serveFile(fp, p, fi, w, r)
+}
+
+// lookupTree finds the file that answers the cleaned path p in an extracted
+// tree: the file itself, or a directory's index.html. The _redirects file is
+// configuration and answers nothing.
+func lookupTree(dir, p string) (string, os.FileInfo, error) {
+ if p == "/"+redirectsFile {
+ return "", nil, fs.ErrNotExist
+ }
+ fp := filepath.Join(dir, filepath.FromSlash(p))
+ fi, err := os.Stat(fp)
+ if err == nil && fi.IsDir() {
+ fp = filepath.Join(fp, "index.html")
+ fi, err = os.Stat(fp)
+ }
+ if err == nil && fi.IsDir() {
+ err = fs.ErrNotExist
+ }
+ return fp, fi, err
+}
+
+// serveRule answers with what a matched _redirects rule asked for: a
+// redirect, or another file of the tree served at this URL — with a 200 for a
+// rewrite and a 404 for a not-found page. A rewrite to a path with no file is
+// a plain 404; rules are not consulted a second time.
+func (s *staticSite) serveRule(dir string, act redirectAction, w http.ResponseWriter, r *http.Request) {
+ if act.location != "" {
+ http.Redirect(w, r, act.location, act.status)
+ return
+ }
+ fp, fi, err := lookupTree(dir, act.file)
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ if act.status == http.StatusOK {
+ s.serveFile(fp, act.file, fi, w, r)
+ return
+ }
+ // A not-found page carries no validator and answers no conditional or
+ // range request: ServeContent knows only how to succeed.
+ f, err := os.Open(fp)
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ defer f.Close()
+ ctype := mime.TypeByExtension(filepath.Ext(fp))
+ if ctype == "" {
+ var head [512]byte
+ n, _ := io.ReadFull(f, head[:])
+ ctype = http.DetectContentType(head[:n])
+ if _, err := f.Seek(0, io.SeekStart); err != nil {
+ http.NotFound(w, r)
+ return
+ }
+ }
+ w.Header().Set("Content-Type", ctype)
+ w.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10))
+ w.WriteHeader(act.status)
+ if r.Method != http.MethodHead {
+ io.Copy(w, f)
+ }
+}
+
+// serveFile sends the tree file at fp, whose tree path is p (for logs), with
+// a content tag and net/http's conditional and range handling.
+func (s *staticSite) serveFile(fp, p string, fi os.FileInfo, w http.ResponseWriter, r *http.Request) {
f, err := os.Open(fp)
if err != nil {
// ServeFile's mapping, kept: a file that vanished since the Stat is