design rev 4: the holding entry's shape and rendering rule without record_seq
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+2
−2
docs/superpowers/specs/2026-10-21-self-serve-offline-moves-design.md+2 −2
CI passed — run details
CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok amadan.net/carlos/platform.git/launchctx 0.013s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
=== vet (1 s)
=== gofmt (0 s)
gofmt -l .: clean
=== rest (141 s)
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.754s
ok github.com/carlosframework/platform/cmd/moveprobe 11.887s
ok github.com/carlosframework/platform/cmd/zddprobe 0.744s
ok github.com/carlosframework/platform/internal/activator 60.345s
ok github.com/carlosframework/platform/internal/agentsock 0.016s
ok github.com/carlosframework/platform/internal/alerting 0.006s
ok github.com/carlosframework/platform/internal/analytics 0.030s
ok github.com/carlosframework/platform/internal/appconfig 2.989s
ok github.com/carlosframework/platform/internal/apperr 0.125s
ok github.com/carlosframework/platform/internal/applog 0.307s
ok github.com/carlosframework/platform/internal/awsinv 0.007s
ok github.com/carlosframework/platform/internal/boxchoice 0.005s
ok github.com/carlosframework/platform/internal/boxid 1.088s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.009s
ok github.com/carlosframework/platform/internal/caps 0.022s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.792s
ok github.com/carlosframework/platform/internal/changelog 0.508s
ok github.com/carlosframework/platform/internal/claimactivation 1.257s
ok github.com/carlosframework/platform/internal/clamd 0.061s
? 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 6.568s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.014s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.003s
ok github.com/carlosframework/platform/internal/docsite 0.011s
ok github.com/carlosframework/platform/internal/domains 2.905s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 45.418s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.011s
ok github.com/carlosframework/platform/internal/fleetapi 0.220s
ok github.com/carlosframework/platform/internal/gate 1.797s
ok github.com/carlosframework/platform/internal/genselect 0.020s
ok github.com/carlosframework/platform/internal/geo 0.014s
ok github.com/carlosframework/platform/internal/geoip 0.050s
ok github.com/carlosframework/platform/internal/historyreadback 0.312s
ok github.com/carlosframework/platform/internal/host 138.096s
ok github.com/carlosframework/platform/internal/hostactors 6.316s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.007s
ok github.com/carlosframework/platform/internal/hostbox 3.202s
ok github.com/carlosframework/platform/internal/hostclaim 0.838s
ok github.com/carlosframework/platform/internal/hostenv 0.009s
ok github.com/carlosframework/platform/internal/hostevidence 0.006s
ok github.com/carlosframework/platform/internal/hostexec 5.615s
ok github.com/carlosframework/platform/internal/hostfence 0.045s
ok github.com/carlosframework/platform/internal/hostfiles 10.293s
ok github.com/carlosframework/platform/internal/hostfloor 0.043s
ok github.com/carlosframework/platform/internal/hostguard 1.077s
ok github.com/carlosframework/platform/internal/hostops 2.259s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.003s
ok github.com/carlosframework/platform/internal/hostquarantine 0.003s
ok github.com/carlosframework/platform/internal/hostrelocate 0.756s
ok github.com/carlosframework/platform/internal/hostresolve 0.195s
ok github.com/carlosframework/platform/internal/hostwrite 0.010s
ok github.com/carlosframework/platform/internal/infraview 0.004s
ok github.com/carlosframework/platform/internal/instancectl 5.259s
ok github.com/carlosframework/platform/internal/instanceexec 2.826s
ok github.com/carlosframework/platform/internal/instancefs 0.009s
ok github.com/carlosframework/platform/internal/instancekey 0.009s
ok github.com/carlosframework/platform/internal/instanceupdates 0.011s
ok github.com/carlosframework/platform/internal/layout 1.965s
ok github.com/carlosframework/platform/internal/lbhealth 0.004s
ok github.com/carlosframework/platform/internal/lease 10.226s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.603s
ok github.com/carlosframework/platform/internal/ledger 0.024s
ok github.com/carlosframework/platform/internal/mail 0.296s
ok github.com/carlosframework/platform/internal/maintenance 0.088s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.620s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.097s
ok github.com/carlosframework/platform/internal/metrics 0.008s
ok github.com/carlosframework/platform/internal/moves 21.506s
ok github.com/carlosframework/platform/internal/moves/archive 1.068s
ok github.com/carlosframework/platform/internal/objstore 0.325s
ok github.com/carlosframework/platform/internal/ops 0.207s
ok github.com/carlosframework/platform/internal/park 0.253s
ok github.com/carlosframework/platform/internal/plans 0.005s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 13.735s
ok github.com/carlosframework/platform/internal/publicrelease 0.010s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.621s
ok github.com/carlosframework/platform/internal/relay 7.227s
ok github.com/carlosframework/platform/internal/release 25.069s
ok github.com/carlosframework/platform/internal/rollout 3.898s
ok github.com/carlosframework/platform/internal/rolloutwords 0.005s
ok github.com/carlosframework/platform/internal/rootaws 0.012s
ok github.com/carlosframework/platform/internal/rootparticipation 0.065s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/runs 0.553s
ok github.com/carlosframework/platform/internal/s3compat 0.007s
ok github.com/carlosframework/platform/internal/safefile 0.005s
ok github.com/carlosframework/platform/internal/schedule 2.760s
ok github.com/carlosframework/platform/internal/seeds 0.018s
ok github.com/carlosframework/platform/internal/sim/hostlife 11.336s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.880s
ok github.com/carlosframework/platform/internal/status 0.081s
ok github.com/carlosframework/platform/internal/steering 0.007s
ok github.com/carlosframework/platform/internal/stopevidence 0.224s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.005s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.006s
ok github.com/carlosframework/platform/internal/unixdial 0.004s
ok github.com/carlosframework/platform/internal/updatewords 0.006s
ok github.com/carlosframework/platform/internal/usage 0.006s
ok github.com/carlosframework/platform/internal/version 0.003s
ok github.com/carlosframework/platform/internal/vet 1.657s
=== internal_console@0 (78 s)
ok github.com/carlosframework/platform/internal/console 75.064s
=== internal_console@1 (38 s)
ok github.com/carlosframework/platform/internal/console 35.548s
=== internal_console@2 (43 s)
ok github.com/carlosframework/platform/internal/console 40.424s
=== internal_console@3 (37 s)
ok github.com/carlosframework/platform/internal/console 35.614s
=== internal_console@4 (37 s)
ok github.com/carlosframework/platform/internal/console 35.420s
=== internal_console@5 (35 s)
ok github.com/carlosframework/platform/internal/console 33.143s
=== internal_console@6 (36 s)
ok github.com/carlosframework/platform/internal/console 34.624s
=== internal_console@7 (43 s)
ok github.com/carlosframework/platform/internal/console 41.101s
=== root@0 (32 s)
ok github.com/carlosframework/platform 30.779s
=== root@1 (44 s)
ok github.com/carlosframework/platform 42.530s
=== root@2 (41 s)
ok github.com/carlosframework/platform 39.747s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 50069666 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.024s
=== RUN TestT4StorageProbeMinio
2026/10/21 22:32:59 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0AC315653A89F, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.022s
minio contract: PASS
diff --git a/docs/superpowers/specs/2026-10-21-self-serve-offline-moves-design.md b/docs/superpowers/specs/2026-10-21-self-serve-offline-moves-design.md| index 452074b5b..37b5597ad 100644 |
| --- a/docs/superpowers/specs/2026-10-21-self-serve-offline-moves-design.md |
| +++ b/docs/superpowers/specs/2026-10-21-self-serve-offline-moves-design.md |
| @@ -115,8 +115,8 @@ The legacy move doors keep their paths and roles. With `CARLOS_SELF_SERVE_MOVES= |
| From `start` until the target's row is ready, the host is held on every source box and has no ready row anywhere. Today every edge answers a platform-domain host with the "not here yet" 503 retry page (`internal/edge/pending.go`) and a customer domain with 404. This design adds a maintenance answer, carried on the existing box to edges-file to edge channel: |
| -- **Publication.** Every box that holds the host (every source-region box; and the target box from `flip` until its row is ready) lists it in a new edges-file field, `offline_moves: [{host, aliases, move_id, record_seq, offline_at, estimate_seconds, display_name}]`, separate from `routes` so no decoder counts it as served (the seal door's "still listed" check reads `routes`; the builder audits every edges-file reader). `aliases` are exactly the alias rows the edges writer would publish for the host as a served route, under the same eligibility rules (DNS pointing, ownership, wildcard handling), so every custom domain that would reach the account gets the page and no other. A box stops publishing the entry when the record no longer carries that move id or when its own row for the host is ready. |
| -- **Rendering.** An edge answers a request for a host or alias that appears in a fresh `offline_moves` entry, from any publisher, and for which no fresh edges file lists a ready route, with 503, `Retry-After: 30`, `noindex`, and the holding page: "`<display_name>` is temporarily offline. Please check back in approximately N minutes." with N from `until` and the estimate, counting down; without a display name, "This account is temporarily offline. ...". A ready route anywhere wins over a holding entry (precedence), so the window between `open` and the first edges-file refresh is the pending page's known 503-then-retry window and nothing worse. The source box's own edge answers its held rows the same way from its registry (the home map excludes a box's own publication). The page reuses the pending page's safe reload mechanics (location.replace, sessionStorage) with its give-up replaced by `until`. |
| +- **Publication.** Every box that holds the host (every source-region box; and the target box from `flip` until its row is ready) lists it in a new edges-file field, `offline_moves: [{host, aliases, move_id, offline_at, estimate_seconds, display_name}]`, separate from `routes` so no decoder counts it as served (the seal door's "still listed" check reads `routes`; the builder audits every edges-file reader). `aliases` are exactly the alias rows the edges writer would publish for the host as a served route, under the same eligibility rules (DNS pointing, ownership, wildcard handling), so every custom domain that would reach the account gets the page and no other. A box stops publishing the entry when the record no longer carries that move id or when its own row for the host is ready. |
| +- **Rendering.** An edge answers a request for a host or alias that appears in an `offline_moves` entry from any publisher, and for which no publication carries a ready route tagged with that move (the ordering rule below), with 503, `Retry-After: 30`, `noindex`, and the holding page: "`<display_name>` is temporarily offline. Please check back in approximately N minutes." with N from `until` and the estimate, counting down; without a display name, "This account is temporarily offline. ...". A ready route anywhere wins over a holding entry (precedence), so the window between `open` and the first edges-file refresh is the pending page's known 503-then-retry window and nothing worse. The source box's own edge answers its held rows the same way from its registry (the home map excludes a box's own publication). The page reuses the pending page's safe reload mechanics (location.replace, sessionStorage) with its give-up replaced by `until`. |
| - **TLS.** A host or alias in a holding entry is allowed for TLS exactly as it would be as a served route (the same `Allowed` decision, wildcard and mirrored certificates included), so the page is reachable on HTTPS; the entry never widens what the host could already present. |
| - **The display name.** Tito supplies it per request; the console validates it (at most 80 characters, printable, no control characters, no newlines) and stores it in the move; the edge renders it through contextual template escaping. It is the one piece of request-supplied text the page shows, by design (the customer named their own account), and it is shown to unauthenticated visitors, as the account's own pages are. Nothing else from the request, no header, path, diagnostic or `requested_by`, reaches the page. |
| - **Ordering.** Claims-off records may carry no `record_seq`, so the order is causal, not a counter. A holding entry names its move id. A box publishes a ready route for a host **with `landed_move: <id>`** when its row was minted by that move's landing (the target), and plain otherwise. For a host with a holding entry for move `<id>` in any fresh publication, an edge serves the holding page unless some fresh publication carries a ready route for the host tagged `landed_move: <id>`; a plain ready route (the source's own stale publication from before the stop, retained as last-good) does not win over a holding entry. A ready route tagged with a move the holding entries do not name (a later move) wins. The source box's own edge answers its held rows the same way from its registry. This replaces rev 2's "a ready route anywhere wins". |