carlos / platform Public

edge asset cache plans: Astra plan review round 1 folded in

Summary

Accepts all 16 plan-review findings: type ordering, census dispositions, trigger-based write-failure fixtures, positive application in the reconcile report, bounded and crash-safe metadata, trailer veto, cold-miss conditionals, ack key selection, upgrade exclusion, serving-build publication trigger, fallback ETag, production-wired attack matrix, copy inventory to EAC-26.
Agent notes
Findings committed unedited as CODEX-FINDINGS-ASSET-CACHE-PLANS.md.
claude agent, operated by paul@keymail.dev 6b2d1776bf79618d2098f1f0d371b29b7556cd4b parent 5b06bed
4 files changed, +92 −37
  • docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md +19 −14
  • docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md +12 −10
  • docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md +28 −13
  • docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS.md +33 −0

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
go: downloading github.com/aws/aws-sdk-go-v2 v1.44.0
go: downloading github.com/aws/aws-sdk-go-v2/service/s3 v1.105.0
go: downloading github.com/aws/smithy-go v1.28.1
go: downloading github.com/aws/aws-sdk-go-v2/service/costexplorer v1.67.5
go: downloading github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.68.0
go: downloading github.com/superfly/ltx v0.5.1
go: downloading golang.org/x/net v0.56.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/config v1.32.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sns v1.43.0
go: downloading github.com/aws/aws-sdk-go-v2/service/route53 v1.64.1
go: downloading github.com/benbjohnson/litestream v0.5.14
go: downloading golang.org/x/crypto v0.53.0
go: downloading golang.org/x/sys v0.46.0
go: downloading github.com/oschwald/maxminddb-golang v1.13.1
go: downloading github.com/aws/aws-sdk-go-v2/service/autoscaling v1.74.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/credentials v1.19.6
go: downloading github.com/aws/aws-sdk-go-v2/service/sts v1.45.6
go: downloading github.com/aws/aws-sdk-go-v2/service/acm v1.45.0
go: downloading github.com/aws/aws-sdk-go-v2/service/iam v1.58.2
go: downloading github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.37.0
go: downloading github.com/aws/aws-sdk-go-v2/service/sesv2 v1.66.6
go: downloading github.com/pion/logging v0.2.4
go: downloading github.com/pion/stun/v3 v3.0.1
go: downloading github.com/pion/turn/v4 v4.1.4
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.38
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.40
go: downloading github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.40
go: downloading github.com/prometheus/client_golang v1.17.0
go: downloading golang.org/x/sync v0.21.0
go: downloading github.com/lmittmann/tint v1.1.3
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18
go: downloading github.com/pion/dtls/v3 v3.0.7
go: downloading github.com/pion/transport/v3 v3.0.8
go: downloading github.com/pion/randutil v0.1.0
go: downloading github.com/pion/transport/v4 v4.0.1
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23
go: downloading github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31
go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14
go: downloading github.com/wlynxg/anet v0.0.5
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading golang.org/x/text v0.38.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12
go: downloading github.com/aws/aws-sdk-go-v2/service/signin v1.0.4
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading golang.org/x/text v0.38.0
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/google/uuid v1.6.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading modernc.org/mathutil v1.7.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== launchctx (4 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.012s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (46 s)
go: downloading github.com/aws/aws-sdk-go-v2/service/ec2 v1.324.0
go: downloading modernc.org/sqlite v1.55.0
go: downloading github.com/aws/aws-sdk-go-v2/service/ssm v1.74.0
go: downloading github.com/klauspost/compress v1.18.0
go: downloading golang.org/x/tools v0.47.0
go: downloading github.com/pierrec/lz4/v4 v4.1.22
go: downloading github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16
go: downloading github.com/aws/aws-sdk-go-v2/service/sso v1.30.8
go: downloading golang.org/x/text v0.38.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/cespare/xxhash/v2 v2.3.0
go: downloading github.com/prometheus/client_model v0.5.0
go: downloading github.com/prometheus/common v0.45.0
go: downloading github.com/prometheus/procfs v0.12.0
go: downloading google.golang.org/protobuf v1.36.10
go: downloading golang.org/x/mod v0.37.0
go: downloading modernc.org/libc v1.74.1
go: downloading modernc.org/memory v1.11.0
go: downloading github.com/dustin/go-humanize v1.0.1
go: downloading github.com/google/uuid v1.6.0
go: downloading modernc.org/mathutil v1.7.1
go: downloading github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec
=== gofmt (1 s)
gofmt -l .: clean
=== rest (201 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.236s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.790s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.741s
ok  	github.com/carlosframework/platform/internal/activator	59.703s
ok  	github.com/carlosframework/platform/internal/agentsock	0.030s
ok  	github.com/carlosframework/platform/internal/alerting	0.006s
ok  	github.com/carlosframework/platform/internal/analytics	0.035s
ok  	github.com/carlosframework/platform/internal/appconfig	2.919s
ok  	github.com/carlosframework/platform/internal/apperr	0.182s
ok  	github.com/carlosframework/platform/internal/applog	0.256s
ok  	github.com/carlosframework/platform/internal/awsinv	0.008s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.008s
ok  	github.com/carlosframework/platform/internal/boxid	0.784s
?   	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.013s
ok  	github.com/carlosframework/platform/internal/certwarm	0.002s
ok  	github.com/carlosframework/platform/internal/cgroup	0.728s
ok  	github.com/carlosframework/platform/internal/changelog	0.473s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.144s
ok  	github.com/carlosframework/platform/internal/clamd	0.058s
?   	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.554s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.023s
ok  	github.com/carlosframework/platform/internal/dbquery	0.007s
ok  	github.com/carlosframework/platform/internal/dbreads	2.122s
?   	github.com/carlosframework/platform/internal/dbreads/dbreadstest	[no test files]
ok  	github.com/carlosframework/platform/internal/dbverify	0.006s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.003s
ok  	github.com/carlosframework/platform/internal/docsite	0.012s
ok  	github.com/carlosframework/platform/internal/domains	2.645s
ok  	github.com/carlosframework/platform/internal/doorkey	0.010s
ok  	github.com/carlosframework/platform/internal/economics	0.006s
ok  	github.com/carlosframework/platform/internal/edge	48.692s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.136s
ok  	github.com/carlosframework/platform/internal/gate	1.655s
ok  	github.com/carlosframework/platform/internal/genselect	0.018s
ok  	github.com/carlosframework/platform/internal/geo	0.024s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.159s
ok  	github.com/carlosframework/platform/internal/host	150.303s
ok  	github.com/carlosframework/platform/internal/hostactors	5.955s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.011s
ok  	github.com/carlosframework/platform/internal/hostbox	3.594s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.743s
ok  	github.com/carlosframework/platform/internal/hostenv	0.006s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.009s
ok  	github.com/carlosframework/platform/internal/hostexec	5.605s
ok  	github.com/carlosframework/platform/internal/hostfence	0.069s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.514s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.025s
ok  	github.com/carlosframework/platform/internal/hostguard	1.033s
ok  	github.com/carlosframework/platform/internal/hostops	2.082s
?   	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.007s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.607s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.182s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.004s
ok  	github.com/carlosframework/platform/internal/infraview	0.003s
ok  	github.com/carlosframework/platform/internal/instancectl	5.256s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.864s
ok  	github.com/carlosframework/platform/internal/instancefs	0.009s
ok  	github.com/carlosframework/platform/internal/instancekey	0.009s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.016s
ok  	github.com/carlosframework/platform/internal/layout	1.811s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.004s
ok  	github.com/carlosframework/platform/internal/lease	10.149s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.534s
ok  	github.com/carlosframework/platform/internal/ledger	0.024s
ok  	github.com/carlosframework/platform/internal/mail	0.264s
ok  	github.com/carlosframework/platform/internal/maintenance	0.080s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.539s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.098s
ok  	github.com/carlosframework/platform/internal/metrics	0.008s
ok  	github.com/carlosframework/platform/internal/moves	29.126s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.951s
ok  	github.com/carlosframework/platform/internal/objstore	0.238s
ok  	github.com/carlosframework/platform/internal/ops	0.234s
ok  	github.com/carlosframework/platform/internal/park	0.297s
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	52.440s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.009s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.765s
ok  	github.com/carlosframework/platform/internal/relay	7.208s
ok  	github.com/carlosframework/platform/internal/release	29.527s
ok  	github.com/carlosframework/platform/internal/rollout	4.130s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.005s
ok  	github.com/carlosframework/platform/internal/rootaws	0.014s
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.613s
ok  	github.com/carlosframework/platform/internal/s3compat	0.008s
ok  	github.com/carlosframework/platform/internal/safefile	0.005s
ok  	github.com/carlosframework/platform/internal/schedule	2.825s
ok  	github.com/carlosframework/platform/internal/seeds	0.010s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.990s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.553s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	4.202s
ok  	github.com/carlosframework/platform/internal/status	0.083s
ok  	github.com/carlosframework/platform/internal/steering	0.010s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.937s
ok  	github.com/carlosframework/platform/internal/svcrole	0.003s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.005s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
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.004s
ok  	github.com/carlosframework/platform/internal/vet	1.674s
=== internal_console@0 (85 s)
ok  	github.com/carlosframework/platform/internal/console	82.772s
=== internal_console@1 (50 s)
ok  	github.com/carlosframework/platform/internal/console	48.416s
=== internal_console@2 (43 s)
ok  	github.com/carlosframework/platform/internal/console	41.651s
=== internal_console@3 (47 s)
ok  	github.com/carlosframework/platform/internal/console	45.587s
=== internal_console@4 (47 s)
ok  	github.com/carlosframework/platform/internal/console	45.690s
=== internal_console@5 (48 s)
ok  	github.com/carlosframework/platform/internal/console	46.198s
=== internal_console@6 (44 s)
ok  	github.com/carlosframework/platform/internal/console	42.012s
=== internal_console@7 (50 s)
ok  	github.com/carlosframework/platform/internal/console	48.627s
=== root@0 (112 s)
ok  	github.com/carlosframework/platform	110.165s
=== root@1 (83 s)
ok  	github.com/carlosframework/platform	81.625s
=== root@2 (95 s)
ok  	github.com/carlosframework/platform	93.094s
=== 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.026s
=== RUN   TestT4StorageProbeMinio
2026/10/24 18:51:12 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E18BD4DAAF9A1C, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.023s
=== RUN   TestS3StoreMinioContract
=== RUN   TestS3StoreMinioContract/conditional_get_and_etag_listing
=== RUN   TestS3StoreMinioContract/bump_through_every_mutation_path
--- PASS: TestS3StoreMinioContract (0.03s)
    --- PASS: TestS3StoreMinioContract/conditional_get_and_etag_listing (0.01s)
    --- PASS: TestS3StoreMinioContract/bump_through_every_mutation_path (0.02s)
PASS
ok  	github.com/carlosframework/platform/internal/release	0.033s
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:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:51:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/24 18:51:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/24 18:51:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/24 18:51:15 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/24 18:51:15 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/24 18:51:15 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/24 18:51:15 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%3A46127: could not determine target mailbox
    agent.go:135: dbreads: dbr_avvqr4ojfvh4mnf2cim7grzrhi: planned 1 files (720 bytes) and restored to 1 in 9ms
    agent.go:135: dbreads: dbr_avvqr4ojfvh4mnf2cim7grzrhi: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:51:16 console: db read dbr_avvqr4ojfvh4mnf2cim7grzrhi: relayed 1 chunks (102 bytes) in 1.005s, ended done
    agent.go:135: dbreads: dbr_ywpdwjmcdctofg47phibqe3cl4: planned 3 files (1544 bytes) and restored to 3 in 10ms
    agent.go:135: dbreads: dbr_ywpdwjmcdctofg47phibqe3cl4: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:51:17 console: db read dbr_ywpdwjmcdctofg47phibqe3cl4: relayed 1 chunks (102 bytes) in 1.004s, ended done
    agent.go:135: dbreads: dbr_eiyd6f6sj3c6qcsnrpjp467fty: planned 3 files (1544 bytes) and held through 3 in 10ms
    agent.go:135: dbreads: dbr_eiyd6f6sj3c6qcsnrpjp467fty: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/24 18:51:18 console: db read dbr_eiyd6f6sj3c6qcsnrpjp467fty: relayed 1 chunks (1641 bytes) in 1.004s, ended done
=== RUN   TestDBForwardMinio/by_disconnect
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_lqr4klhdi7pcrh6qi4j4cfdngi: planned 3 files (1544 bytes) and restored to 3 in 10ms
2026/10/24 18:51:18 console: db read dbr_lqr4klhdi7pcrh6qi4j4cfdngi: relayed 0 chunks (0 bytes) in 16ms, ended client gone
    agent.go:135: dbreads: dbr_lqr4klhdi7pcrh6qi4j4cfdngi: output 5 bytes in 1 confirmed chunks in 50ms
=== RUN   TestDBForwardMinio/by_timeout
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_tknzaziaqlpnfiwbznhx4o7tse: planned 3 files (1544 bytes) and restored to 3 in 10ms
2026/10/24 18:51:38 console: db read dbr_tknzaziaqlpnfiwbznhx4o7tse: relayed 0 chunks (0 bytes) in 19.206s, ended no-result
    agent.go:135: dbreads: dbr_tknzaziaqlpnfiwbznhx4o7tse: output 5 bytes in 1 confirmed chunks in 19.193s
--- PASS: TestDBForwardMinio (22.59s)
    --- PASS: TestDBForwardMinio/by_disconnect (0.17s)
    --- PASS: TestDBForwardMinio/by_timeout (19.32s)
PASS
ok  	github.com/carlosframework/platform/internal/console	22.615s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md
index 5ecb9c342..5efd47a00 100644
--- a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md
+++ b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md
@@ -42,14 +42,14 @@ Every task's requirements include this section.
## Census impact
- **Plan 14 root-read census:** one overlay, `plan14-root-reads-edge-asset-cache-1.tsv`, rows `rev3=PROPOSED-edge-asset-cache-1`.
- - Changed frozen units, re-fingerprinted as `@legacy` with the same class: `internal/host/edgesfile.go` `edgesFileWriteClaims` (authority F,J1; it now reads manifests through the injected cache), `internal/domains/aliases.go` `ReconcileAliases` and `deriveInstanceAliases` (legacy).
- - New units: `internal/host/assetmanifests.go` `assetManifestCache.paths` (reads `release.GetManifest`, class `authority`, task J1, the same class as other manifest readers).
+ - Changed frozen units keep their rows' form and are re-digested with class, task and basis unchanged: `internal/host/edgesfile.go` `edgesFileWriteClaims` stays a plain row of class `authority`, task `F,J1`, basis `root-route;kept=F;scope=protected-decision-only` (the `plan14-root-reads-region-ha-2a.tsv:26` precedent; the loader requires an authority row's task to name a kept family, `plan14_root_reads_test.go:507-515`); `internal/domains/aliases.go` `ReconcileAliases` and `deriveInstanceAliases` keep their `@legacy` rows (class `legacy`, re-fingerprinted by `rootReadLegacyFingerprint`).
+ - New unit `internal/host/assetmanifests.go` `assetManifestCache.paths` (reads `release.GetManifest` for the same protected publication decision): a plain row of class `authority`, task `F,J1`, basis `root-route;kept=F;scope=protected-decision-only`, the writer's own disposition.
- Envelopes re-pinned: `.`, `internal/release`, `internal/provision`, `internal/registry`, `internal/domains`, `internal/host`, `internal/edge`, `internal/edgesfile`, `internal/console`.
- Notes file: `docs/superpowers/notes/census-proposal-edge-asset-cache-1.md`, ruling asked on #280.
## Copy strings
-Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text is in the copy record, not here):
+Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text is in the copy record, not here). The inventory is EAC-1 to EAC-24; plan 3 adds EAC-25 and EAC-26 (astra P1-16):
| id | where | what it says |
|---|---|---|
@@ -63,6 +63,9 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text
| EAC-14 | `assets_cli.go` | the status table's column heads and its bound sentence (spec §6) |
| EAC-15 | `internal/console/apiassets.go` | refusal for a set-assets body that is not `on` or `off` |
| EAC-16 to EAC-19 | `docs/site/cli.md` | `### carlos assets`, `### carlos instances set-assets`, and the `--assets` paragraphs under `carlos ship` and `carlos deploy` |
+| EAC-22 | `deploy.go` | `--assets` and `--no-assets` given together |
+| EAC-23 | `internal/console/apiship_assets.go` | a re-ship of a version with different asset paths refused |
+| EAC-24 | `apimode.go` (`checkAssetsEcho`) | the console did not echo the asset paths: it predates them |
## File structure
@@ -100,10 +103,10 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text
**Interfaces:**
- Produces: `assetCacheCopyBuilt []string` in the gate (ids a task has built), extended by each task that writes a row.
-- [ ] **Step 1:** Write the copy record: a table `| id | file | text | status |` with rows EAC-1 to EAC-19 drafted from spec §3, §6 and the table above, every row `pending`. No em dashes.
-- [ ] **Step 2:** Write `TestAssetCacheCopyInventory` and `TestAssetCacheCopyBuiltRowsAreApprovedAndPresent` in `approved_asset_cache_copy_test.go`, reusing `parseCopyTable` and `checkMovesNoEmDash` from `approved_moves_copy_test.go`: the id set is exactly EAC-1 to EAC-19; every id in `assetCacheCopyBuilt` is `approved` and its text appears verbatim in its file. Start with `assetCacheCopyBuilt` empty.
+- [ ] **Step 1:** Write the copy record: a table `| id | file | text | status |` with rows EAC-1 to EAC-24 drafted from spec §3, §6 and the table above, every row `pending`. No em dashes.
+- [ ] **Step 2:** Write `TestAssetCacheCopyInventory` and `TestAssetCacheCopyBuiltRowsAreApprovedAndPresent` in `approved_asset_cache_copy_test.go`, reusing `parseCopyTable` and `checkMovesNoEmDash` from `approved_moves_copy_test.go`: the id set is exactly EAC-1 to EAC-24 (plan 3 Task 0 extends it to EAC-26); every id in `assetCacheCopyBuilt` is `approved` and its text appears verbatim in its file. Start with `assetCacheCopyBuilt` empty.
- [ ] **Step 3:** Run `go test . -run 'TestAssetCacheCopy' -count=1`. Expected: PASS.
-- [ ] **Step 4:** Run the `copy-review` skill on the 19 rows. Apply Paul's edits verbatim and mark each row `approved`. Rows he defers stay `pending`, and the task that builds them waits.
+- [ ] **Step 4:** Run the `copy-review` skill on the 24 rows. Apply Paul's edits verbatim and mark each row `approved`. Rows he defers stay `pending`, and the task that builds them waits.
- [ ] **Step 5:** Commit.
### Task 1: The manifest's `assets` block and its grammar
@@ -144,7 +147,7 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text
- `TestAPISessionShipCarriesAssetsInMeta` (root, `newTestSession`): the meta part has `assets.paths`.
- `TestAPIShipCarriesAssetsIntoTheManifest`, `TestAPIShipRefusesBadAssets` (a CLI that skipped the check; body with `/`), `TestAPIShipRefusesAssetsOnStatic`, `TestAPIShipRefusesReassetingAReship` (same version, different paths, 409 like capabilities), `TestAPIShipEchoesTheStoredAssets` (console, `newConsoleFixture`, `postShip`, a `metaPartAssets` helper beside `metaPartSwap`).
- [ ] **Step 2:** Run `go test . -run 'TestShip.*Assets|TestAPISessionShipCarriesAssets' -count=1` and `go test ./internal/console/ -run 'TestAPIShip.*Assets' -count=1`. Expected: FAIL.
-- [ ] **Step 3:** Implement the flag (help EAC-1), the direct-mode manifest field, `shipAssets` and the meta field, the echo check; in the console, decode meta `assets`, call `refuseBadAssets` before any write, put it in the manifest, compare it in the reship short-circuit, echo it. Add EAC-1 to EAC-8 to `assetCacheCopyBuilt`.
+- [ ] **Step 3:** Implement the flag (help EAC-1), the direct-mode manifest field, `shipAssets` and the meta field, the echo check; in the console, decode meta `assets`, call `refuseBadAssets` before any write, put it in the manifest, compare it in the reship short-circuit, echo it. Add EAC-1 to EAC-8, EAC-23 and EAC-24 to `assetCacheCopyBuilt`.
- [ ] **Step 4:** Run both commands. Expected: PASS.
- [ ] **Step 5:** Commit.
@@ -158,9 +161,9 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text
- Consumes: `assetPathsFlag`, `apiSession.shipAssets` (Task 2).
- Produces: `cliConfig.Assets []string \`json:"assets,omitempty"\``; `func deployAssets(flagSet bool, flag []string, noAssets bool, saved []string, kind string) (paths []string, notice string, err error)` in `deploy.go`.
-- [ ] **Step 1: Write the failing tests.** `TestDeployAssetsResolution` (table over `deployAssets`): flag beats saved; `--no-assets` gives `nil` even with a saved list; both flags together is an error; saved list used when neither flag is set; static kind with a flag is EAC-8, static kind with only a saved list returns `nil` and notice EAC-9. `TestConfigLoadsAssetsList` and `TestConfigRewritesPreserveAssets` (a `writeConfigKeys` round trip keeps the list). `TestDeployWithoutSavedAssetsDeclaresNone`: a config written by today's CLI (no `assets` key) yields `nil`. `TestZeroArgDeployCarriesSavedAssets` (subprocess, `buildCarlos`, the pattern of `TestZeroArgDeployRunsFromSavedDefaults`): the console fixture receives meta `assets.paths` equal to the saved list.
+- [ ] **Step 1: Write the failing tests.** `TestDeployAssetsResolution` (table over `deployAssets`): flag beats saved; `--no-assets` gives `nil` even with a saved list; both flags together is EAC-22; saved list used when neither flag is set; static kind with a flag is EAC-8, static kind with only a saved list returns `nil` and notice EAC-9. `TestConfigLoadsAssetsList` and `TestConfigRewritesPreserveAssets` (a `writeConfigKeys` round trip keeps the list). `TestDeployWithoutSavedAssetsDeclaresNone`: a config written by today's CLI (no `assets` key) yields `nil`. `TestZeroArgDeployCarriesSavedAssets` (subprocess, `buildCarlos`, the pattern of `TestZeroArgDeployRunsFromSavedDefaults`): the console fixture receives meta `assets.paths` equal to the saved list.
- [ ] **Step 2:** Run `go test . -run 'TestDeployAssets|TestConfigLoadsAssets|TestConfigRewritesPreserveAssets|TestDeployWithoutSavedAssets|TestZeroArgDeployCarriesSavedAssets' -count=1`. Expected: FAIL.
-- [ ] **Step 3:** Implement. `loadConfig` decodes `assets` as a list (it is the first non-string field: trim each entry, drop empties); `overlayDestination` copies a destination's list when non-nil; `offerSaveProjectDefaults` offers to save the list when the run used `--assets`. Add EAC-2 and EAC-9 to `assetCacheCopyBuilt`.
+- [ ] **Step 3:** Implement. `loadConfig` decodes `assets` as a list (it is the first non-string field: trim each entry, drop empties); `overlayDestination` copies a destination's list when non-nil; `offerSaveProjectDefaults` offers to save the list when the run used `--assets`. Add EAC-2, EAC-9 and EAC-22 to `assetCacheCopyBuilt`.
- [ ] **Step 4:** Run the same command. Expected: PASS.
- [ ] **Step 5:** Commit.
@@ -222,9 +225,9 @@ Task 0 drafts every row as `pending` and runs the copy review. Rows (draft text
- `type ReconcileReport struct { Complete map[string]bool }` (keys `"<acct>/<app>"`)
- `ReconcileConfig.Report *ReconcileReport` (nil is today's behaviour; when set, `Reconcile` fills it before returning)
-An app is complete when: the listing was not `partial`, no record body under the app failed to read, no record under it was `protect`ed, and every route write for its in-scope records returned nil. `readFailed` from an unreadable account manifest makes every app of that account incomplete. A pass that returns an error leaves `Complete` empty.
+An app is complete when: the listing was not `partial`; no record body under the app failed to read; no record under it was `protect`ed; and **every** in-scope record was positively applied, meaning `convergeOwned`, a mint or an adoption ran for it without error, or its existing row's `AssetsOff` and `AssetGen` already equal `RouteFor`'s. A record the pass skipped for any other reason (a trash marker that could not be read sets `declineMint`, `reconcile.go:1137`, and the loop skips the record at `:1174` with no failing write; a held claim; a deferred adoption) makes its app incomplete (astra P1-7). `readFailed` from an unreadable account manifest makes every app of that account incomplete. A pass that returns an error leaves `Complete` empty.
-- [ ] **Step 1: Write the failing tests** (`setup`, `acmeRecord`, `putReplace` helpers): `TestReportCompleteOnACleanPass`; `TestReportOmitsAnAppWithAnUnreadableRecord` (a `viewCountingStore`-style wrapper failing one Get; the other app is still complete); `TestReportOmitsAProtectedRecordsApp` (malformed body); `TestReportOmitsEveryAppOnAPartialListing`; `TestReportEmptyWhenThePassFails`; `TestReportNilKeepsTodaysBehaviour`.
+- [ ] **Step 1: Write the failing tests** (`setup`, `acmeRecord`, `putReplace` helpers): `TestReportCompleteOnACleanPass`; `TestReportOmitsAnAppWithAnUnreadableRecord` (a `viewCountingStore`-style wrapper failing one Get; the other app is still complete); `TestReportOmitsAProtectedRecordsApp` (malformed body); `TestReportOmitsEveryAppOnAPartialListing`; `TestReportEmptyWhenThePassFails`; `TestReportOmitsAnAppWhoseRecordWasSkipped` (an unreadable trash marker with the record's `AssetGen` changed: the row keeps the old generation and the app is not complete); `TestReportNilKeepsTodaysBehaviour`.
- [ ] **Step 2:** Run `go test ./internal/provision/ -run 'TestReport' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement by tracking per-app failure inside the existing scan and convergence loops; no new reads.
- [ ] **Step 4:** Run the same command. Expected: PASS.
@@ -239,7 +242,7 @@ An app is complete when: the listing was not `partial`, no record body under the
**Interfaces:**
- Produces: `type AliasReport struct { Complete bool }`; `func ReconcileAliasesReport(ctx context.Context, s release.Store, reg *registry.Registry, now func() time.Time, deploymentRoutes map[string]bool, cache *ClaimCache, rep *AliasReport) error`. `ReconcileAliases` keeps its signature and calls it with a nil report. `ClaimCache.listWithHosts` gains a `dropped int` result. The caller passes a `deploymentRoutes` read error through so the pass can see it: `ReconcileAliasesReport` treats a nil `deploymentRoutes` as incomplete.
-- [ ] **Step 1: Write the failing tests.** `TestAliasReportCompleteOnACleanPass`, then one test per incomplete cause, each with the pass still returning nil: label count fails (extend `TestInstanceCatchallKeepsNamesWhenTheListingFails` to assert `Complete == false`), alias upsert fails, catch-all upsert fails, alias delete fails, wildcard upsert or delete fails, path upsert fails, path delete fails, a claim body unreadable, `deploymentRoutes` nil. Registry write failures are injected with a registry wrapper or a closed-handle trick already used in `aliases_test.go`; if none exists, add `aliasRegFailing` in the test file.
+- [ ] **Step 1: Write the failing tests.** `TestAliasReportCompleteOnACleanPass`, then one test per incomplete cause, each with the pass still returning nil: label count fails (extend `TestInstanceCatchallKeepsNamesWhenTheListingFails` to assert `Complete == false`), alias upsert fails, catch-all upsert fails, alias delete fails, wildcard upsert or delete fails, path upsert fails, path delete fails, a claim body unreadable, `deploymentRoutes` nil. Registry write failures are injected with SQLite triggers on the test registry's own database file: the test opens the file a second time through `database/sql` (the driver `internal/registry` uses) and creates `BEFORE INSERT`, `BEFORE UPDATE` or `BEFORE DELETE` triggers on `route_aliases`, `wildcard_domains` or `route_paths` that `SELECT RAISE(FAIL, 'injected')` for one named row, so reads succeed and only the named write fails (astra P1-5). A closed handle is not usable: `ListAliases` would fail first (`aliases.go:245`) and the pass would return an error.
- [ ] **Step 2:** Run `go test ./internal/domains/ -run 'TestAliasReport|TestInstanceCatchallKeepsNamesWhenTheListingFails' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement: every log-and-continue point also sets a local `incomplete`; the report is written once at the end.
- [ ] **Step 4:** Run the same command. Expected: PASS.
@@ -257,6 +260,8 @@ An app is complete when: the listing was not `partial`, no record body under the
- `type assetAuthority struct` with `func (a *assetAuthority) recordsPass(start time.Time, rep provision.ReconcileReport)`, `func (a *assetAuthority) aliasPass(start time.Time, rep domains.AliasReport)`, `func (a *assetAuthority) hostAt(acct, app string) (time.Time, bool)` (the older of the two; false until both have one)
- `type assetManifestCache struct` with `func (c *assetManifestCache) paths(ctx context.Context, acct, app, version string) ([]string, bool)` (bucket read through `release.GetManifest`; keeps only the declared paths per key once read; false on a read failure, never cached) and `func (c *assetManifestCache) prune(keep map[string]bool)` (drops every key not in `keep`; the writer calls it at the end of each pass with the installed and serving builds of its local routes, spec §4.1, R5-1)
- `type servingBuild func(host string) (build string, ok bool)` handed to the writer; `ServingGen(host).Build` on boxes with generations, always false otherwise
+ - `func servingBuildsDigest(routes []registry.Route, sb servingBuild) string` (hash of every local route's serving build): `edgesFileLoop` computes it every 2 s tick and runs a pass when it changes, beside the `Fingerprint` check (`edgesfile.go:888`), so a generation switch republishes within seconds with the registry unchanged (astra P1-13)
+ - `internal/edge/assettypes.go` (create, types only, so this task builds): `type HostAssets struct { Account, App string; Paths []string; Gen string; At time.Time }` and `type LocalAssets interface { For(host string) (HostAssets, bool); Declared(host, build, path string) bool }` (astra P1-1)
- `edgeHost.Assets *edgeAssets \`json:"assets,omitempty"\`` with `type edgeAssets struct { Paths []string \`json:"paths"\`; Gen string \`json:"gen"\`; At int64 \`json:"at"\` }`
- `func assetCacheArmed(getenv func(string) string) bool` (`CARLOS_ASSET_CACHE == "1"`)
- `type assetPolicy struct` (holds the registry, the authority, the manifest cache and `servingBuild`), with `func (p *assetPolicy) Publish(host string) (edgeAssets, bool)` (the publication rule below) and two methods plan 3's edge consumes through `edge.LocalAssets`: `func (p *assetPolicy) For(host string) (edge.HostAssets, bool)` (the same rule **without** the pooled exclusion, spec §3.3: a pooled route still caches on the boxes that serve it) and `func (p *assetPolicy) Declared(host, build, path string) bool` (true when `build`'s cached paths contain a prefix of `path`; false when the build's manifest is not cached)
@@ -273,6 +278,7 @@ Publication rule for a host, applied in `edgesFileWriteClaims` through `assetPol
- `TestAliasPublishesItsPrimarysBlock`; `TestCatchallAliasNeverPublishes`.
- `TestConsoleClassRouteDoesNotPublish` (Review Focus 4).
- `TestRewritesWhenAtAdvancesTwentyMinutes` and `TestDoesNotRewriteForASmallerAdvance` (`putCountingStore` counts PUTs).
+ - `TestServingBuildSwitchRepublishesWithAnUnchangedFingerprint`.
- `TestFailedRecordReadsStopAtFromAdvancing`: a `ReconcileReport` omitting the app for 61 minutes of fake time while edges-file writes succeed; the published `at` stays at the last complete pass.
- [ ] **Step 2:** Run `go test ./internal/host/ -run 'TestAssetAuthority|TestAssetManifestCache|TestPublishes|TestUnarmedBoxPublishesNoAssets|TestSleepingInstance|TestAliasPublishes|TestCatchallAliasNever|TestConsoleClassRoute|TestRewritesWhenAt|TestDoesNotRewrite|TestFailedRecordReads' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement; wire the clocks into `reconcileSerializedWith` and both alias call sites, record pass start times, hand the writer the authority, the manifest cache and `servingBuild`.
@@ -286,10 +292,9 @@ Publication rule for a host, applied in `edgesFileWriteClaims` through `assetPol
- Test: `internal/edge/homemap_assets_test.go` (create), `internal/edgesfile/edgesfile_test.go`
**Interfaces:**
+- Consumes: `HostAssets`, `LocalAssets` (declared in Task 8's `internal/edge/assettypes.go`).
- Produces:
- - `type HostAssets struct { Account, App string; Paths []string; Gen string; At time.Time }` in `internal/edge`
- `func (m *homeMap) AssetsFor(host string, now time.Time) (HostAssets, bool)`: false until the home map's first successful hosts refresh after start (spec §7, "after a restart"); exact hosts only (no one-label wildcard match); owner present; `now.Sub(At) <= time.Hour`
- - `type LocalAssets interface { For(host string) (HostAssets, bool); Declared(host, build, path string) bool }` (implemented by `internal/host`'s `assetPolicy`, consumed by plan 3)
- a golden file `internal/edgesfile/testdata/assets-host.golden.json`: Task 8's writer test asserts its output entry equals it, and this task's parse test reads it, so writer and reader pin one shape
- `edgesfile.Host.Assets *edgesfile.HostAssets` with the same wire shape as `edgeAssets`
diff --git a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md
index a353ff9fc..22fa137ea 100644
--- a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md
+++ b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md
@@ -4,7 +4,7 @@
**Goal:** A self-contained package, `internal/assetcache`, that stores cached asset responses on a box's disk under a budget, with worst-case reservations, a free-space floor, per-box and per-app fill bounds, fair-share eviction, expiry and a sweep, and that nothing calls yet (plan 3 does).
-**Architecture:** One `Store` per process over one directory. Each entry is three files named by the hex sha256 of its key: `<id>.body` (identity), optional `<id>.gz`, and `<id>.meta` (JSON, written last, which is what makes an entry exist). An in-memory index is rebuilt from the `.meta` files at `Open`. A fill is a `Fill` holding a reservation; it writes temporaries in the same directory and renames them on `Commit`. Free space comes from an `FS` interface whose Linux implementation is `statfs`. The package knows nothing about HTTP semantics beyond storing headers it is given: admission, freshness rules and serving belong to plan 3. The design is spec §7, written so the media roadmap's stage 1 can reuse it with its own directory and budget (spec §11).
+**Architecture:** One `Store` per process over one directory. Each entry is a `<id>.meta` file (JSON) naming its versioned body files, `<id>.<v>.body` (identity) and optional `<id>.<v>.gz`, where `<id>` is the hex sha256 of the key and `<v>` a random version drawn per commit. A commit writes the new versioned bodies, then renames a temporary over `<id>.meta`, then deletes the previous version's bodies, so a crash at any point leaves either the old entry or the new one, never new bytes under old metadata (astra P1-9). An in-memory index is rebuilt from the `.meta` files at `Open`. A fill is a `Fill` holding a reservation; it writes temporaries in the same directory and renames them on `Commit`. Free space comes from an `FS` interface whose Linux implementation is `statfs`. The package knows nothing about HTTP semantics beyond storing headers it is given: admission, freshness rules and serving belong to plan 3. The design is spec §7, written so the media roadmap's stage 1 can reuse it with its own directory and budget (spec §11).
**Tech Stack:** Go 1.26 standard library (`os`, `compress/gzip`, `crypto/sha256`, `encoding/json`), `golang.org/x/sys/unix` for `Statfs` on Linux.
@@ -36,7 +36,7 @@ Every task's requirements include this section.
## Census impact
-- New package `internal/assetcache`: its file-reading declarations (`Open`'s scan, `Store.Get`'s open of `.body`/`.gz`) are new `@legacy` rows of class `cache`, task `accepted-legacy`, with their envelope pinned. If the census has no `cache` class, use the class the static site cache's units carry (`internal/edge/static.go` `*`) and say so in the notes file.
+- New package `internal/assetcache`: its file-reading declarations (`Open`'s scan, `Entry.Open`) are new plain rows of class `legacy`, task `accepted-legacy`, basis `accepted-legacy;rev3=PROPOSED-edge-asset-cache-2;asset cache files under $CARLOS_DATA_DIR/asset-cache, written and read only by this package`, the form the loader requires (`plan14_root_reads_test.go:478-485`); there is no `cache` class (astra P1-3). Its envelope is pinned.
- Notes: `docs/superpowers/notes/census-proposal-edge-asset-cache-2.md`, ruling asked on #280.
## File structure
@@ -56,17 +56,18 @@ Every task's requirements include this section.
### Task 1: Keys, metadata and `Open`
**Files:**
-- Create: `internal/assetcache/key.go`, `meta.go`, `store.go`, `store_test.go`
+- Create: `internal/assetcache/key.go`, `meta.go`, `store.go`, `fs.go` (the interface only), `store_test.go`
**Interfaces:**
- Produces:
- `type Key struct { Account, App, Gen, Host, Path, RawQuery string }`; `func (k Key) ID() string` (hex sha256 over the six fields, each length-prefixed so no two keys collide); `func (k Key) AppScope() string` (`Account + "/" + App`)
- - `type Meta struct { Key Key; Header http.Header; Lifetime time.Duration; Expires time.Time; StoredAt time.Time; Size int64; GzipSize int64 }` (`GzipSize == 0` means no gzip copy)
+ - `type Meta struct { Key Key; Version string; Header http.Header; Lifetime time.Duration; Expires time.Time; StoredAt time.Time; Size int64; GzipSize int64 }` (`GzipSize == 0` means no gzip copy; `Version` names the body files)
+ - `type FS interface { Space(dir string) (total, free uint64, err error) }` in `fs.go` (declared here so this task builds; Task 2 adds the implementations) (astra P1-1)
- `type Config struct { Dir string; Budget int64; FS FS; Now func() time.Time }`
- - `func Open(cfg Config) (*Store, error)`: creates `Dir` (0700), deletes every `*.tmp-*` file, deletes `.body`/`.gz` with no `.meta`, deletes a `.meta` that does not parse together with its files, and indexes the rest.
+ - `func Open(cfg Config) (*Store, error)`: creates `Dir` (0700), deletes every `*.tmp-*` file, deletes every `.body`/`.gz` file no parsed `.meta` names (orphans and superseded versions), deletes a `.meta` that does not parse together with its files, and indexes the rest.
- `type Stats struct { Entries int; Bytes int64; Reserved int64; Apps int }`; `func (s *Store) Stats() Stats`
-- [ ] **Step 1: Write the failing tests.** `TestKeyIDIsStableAndDistinct` (same key same id; moving one character between `Path` and `RawQuery` changes it); `TestOpenCreatesTheDirectory`; `TestOpenRemovesOrphanBodiesAndTemporaries`; `TestOpenKeepsMetaWithUnknownFields`; `TestOpenDropsUnparseableMeta`; `TestOpenIndexesExistingEntries` (write three entries' files by hand in the documented layout, `Open`, `Stats` counts them and their bytes).
+- [ ] **Step 1: Write the failing tests.** `TestKeyIDIsStableAndDistinct` (same key same id; moving one character between `Path` and `RawQuery` changes it); `TestOpenCreatesTheDirectory`; `TestOpenRemovesOrphanBodiesAndTemporaries`; `TestOpenKeepsMetaWithUnknownFields`; `TestOpenDropsUnparseableMeta`; `TestOpenIndexesExistingEntries` (write three entries' files by hand in the documented layout, `Open`, `Stats` counts them and their bytes); `TestOpenDeletesSupersededVersions` (a `.meta` naming version `b` beside bodies for `a` and `b`: `a` is removed).
- [ ] **Step 2:** Run `go test ./internal/assetcache/ -count=1`. Expected: FAIL (package missing).
- [ ] **Step 3:** Implement.
- [ ] **Step 4:** Run the same command. Expected: PASS.
@@ -75,11 +76,11 @@ Every task's requirements include this section.
### Task 2: Free space
**Files:**
-- Create: `internal/assetcache/fs.go`, `statfs_linux.go`, `statfs_other.go`, `fs_test.go`, `fs_linux_test.go`
+- Create: `internal/assetcache/statfs_linux.go`, `statfs_other.go`, `fs_test.go`, `fs_linux_test.go`
**Interfaces:**
+- Consumes: `FS` (Task 1).
- Produces:
- - `type FS interface { Space(dir string) (total, free uint64, err error) }`
- `var Statfs FS` (Linux: `unix.Statfs`, `free` from `Bavail * Bsize`; other systems: returns `ErrUnsupported`)
- `var ErrUnsupported = errors.New("assetcache: free space unknown on this system")`
- `func DefaultBudget(total uint64) int64` (the smaller of 4 GiB and `total/10`)
@@ -123,12 +124,13 @@ Every task's requirements include this section.
**Interfaces:**
- Produces:
- - `func (f *Fill) Commit(m Meta, gzipCopy bool) error`: gzips the identity body into a temporary when `gzipCopy`, abandoning the copy (`GzipSize = 0`) if it would exceed the identity size or the reservation's gzip half; fsyncs and renames `.body`, then `.gz`, then writes `.meta` last; sets `Size`, `GzipSize`, `StoredAt = Now()`; releases the reservation; on any error behaves as `Abort`. A `Commit` for a key that already has an entry replaces it.
+ - `const MaxMeta = metaReserve` (64 KiB)
+ - `func (f *Fill) Commit(m Meta, gzipCopy bool) error`: encodes the meta first and returns `ErrTooLarge` (behaving as `Abort`) when it exceeds `MaxMeta`, so no write passes the reservation (astra P1-8); gzips the identity body into a temporary when `gzipCopy`, abandoning the copy (`GzipSize = 0`) if it would exceed the identity size or the reservation's gzip half; draws a fresh `Version`; fsyncs and renames the bodies to their versioned names, then renames the meta temporary over `<id>.meta`, then deletes the previous version's bodies; sets `Size`, `GzipSize`, `StoredAt = Now()`; counts the meta's bytes in the store's used bytes; releases the reservation; on any error behaves as `Abort`. A `Commit` for a key that already has an entry replaces it. A reader holding the old files keeps reading them (unlinked files stay readable on Linux).
- `type Entry struct { Meta Meta }`; `func (e *Entry) Open(gzip bool) (*os.File, error)`
- `func (s *Store) Get(k Key) (*Entry, bool)`: false and the entry deleted when `Now()` is at or past `Meta.Expires`; marks the entry recently used.
- `func (m Meta) Age(now time.Time) time.Duration` (`Lifetime - Expires.Sub(now)`, never below zero)
-- [ ] **Step 1: Write the failing tests.** `TestCommitThenGetRoundTrips` (header and body bytes equal); `TestCommitWritesMetaLast` (a crash simulated by stopping before the meta rename leaves `Open` with no entry); `TestGzipCopyIsAbandonedWhenLarger` (random bytes); `TestGzipCopyIsKept` (CSS text; reading `.gz` through `gzip.NewReader` gives the identity bytes); `TestGetExpires` (fake `Now` past `Expires`: false, files gone); `TestCommitReplacesAnEntry`; `TestAgeNeverNegative`.
+- [ ] **Step 1: Write the failing tests.** `TestCommitThenGetRoundTrips` (header and body bytes equal); `TestCommitWritesMetaLast` (a crash simulated by stopping before the meta rename leaves `Open` with no entry); `TestReplacementCrashKeepsTheOldEntry` (an existing entry, a replacement stopped after its bodies are renamed but before the meta: `Open` serves the old bytes with the old headers and removes the new bodies); `TestOversizedMetaIsRefused` (a 70 KiB header value and a 70 KiB query each give `ErrTooLarge`, nothing left on disk, reservation released); `TestGzipCopyIsAbandonedWhenLarger` (random bytes); `TestGzipCopyIsKept` (CSS text; reading `.gz` through `gzip.NewReader` gives the identity bytes); `TestGetExpires` (fake `Now` past `Expires`: false, files gone); `TestCommitReplacesAnEntry`; `TestAgeNeverNegative`.
- [ ] **Step 2:** Run `go test ./internal/assetcache/ -run 'TestCommit|TestGzipCopy|TestGetExpires|TestAge' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement.
- [ ] **Step 4:** Run the same command. Expected: PASS.
diff --git a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md
index 6dd505e4a..9896fa90d 100644
--- a/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md
+++ b/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md
@@ -34,7 +34,7 @@ Every task's requirements include this section.
## Census impact
- Changed frozen units, re-fingerprinted `@legacy` with the same class: `internal/edge/edge.go` `Edge.ServeHTTP`, `Edge.backhaulTo` (if Task 6 shares its preamble), `RunWithActivatorReady` (arming), `internal/edge/usagemeter.go` `UsageMeter.Record` only if its body changes (it should not).
-- New units: `internal/edge/assetcatch.go` (file reads through `assetcache`), class `cache`, task `accepted-legacy`.
+- New units, if the census flags any (reads go through `assetcache`, so likely none): plain rows of class `legacy`, task `accepted-legacy`, basis `accepted-legacy;rev3=PROPOSED-edge-asset-cache-3;...` (astra P1-3).
- Envelopes re-pinned: `internal/edge`, `internal/host`.
- Notes: `docs/superpowers/notes/census-proposal-edge-asset-cache-3.md`, ruling asked on #280.
@@ -44,8 +44,8 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0
| id | where | what it says |
|---|---|---|
-| EAC-20 | `docs/site/cli.md`, under `carlos ship --assets` | files under an asset path are fetched without the visitor's cookies (spec §5.2) |
-| EAC-21 | same | what `Carlos-Cache: hit`, `miss` and `bypass` mean, and that only responses marked cacheable, status 200, not HTML and without `Set-Cookie` are kept |
+| EAC-25 | `docs/site/cli.md`, under `carlos ship --assets` | files under an asset path are fetched without the visitor's cookies (spec §5.2) |
+| EAC-26 | same | what `Carlos-Cache: hit`, `miss` and `bypass` mean, and that only responses marked cacheable, status 200, not HTML and without `Set-Cookie` are kept |
## File structure
@@ -65,7 +65,7 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0
### Task 0: Copy rows
-- [ ] **Step 1:** Add EAC-20 and EAC-21 to the copy record as `pending` and extend the gate's inventory to EAC-1 to EAC-21.
+- [ ] **Step 1:** Add EAC-25 and EAC-26 to the copy record as `pending` and extend the gate's inventory to EAC-1 to EAC-26.
- [ ] **Step 2:** Run the `copy-review` skill on the two rows; apply Paul's edits verbatim; mark them `approved`.
- [ ] **Step 3:** `go test . -run 'TestAssetCacheCopy' -count=1`. Expected: PASS. Commit.
@@ -80,11 +80,11 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0
- `const assetFillHeader = "X-Carlos-Asset-Fill"`, `assetAckHeader = "X-Carlos-Asset-Fill-Ack"`, `assetExpiresHeader = "X-Carlos-Asset-Expires"`, `assetCacheHeader = "Carlos-Cache"`
- `type assetFillClaims struct { Host, PathQuery, Account, App, Gen string }`
- `func signAssetFill(key []byte, c assetFillClaims, now time.Time) string` (`"<unix>:<hex>"`, HMAC-SHA256 over `carlos-asset-fill-v1`, then the five fields and the timestamp, newline-separated, host canonicalized)
- - `func verifyAssetFill(current, next []byte, c assetFillClaims, header string, now time.Time) bool` (either key, `±hopSkew`, constant-time)
+ - `func verifyAssetFill(current, next []byte, c assetFillClaims, header string, now time.Time) (matched []byte, ok bool)` (either key, `±hopSkew`, constant-time; returns the key that verified, and the home signs the acknowledgement with **that** key, so a home that already holds `.next` answers a far edge still signing with the current key in the far edge's key (astra P1-11))
- `func signAssetAck(key []byte, assertion string, expires int64) string` (hex HMAC over `carlos-asset-fill-ack-v1`, the assertion and the decimal expiry)
- `func verifyAssetAck(key []byte, assertion string, expires int64, ack string) bool` (the far edge checks with the key it signed the assertion with)
-- [ ] **Step 1: Write the failing tests.** `TestAssetFillRoundTrips`; `TestAssetFillRefusesEachChangedClaim` (table: host, path-query, account, app, gen, timestamp outside skew); `TestHopSignatureIsNeverAnAssetFill` (a `signHop` value for the same host and time fails `verifyAssetFill`); `TestAssetFillAcceptsTheNextKey`; `TestAssetAckBindsAssertionAndExpiry` (changing either fails).
+- [ ] **Step 1: Write the failing tests.** `TestAssetFillRoundTrips`; `TestAssetFillRefusesEachChangedClaim` (table: host, path-query, account, app, gen, timestamp outside skew); `TestHopSignatureIsNeverAnAssetFill` (a `signHop` value for the same host and time fails `verifyAssetFill`); `TestAssetFillAcceptsTheNextKey`; `TestAssetAckBindsAssertionAndExpiry` (changing either fails); `TestAckUsesTheKeyTheAssertionMatched` (home holds current and next, far signs with current only: the ack verifies at the far edge).
- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetFill|TestHopSignatureIsNever|TestAssetAck' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement.
- [ ] **Step 4:** Run the same command. Expected: PASS.
@@ -101,10 +101,11 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0
- `type assetVerdict struct { Store bool; Reason string; Expires time.Time; Lifetime time.Duration; Gzip bool }`
- `func assetAdmit(status int, h http.Header, requestTime, responseTime time.Time, declared bool) assetVerdict`: the rules of spec §5.3 that can be decided from headers, in order: status 200; no `Set-Cookie`; `Cache-Control` present, not `private`, `no-store` or `no-cache`, positive `s-maxage` else `max-age`, capped at 365 days; `Vary` absent or exactly `Accept-Encoding` (case-insensitive, whitespace-tolerant); `Content-Encoding` absent or `identity`; `Content-Type` present and not `text/html` or `application/xhtml+xml`; `Access-Control-Allow-Origin` absent or `*`; `declared`; `Content-Length`, when present, at most 32 MiB; still fresh, with `Expires = responseTime + lifetime - correctedInitialAge`. `Gzip` is `compressible(h, 200)`.
- `func correctedInitialAge(h http.Header, requestTime, responseTime time.Time) time.Duration` (RFC 9111 §4.2.3: `max(apparent_age, Age + response_delay)`, `apparent_age = max(0, responseTime - Date)`)
+ - `func assetTrailerVeto(trailer http.Header) bool` (true when the trailers carry `Set-Cookie`, announced in `Trailer` or sent unannounced with `http.TrailerPrefix`); every fill, home and far, checks it after the last body byte and before `Commit`, and the far edge reads the backhauled response's own trailers, which the transport passes through (astra P1-6)
- `func assetStoredHeader(h http.Header) http.Header`: a clone without hop-by-hop headers (`Connection` and the names it lists, `Keep-Alive`, `Transfer-Encoding`, `Upgrade`, `TE`, `Trailer`, `Proxy-*`), `Date`, `Age`, `Content-Length`, `Carlos-Purge`, `Carlos-Purge-Host`, `Carlos-Cache`, and every `X-Carlos-*`
-- [ ] **Step 1: Write the failing tests.** `TestAssetAdmitTable`, one row per rule above, each failing alone with its `Reason`; rows that store: `public, max-age=31536000, immutable` CSS; `s-maxage` beating `max-age`; `max-age` over a year capped at 365 days. `TestAssetAdmitRefusesALongLivedHTMLFallback` (`200 text/html`, `public, max-age=31536000`). `TestAssetAdmitRefusesNoContentType`. `TestAssetAdmitRefusesAnUndeclaringBuild`. `TestExpiryIsFromResponseTime` (request 0, response 20, `Age: 0`, lifetime 30: expires at 30). `TestAnAlreadyExpiredResponseIsNotStored` (`Age` larger than lifetime). `TestStoredHeaderKeepsCSPAndCORP` and `TestStoredHeaderDropsHopAndPlatformHeaders`.
-- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetAdmit|TestExpiryIsFromResponseTime|TestAnAlreadyExpired|TestStoredHeader' -count=1`. Expected: FAIL.
+- [ ] **Step 1: Write the failing tests.** `TestAssetAdmitTable`, one row per rule above, each failing alone with its `Reason`; rows that store: `public, max-age=31536000, immutable` CSS; `s-maxage` beating `max-age`; `max-age` over a year capped at 365 days. `TestAssetAdmitRefusesALongLivedHTMLFallback` (`200 text/html`, `public, max-age=31536000`). `TestAssetAdmitRefusesNoContentType`. `TestAssetAdmitRefusesAnUndeclaringBuild`. `TestExpiryIsFromResponseTime` (request 0, response 20, `Age: 0`, lifetime 30: expires at 30). `TestAnAlreadyExpiredResponseIsNotStored` (`Age` larger than lifetime). `TestStoredHeaderKeepsCSPAndCORP` and `TestStoredHeaderDropsHopAndPlatformHeaders`; `TestTrailerVetoAnnouncedAndUnannounced`.
+- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetAdmit|TestExpiryIsFromResponseTime|TestAnAlreadyExpired|TestStoredHeader|TestTrailerVeto' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement.
- [ ] **Step 4:** Run the same command. Expected: PASS.
- [ ] **Step 5:** Commit.
@@ -154,7 +155,9 @@ Added to `docs/superpowers/plans/2026-10-24-edge-asset-cache-copy.md` by Task 0
- Produces:
- `func (e *Edge) serveAssetLocal(w http.ResponseWriter, r *http.Request, host string, up upstream) bool` (true when it answered)
- `func (e *Edge) serveAssetFillForHop(w http.ResponseWriter, r *http.Request, host string, up upstream) bool` (true when it handled a verified, valid fill; it marks the request so the meter defer skips it)
- - `func (e *Edge) fillFromApp(ctx context.Context, w http.ResponseWriter, r *http.Request, up upstream, k assetcache.Key, a HostAssets, onHeader func(v assetVerdict, h http.Header)) (*assetcache.Entry, error)`: builds the allow-listed request (`Host`, `User-Agent`, `Accept`, `Accept-Encoding: identity`; method `GET` even for `HEAD`), sends it through the activator for a hibernating route or `up.proxy` otherwise, reads the answering build from the response's `X-Carlos-Version` and asks `a`'s `Declared`, calls `assetAdmit`, calls `onHeader` before the first body byte, tees the body to the client and to a `Fill` while the verdict says store, abandons the fill (keeping the client stream) at the 60 s deadline or on a short body, and commits on success.
+ - `func (e *Edge) fillFromApp(ctx context.Context, w http.ResponseWriter, r *http.Request, up upstream, k assetcache.Key, a HostAssets, onHeader func(v assetVerdict, h http.Header)) (*assetcache.Entry, error)`: builds the allow-listed request (`Host`, `User-Agent`, `Accept`, `Accept-Encoding: identity`; method `GET` even for `HEAD`), sends it through the activator for a hibernating route or `up.proxy` otherwise, reads the answering build from the response's `X-Carlos-Version` and asks `e.assets.local.Declared(host, build, r.URL.Path)` with the request hostname (astra P1-2), calls `assetAdmit`, calls `onHeader` before the first body byte, writes the body to a `Fill` while the verdict says store while hashing it with SHA-256, abandons the fill at the 60 s deadline, on a short body or on `assetTrailerVeto`, and on success sets a strong `ETag` of `"<hex sha256>"` in the stored header when the app sent none (spec §5.4; the far edge computes the same over the same bytes, so home and far validators match) and commits (astra P1-14).
+
+**The initiating request.** When the initiator's request has no `Range`, `If-None-Match`, `If-Modified-Since` or `If-Range`, the body is teed to it as it fills. When it has any of them, nothing is teed: the initiator waits for the commit and is answered by `serveAssetEntry`, so its conditional or range is honoured on a cold miss as on a hit (astra P1-10). If the verdict refuses at header time, the full response is streamed to it unmodified (a 200 to a range request is valid HTTP) with `Carlos-Cache: bypass`; if the fill is abandoned after the body (deadline, short body, trailer veto), the initiator's request is forwarded again as today and answered `bypass`.
On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Carlos-Asset-Fill`; it checks `verifyAssetFill`, that `e.assets.local.For(host)` gives the asserted account, app and gen and catches the path; on any failure it returns false and the request continues as an ordinary hop. It answers a hit from the store with `X-Carlos-Asset-Expires` set to the entry's expiry, or fills; in both cases it deletes any app-set `X-Carlos-Asset-Fill-Ack` and `X-Carlos-Asset-Expires`, sets `X-Carlos-Asset-Expires` (`0` when the verdict does not store) and `X-Carlos-Asset-Fill-Ack`, and never touches the shared LRU.
@@ -165,6 +168,9 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car
- `TestLocalUndeclaringBuildIsNotStored` (`Declared` false for the answering `X-Carlos-Version`).
- `TestHibernatingRouteHitDoesNotWake` (the `genActivator` fixture from `genedge_test.go:16`; a hit leaves its wake count at zero).
- `TestHeadFillsWithGetAndAnswersHead`, `TestQueryIsPartOfTheKey`, `TestFillSurvivesTheInitiatorDisconnecting`, `TestShortBodyIsNotStored`.
+ - `TestColdMissAnswersARangeFromTheEntry` and `TestColdMissAnswersIfNoneMatchFromTheEntry` (the initiator gets 206 or 304, not the fetched 200).
+ - `TestFallbackETagIsTheBodyHash` (no app `ETag`: the entry's is `"<sha256>"`, a conditional hit with it is 304).
+ - `TestSetCookieTrailerVetoesTheFill` (announced and unannounced).
- `TestStalledFillIsAbandonedAtTheDeadline` (deadline hook `assetFillDeadline`; reservation released, `Stats().Reserved == 0`, waiters forwarded).
- `TestNonCanonicalPathBypasses`, `TestPostUnderAPrefixPassesThrough`.
- `TestHomeFillAcknowledgesAndSkipsTheMeter` (a verified hop with a valid assertion: response has a verifiable ack, the usage meter recorded nothing, the shared LRU has no entry).
@@ -184,7 +190,7 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car
**Interfaces:**
- Consumes: Tasks 1 to 5, `homeMap.AssetsFor`, `homeMap.Home`, `signingKey`, `backhaulAcquire`, `backhaulTransport`, `signHop`, `UsageMeter.Record`.
-- Produces: `func (e *Edge) serveAssetFar(w http.ResponseWriter, r *http.Request, host string) bool`: when `AssetsFor(host, now)` is true, the path is canonical and matches, and the method is `GET` or `HEAD`: answer a hit from the store (and meter it with `Record(registry.Route{Host: host, Account: a.Account, App: a.App}, 1, n, false)`); on a miss lead or join a flight; the leader sends the allow-listed request to the home through `backhaulAcquire` and `backhaulTransport`, with `X-Carlos-Hop`, `X-Carlos-Hop-Auth` and `X-Carlos-Asset-Fill`; with a valid ack it meters the response once and stores it when `X-Carlos-Asset-Expires` is non-zero and the plan 2 store admits it, using that expiry unchanged; without a valid ack it streams the response unmodified with `Carlos-Cache: bypass`, does not meter, does not store. `X-Carlos-Asset-Fill-Ack` and `X-Carlos-Asset-Expires` never reach the client. Returns false (so `backhaul` runs as today) when the host does not catch.
+- Produces: `func (e *Edge) serveAssetFar(w http.ResponseWriter, r *http.Request, host string) bool`: when `AssetsFor(host, now)` is true, the path is canonical and matches, the method is `GET` or `HEAD`, and `isUpgradeRequest(r)` is false (this hook runs before `ServeHTTP`'s upgrade branch, so it must exclude upgrades itself, astra P1-12): answer a hit from the store (and meter it with `Record(registry.Route{Host: host, Account: a.Account, App: a.App}, 1, n, false)`); on a miss lead or join a flight; the leader sends the allow-listed request to the home through `backhaulAcquire` and `backhaulTransport`, with `X-Carlos-Hop`, `X-Carlos-Hop-Auth` and `X-Carlos-Asset-Fill`; with a valid ack it meters the response once and stores it when `X-Carlos-Asset-Expires` is non-zero and the plan 2 store admits it, using that expiry unchanged, after `assetTrailerVeto` on the backhauled trailers and with the same fallback `ETag` rule and initiating-request rule as Task 5; without a valid ack it streams the response unmodified with `Carlos-Cache: bypass`, does not meter, does not store. `X-Carlos-Asset-Fill-Ack` and `X-Carlos-Asset-Expires` never reach the client. Returns false (so `backhaul` runs as today) when the host does not catch.
- [ ] **Step 1: Write the failing tests** (a far `Edge` and the `fakeHomeEdge` from `homemap_test.go:709` extended to answer with or without an ack; "not acceptance").
- `TestFarMissFillsThenHits` and `TestFarHitDoesNotDialTheHome` (`e2eDialCounter`).
@@ -194,6 +200,9 @@ On the home, `serveAssetFillForHop` runs only for a verified hop carrying `X-Car
- `TestFarStaleAtForwards` (`at` 61 minutes old: plain backhaul, nothing stored).
- `TestFarFillCarriesNoCookie` (asserted at the fake home).
- `TestClientSentFillHeaderIsStripped`.
+ - `TestFarUpgradeUnderAPrefixIsBackhauledIntact` (a WebSocket `GET` under `/assets/`: not caught, upgrade headers reach the fake home).
+ - `TestFarColdMissAnswersARangeFromTheEntry`.
+ - `TestFarFallbackETagMatchesTheHomes` (no app `ETag`: home and far entries carry the same value).
- `TestFarCoalescedWaitersGetTheEntry` and `TestFarRefusedFillReachesOnlyTheInitiator` (differing `Accept`, a `Set-Cookie` response, an unacknowledged fill; each waiter's request reaches the fake home on its own).
- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestFar|TestClientSentFillHeader' -count=1`. Expected: FAIL.
- [ ] **Step 3:** Implement.
@@ -236,20 +245,26 @@ This task adds no production code unless a test fails. It drives spec §12's aut
- Create: `internal/edge/asset_e2e_test.go`
- Modify: `internal/edge/backhaul_e2e_test.go` (scene routes gain `Account`, `App`, `AssetGen`; `publishEdgesFile` gains an option to write the golden asset entry)
-- [ ] **Step 1: Write the tests** on `newBackhaulScene` with real stores on both edges:
+- [ ] **Step 1: Write the tests** on `newBackhaulScene` with real stores on both edges. Extend `newBackhaulEdge` to take an activator, so the home can run the `genActivator` fixture (`genedge_test.go:16`) for the generation cases. Spec §12's attack matrix runs here through production wiring, not only in Tasks 5 and 6's unit tests (astra P1-15):
- `TestAssetE2EFarMissThenHit`: an asset requested through the far edge is `miss` once and `hit` after; the home's backend saw exactly one request.
- `TestAssetE2EGzipFromTheFarEdge`: a 5 MiB CSS bundle is served gzip-compressed from the far edge with `Vary: Accept-Encoding` and a weak `ETag`.
- `TestAssetE2EPurgeMakesEveryEdgeMiss`: changing the generation in the published entry and converging makes both edges miss once.
- `TestAssetE2EOldFarEdgeForgedMarkerIsBilledAsAHop`: a far edge with the cache unarmed forwards a client-sent `X-Carlos-Asset-Fill`; the home bills it as an ordinary hop (`TestHopServedRequestMetersOnceWithHopCounters`'s counters) and sends no ack.
- `TestAssetE2EDisarmedHomeFillsNothing`: the home unarmed with a stale published entry still on the far edge: responses arrive `bypass`, metered once at the home, nothing stored.
+ - `TestAssetE2EFillCarriesNoCredentials`: a client `Cookie` and `Authorization` sent to the far edge never reach the home's backend.
+ - `TestAssetE2ERefusedFillsAreStoredNowhere`: `Set-Cookie`, a `Set-Cookie` trailer, `Vary: Origin`, a long-lived HTML fallback and a 404, each through the far edge: answered `bypass`, both stores empty, metered once.
+ - `TestAssetE2ECoalescedWaitersAndRefusals`: concurrent far requests for one key share an admitted entry; with a refused response, each waiter reaches the home on its own.
+ - `TestAssetE2EStalledBodyIsAbandoned`: the home's backend stalls after headers; the far fill is abandoned at the deadline with both reservations released.
+ - `TestAssetE2EUndeclaringBuildIsNotStored`: during a generation switch where only the installed build declares the prefix, the serving build's cacheable fallback is stored on neither edge.
+ - `TestAssetE2EWarmSleepRestart`: a generation-managed instance warms, sleeps and restarts its edge process; hits continue on both edges with no wake.
- [ ] **Step 2:** Run `go test ./internal/edge/ -run 'TestAssetE2E' -count=1` with the sandbox off. Expected: PASS.
- [ ] **Step 3:** Commit.
### Task 10: Docs, census, merge and CI
-- [ ] **Step 1:** Write EAC-20 and EAC-21 into `docs/site/cli.md`; add them to `assetCacheCopyBuilt`; `go test . -run 'TestAssetCacheCopy' -count=1` and `go test ./internal/docsite/ -count=1` PASS.
+- [ ] **Step 1:** Write EAC-25 and EAC-26 into `docs/site/cli.md`; add them to `assetCacheCopyBuilt`; `go test . -run 'TestAssetCacheCopy' -count=1` and `go test ./internal/docsite/ -count=1` PASS.
- [ ] **Step 2:** `git fetch origin && git merge origin/main`; census test FAILs naming the units and envelopes above; write the overlay and notes file; post on #280; census PASS.
-- [ ] **Step 3:** Provider boundary (spec §9): `TestAssetPackagesImportNoAWSSDK` in `internal/edge` runs `go list -deps` over `internal/assetcache` and checks that no `github.com/aws/` package appears (`internal/edge` itself already imports none; assert it too). Task 9's e2e is the no-AWS-configuration run.
+- [ ] **Step 3:** Provider boundary (spec §9): `TestAssetCodeImportsNoAWSSDK` in `internal/edge` runs `go list -deps ./internal/assetcache` and checks no `github.com/aws/` package appears, and parses the imports of this plan's `internal/edge/asset*.go` files with `go/parser` and checks the same. It does not check `internal/edge` as a whole, which imports the AWS SDK for its Route 53 adapter (`route53dns.go:25`) (astra P1-4). Task 9's e2e is the no-AWS-configuration run.
- [ ] **Step 4:** Cross-compiles and `gofmt -l .` clean; weights for any new test over a second; commit, push, CI green on the tip, merge per plan 1's Global Constraints.
### Task 11: Acceptance on dev (needs Paul's go-ahead to roll)
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS.md b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS.md
new file mode 100644
index 000000000..421e67935
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-ASSET-CACHE-PLANS.md
@@ -0,0 +1,33 @@
+1. **P1 — Shared types are introduced after tasks that require them.** Plan 1 Task 8 defines `assetPolicy.For` returning `edge.HostAssets`, but Task 9 first introduces that type and `LocalAssets` ([plan 1:262](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:262), [plan 1:290](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:290)). Likewise, Plan 2 Task 1 defines `Config.FS FS`, while Task 2 introduces `FS` ([plan 2:65](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:65), [plan 2:82](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:82)). Neither earlier task can reach its required passing build. **Smallest fix:** move the shared type declarations before their first consumers; leave their implementations in the later tasks.
+
+2. **P1 — Plan 3 Task 5 calls a method on the wrong interface.** `fillFromApp` takes `a HostAssets` and asks “`a`’s `Declared`” ([plan 3:157](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:157)). `HostAssets` is a data struct; `Declared` belongs to `LocalAssets` ([plan 1:290](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:290)). **Smallest fix:** specify `e.assets.local.Declared(host, answeringBuild, path)`, using the request hostname.
+
+3. **P1 — The census instructions prescribe invalid dispositions.** Plan 1 Task 11’s inventory calls for an `@legacy` row retaining class `authority`, and a new authority row owned solely by `J1` ([plan 1:45](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:45)). Plan 3 Task 10 prescribes class `cache` ([plan 3:37](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:37)). The loader requires `@legacy` rows to have class `legacy`, has no `cache` class, and does not accept `J1` alone as an authority owner ([census:420](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/plan14_root_reads_test.go:420), [census:480](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/plan14_root_reads_test.go:480), [census:507](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/plan14_root_reads_test.go:507)). **Smallest fix:** preserve existing authority rows as ordinary authority entries, assign valid kept-family ownership, and explicitly use the existing legacy format for applicable cache reads.
+
+4. **P1 — Plan 3 Task 10’s provider test necessarily fails on existing code.** It asserts that `go list -deps` for `internal/edge` contains no AWS packages, claiming the package already imports none ([plan 3:252](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:252)). `route53dns.go` directly imports four AWS packages ([route53dns.go:25](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/route53dns.go:25)). **Smallest fix:** retain the dependency check for the new `assetcache` package and check the new asset implementation files for AWS imports; retain the credential-free `DirStore` integration test.
+
+5. **P1 — Plan 1 Task 7’s proposed write-failure fixtures cannot exercise the named branches.** The plan keeps a concrete `*registry.Registry` parameter but proposes an `aliasRegFailing` wrapper or closing the registry ([plan 1:240](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:240)). A wrapper cannot satisfy that concrete parameter. Closing the handle makes `ListAliases` fail before the write branches, so the pass returns an error instead of the required nil-with-incomplete result ([aliases.go:245](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/domains/aliases.go:245), [registry.go:2755](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/registry/registry.go:2755)). **Smallest fix:** prescribe selective SQLite write-failure triggers on the test database, or introduce a narrow registry interface.
+
+6. **P2 — Plan 3 Tasks 2, 5 and 6 omit the `Set-Cookie` trailer veto.** Admission checks headers, and the fill instructions commit after a successful body without a trailer check ([plan 3:102](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:102), [plan 3:157](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:157)). The spec explicitly prohibits `Set-Cookie` trailers ([spec:491](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:491)); the existing shared cache already checks them ([edge.go:1875](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/edge.go:1875)). **Smallest fix:** require a final trailer veto before committing on both edges, preserving the information through backhaul; test announced and unannounced cookie trailers.
+
+7. **P2 — Plan 1 Task 6 can confirm records that were never applied.** Its completeness predicate checks successful reads and successful attempted writes ([plan 1:225](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:225)). However, an unreadable trash marker sets `declineMint`, and the record loop skips the record before `convergeOwned`, without a failing route write ([reconcile.go:1137](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/reconcile.go:1137), [reconcile.go:1174](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/provision/reconcile.go:1174)). A newly purged generation can therefore remain unapplied while authority time advances. **Smallest fix:** require positive application or verified equality for every relevant existing route; mark skipped application incomplete and test this case with a changed generation.
+
+8. **P2 — Plan 2 Tasks 3 and 4 do not bound metadata writes.** Reservations allocate 64 KiB for metadata, but the specified write limits cover only identity and gzip bodies; `Commit` serializes arbitrary `Meta.Header` and key contents ([plan 2:103](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:103), [plan 2:126](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:126)). This does not implement the spec’s prohibition on writes past the reservation ([spec:643](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:643)). **Smallest fix:** bound the encoded metadata before writing it, include its bytes in accounting, and test oversized headers and queries.
+
+9. **P2 — Plan 2 Task 4’s replacement sequence is not crash-safe.** `Commit` replaces `.body`, then `.gz`, then `.meta`, including for an existing key ([plan 2:126](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:126)). A crash after replacing the body leaves the previous valid metadata, so `Open` indexes new bytes with old headers, sizes or expiry. The proposed missing-meta crash test does not catch this. **Smallest fix:** invalidate the old metadata before replacing its files, with coordinated readers, or publish versioned body files through the metadata commit. Add a replacement-crash test.
+
+10. **P2 — Plan 3 Task 5 does not apply conditional or range semantics to the initiating miss.** It strips those request headers upstream and tees the resulting body directly to the client ([plan 3:157](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:157)). Task 3 tests those semantics only against an existing entry. The spec requires the edge to answer them itself ([spec:400](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:400)). **Smallest fix:** specify how an admitted cold fill answers its initiating conditional/range request through `serveAssetEntry`, and add cold local and far tests.
+
+11. **P2 — Plan 3 Tasks 1 and 5 leave acknowledgement key selection undefined during rotation.** `verifyAssetFill` accepts either key but returns only a boolean; `verifyAssetAck` accepts only the key the far edge used ([plan 3:83](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:83)). The existing `signingKey` prefers `.next` immediately ([mesh.go:167](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/edge/mesh.go:167)). If the home has `.next` and the far edge still signs with current, signing the acknowledgement with the home’s preferred key makes both edges skip billing. **Smallest fix:** return the matched verification key and sign the acknowledgement with it; test asymmetric key availability.
+
+12. **P2 — Plan 3 Task 6 catches upgrade requests.** Its far-edge predicate accepts any matching canonical GET or HEAD, with no upgrade exclusion ([plan 3:187](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:187)). Unlike the local hook, this executes before the existing upgrade branch. A WebSocket GET under an asset prefix can receive a cached body or lose its upgrade headers during filling, contrary to [spec:385](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:385). **Smallest fix:** explicitly reject `isUpgradeRequest(r)` before far-cache lookup and test intact backhaul of the upgrade.
+
+13. **P2 — Plan 1 Task 8 lacks a publication trigger for serving-generation changes.** It adds the serving build to the path union and a trigger for authority-time advances, but no trigger for a serving build changing while registry fields stay unchanged ([plan 1:264](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:264)). The current loop skips the writer until the registry fingerprint changes or its timer expires ([edgesfile.go:888](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/internal/host/edgesfile.go:888)). Thus a retiring build’s prefixes can remain published until the five-minute refresh, contrary to [spec:255](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:255). **Smallest fix:** include serving-build changes in the scheduling signal and test a generation switch with an unchanged registry fingerprint.
+
+14. **P2 — No task implements the fallback ETag.** Plan 2 Task 4 stores supplied headers, while Plan 3 Task 3 only preserves or weakens an existing ETag ([plan 2:126](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-2-disk-store.md:126), [plan 3:119](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:119)). The spec requires a strong body-SHA256 ETag when the app supplies none ([spec:560](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:560)). **Smallest fix:** assign hashing and fallback-header creation to the fill/commit task, with tests for identical home/far validators and conditional hits.
+
+15. **P2 — Plan 3’s acceptance tasks omit required production-wired attacks.** Tasks 5 and 6 explicitly use fake `LocalAssets` and a fake home; Task 9’s real two-edge tests cover only five scenarios ([plan 3:161](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:161), [plan 3:189](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:189), [plan 3:239](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-3-serve.md:239)). The spec requires production wiring for the attack matrix, including credential stripping, refused fills, coalescing, deadlines and answering-generation checks ([spec:717](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/specs/2026-10-24-edge-asset-cache-design.md:717)). **Smallest fix:** assign those cases explicitly to the real two-edge suite, including the warm/sleep/restart generation lifecycle.
+
+16. **P2 — Plan 1’s fixed copy inventory omits newly required refusals.** Task 0 pins exactly EAC-1 through EAC-19 ([plan 1:104](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:104)), but Task 3 requires a new error for combining `--assets` and `--no-assets`, and Task 2 requires echo and changed-assets reship refusals, without corresponding rows ([plan 1:138](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:138), [plan 1:161](/home/paulca/amadan.net/carlos/platform-edge-asset-cache/docs/superpowers/plans/2026-10-24-edge-asset-cache-plan-1-declare-publish.md:161)). **Smallest fix:** inventory these strings before Task 0’s review, update the exact-ID gate, and explicitly identify any existing approved wording reused.
+
+reject
\ No newline at end of file