spec: edge asset cache rev 6 (Astra round 3 folded in)
Summary
States the freshness bound as an invariant over every input the catch decision reads (records, domain claims, manifests), requires ReconcileAliases to report completeness, adds fill and waiter deadlines, refuses HTML at admission, and arms the home first in rollout.
Agent notes
Findings committed unedited as CODEX-FINDINGS-ASSET-CACHE-R3.md. Third round finding a missed authority input, so the fix is an invariant plus a table test rather than one more case.
2 files changed,
+135
−21
docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md+78 −21docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R3.md+57 −0
CI passed — run details
CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok amadan.net/carlos/platform.git/launchctx 0.016s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
=== vet (1 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (143 s)
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.792s
ok github.com/carlosframework/platform/cmd/moveprobe 12.003s
ok github.com/carlosframework/platform/cmd/zddprobe 0.727s
ok github.com/carlosframework/platform/internal/activator 56.035s
ok github.com/carlosframework/platform/internal/agentsock 0.035s
ok github.com/carlosframework/platform/internal/alerting 0.007s
ok github.com/carlosframework/platform/internal/analytics 0.025s
ok github.com/carlosframework/platform/internal/appconfig 2.995s
ok github.com/carlosframework/platform/internal/apperr 0.130s
ok github.com/carlosframework/platform/internal/applog 0.294s
ok github.com/carlosframework/platform/internal/awsinv 0.014s
ok github.com/carlosframework/platform/internal/boxchoice 0.007s
ok github.com/carlosframework/platform/internal/boxid 0.537s
? 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.010s
ok github.com/carlosframework/platform/internal/caps 0.042s
ok github.com/carlosframework/platform/internal/certwarm 0.003s
ok github.com/carlosframework/platform/internal/cgroup 0.772s
ok github.com/carlosframework/platform/internal/changelog 0.547s
ok github.com/carlosframework/platform/internal/claimactivation 1.339s
ok github.com/carlosframework/platform/internal/clamd 0.059s
? 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 7.962s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.015s
ok github.com/carlosframework/platform/internal/dbquery 0.006s
ok github.com/carlosframework/platform/internal/dbreads 2.103s
? github.com/carlosframework/platform/internal/dbreads/dbreadstest [no test files]
ok github.com/carlosframework/platform/internal/dbverify 0.012s
ok github.com/carlosframework/platform/internal/dnsrec 0.004s
ok github.com/carlosframework/platform/internal/docsite 0.016s
ok github.com/carlosframework/platform/internal/domains 2.972s
ok github.com/carlosframework/platform/internal/doorkey 0.012s
ok github.com/carlosframework/platform/internal/economics 0.005s
ok github.com/carlosframework/platform/internal/edge 45.240s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.010s
ok github.com/carlosframework/platform/internal/fleetapi 0.247s
ok github.com/carlosframework/platform/internal/gate 1.996s
ok github.com/carlosframework/platform/internal/genselect 0.021s
ok github.com/carlosframework/platform/internal/geo 0.015s
ok github.com/carlosframework/platform/internal/geoip 0.049s
ok github.com/carlosframework/platform/internal/historyreadback 0.299s
ok github.com/carlosframework/platform/internal/host 140.043s
ok github.com/carlosframework/platform/internal/hostactors 6.108s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.019s
ok github.com/carlosframework/platform/internal/hostbox 3.326s
ok github.com/carlosframework/platform/internal/hostclaim 0.826s
ok github.com/carlosframework/platform/internal/hostenv 0.019s
ok github.com/carlosframework/platform/internal/hostevidence 0.019s
ok github.com/carlosframework/platform/internal/hostexec 5.413s
ok github.com/carlosframework/platform/internal/hostfence 0.076s
ok github.com/carlosframework/platform/internal/hostfiles 10.243s
ok github.com/carlosframework/platform/internal/hostfloor 0.019s
ok github.com/carlosframework/platform/internal/hostguard 1.076s
ok github.com/carlosframework/platform/internal/hostops 2.152s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.018s
ok github.com/carlosframework/platform/internal/hostquarantine 0.019s
ok github.com/carlosframework/platform/internal/hostrelocate 0.587s
ok github.com/carlosframework/platform/internal/hostresolve 0.182s
ok github.com/carlosframework/platform/internal/hostwrite 0.019s
ok github.com/carlosframework/platform/internal/infraview 0.020s
ok github.com/carlosframework/platform/internal/instancectl 5.258s
ok github.com/carlosframework/platform/internal/instanceexec 2.763s
ok github.com/carlosframework/platform/internal/instancefs 0.006s
ok github.com/carlosframework/platform/internal/instancekey 0.003s
ok github.com/carlosframework/platform/internal/instanceupdates 0.007s
ok github.com/carlosframework/platform/internal/layout 1.835s
ok github.com/carlosframework/platform/internal/lbhealth 0.008s
ok github.com/carlosframework/platform/internal/lease 10.101s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.502s
ok github.com/carlosframework/platform/internal/ledger 0.021s
ok github.com/carlosframework/platform/internal/mail 0.206s
ok github.com/carlosframework/platform/internal/maintenance 0.091s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.564s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.077s
ok github.com/carlosframework/platform/internal/metrics 0.006s
ok github.com/carlosframework/platform/internal/moves 21.354s
ok github.com/carlosframework/platform/internal/moves/archive 1.063s
ok github.com/carlosframework/platform/internal/objstore 0.247s
ok github.com/carlosframework/platform/internal/ops 0.261s
ok github.com/carlosframework/platform/internal/park 0.261s
ok github.com/carlosframework/platform/internal/plans 0.006s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 14.188s
ok github.com/carlosframework/platform/internal/publicrelease 0.007s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 5.703s
ok github.com/carlosframework/platform/internal/relay 7.241s
ok github.com/carlosframework/platform/internal/release 25.416s
ok github.com/carlosframework/platform/internal/rollout 3.710s
ok github.com/carlosframework/platform/internal/rolloutwords 0.005s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.059s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/runs 0.625s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.006s
ok github.com/carlosframework/platform/internal/schedule 2.778s
ok github.com/carlosframework/platform/internal/seeds 0.010s
ok github.com/carlosframework/platform/internal/sim/engines 2.833s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.401s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.485s
ok github.com/carlosframework/platform/internal/status 0.079s
ok github.com/carlosframework/platform/internal/steering 0.007s
ok github.com/carlosframework/platform/internal/stopevidence 0.254s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.006s
ok github.com/carlosframework/platform/internal/termfence 0.008s
ok github.com/carlosframework/platform/internal/tunnelclient 0.005s
ok github.com/carlosframework/platform/internal/unixdial 0.002s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.005s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.672s
=== internal_console@0 (85 s)
ok github.com/carlosframework/platform/internal/console 82.757s
=== internal_console@1 (44 s)
ok github.com/carlosframework/platform/internal/console 41.477s
=== internal_console@2 (41 s)
ok github.com/carlosframework/platform/internal/console 38.896s
=== internal_console@3 (40 s)
ok github.com/carlosframework/platform/internal/console 37.692s
=== internal_console@4 (42 s)
ok github.com/carlosframework/platform/internal/console 40.067s
=== internal_console@5 (40 s)
ok github.com/carlosframework/platform/internal/console 38.339s
=== internal_console@6 (36 s)
ok github.com/carlosframework/platform/internal/console 34.293s
=== internal_console@7 (45 s)
ok github.com/carlosframework/platform/internal/console 42.651s
=== root@0 (109 s)
ok github.com/carlosframework/platform 106.742s
=== root@1 (84 s)
ok github.com/carlosframework/platform 81.919s
=== root@2 (91 s)
ok github.com/carlosframework/platform 88.641s
=== 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: 50528418 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.022s
=== RUN TestT4StorageProbeMinio
2026/10/24 18:17:31 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E189FE48A5CB05, 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.00s)
PASS
ok github.com/carlosframework/platform/internal/host 0.020s
=== RUN TestS3StoreMinioContract
=== RUN TestS3StoreMinioContract/conditional_get_and_etag_listing
=== RUN TestS3StoreMinioContract/bump_through_every_mutation_path
--- PASS: TestS3StoreMinioContract (0.02s)
--- PASS: TestS3StoreMinioContract/conditional_get_and_etag_listing (0.01s)
--- PASS: TestS3StoreMinioContract/bump_through_every_mutation_path (0.01s)
PASS
ok github.com/carlosframework/platform/internal/release 0.029s
Bucket created successfully `local/dbf-deploy`.
Bucket created successfully `local/dbf-data-ie`.
Bucket created successfully `local/dbf-data-uk`.
local/dbf-deploy versioning is enabled
Created policy `dbf-console` successfully.
Created policy `dbf-ukbox` successfully.
Added user `dbfconsole` successfully.
Added user `dbfukbox` successfully.
Attached Policies: [dbf-console]
To User: dbfconsole
Attached Policies: [dbf-ukbox]
To User: dbfukbox
=== RUN TestDBForwardMinio
2026/10/24 18:17:34 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:17:34 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:17:34 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:17:34 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 18:17:34 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 18:17:34 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 18:17:34 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 18:17:34 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 18:17:34 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 18:17:34 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:17:34 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:17:34 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:17:34 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 18:17:34 console: could not notify member@example.com of a CLI token issue: keymail: lookup member@example.com: keymail: GET /api/federation/lookup?addr=member%40example.com&from=console%40127.0.0.1%3A43831: could not determine target mailbox
agent.go:135: dbreads: dbr_ea3xm5slmkdaeaiatvb7j45na4: planned 3 files (1544 bytes) and restored to 3 in 9ms
agent.go:135: dbreads: dbr_ea3xm5slmkdaeaiatvb7j45na4: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:17:35 console: db read dbr_ea3xm5slmkdaeaiatvb7j45na4: relayed 1 chunks (102 bytes) in 1.004s, ended done
agent.go:135: dbreads: dbr_z5jtvm4eurb3sey536ncvhp4ey: planned 1 files (720 bytes) and restored to 1 in 7ms
agent.go:135: dbreads: dbr_z5jtvm4eurb3sey536ncvhp4ey: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:17:36 console: db read dbr_z5jtvm4eurb3sey536ncvhp4ey: relayed 1 chunks (102 bytes) in 1.003s, ended done
agent.go:135: dbreads: dbr_kcbisahpbramiubcw7vuakvm3e: planned 3 files (1544 bytes) and held through 3 in 8ms
agent.go:135: dbreads: dbr_kcbisahpbramiubcw7vuakvm3e: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:17:37 console: db read dbr_kcbisahpbramiubcw7vuakvm3e: relayed 1 chunks (1641 bytes) in 1.004s, ended done
=== RUN TestDBForwardMinio/by_disconnect
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_2ck6lsgpfkzkheughhjubgvzbe: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 18:17:37 console: db read dbr_2ck6lsgpfkzkheughhjubgvzbe: relayed 0 chunks (0 bytes) in 12ms, ended client gone
agent.go:135: dbreads: dbr_2ck6lsgpfkzkheughhjubgvzbe: output 5 bytes in 1 confirmed chunks in 43ms
=== RUN TestDBForwardMinio/by_timeout
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_7nggtwznbgurs2d7cytgvjsu3q: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 18:17:57 console: db read dbr_7nggtwznbgurs2d7cytgvjsu3q: relayed 0 chunks (0 bytes) in 19.676s, ended no-result
agent.go:135: dbreads: dbr_7nggtwznbgurs2d7cytgvjsu3q: output 5 bytes in 1 confirmed chunks in 19.654s
--- PASS: TestDBForwardMinio (23.03s)
--- PASS: TestDBForwardMinio/by_disconnect (0.16s)
--- PASS: TestDBForwardMinio/by_timeout (19.78s)
PASS
ok github.com/carlosframework/platform/internal/console 23.048s
minio contract: PASS
diff --git a/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md b/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md| index 12844a412..ed8454056 100644 |
| --- a/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md |
| +++ b/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md |
| @@ -1,6 +1,6 @@ |
| # Edge asset cache: versioned build assets served from every edge |
| -Branch `edge-asset-cache`. Status: rev 5, 2026-10-24, builder check folded in; rulings R1 to R3 (§13). Approved by Paul 2026-10-24; Astra review round 3 next. |
| +Branch `edge-asset-cache`. Status: rev 6, 2026-10-24, builder check folded in; rulings R1 to R3 (§13). Approved by Paul 2026-10-24; Astra review round 4 next. |
| Related: media roadmap (`2026-09-07-media-platform-roadmap-design.md`), |
| which this does not amend; #368 (the shared cache stores 206 responses). |
| @@ -226,9 +226,9 @@ that catches gains: |
| which already holds them; no other box reads them. |
| - `gen` is the instance's asset generation (§6), copied from the instance |
| record through the route row. |
| -- Aliases (custom domains) publish the same block as their primary, with |
| - their own `at` (§4.2): an alias's ownership comes from domain claims, |
| - which are reconciled separately from the app's records. |
| +- Aliases (custom domains, and the exact names instance catch-alls |
| + derive) publish the same block as their primary, with their `at` |
| + computed by the same invariant (§4.2). |
| - Additive and `omitempty`: a file without it parses byte-identically, and |
| an older reader ignores it. |
| @@ -265,17 +265,38 @@ and the snapshot is published incomplete) does not advance that scope's |
| time. Publishing never advances it. The publisher copies the scope's |
| confirmed time into each host's `at`. |
| -An **alias** depends on more than the app's records: its ownership is a |
| -domain claim, read and applied by `ReconcileAliases` |
| -(`internal/domains/aliases.go:232`), which returns before touching any |
| -alias row when the claim listing fails, while the edges-file writer keeps |
| -publishing the existing rows (`internal/host/edgesfile.go:513`). So the box |
| -also records the completion time of its last `ReconcileAliases` pass that |
| -listed the claims and applied them without error, and an alias's `at` is |
| -the older of that time and its primary's scope time. A box that cannot |
| -read claims for an hour stops catching every alias it serves, on every |
| -edge **(astra R2-1)**. A catch-all (`*.parent`) alias is never published |
| -(§3.3 and `edgesfile.go:513-520`), so it adds no third dependency. |
| +**The invariant.** A host's `at` may only be a time at which *every* |
| +input the catch decision for that host reads was known complete and |
| +applied on the publishing box. Rounds 1 to 3 of review each found one |
| +more input left out (records, then aliases, then catch-all aliases and |
| +path rows), so the spec names the inputs instead of the cases |
| +**(astra R2-1, R3-1, R3-2)**. The catch decision (§3.3, §4.1) reads: |
| + |
| +1. **The app's records** (the instance record, its route row, gate, |
| + import gate, opt-out, generation): bounded by the record view's |
| + per-scope confirmed time above. |
| +2. **Domain claims**, which decide whether a hostname has path rows, |
| + which aliases exist, and which aliases instance catch-alls derive |
| + (`internal/domains/aliases.go`, `ReconcileAliases`): bounded by the |
| + box's **domain confirmed time**, the completion time of the last |
| + `ReconcileAliases` pass that was *complete*. Today the pass returns an |
| + error only when the claim listing fails; a failed label count for a |
| + catch-all (`aliases.go:161`), a failed alias upsert (`:310`) or a |
| + failed path delete (`:590`) is logged and the pass still returns nil. |
| + So `ReconcileAliases` must also report completeness, and any of those |
| + failures makes the pass incomplete. An incomplete pass does not |
| + advance the domain confirmed time. |
| +3. **Release manifests**: immutable once read, so they add no bound. |
| + |
| +Every published host, primary or alias, gets `at` = the older of (1) for |
| +its app's scope and (2) for the box. This applies to primary hostnames |
| +as much as aliases, because proving a hostname has **no** path row is a |
| +domain read too (R3-2). A box whose domain reads fail for an hour stops |
| +catching every host it serves, on every edge. Any new input a future |
| +change adds to the catch decision must bring its own completeness report |
| +into this minimum, and the plan states this as a test (§12): a table of |
| +inputs, each failed in turn for over an hour with every other input |
| +healthy, must stop catching on home and far edges. |
| - A far edge serves and fills a host's caught paths only while that |
| host's `at` is within the last hour by the reader's clock. Past that it |
| @@ -346,6 +367,17 @@ Fills are bounded per box (16 concurrent) and per app (4); over either |
| bound the request is forwarded as today, uncached, rather than queued. |
| Fills from a far edge go through `backhaulAcquire` like any hop. |
| +**Every fill has a deadline, and so does every waiter** **(astra R3-3)**. |
| +Ordinary backhaul deliberately has no body deadline (`edge.go:2837`), so a |
| +fill must bring its own: 60 seconds from start to the last body byte. A |
| +fill that misses it is abandoned for caching; the initiating client keeps |
| +receiving the bytes as an ordinary response would, but nothing is stored |
| +and the reservation (§7) is released at the moment of abandonment, not |
| +when the stream ends. At most 32 requests wait on one fill, and each waits |
| +at most 10 seconds; a request over the cap or past its wait is forwarded |
| +as today, which puts it back under `backhaulAcquire`'s per-home limit |
| +(`edge.go:2903`) instead of beside it. |
| + |
| **Coalescing shares only an admitted entry** **(astra R2-5)**. Concurrent |
| requests for one key wait on a single fill, but what they wait for is the |
| entry, not the response. If the fill produces an entry, each waiter is |
| @@ -434,6 +466,11 @@ A response is stored only when all hold: |
| - `Vary` absent, or exactly `Accept-Encoding`; anything else (`Origin`, |
| `Cookie`, `*`, `Accept`) is refused; |
| - no `Content-Encoding` other than identity (the fill asked for identity); |
| +- the `Content-Type` is not HTML (`text/html`, `application/xhtml+xml`, |
| + or absent with a body the edge would sniff as HTML). Build assets are |
| + never pages, and an SPA fallback can carry the asset path's long |
| + `Cache-Control` as easily as `no-cache`, so the lifetime alone cannot be |
| + what refuses it **(astra R3-4)**; |
| - body at most 32 MiB, measured while streaming; a larger body is streamed |
| to the client and discarded; |
| - **the build that answered declared the prefix** **(astra R2-2)**. The |
| @@ -609,10 +646,13 @@ any touched package; a test runs the full fill/hit/purge path against |
| caught for its hosts anywhere. An old far edge ignores the field and |
| backhauls. Every combination is today's behaviour or the new one, never |
| a half. |
| -- **Order.** Release with the feature dormant; roll dev boxes, dev console, |
| - check, flagship boxes, flagship console. Arm one dev box far from a test |
| - app's home, prove the gate (§12), arm the rest of dev, then flagship one |
| - box at a time, a small box (US or AU) first to watch disk. |
| +- **Order.** Release with the feature dormant; roll dev boxes, dev |
| + console, check, flagship boxes, flagship console. Then arm a test app's |
| + **home** dev box first (only an armed home publishes, §4.1), then one |
| + dev box in another region, and run the two-edge acceptance gate (§12) |
| + before arming the rest of dev **(astra R3-5)**. On flagship, arm the |
| + console box (which homes most apps) first, then one small box (US or |
| + AU) to watch disk, then the rest one at a time. |
| - **CLI.** `carlos ship --assets`, `carlos deploy --assets` and |
| `--no-assets`, the `assets` project default, `carlos assets purge`, |
| `carlos assets status`, `carlos instances update --assets` get |
| @@ -641,8 +681,17 @@ routes. Attacks first: |
| - a response with `Set-Cookie`, `Vary: Cookie`, `Vary: Origin`, |
| `Cache-Control: private`, `no-cache`, a 206, a 404, or a body over the cap |
| is served to the requester and not stored; |
| -- an SPA fallback (`200 text/html`, `no-cache`) for an unknown hashed name |
| - is not stored; |
| +- an SPA fallback for an unknown hashed name is not stored, both with |
| + `no-cache` and with `Cache-Control: public, max-age=31536000`; |
| +- the authority table: each input of §4.2's invariant (record scope, |
| + claim listing, catch-all label count, alias upsert, path delete) failed |
| + alone for over an hour, with every other input and the edges-file writes |
| + healthy, stops catching on home and far edges. The cases include an |
| + exact claim replacing a derived catch-all alias, and a claim adding a |
| + path row to a gated target under a primary hostname; |
| +- a fill whose app stalls its body after admissible headers is abandoned |
| + at 60 seconds with its reservation released, at most 32 requests wait on |
| + it, and the rest are forwarded under the backhaul limit; |
| - a gated route, an import-gated route, a pooled route, an opted-out |
| instance, a hostname with any path-prefix row and a hostname split |
| between two apps publish nothing and catch nothing; |
| @@ -752,3 +801,11 @@ On flagship, the same from a small box with the disk figures recorded. |
| acknowledgement transfers billing and `X-Carlos-Asset-Expires: 0` says |
| "not storable"; R2-4 expiry is measured from response time; R2-5 |
| coalescing shares only an admitted entry. |
| +- rev 6, 2026-10-24: Astra review round 3 |
| + (`CODEX-FINDINGS-ASSET-CACHE-R3.md`, reject: 2 P1, 2 P2, 1 P3; R2-1 and |
| + R1-3 partly fixed). All five accepted after checking the citations. The |
| + two P1s were the third round in a row to find one more input left out |
| + of the freshness bound, so §4.2 now states it as an invariant over every |
| + input the catch decision reads, requires `ReconcileAliases` to report |
| + completeness, and §12 tests it as a table (R3-1, R3-2). R3-3 fill and |
| + waiter deadlines; R3-4 HTML is never stored; R3-5 arm the home first. |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R3.md b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R3.md| new file mode 100644 |
| index 000000000..ca956e903 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R3.md |
| @@ -0,0 +1,57 @@ |
| +| Finding | Disposition | Why | |
| +|---|---|---| |
| +| R2-1: stale alias authority | Partly fixed | Failed claim listings now stop renewal, but derived instance aliases can survive a failed dependency read while reconciliation returns success; see 1. | |
| +| R2-2: fills from undeclaring builds | Fixed | Admission checks the answering build’s declaration, and the home signals rejection to the far edge with expiry `0`. | |
| +| R2-3: billing for uncacheable fills | Fixed | Acknowledgement transfers billing independently of storage eligibility. | |
| +| R2-4: response delay counted twice | Fixed | Initial expiry now uses response time plus lifetime minus corrected initial age. | |
| +| R2-5: rejected responses shared among waiters | Fixed | Coalescing shares only admitted entries; rejected responses reach only their initiating requester. | |
| +| R1-3: indefinitely renewed stale authority | Partly fixed | App-scope confirmation is addressed, but domain-derived eligibility still has uncovered dependencies; see 1 and 2. | |
| +| R1-6: renewed lifetime at far edge | Fixed | Far copies inherit the home’s absolute deadline, and the initial-expiry calculation is corrected. | |
| + |
| +1. **P1 — Instance catch-all aliases can still renew stale ownership indefinitely.** |
| + |
| + **Claim:** The revised alias freshness rule excludes a dependency that exists in the current code. |
| + |
| + **Evidence:** [Design:273](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:273) uses successful alias reconciliation to renew authority and states that catch-all aliases are never published. However, instance catch-alls produce ordinary exact-name aliases ([aliases.go:69](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/domains/aliases.go:69)); the publisher excludes only names beginning `*.` ([edgesfile.go:517](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/host/edgesfile.go:517)). |
| + |
| + When `InstanceLabelCounts` fails, derivation preserves existing aliases before checking whether an exact claim now supersedes them ([aliases.go:161](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/domains/aliases.go:161)). That failure is logged, and `ReconcileAliases` still returns nil ([aliases.go:594](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/domains/aliases.go:594)). Thus, an exact name reassigned to another account can remain served under its previous derived alias while successful instance confirmation and nominally successful alias passes keep renewing `at`. |
| + |
| + **Smallest fix:** Distinguish wildcard aliases from exact aliases derived by instance catch-alls. Require explicit dependency-completeness reporting, including the label-count read and registry writes, before advancing authority. Test an exact claim replacing a derived alias while label-count reads fail for over an hour. |
| + |
| +2. **P1 — A primary hostname’s path-routing exclusion has no domain freshness bound.** |
| + |
| + **Claim:** Domain-read failures can leave a primary hostname caching the wrong route indefinitely, even after its authoritative configuration adds a path target. |
| + |
| + **Evidence:** [Design:184](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:184) excludes hostnames with path-prefix rows. Those rows come from domain claims through `ReconcileAliases` ([aliases.go:315](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/domains/aliases.go:315)), but [design:256](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:256) bounds a primary hostname only by its app-record scope; the domain-reconciliation bound applies specifically to aliases. |
| + |
| + Start with primary hostname H serving cached `/assets/` from A. Publish a domain path mapping directing `/assets/` to B, then fail domain listings while app-record confirmation continues succeeding. No path row appears, so H remains eligible and its authority keeps renewing. Cached A responses can continue indefinitely instead of reaching B and its gates. |
| + |
| + **Smallest fix:** Bound primary-host eligibility by successful domain reconciliation too, because proving the absence of path routing requires those reads. Add this failed-domain-listing case to §12, with a gated path target and both home and far caches. |
| + |
| +3. **P2 — Coalesced waiters and fill duration are unbounded.** |
| + |
| + **Claim:** A stalled fill can retain disk reservations indefinitely and accumulate waiting requests outside the existing backhaul concurrency limit. |
| + |
| + **Evidence:** [Design:345](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:345) limits active fills but allows concurrent requests for one key to wait without any waiter limit or deadline. Reservations remain held until the fill exits ([design:554](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:554)). Existing backhaul deliberately has no response-body deadline ([edge.go:2837](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/edge.go:2837)). |
| + |
| + An app can send admissible headers and then stall its body. Repeated requests for that key join the same fill indefinitely. Only the leader takes a backhaul slot; the waiters therefore escape the containment that otherwise refuses excess requests ([edge.go:2903](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/edge.go:2903)). A byte ceiling does not bound this case. |
| + |
| + **Smallest fix:** Specify a finite cache-fill deadline covering body consumption, bounded waiter counts, and reservation release when caching is abandoned. Test a stalled body with excess same-key requests and verify bounded waiters and timely reservation release. |
| + |
| +4. **P2 — Cacheable HTML fallbacks still satisfy every admission rule.** |
| + |
| + **Claim:** A declared asset prefix can cache an SPA fallback for a year, contrary to the explicit anti-goal. |
| + |
| + **Evidence:** [Design:111](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:111) forbids caching `200 index.html` as a script. The admission rules at [design:417](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:417) contain no HTML exclusion. A declaring build returning `200 text/html`, `Cache-Control: public, max-age=31536000`, and no cookies or disallowed `Vary` passes them all. This can happen when asset-path cache headers also apply to the SPA fallback. The proposed test uses `no-cache` ([design:644](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:644)), so it cannot detect this omission. |
| + |
| + **Smallest fix:** Explicitly refuse HTML responses at admission and add a fallback test with a positive, long cache lifetime. |
| + |
| +5. **P3 — The prescribed first rollout test cannot exercise the cache.** |
| + |
| + **Claim:** The rollout sequence asks for a successful far-edge fill before arming its home. |
| + |
| + **Evidence:** [Design:207](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:207) correctly requires an armed home to publish asset declarations. But [design:613](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:613) says to arm only a far dev box, prove the acceptance gate, then arm the rest. With the home unarmed, that first box cannot catch the test app’s assets. |
| + |
| + **Smallest fix:** Arm the test app’s home first, then one far dev edge, and run the two-edge acceptance gate before expanding. |
| + |
| +reject |
| \ No newline at end of file |