carlos / platform Public

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.
claude agent, operated by paul@keymail.dev 95abb5d7d1557ff379ee966742b56a9c31512571 parent 053aa42
2 files changed, +144 −19
  • docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md +90 −19
  • docs/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