spec: edge asset cache rev 5 (Astra round 2 folded in)
Summary
Accepts all five round-2 findings: alias authority bounded by alias reconcile, fills admitted only from a build that declared the prefix, acknowledgement separated from storability, expiry from response time, coalescing shares only admitted entries.
Agent notes
Findings committed unedited as CODEX-FINDINGS-ASSET-CACHE-R2.md.
2 files changed,
+144
−19
docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md+90 −19docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R2.md+54 −0
CI passed — run details
CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok amadan.net/carlos/platform.git/launchctx 0.014s
? 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 (145 s)
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 3.854s
ok github.com/carlosframework/platform/cmd/moveprobe 11.957s
ok github.com/carlosframework/platform/cmd/zddprobe 0.751s
ok github.com/carlosframework/platform/internal/activator 59.005s
ok github.com/carlosframework/platform/internal/agentsock 0.019s
ok github.com/carlosframework/platform/internal/alerting 0.007s
ok github.com/carlosframework/platform/internal/analytics 0.019s
ok github.com/carlosframework/platform/internal/appconfig 3.066s
ok github.com/carlosframework/platform/internal/apperr 0.161s
ok github.com/carlosframework/platform/internal/applog 0.215s
ok github.com/carlosframework/platform/internal/awsinv 0.004s
ok github.com/carlosframework/platform/internal/boxchoice 0.007s
ok github.com/carlosframework/platform/internal/boxid 1.254s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.016s
ok github.com/carlosframework/platform/internal/caps 0.022s
ok github.com/carlosframework/platform/internal/certwarm 0.004s
ok github.com/carlosframework/platform/internal/cgroup 0.768s
ok github.com/carlosframework/platform/internal/changelog 0.527s
ok github.com/carlosframework/platform/internal/claimactivation 1.226s
ok github.com/carlosframework/platform/internal/clamd 0.057s
? 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 8.098s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.010s
ok github.com/carlosframework/platform/internal/dbquery 0.007s
ok github.com/carlosframework/platform/internal/dbreads 2.130s
? github.com/carlosframework/platform/internal/dbreads/dbreadstest [no test files]
ok github.com/carlosframework/platform/internal/dbverify 0.005s
ok github.com/carlosframework/platform/internal/dnsrec 0.005s
ok github.com/carlosframework/platform/internal/docsite 0.010s
ok github.com/carlosframework/platform/internal/domains 2.958s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.005s
ok github.com/carlosframework/platform/internal/edge 45.410s
? 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.271s
ok github.com/carlosframework/platform/internal/gate 2.066s
ok github.com/carlosframework/platform/internal/genselect 0.064s
ok github.com/carlosframework/platform/internal/geo 0.021s
ok github.com/carlosframework/platform/internal/geoip 0.058s
ok github.com/carlosframework/platform/internal/historyreadback 0.211s
ok github.com/carlosframework/platform/internal/host 142.234s
ok github.com/carlosframework/platform/internal/hostactors 5.890s
ok github.com/carlosframework/platform/internal/hostactors/awsmembership 0.005s
ok github.com/carlosframework/platform/internal/hostbox 3.206s
ok github.com/carlosframework/platform/internal/hostclaim 0.858s
ok github.com/carlosframework/platform/internal/hostenv 0.011s
ok github.com/carlosframework/platform/internal/hostevidence 0.011s
ok github.com/carlosframework/platform/internal/hostexec 5.643s
ok github.com/carlosframework/platform/internal/hostfence 0.040s
ok github.com/carlosframework/platform/internal/hostfiles 10.197s
ok github.com/carlosframework/platform/internal/hostfloor 0.030s
ok github.com/carlosframework/platform/internal/hostguard 1.066s
ok github.com/carlosframework/platform/internal/hostops 2.388s
? github.com/carlosframework/platform/internal/hostops/fencerelease [no test files]
ok github.com/carlosframework/platform/internal/hostops/internal/fencerelease 0.004s
ok github.com/carlosframework/platform/internal/hostquarantine 0.005s
ok github.com/carlosframework/platform/internal/hostrelocate 0.688s
ok github.com/carlosframework/platform/internal/hostresolve 0.168s
ok github.com/carlosframework/platform/internal/hostwrite 0.003s
ok github.com/carlosframework/platform/internal/infraview 0.003s
ok github.com/carlosframework/platform/internal/instancectl 5.223s
ok github.com/carlosframework/platform/internal/instanceexec 2.808s
ok github.com/carlosframework/platform/internal/instancefs 0.007s
ok github.com/carlosframework/platform/internal/instancekey 0.002s
ok github.com/carlosframework/platform/internal/instanceupdates 0.016s
ok github.com/carlosframework/platform/internal/layout 2.022s
ok github.com/carlosframework/platform/internal/lbhealth 0.003s
ok github.com/carlosframework/platform/internal/lease 10.082s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.568s
ok github.com/carlosframework/platform/internal/ledger 0.025s
ok github.com/carlosframework/platform/internal/mail 0.263s
ok github.com/carlosframework/platform/internal/maintenance 0.089s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.578s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.081s
ok github.com/carlosframework/platform/internal/metrics 0.006s
ok github.com/carlosframework/platform/internal/moves 21.437s
ok github.com/carlosframework/platform/internal/moves/archive 1.168s
ok github.com/carlosframework/platform/internal/objstore 0.243s
ok github.com/carlosframework/platform/internal/ops 0.316s
ok github.com/carlosframework/platform/internal/park 0.281s
ok github.com/carlosframework/platform/internal/plans 0.006s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 14.321s
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.641s
ok github.com/carlosframework/platform/internal/relay 7.243s
ok github.com/carlosframework/platform/internal/release 25.588s
ok github.com/carlosframework/platform/internal/rollout 3.826s
ok github.com/carlosframework/platform/internal/rolloutwords 0.005s
ok github.com/carlosframework/platform/internal/rootaws 0.006s
ok github.com/carlosframework/platform/internal/rootparticipation 0.063s
ok github.com/carlosframework/platform/internal/rootstate 0.026s
ok github.com/carlosframework/platform/internal/runs 0.546s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.631s
ok github.com/carlosframework/platform/internal/seeds 0.015s
ok github.com/carlosframework/platform/internal/sim/engines 2.935s
ok github.com/carlosframework/platform/internal/sim/hostlife 10.943s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.526s
ok github.com/carlosframework/platform/internal/status 0.090s
ok github.com/carlosframework/platform/internal/steering 0.013s
ok github.com/carlosframework/platform/internal/stopevidence 0.156s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.007s
ok github.com/carlosframework/platform/internal/termfence 0.005s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.009s
ok github.com/carlosframework/platform/internal/updatewords 0.005s
ok github.com/carlosframework/platform/internal/usage 0.005s
ok github.com/carlosframework/platform/internal/version 0.003s
ok github.com/carlosframework/platform/internal/vet 1.664s
=== internal_console@0 (83 s)
ok github.com/carlosframework/platform/internal/console 81.155s
=== internal_console@1 (44 s)
ok github.com/carlosframework/platform/internal/console 41.649s
=== internal_console@2 (41 s)
ok github.com/carlosframework/platform/internal/console 38.867s
=== internal_console@3 (40 s)
ok github.com/carlosframework/platform/internal/console 38.050s
=== internal_console@4 (42 s)
ok github.com/carlosframework/platform/internal/console 39.761s
=== internal_console@5 (41 s)
ok github.com/carlosframework/platform/internal/console 39.141s
=== internal_console@6 (37 s)
ok github.com/carlosframework/platform/internal/console 34.389s
=== internal_console@7 (45 s)
ok github.com/carlosframework/platform/internal/console 42.765s
=== root@0 (109 s)
ok github.com/carlosframework/platform 106.783s
=== root@1 (84 s)
ok github.com/carlosframework/platform 82.246s
=== root@2 (91 s)
ok github.com/carlosframework/platform 88.719s
=== 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.021s
=== RUN TestT4StorageProbeMinio
2026/10/24 18:10:01 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E189958437F21D, 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.021s
=== 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:10:04 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:10:04 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:10:04 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:10:04 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 18:10:04 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 18:10:04 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 18:10:04 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 18:10:04 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 18:10:04 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 18:10:04 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:10:04 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:10:04 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:10:04 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 18:10:04 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%3A34129: could not determine target mailbox
agent.go:135: dbreads: dbr_3zfhx7442ixxv2ardosraqi6yu: planned 1 files (720 bytes) and restored to 1 in 8ms
agent.go:135: dbreads: dbr_3zfhx7442ixxv2ardosraqi6yu: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:10:05 console: db read dbr_3zfhx7442ixxv2ardosraqi6yu: relayed 1 chunks (102 bytes) in 1.003s, ended done
agent.go:135: dbreads: dbr_lukl4bvvuskd2rwuwcjuxfwp6i: planned 3 files (1544 bytes) and restored to 3 in 9ms
agent.go:135: dbreads: dbr_lukl4bvvuskd2rwuwcjuxfwp6i: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:10:06 console: db read dbr_lukl4bvvuskd2rwuwcjuxfwp6i: relayed 1 chunks (102 bytes) in 1.004s, ended done
agent.go:135: dbreads: dbr_ilxfz5erdqjmocvkfcjxpa44ta: planned 3 files (1544 bytes) and held through 3 in 8ms
agent.go:135: dbreads: dbr_ilxfz5erdqjmocvkfcjxpa44ta: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:10:07 console: db read dbr_ilxfz5erdqjmocvkfcjxpa44ta: relayed 1 chunks (1641 bytes) in 1.003s, ended done
=== RUN TestDBForwardMinio/by_disconnect
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_pfxwobjvgiaiv2w5imtd7ifxfi: planned 3 files (1544 bytes) and restored to 3 in 9ms
2026/10/24 18:10:07 console: db read dbr_pfxwobjvgiaiv2w5imtd7ifxfi: relayed 0 chunks (0 bytes) in 17ms, ended client gone
agent.go:135: dbreads: dbr_pfxwobjvgiaiv2w5imtd7ifxfi: output 5 bytes in 1 confirmed chunks in 104ms
=== RUN TestDBForwardMinio/by_timeout
=== NAME TestDBForwardMinio
agent.go:135: dbreads: dbr_zp2wbsqcic3g2wjx3rnkfdeqmm: planned 3 files (1544 bytes) and restored to 3 in 8ms
2026/10/24 18:10:27 console: db read dbr_zp2wbsqcic3g2wjx3rnkfdeqmm: relayed 0 chunks (0 bytes) in 19.556s, ended no-result
agent.go:135: dbreads: dbr_zp2wbsqcic3g2wjx3rnkfdeqmm: output 5 bytes in 1 confirmed chunks in 19.534s
--- PASS: TestDBForwardMinio (22.98s)
--- PASS: TestDBForwardMinio/by_disconnect (0.22s)
--- PASS: TestDBForwardMinio/by_timeout (19.66s)
PASS
ok github.com/carlosframework/platform/internal/console 23.000s
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 c16a58503..12844a412 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 4, 2026-10-24, builder check folded in; rulings R1 to R3 (§13). Approved by Paul 2026-10-24; Astra review round 2 next. |
| +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. |
| Related: media roadmap (`2026-09-07-media-platform-roadmap-design.md`), |
| which this does not amend; #368 (the shared cache stores 206 responses). |
| @@ -226,7 +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. |
| +- 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. |
| - Additive and `omitempty`: a file without it parses byte-identically, and |
| an older reader ignores it. |
| @@ -263,6 +265,18 @@ 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. |
| + |
| - 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 |
| stops admitting hits and fills and forwards as today. |
| @@ -328,11 +342,21 @@ Range, `If-None-Match` and `If-Modified-Since` are answered by the edge |
| ### 5.3 Fill, admission and coalescing |
| -One fill per key at a time (singleflight); concurrent requests for the |
| -same key wait for it. 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. |
| +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. |
| + |
| +**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 |
| +then answered from it as a hit, with its own conditional headers, `Range` |
| +and encoding applied separately (§5.5). If it does not (refused at |
| +admission, no acknowledgement, an error), only the requester that started |
| +the fill gets that response, and every other waiter makes its own |
| +request, forwarded as today and subject to the same fill bounds. A refused |
| +response, which may carry `Set-Cookie` or vary on `Accept`, therefore |
| +never reaches anyone but the client it was made for, and every client |
| +delivery is a request somebody metered. |
| **A far edge's fill is a signed request, answered with a signed |
| acknowledgement.** Ordinary backhaul is not enough: the home meters every |
| @@ -360,17 +384,28 @@ bytes under the new one **(astra R1-2)**. |
| own asset cache under that key, or fills it from the app (§5.2), never |
| reads or writes its shared LRU for the request and never meters it. It |
| first deletes any `X-Carlos-Asset-Fill-Ack` and |
| - `X-Carlos-Asset-Expires` the app's response carries, then sets its own: |
| - `X-Carlos-Asset-Expires` is the entry's absolute expiry (§5.4) and |
| - `X-Carlos-Asset-Fill-Ack` is HMAC-SHA256 under the mesh key over |
| - `carlos-asset-fill-ack-v1`, the request's assertion and the expiry. |
| + `X-Carlos-Asset-Expires` the app's response carries, then sets its own |
| + on the response headers: |
| + - `X-Carlos-Asset-Expires` is the entry's absolute expiry (§5.4) when |
| + the response is storable by the header-time rules below, and `0` when |
| + it is not (no lifetime, `no-store`, non-200, a disallowed `Vary`, an |
| + undeclaring build, and so on); |
| + - `X-Carlos-Asset-Fill-Ack` is HMAC-SHA256 under the mesh key over |
| + `carlos-asset-fill-ack-v1`, the request's assertion and the |
| + `X-Carlos-Asset-Expires` value. |
| + |
| + The acknowledgement means "the home handled this as a fill and did not |
| + meter it", whatever the expiry says; storability is the expiry's job |
| + **(astra R2-3)**. |
| - **Anything else is an ordinary hop**: a missing or invalid assertion, an |
| unarmed or old home, a generation or owner mismatch, a host past its |
| freshness bound. The home meters it as today and sends no |
| acknowledgement. |
| -- **Far edge.** It stores a response only when it carries a valid |
| - acknowledgement of its own assertion, and meters only acknowledged fills |
| - (and its own hits). A response without one is served to the requester |
| +- **Far edge.** It meters every response that carries a valid |
| + acknowledgement of its own assertion, storable or not, and its own hits. |
| + It stores an acknowledged response only when `X-Carlos-Asset-Expires` is |
| + non-zero and its own admission checks (body size, reservation) pass. A |
| + response without a valid acknowledgement is served to the requester |
| unmodified, answered `Carlos-Cache: bypass`, not stored and not metered |
| at the far edge, because the home metered it. So every response is |
| counted exactly once, a disarmed or lagging home can never populate a |
| @@ -388,9 +423,11 @@ A response is stored only when all hold: |
| `max-age`, capped at 365 days. There is no five-minute clamp: a declared |
| prefix plus an explicit lifetime is the app saying the bytes hold; |
| - the response is still fresh on arrival. On a fill from the app the edge |
| - computes the entry's absolute expiry as the request time plus the |
| - lifetime minus the corrected initial age (RFC 9111 §4.2.3, from the |
| - response's `Age` and the response delay). On an acknowledged far fill |
| + computes the entry's absolute expiry as the **response time** plus the |
| + lifetime minus the corrected initial age (RFC 9111 §4.2.3: the corrected |
| + initial age already includes the response delay, so the reference point |
| + is when the response arrived, not when the request left) |
| + **(astra R2-4)**. On an acknowledged far fill |
| the expiry is the home's `X-Carlos-Asset-Expires`, never recomputed, so |
| a copy of a nearly expired home entry expires when the home's does |
| **(astra R1-6)**. An already expired response is served and not stored; |
| @@ -398,7 +435,18 @@ A response is stored only when all hold: |
| `Cookie`, `*`, `Accept`) is refused; |
| - no `Content-Encoding` other than identity (the fill asked for identity); |
| - body at most 32 MiB, measured while streaming; a larger body is streamed |
| - to the client and discarded. |
| + to the client and discarded; |
| +- **the build that answered declared the prefix** **(astra R2-2)**. The |
| + §4.1 union decides what is *caught* and which existing entries may be |
| + *served*, but during an update the proxy picks the serving generation on |
| + its own (`internal/edge/genproxy.go:255`), so an older build that never |
| + declared `/bundles/` could answer a request for it with a cacheable |
| + fallback, and with no version in the key that entry would outlive the |
| + update. On a generation-managed route the home knows which generation |
| + answered (`genproxy.go:298-302`, `noteServedGen`) and stores only when |
| + that generation's build declared the matching prefix; on any other route |
| + the answering build is `Route.Version`'s. A far fill inherits this |
| + through the home's `X-Carlos-Asset-Expires: 0`. |
| A refused response is still served to the requester, unmodified, so a |
| caught prefix that the app does not mark cacheable behaves exactly as an |
| @@ -611,7 +659,22 @@ routes. Attacks first: |
| - an app response carrying its own `X-Carlos-Asset-Fill-Ack` or |
| `X-Carlos-Asset-Expires` has them removed before the home signs; |
| - a nearly expired home entry filled through a far edge expires at the |
| - home's deadline on both boxes; |
| + home's deadline on both boxes; a delayed origin whose response arrives |
| + after half its lifetime is stored and expires at response time plus |
| + lifetime minus age; |
| +- an acknowledged fill whose response has no `Cache-Control`, `no-store`, |
| + a 404 or an oversized body is metered once at the far edge and stored |
| + nowhere; |
| +- during an update where only the installed build declares `/bundles/`, |
| + a cacheable fallback answered by the current build is not stored on |
| + home or far edges; the same when only the retiring build declares it; |
| +- concurrent requests for one key with different `Accept` headers, a |
| + refused response carrying `Set-Cookie`, and an unacknowledged fill each |
| + reach only their initiating client, and the other waiters are metered |
| + separately; |
| +- an alias detached while the box's claim listing fails for over an hour, |
| + with record reads and edges-file writes succeeding, stops being served |
| + from cache on home and far edges; |
| - an SVG with a restrictive `Content-Security-Policy` and |
| `Cross-Origin-Resource-Policy` carries both on a hit as on the miss; |
| - identity and gzip responses from one entry both carry |
| @@ -681,3 +744,11 @@ On flagship, the same from a small box with the disk figures recorded. |
| the paths union so a sleeping instance keeps catching; R1-6 absolute |
| expiry, age-corrected, carried to far copies; R1-7 representation rules |
| from `compress.go`; R1-8 worst-case disk reservations. |
| +- rev 5, 2026-10-24: Astra review round 2 |
| + (`CODEX-FINDINGS-ASSET-CACHE-R2.md`, reject: 1 P1, 4 P2; R1-3 and R1-6 |
| + partly fixed). All five accepted after checking the citations: R2-1 |
| + alias `at` also bounded by the last successful alias reconcile; R2-2 |
| + only a build that declared the prefix may fill it; R2-3 the |
| + 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. |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R2.md b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R2.md| new file mode 100644 |
| index 000000000..c4669ffec |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-R2.md |
| @@ -0,0 +1,54 @@ |
| +| Round-1 finding | Disposition | Why | |
| +|---|---|---| |
| +| R1-1: forged fill marker | Fixed | The separate, domain-separated signature cannot be manufactured from ordinary hop authentication. | |
| +| R1-2: old bytes admitted under a new generation | Fixed | Admission requires an acknowledgement bound to the expected owner and generation. | |
| +| R1-3: indefinitely renewed stale authority | Partly fixed | Instance convergence now controls freshness, but independently reconciled alias ownership does not; see finding 1. | |
| +| R1-4: lost security headers | Fixed | Entries retain end-to-end security headers, with CSP/CORP coverage required in §12. | |
| +| R1-5: hibernation withdraws declarations | Fixed | Including the installed build preserves declarations while the instance sleeps. | |
| +| R1-6: renewed lifetime at the far edge | Partly fixed | Far copies inherit the home deadline, but the new initial-expiry formula subtracts response delay twice; see finding 4. | |
| +| R1-7: incorrect gzip representation semantics | Fixed | The spec now requires `Vary`, weakened gzip validators and respect for `no-transform`. | |
| +| R1-8: concurrent fills cross the disk floor | Fixed | Outstanding reservations now account for concurrent bodies, gzip copies and metadata. | |
| + |
| +1. **P1 — Alias ownership can remain stale indefinitely while its asset authority keeps renewing.** |
| + |
| + **Claim:** A failed domain-claim listing can leave a detached or reassigned alias publishing and serving the previous owner’s cached bytes beyond the one-hour bound. |
| + |
| + **Evidence:** [Design:229](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:229) gives aliases their primary’s block, while [design:254](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:254) derives its authority time solely from the app’s record scope. Alias claims are read separately from the bucket ([changeindex.go:302](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/host/changeindex.go:302)). If that listing fails, reconciliation returns before changing existing alias rows ([aliases.go:232](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/domains/aliases.go:232)); the publisher continues advertising those rows ([edgesfile.go:513](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/host/edgesfile.go:513)). Successful instance reads can therefore keep renewing `at` despite never confirming that the account still owns the alias. |
| + |
| + **Smallest fix:** Bound alias authority by the oldest successfully validated and applied dependency, including its domain claim and any parent/catch-all claims. Reuse existing reconciliation reads. Add a test that detaches an alias, fails domain listings for over an hour while instance reads and publication succeed, and requires home and far caches to stop serving it. |
| + |
| +2. **P2 — The declaration union authorizes fills from builds that never declared the requested prefix.** |
| + |
| + **Claim:** Installing a successor can cause an older serving build’s response to be cached as the successor’s asset and retained after the update completes. |
| + |
| + **Evidence:** [Design:217](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:217) includes installed and live builds’ prefixes. However, the proxy selects the current serving generation independently ([genproxy.go:255](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/genproxy.go:255)). Suppose installed build B declares `/bundles/`, while current build A does not and answers that path with a cacheable `200` fallback. A request during the update satisfies every admission rule, but stores A’s fallback. When B becomes current, the entry still matches because release version is deliberately absent from the key ([design:412](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:412)). The same problem occurs when a retiring build alone declares a prefix. |
| + |
| + **Smallest fix:** Keep the union for catching requests and serving existing entries, but admit a new response only if the build that actually answered declared that prefix. The proxy already records the answering generation ([genproxy.go:298](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/genproxy.go:298)). Test distinct prefixes during an update, with a cacheable fallback from the non-declaring build. |
| + |
| +3. **P2 — The acknowledgement protocol has no defined billing outcome for an accepted fill that produces no cache entry.** |
| + |
| + **Claim:** The protocol couples metering acknowledgement to an entry expiry that need not exist. |
| + |
| + **Evidence:** [Design:356](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:356) says a recognized fill is never metered at home and its acknowledgement signs “the entry’s absolute expiry.” Yet [design:382](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:382) allows the upstream response to have no cache lifetime, be `no-store`, or otherwise produce no entry. There is no specified expiry or acknowledgement representation for that result. Omitting the acknowledgement makes the far edge skip metering too, under [design:371](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:371), although the home already skipped it. |
| + |
| + **Smallest fix:** Separate “the home handled this as a fill and transferred billing” from “this response is eligible for storage.” Define a signed acknowledgement for uncacheable responses, with no usable expiry, and require far-edge metering. Test missing `Cache-Control`, `no-store`, non-200 responses and oversized bodies through both edges. |
| + |
| +4. **P2 — The new expiry calculation counts response delay twice.** |
| + |
| + **Claim:** Slow but still-fresh responses can be immediately discarded, defeating caching on the wake path. |
| + |
| + **Evidence:** [Design:390](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:390) specifies `request_time + lifetime − corrected_initial_age`. Corrected initial age already includes response delay; its reference point is response receipt. The deadline should therefore use `response_time`. This follows the cited age calculation in [https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.3](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.3). |
| + |
| + For example, request time 0, response time 20, `Age: 0`, and lifetime 30 produce a specified expiry of 10—already expired. The correct expiry is 30, leaving ten seconds of freshness. |
| + |
| + **Smallest fix:** Specify `response_time + lifetime − corrected_initial_age`, retaining the absolute deadline for far copies. Add a delayed-origin test where the response arrives after half its lifetime but remains fresh. |
| + |
| +5. **P2 — Coalesced requests have no safe completion rule when admission fails.** |
| + |
| + **Claim:** Sharing a rejected fill’s response can distribute one requester’s cookies or representation to other waiting clients; fallback accounting can also undercount them. |
| + |
| + **Evidence:** [Design:331](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:331) coalesces all requests by cache key. That key excludes `Accept` and `User-Agent`, although fills forward both ([design:317](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:317)). Admission rejects `Vary: Accept`, `Set-Cookie` and private responses, but [design:403](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:403) only says to serve the rejected response to “the requester.” It never defines what happens to the other waiters. Broadcasting that result bypasses the admission protections. Broadcasting an unacknowledged ordinary-hop response also bills one home exchange for multiple client deliveries. |
| + |
| + **Smallest fix:** Coalesce only the production of an admitted entry. If admission fails or the home does not acknowledge, return the response solely to the initiating requester and let other waiters make independently bounded requests. Apply each waiter’s conditions and encoding preferences separately after successful admission. Test concurrent differing `Accept` headers, rejected `Set-Cookie` responses and unacknowledged fills. |
| + |
| +reject |
| \ No newline at end of file |