carlos / platform Public

plans 1, 3: Astra plan review R8 (resize holds the lease, SizeControl interface, audit-only failure test)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 87a580bed28e8836c7acaef7dac93acad5d5f4a0 parent f112bfe
3 files changed, +61 −10
  • docs/superpowers/plans/2026-10-21-instance-policy-1-foundation.md +18 −4
  • docs/superpowers/plans/2026-10-21-instance-policy-3-sizes.md +33 −6
  • docs/superpowers/plans/CODEX-FINDINGS-instance-policy-plans-R8.md +10 −0

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.016s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (1 s)
=== gofmt (0 s)
gofmt -l .: clean
=== rest (146 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	4.519s
ok  	github.com/carlosframework/platform/cmd/moveprobe	11.902s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.773s
ok  	github.com/carlosframework/platform/internal/activator	58.300s
ok  	github.com/carlosframework/platform/internal/agentsock	0.019s
ok  	github.com/carlosframework/platform/internal/alerting	0.006s
ok  	github.com/carlosframework/platform/internal/analytics	0.021s
ok  	github.com/carlosframework/platform/internal/appconfig	3.033s
ok  	github.com/carlosframework/platform/internal/apperr	0.120s
ok  	github.com/carlosframework/platform/internal/applog	0.271s
ok  	github.com/carlosframework/platform/internal/assetcache	0.143s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.007s
ok  	github.com/carlosframework/platform/internal/boxid	0.919s
?   	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.008s
ok  	github.com/carlosframework/platform/internal/caps	0.013s
ok  	github.com/carlosframework/platform/internal/certwarm	0.003s
ok  	github.com/carlosframework/platform/internal/cgroup	0.760s
ok  	github.com/carlosframework/platform/internal/changelog	0.516s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.176s
ok  	github.com/carlosframework/platform/internal/clamd	0.068s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	7.780s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.017s
ok  	github.com/carlosframework/platform/internal/dbquery	0.006s
ok  	github.com/carlosframework/platform/internal/dbreads	2.116s
?   	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.005s
ok  	github.com/carlosframework/platform/internal/docsite	0.020s
ok  	github.com/carlosframework/platform/internal/domains	2.904s
ok  	github.com/carlosframework/platform/internal/doorkey	0.009s
ok  	github.com/carlosframework/platform/internal/economics	0.007s
ok  	github.com/carlosframework/platform/internal/edge	45.201s
?   	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.135s
ok  	github.com/carlosframework/platform/internal/gate	1.732s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.018s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.156s
ok  	github.com/carlosframework/platform/internal/host	141.650s
ok  	github.com/carlosframework/platform/internal/hostactors	5.491s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.007s
ok  	github.com/carlosframework/platform/internal/hostbox	3.162s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.838s
ok  	github.com/carlosframework/platform/internal/hostenv	0.005s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.007s
ok  	github.com/carlosframework/platform/internal/hostexec	5.271s
ok  	github.com/carlosframework/platform/internal/hostfence	0.083s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.177s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.025s
ok  	github.com/carlosframework/platform/internal/hostguard	1.040s
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.005s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.005s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.602s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.220s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.007s
ok  	github.com/carlosframework/platform/internal/infraview	0.006s
ok  	github.com/carlosframework/platform/internal/instancectl	5.227s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.817s
ok  	github.com/carlosframework/platform/internal/instancefs	0.023s
ok  	github.com/carlosframework/platform/internal/instancekey	0.004s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.007s
ok  	github.com/carlosframework/platform/internal/layout	1.768s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.008s
ok  	github.com/carlosframework/platform/internal/lease	10.160s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.486s
ok  	github.com/carlosframework/platform/internal/ledger	0.017s
ok  	github.com/carlosframework/platform/internal/mail	0.226s
ok  	github.com/carlosframework/platform/internal/maintenance	0.089s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.651s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.100s
ok  	github.com/carlosframework/platform/internal/metrics	0.011s
ok  	github.com/carlosframework/platform/internal/moves	22.143s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.086s
ok  	github.com/carlosframework/platform/internal/objstore	0.297s
ok  	github.com/carlosframework/platform/internal/ops	0.221s
ok  	github.com/carlosframework/platform/internal/park	0.320s
ok  	github.com/carlosframework/platform/internal/plans	0.007s
ok  	github.com/carlosframework/platform/internal/privatefile	0.006s
ok  	github.com/carlosframework/platform/internal/provision	21.549s
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.625s
ok  	github.com/carlosframework/platform/internal/relay	7.232s
ok  	github.com/carlosframework/platform/internal/release	26.434s
ok  	github.com/carlosframework/platform/internal/rollout	3.721s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.004s
ok  	github.com/carlosframework/platform/internal/rootaws	0.010s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.066s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/runs	0.532s
ok  	github.com/carlosframework/platform/internal/s3compat	0.014s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.720s
ok  	github.com/carlosframework/platform/internal/seeds	0.012s
ok  	github.com/carlosframework/platform/internal/sim/engines	2.430s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	10.356s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.573s
ok  	github.com/carlosframework/platform/internal/status	0.085s
ok  	github.com/carlosframework/platform/internal/steering	0.009s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.352s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.005s
ok  	github.com/carlosframework/platform/internal/termfence	0.008s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.005s
ok  	github.com/carlosframework/platform/internal/updatewords	0.007s
ok  	github.com/carlosframework/platform/internal/usage	0.006s
ok  	github.com/carlosframework/platform/internal/version	0.002s
ok  	github.com/carlosframework/platform/internal/vet	1.601s
=== internal_console@0 (81 s)
ok  	github.com/carlosframework/platform/internal/console	78.881s
=== internal_console@1 (41 s)
ok  	github.com/carlosframework/platform/internal/console	39.775s
=== internal_console@2 (43 s)
ok  	github.com/carlosframework/platform/internal/console	41.744s
=== internal_console@3 (45 s)
ok  	github.com/carlosframework/platform/internal/console	43.080s
=== internal_console@4 (44 s)
ok  	github.com/carlosframework/platform/internal/console	42.195s
=== internal_console@5 (38 s)
ok  	github.com/carlosframework/platform/internal/console	36.103s
=== internal_console@6 (44 s)
ok  	github.com/carlosframework/platform/internal/console	42.071s
=== internal_console@7 (47 s)
ok  	github.com/carlosframework/platform/internal/console	45.530s
=== root@0 (112 s)
ok  	github.com/carlosframework/platform	109.355s
=== root@1 (99 s)
ok  	github.com/carlosframework/platform	96.667s
=== root@2 (81 s)
ok  	github.com/carlosframework/platform	78.276s
=== 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: 50725026 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.023s
=== RUN   TestT4StorageProbeMinio
2026/10/25 00:55:44 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E19FB91A1C048F, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.022s
=== 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.032s
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/25 00:55:46 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/25 00:55:46 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/25 00:55:46 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/25 00:55:46 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000002
2026/10/25 00:55:46 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000002 maxTXID=0000000000000002 size=370
2026/10/25 00:55:46 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000002
2026/10/25 00:55:46 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000002 txid.db=0000000000000003
2026/10/25 00:55:46 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000003 maxTXID=0000000000000003 size=454
2026/10/25 00:55:46 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000003 txid.db=0000000000000003
2026/10/25 00:55:46 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000000 txid.db=0000000000000001
2026/10/25 00:55:46 INFO ltx file uploaded system=store db=source.db replica=file level=0 minTXID=0000000000000001 maxTXID=0000000000000001 size=720
2026/10/25 00:55:46 INFO replica sync system=store db=source.db replica=file txid.replica=0000000000000001 txid.db=0000000000000001
2026/10/25 00:55:46 console: CARLOS_SECRETS_PUBKEY is unset, so no region outside the home can be chosen automatically
2026/10/25 00:55:46 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%3A44023: could not determine target mailbox
    agent.go:135: dbreads: dbr_fdum74mnnxs2ldiukoovoiopcu: planned 3 files (1544 bytes) and restored to 3 in 9ms
    agent.go:135: dbreads: dbr_fdum74mnnxs2ldiukoovoiopcu: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/25 00:55:47 console: db read dbr_fdum74mnnxs2ldiukoovoiopcu: relayed 1 chunks (102 bytes) in 1.004s, ended done
    agent.go:135: dbreads: dbr_2ykksrzwr2zmprez45wlzk7wki: planned 1 files (720 bytes) and restored to 1 in 8ms
    agent.go:135: dbreads: dbr_2ykksrzwr2zmprez45wlzk7wki: output 5 bytes in 1 confirmed chunks in 1ms
2026/10/25 00:55:48 console: db read dbr_2ykksrzwr2zmprez45wlzk7wki: relayed 1 chunks (102 bytes) in 1.003s, ended done
    agent.go:135: dbreads: dbr_oxe3q3pmmk6ha2awjlkyfwax4y: planned 3 files (1544 bytes) and held through 3 in 8ms
    agent.go:135: dbreads: dbr_oxe3q3pmmk6ha2awjlkyfwax4y: output 1544 bytes in 1 confirmed chunks in 1ms
2026/10/25 00:55:49 console: db read dbr_oxe3q3pmmk6ha2awjlkyfwax4y: relayed 1 chunks (1641 bytes) in 1.004s, ended done
=== RUN   TestDBForwardMinio/by_disconnect
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_3s6htslkg7entlm76gx3j724pe: planned 3 files (1544 bytes) and restored to 3 in 10ms
2026/10/25 00:55:49 console: db read dbr_3s6htslkg7entlm76gx3j724pe: relayed 0 chunks (0 bytes) in 33ms, ended client gone
    agent.go:135: dbreads: dbr_3s6htslkg7entlm76gx3j724pe: output 5 bytes in 1 confirmed chunks in 12ms
=== RUN   TestDBForwardMinio/by_timeout
=== NAME  TestDBForwardMinio
    agent.go:135: dbreads: dbr_yxhapigmaj63clydgccmdfu4je: planned 3 files (1544 bytes) and restored to 3 in 10ms
2026/10/25 00:56:09 console: db read dbr_yxhapigmaj63clydgccmdfu4je: relayed 0 chunks (0 bytes) in 19.308s, ended no-result
    agent.go:135: dbreads: dbr_yxhapigmaj63clydgccmdfu4je: output 5 bytes in 1 confirmed chunks in 19.279s
--- PASS: TestDBForwardMinio (22.67s)
    --- PASS: TestDBForwardMinio/by_disconnect (0.15s)
    --- PASS: TestDBForwardMinio/by_timeout (19.42s)
PASS
ok  	github.com/carlosframework/platform/internal/console	22.692s
minio contract: PASS
diff --git a/docs/superpowers/plans/2026-10-21-instance-policy-1-foundation.md b/docs/superpowers/plans/2026-10-21-instance-policy-1-foundation.md
index 33a45f0d3..6601fd916 100644
--- a/docs/superpowers/plans/2026-10-21-instance-policy-1-foundation.md
+++ b/docs/superpowers/plans/2026-10-21-instance-policy-1-foundation.md
@@ -821,13 +821,13 @@ PUT order (each refusal writes nothing):
- `b` verified at a revision above `h` (a database restored from backup, or a record this console signed and then lost the row of) → adopt it: `AdoptAppPolicyContext(b.Revision, its bytes, etag)`, base `b`;
- `h > 0` → base `OpenKey(pub, rw, acct.Sqid, app)` (the recorded content; an error here → 502), whatever the bucket holds; when the bucket's object is unverifiable, below `h`, or at `h` with other content (`rw`'s payload differs), log `console: app policy %s/%s: not building on the stored object: %v` (the operator's repair path);
- no row and `b` verified → adopt it as above; no row and `ErrUnverifiable` → `{V:1}` with the object's ETag.
- With `s.cfg.HostClaim`: `cur := s.store.ClaimAppID(acct.ID, app)` (error → 503); a base whose `AppID` is not `cur` (a predecessor's policy a failed create-time clear left, Task 11) is replaced by `{V:1}` **before** the change is applied, keeping the object's ETag and the revision floor, logged `console: app policy %s/%s: stored policy is for incarnation %q, not %q; starting empty` (Astra plan R1-2). With `commit`, `s.policySynced[sqid+"/"+app] = hold.n` once the base is chosen (a map under its own mutex). The precondition must equal the bucket's current ETag, else 412. `next := max(h, revision of a verified b) + 1`. `old` (for step 10 and the audit) is the base after these rules. 8. Apply: `egressaddr.Normalize` (400 with its message); `default_size` via `cgroup.SizeByName` or `""`; `size`: `ValidHostShape` (400 `invalid host`), `provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, strings.ToLower(host))` (absent → 404 `no instance %s in this app`), then set `{value, rec.CreatedAt}` or delete. Drop every other override whose record is absent or whose `CreatedAt` differs (the spec's "removes the entry, for tidiness", done here so no member door writes the object). 9. With `s.cfg.HostClaim`, `p.AppID = cur` (step 7); without, `AppID` stays as read (empty). 10. `checkEgressGrant(ctx, acct, app, old.EgressAllow, p.EgressAllow)` whenever `egress_allow` was sent. 11. `s.appMutation(ctx, acct, app, "app-policy", ...)`: inside, `s.policyAttempt(ctx, hold)` first, the holding step 7 took (a refusal → 503 `policyLeaseLostMsg`: nothing audited, recorded or written); then `auditRequired` (failure → 503, no write); then set `p.Account, p.App, p.Revision, p.SignedAt = acct.Sqid, app, next, time.Now().UTC()`, `policyAttempt(ctx, hold)` again (the check before signing; a refusal → 503 `policyLeaseLostMsg`, nothing recorded), `wire := apppolicy.Sign(s.cfg.Key, p)`, and **record before publishing**: `RecordAppPolicyContext(ctx, acct.ID, app, next, wire)` (failure → 503 `the change could not be recorded, so it was not made. Try again`, no bucket write). Then publish, **each attempt bound to the holding** (Astra plan R3-1): every attempt, the first and each retry, begins `actx, cancel, ref := s.policyAttempt(ctx, hold)`; a refusal, or `actx` ending during the attempt, → 503 `policyLeaseLostMsg` and no further attempt. The attempt is `apppolicy.PutWire(actx, s.cfg.Releases, acct.Sqid, app, wire, etag)`; on success `SyncedAppPolicyContext(ctx, acct.ID, app, next, newETag)` (a failure is logged only: the row stays `pending`, so it stays among Task 10's candidates on every pass, never excluded, and the next touch finds the bytes equal to `rw` and treats them as this console's; Astra plan R4-2). `ErrCASConflict` (the object changed after step 7, which under the lock and a live holding only a bucket writer can do: two operators are serialised by the lock and refused at the precondition, a second console by the lease) → re-read with `GetForUpdate(actx, ...)` under that attempt's check. A record that verifies for this key at a revision of `next` or above with bytes other than `wire` is another writer's (only the release key signs one) → 503 `policyLeaseLostMsg`, never overwritten. Otherwise take its ETag and start the next attempt with the same bytes, up to `doorKeyPublishAttempts` (the door keys' publish loop, `apidoorkeys.go:106-155`). Any other failure, or the attempts spent → 502 `the change is recorded but not yet published. Run the command again to publish it now` (the door keys' 502 precedent, `apidoorkeys.go:321`). The change stands once recorded: Task 10's pass (at console start and every 12 h) publishes the recorded content over whatever an existing object holds, and the next operator write builds on it. After a lost holding the recorded revision is published only if this console's next holding finds the bucket still holding its last publication (Task 9 step 7's new-holding rule); otherwise the next holding builds on the other writer's record. 12. `s.bumpSerial(ctx)`. 13. 200 with the reply.
+ With `s.cfg.HostClaim`: `cur := s.store.ClaimAppID(acct.ID, app)` (error → 503); a base whose `AppID` is not `cur` (a predecessor's policy a failed create-time clear left, Task 11) is replaced by `{V:1}` **before** the change is applied, keeping the object's ETag and the revision floor, logged `console: app policy %s/%s: stored policy is for incarnation %q, not %q; starting empty` (Astra plan R1-2). With `commit`, `s.policySynced[sqid+"/"+app] = hold.n` once the base is chosen (a map under its own mutex). The precondition must equal the bucket's current ETag, else 412. `next := max(h, revision of a verified b) + 1`. `old` (for step 10 and the audit) is the base after these rules. 8. Apply: `egressaddr.Normalize` (400 with its message); `default_size` via `cgroup.SizeByName` or `""`; `size`: `ValidHostShape` (400 `invalid host`), `provision.GetRecord(ctx, s.cfg.Releases, acct.Sqid, app, strings.ToLower(host))` (absent → 404 `no instance %s in this app`), then set `{value, rec.CreatedAt}` or delete. Drop every other override whose record is absent or whose `CreatedAt` differs (the spec's "removes the entry, for tidiness", done here so no member door writes the object). 9. With `s.cfg.HostClaim`, `p.AppID = cur` (step 7); without, `AppID` stays as read (empty). 10. `checkEgressGrant(ctx, acct, app, old.EgressAllow, p.EgressAllow)` whenever `egress_allow` was sent. 11. `s.appMutation(ctx, acct, app, "app-policy", ...)`: inside, `s.policyAttempt(ctx, hold)` first, the holding step 7 took (a refusal → 503 `policyLeaseLostMsg`: nothing audited, recorded or written); then `auditRequired` (failure → 503 `policyAuditFailedMsg`: nothing recorded, signed or written); then set `p.Account, p.App, p.Revision, p.SignedAt = acct.Sqid, app, next, time.Now().UTC()`, `policyAttempt(ctx, hold)` again (the check before signing; a refusal → 503 `policyLeaseLostMsg`, nothing recorded), `wire := apppolicy.Sign(s.cfg.Key, p)`, and **record before publishing**: `RecordAppPolicyContext(ctx, acct.ID, app, next, wire)` (failure → 503 `the change could not be recorded, so it was not made. Try again`, no bucket write). Then publish, **each attempt bound to the holding** (Astra plan R3-1): every attempt, the first and each retry, begins `actx, cancel, ref := s.policyAttempt(ctx, hold)`; a refusal, or `actx` ending during the attempt, → 503 `policyLeaseLostMsg` and no further attempt. The attempt is `apppolicy.PutWire(actx, s.cfg.Releases, acct.Sqid, app, wire, etag)`; on success `SyncedAppPolicyContext(ctx, acct.ID, app, next, newETag)` (a failure is logged only: the row stays `pending`, so it stays among Task 10's candidates on every pass, never excluded, and the next touch finds the bytes equal to `rw` and treats them as this console's; Astra plan R4-2). `ErrCASConflict` (the object changed after step 7, which under the lock and a live holding only a bucket writer can do: two operators are serialised by the lock and refused at the precondition, a second console by the lease) → re-read with `GetForUpdate(actx, ...)` under that attempt's check. A record that verifies for this key at a revision of `next` or above with bytes other than `wire` is another writer's (only the release key signs one) → 503 `policyLeaseLostMsg`, never overwritten. Otherwise take its ETag and start the next attempt with the same bytes, up to `doorKeyPublishAttempts` (the door keys' publish loop, `apidoorkeys.go:106-155`). Any other failure, or the attempts spent → 502 `the change is recorded but not yet published. Run the command again to publish it now` (the door keys' 502 precedent, `apidoorkeys.go:321`). The change stands once recorded: Task 10's pass (at console start and every 12 h) publishes the recorded content over whatever an existing object holds, and the next operator write builds on it. After a lost holding the recorded revision is published only if this console's next holding finds the bucket still holding its last publication (Task 9 step 7's new-holding rule); otherwise the next holding builds on the other writer's record. 12. `s.bumpSerial(ctx)`. 13. 200 with the reply.
Audit detail (copy, Task 15): the changed parts joined with `"; "`, each `egress allow: <old> -> <new>`, `default size: <old> -> <new>`, `size of <host>: <old> -> <new>`, with `none`, `the build's size` and `the app default` for empty values, ending ` (cli)`.
GET: steps 1-2, then step 7's reads and base rules without writing (`policyBase(..., commit false)`: no adoption is recorded, no holding is marked, and no lease is taken; a console not holding the lease evaluates the rules as a new holding would, so it shows what a takeover would build on); 200 with the base, the bucket's ETag, and `stale_overrides` (empty for `{V:1}`). So the GET shows exactly what a PUT would build on: the recorded content, `{V:1}` for an absent or unusable object, `{V:1}` for another incarnation's policy under claims. An expired record is shown as stored (its `signed_at` says when). `Stale` lists override hosts whose record is absent or whose `CreatedAt` differs.
-Door messages (copy, Task 15): `nothing to set: send egress_allow, default_size or size`; `default_size must be small, standard, large or xlarge, or empty for the build's size`; `size must be small, standard, large, xlarge or default`; `no instance %s in this app`; `send If-Match with the policy's etag, or If-None-Match: * when there is none`; `the policy changed since it was read. Read it again and retry`; `the change could not be recorded, so it was not made. Try again`; `the change is recorded but not yet published. Run the command again to publish it now`; `could not read the policy. Try again`.
+Door messages (copy, Task 15): `nothing to set: send egress_allow, default_size or size`; `default_size must be small, standard, large or xlarge, or empty for the build's size`; `size must be small, standard, large, xlarge or default`; `no instance %s in this app`; `send If-Match with the policy's etag, or If-None-Match: * when there is none`; `the policy changed since it was read. Read it again and retry`; `the change could not be recorded, so it was not made. Try again`; `policyAuditFailedMsg` = `the change could not be audited, so it was not made. Try again` (Astra plan R8-3: distinct from the record refusal, so a test can tell which write failed); `the change is recorded but not yet published. Run the command again to publish it now`; `could not read the policy. Try again`.
- [ ] **Step 1: Write the failing tests** (each `t.Parallel()`; `newConsoleFixture`, `paul@keymail.dev` is the ops owner; a local helper `policyReq(t, fx, method, app, token, body string, hdr map[string]string) (int, http.Header, string)`. `newConsoleFixture` has no `cfg.Bucket`, so its door runs sole; a fixture that sets `Bucket` (the claim-enabled `newClaimCase` among them) also sets `cfg.PolicyLease` to Task 8's fake leaser)
@@ -864,8 +864,16 @@ func TestPolicyGetMarksAndPutDropsStaleOverrides(t *testing.T) {
// PUT {"default_size":"small"} -> the stale entry is gone from the stored object
}
func TestPolicyPutRefusesWhenTheAuditFails(t *testing.T) {
- // close fx.st's database handle (or a store fault seam if one exists) -> 503 with the audit message,
- // object not written
+ // Astra plan R8-3. Only the audit insertion fails: the authorization and account reads before it
+ // (apiSystemRoller's Role, AccountBySqid, HasAppIn) use the same database, so closing it never reaches the audit.
+ // PUT If-None-Match:* {"default_size":"small"} -> 200; keep the stored bytes, its etag, the row
+ // (AppPolicyRecordContext: Revision 1, Wire, SyncedETag) and the serial
+ // fx.st.db.Exec(`CREATE TRIGGER fail_policy_audit BEFORE INSERT ON audit WHEN NEW.action = 'app-policy'
+ // BEGIN SELECT RAISE(ABORT, 'test audit failure'); END`) (the restore_test.go:365 precedent)
+ // PUT If-Match <that etag> {"default_size":"large"} -> 503, body policyAuditFailedMsg (not the record message)
+ // the stored object's bytes and etag unchanged (default_size still small); the row still Revision 1 with the
+ // same Wire and SyncedETag; the serial unchanged; exactly one "app-policy" audit row
+ // DROP TRIGGER fail_policy_audit; the same PUT -> 200, Revision 2 (the refused write used no revision)
}
func TestPolicyPutSignsOneRevisionAboveTheCurrent(t *testing.T) {
// PUT -> the stored bytes pass apppolicy.Verify(fx's release pub, wire, now) and VerifyKey for this
@@ -1400,3 +1408,9 @@ All three findings' citations were checked against the code and hold: `server.go
|---|---|
| R7-1 the change-index store test would make `release` import `apppolicy` | Task 3: the test moves to `internal/apppolicy/changeindex_test.go`; the release run commands drop it (the `apppolicy -run .` run covers it). |
| R7-2 `claimUnitTemplate` does not exist | Task 1: the fence test names `claimBoundTemplate` (`internal/host/claimunit_template.go:3`). |
+
+## Review dispositions (Astra plan R8)
+
+| Finding | Change |
+|---|---|
+| R8-3 (P2) closing the database cannot test mandatory-audit failure | Verified: `apiSystemRoller` reads the role from the store first (`apisystemupdate.go:149`) and `AccountBySqid` reads the same database (`store.go:1063`), so a closed handle refuses before the audit. The audit row is an `INSERT INTO audit` (`claimRawRecordActivity`, `store.go:2589`); the store has no fault seam for it. Task 9: `TestPolicyPutRefusesWhenTheAuditFails` fails only that insertion with an aborting trigger on `audit` scoped to `action = 'app-policy'`, and asserts the new `policyAuditFailedMsg`, the unchanged object and ETag, the unchanged recorded revision and serial. Step 11 names the message; it joins Task 9's door messages for Task 15's copy review. |
diff --git a/docs/superpowers/plans/2026-10-21-instance-policy-3-sizes.md b/docs/superpowers/plans/2026-10-21-instance-policy-3-sizes.md
index 76e5688b9..4a7a6d2de 100644
--- a/docs/superpowers/plans/2026-10-21-instance-policy-3-sizes.md
+++ b/docs/superpowers/plans/2026-10-21-instance-policy-3-sizes.md
@@ -312,17 +312,22 @@ func TestClientSizeMapsRefusalsToErrSizeRefused(t *testing.T) // Size and ReadS
### Task 6: The activator applies live changes
**Files:**
-- Modify: `internal/activator/opset.go:72-87` (`opSet`), `opset.go:~108` (`joinOps` also waits on `resizeDone`)
+- Modify: `internal/activator/opset.go:72-87` (`opSet`), `opset.go:~108` (`joinOps` also waits on `resizeDone`), `opset.go:681-689` (`releasable` refuses while `resizing`), `opset.go:710-716` (`reapCurrentGen`'s generations-off proof refuses while `resizing`)
+- Modify: `internal/activator/control.go` (new `SizeControl` beside `GenControl`, ~46), `internal/activator/containment.go:276-286` (`sizeControl()` beside `genControl()`; `errSizeNotLive` in the error block at ~16)
- Modify: `internal/activator/activator.go:469` (`actEntry`: `sizeSeen string`, `sizeSeenSet bool`, `sizeDirty bool`), `activator.go:4189-4206` (`finishPendingStops` calls `retrySizeLocked(e)` after `retryParentRestoreLocked(e)`)
- Modify: `internal/activator/swap.go:467-471` (admission refuses while a change is in flight or unsettled)
- Modify: `internal/activator/size.go` (Task 3's file), `internal/activator/containment.go` (ExecBackend's `SizeBackend` methods, beside `ParentLimit` ~365), `internal/activator/genbackend.go` (`SplitBackend` delegation, beside ~369)
-- Test: `internal/activator/size_live_test.go` (create), `internal/activator/swap_fixture_test.go` (the fake gains `SizeBackend`)
+- Test: `internal/activator/size_live_test.go` (create), `internal/activator/swap_fixture_test.go` (the fake gains `SizeBackend`), `internal/activator/containment_test.go` (`fakeSizeControl`, Astra plan R8-2)
**Interfaces:**
- Consumes: `Client.Size`, `Client.ReadSize`, `ErrSizeRefused`, `SizeReadBack` (Task 5); `EffectiveSize`, `SizedLimit` (Task 3); `cgroup.MaxLimit` (Task 1).
- Produces:
- `opSet` gains `resizing bool`, `resizeDone chan struct{}`, `sizeUnsettled bool`.
- - `type SizeBackend interface { ResizeLive(ctx context.Context, rt registry.Route, gen uint64, size cgroup.Size, memoryMax int64) (instancectl.SizeReadBack, error); ReadSize(ctx context.Context, rt registry.Route, gen uint64) (instancectl.SizeReadBack, error) }`. `ExecBackend` implements it for `LayoutNested` only, through `genControl()`; any other layout returns `errSizeNotLive`.
+ - A pending stop is unreleasable while `e.ops.resizing` (Astra plan R8-1), with generations on and off. `joinOps` waits for `resizeDone` only up to `opJoinBound` (`opset.go:137`); after that the stop pends and the sweep's `reapCurrentGen` finishes it, so both of its proofs refuse while a resize runs: `releasable` (`opset.go:681`, generations on, and stop()'s own check at `activator.go:4163`) reads `resizing` under `a.mu` beside `restoring` and returns false; the generations-off closure in `reapCurrentGen` (`opset.go:711`) becomes `!a.resizeInFlight(e) && a.sidecarsExited(e, true) && a.stopHostProven(e)`, where `func (a *Service) resizeInFlight(e *actEntry) bool` reads `e.ops.resizing` under `a.mu`. The resize writes the host's slice, which a later instance of the host would share, so the lease is held until it returns. `retrySizeLocked` already skips an entry that is not `actAwake`, so no new resize starts on a stopping entry.
+ - `type SizeBackend interface { ResizeLive(ctx context.Context, rt registry.Route, gen uint64, size cgroup.Size, memoryMax int64) (instancectl.SizeReadBack, error); ReadSize(ctx context.Context, rt registry.Route, gen uint64) (instancectl.SizeReadBack, error) }`. `ExecBackend` implements it for `LayoutNested` only, through `sizeControl()`: `ResizeLive` calls `sc.Size(ctx, rt.Host, gen, size.Name, memoryMax)`, `ReadSize` calls `sc.ReadSize(ctx, rt.Host, gen)`. Any other layout, or a `sizeControl()` refusal, returns `errSizeNotLive`.
+ - `type SizeControl interface { Size(ctx context.Context, host string, gen uint64, size string, memoryMax int64) (instancectl.SizeReadBack, error); ReadSize(ctx context.Context, host string, gen uint64) (instancectl.SizeReadBack, error) }` in `control.go`, with `var _ SizeControl = (*instancectl.Client)(nil)` (Task 5's two methods). It is a separate interface, as `StagedExeControl` is, and `GenControl` is unchanged: `genControl()` returns `GenControl` (`containment.go:277`), whose method set (`control.go:38-45`) has no size verb, so a call through it does not compile (Astra plan R8-2).
+ - `func (b *ExecBackend) sizeControl() (SizeControl, error)`: `b.Control` asserted to `SizeControl`; nil or not one → `errSizeNotLive`, the refusal arm (nothing written, the change waits for the next start, no unsettled mark). `var errSizeNotLive = errors.New("activator: no live size change for this host")`.
+ - Fakes: `GenControl`'s three fakes (`fakeGenControl`, `containment_test.go:21`; `exeControl`, `swap_nested_test.go:85`; `identityControl`, `identity_test.go:333`) need no new method, since `GenControl` does not change. One new fake gains both: `type fakeSizeControl struct{ fakeGenControl; rb instancectl.SizeReadBack; err error }` in `containment_test.go`, recording `size <host> <gen> <name> <mem>` and `readsize <host> <gen>`. The swap fixture's backend fake gains `SizeBackend` directly (it is a backend, not a control channel).
- `func (a *Service) NoteSize(host string)`: sets `sizeDirty` on the entry, if any. It never blocks.
- `func genUnitOf(rt registry.Route, g Gen) uint64` (`size.go`): `g.N` when the entry runs in a real generation unit (`!g.Worker && linksCanonical(rt, g)`, `canonlink.go:27`: the generation bound a socket of its own, so `startRole` started it through `GenControl`), else `0`. A `canonicalGen` handle (`generations.go:168`) is numbered on the ordinary run path even with generations off (`activator.go:1870`), but there is no unit by that number: root's `/size` with that `gen` would check a unit that does not exist and answer 409 (Astra plan R1-8). `0` is root's run-template path: the slice alone.
- `type SizeState struct { Gen uint64; Raised bool; Unsettled bool; Want cgroup.Size }`; `func (a *Service) SizeState(host string) (SizeState, bool)`: false unless awake and not a worker. `Gen` is `genUnitOf(e.route, e.gen)`.
@@ -404,13 +409,28 @@ func TestNoLiveChangeUnderClaims(t *testing.T)
// the next sweep; the next spawn (a wake after sleep) carries 2 GiB.
func TestJoinOpsWaitsForAResizeInFlight(t *testing.T)
// ResizeLive gated; a stop joins and returns only after the gate opens.
+func TestPendingStopKeepsTheLeaseWhileAResizeRuns(t *testing.T)
+// Astra plan R8-1. Not t.Parallel (sets opJoinBound). Subtests "generations on" and "generations off":
+// awakeForBarrier(t, gens) (opset_test.go:58); x.a.backend = a fakeBackend wrapper implementing SizeBackend
+// whose ReadSize answers slice 536870912 and whose ResizeLive signals entered and blocks on a gate;
+// opJoinBound = 100 * time.Millisecond. NoteSize(host); sweepOnce; wait for entered.
+// rep := x.b.lastRep(); Bounce(host); waitFor e.pendingStop (joinOps passed opJoinBound).
+// sweepOnce; waitFor a reapCurrentGen pass to finish (!e.genReaping after it was set);
+// rep.isClosed() false, e.pendingStop true, e.ops.resizing true. Twice, so a second sweep cannot release it either.
+// open the gate; waitFor !e.ops.resizing; sweepOnce; waitFor rep.isClosed.
+// Without the fix, the first sweep releases the lease with the resize still running.
+func TestExecBackendSizesThroughSizeControl(t *testing.T)
+// Astra plan R8-2, containment_test.go. nestedBackend(t, ctl, &fakeUnit{}) with b.Control = fakeSizeControl{...}:
+// ReadSize(rt, 3) then ResizeLive(rt, 3, large, 1073741824) -> calls "readsize h.example.com 3\nsize h.example.com 3 large 1073741824",
+// each returning the fake's read-back. b.Control = fakeGenControl{ctl} (no size verbs) -> both errors.Is(err, errSizeNotLive), no call.
+// A fenced layout (built as instancekey_test.go:866-885 builds one) -> errSizeNotLive, no call.
```
-- [ ] **Step 2: Run to see them fail**: `go test ./internal/activator/ -run 'LiveGrowth|LiveChangeWaits|SwapRefusedWhileResizing|PolicyReversalAfterAPartialGrowth|MemoryShrink|SizeBack|NoLiveCall|NoLiveChangeUnderClaims|JoinOpsWaitsForAResize'`. Expected: FAIL.
+- [ ] **Step 2: Run to see them fail**: `go test ./internal/activator/ -run 'LiveGrowth|LiveChangeWaits|SwapRefusedWhileResizing|PolicyReversalAfterAPartialGrowth|MemoryShrink|SizeBack|NoLiveCall|NoLiveChangeUnderClaims|JoinOpsWaitsForAResize|PendingStopKeepsTheLeaseWhileAResize|ExecBackendSizesThroughSizeControl'`. Expected: FAIL.
-- [ ] **Step 3: Implement** as above. `ExecBackend.ResizeLive`/`ReadSize` mirror `ParentLimit`'s layout switch (`containment.go:365-386`).
+- [ ] **Step 3: Implement** as above. `ExecBackend.ResizeLive`/`ReadSize` mirror `ParentLimit`'s layout switch (`containment.go:365-386`), calling through `sizeControl()`, not `genControl()`, and without `ParentLimit`'s `GenContainment` refusal (gen 0 is the run template's slice).
-- [ ] **Step 4: Run** the step 2 command and `go test ./internal/activator/ -run 'Swap|Restore|Join'`. Expected: PASS.
+- [ ] **Step 4: Run** the step 2 command and `go test ./internal/activator/ -run 'Swap|Restore|Join|Barrier|Pending'`. Expected: PASS.
- [ ] **Step 5: Commit** (`activator: live size changes under the operation set; captured tier from read-back; unsettled hosts start no swap`)
@@ -688,3 +708,10 @@ Spec coverage: §5.5's refusal now covers every box an app can be placed on, not
| Finding | Change |
|---|---|
| R6-1 a refused live resize never reaches a flat overlap swap | Task 6: the refusal arm updates `e.route.Size` too, leaving the captured tier and `unsettled` alone. Test `TestARefusedLiveResizeStillReachesTheNextSwap`. |
+
+## Review dispositions (Astra plan R8)
+
+| Finding | Change |
+|---|---|
+| R8-1 (P1) a timed-out resize join can release the host while its resize still runs | Verified: `joinOps` is bounded (`opset.go:137`), and neither `releasable` (`opset.go:681`) nor the generations-off proof in `reapCurrentGen` (`opset.go:711`) looks at a resize. Task 6: both refuse while `e.ops.resizing` (`releasable` beside `restoring`; the generations-off closure through `resizeInFlight`). Test `TestPendingStopKeepsTheLeaseWhileAResizeRuns`, generations on and off, past `opJoinBound`, through the pending-stop sweep. |
+| R8-2 (P1) `GenControl` has no size verb | Verified: `genControl()` returns `GenControl` (`containment.go:277`), whose methods (`control.go:38-45`) have no `Size` or `ReadSize`. Task 6: a separate `SizeControl` interface and `sizeControl()` accessor; `GenControl` and its three fakes are unchanged; a new `fakeSizeControl` gains both methods. Test `TestExecBackendSizesThroughSizeControl`. Task 5 needs no change: `Client.Size` and `Client.ReadSize` already have the signatures `SizeControl` names. |
diff --git a/docs/superpowers/plans/CODEX-FINDINGS-instance-policy-plans-R8.md b/docs/superpowers/plans/CODEX-FINDINGS-instance-policy-plans-R8.md
new file mode 100644
index 000000000..0d22aa6d1
--- /dev/null
+++ b/docs/superpowers/plans/CODEX-FINDINGS-instance-policy-plans-R8.md
@@ -0,0 +1,10 @@
+R7-1 — RESOLVED: the change-index integration test now lives in `internal/apppolicy`.
+R7-2 — RESOLVED: the fence test now names `claimBoundTemplate`.
+
+1. **P1 — Plan 3, Task 6: a timed-out resize join can release the host while its resize still runs.** [Sizes:315](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/docs/superpowers/plans/2026-10-21-instance-policy-3-sizes.md:315) adds `resizeDone` to `joinOps`, but does not extend pending-stop completion. That join is bounded ([opset.go:137](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/internal/activator/opset.go:137)); after timeout, the sweep can finish the stop because neither [releasable:681](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/internal/activator/opset.go:681) nor the generations-off proof in [reapCurrentGen:710](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/internal/activator/opset.go:710) checks resizing. The outstanding write targets the host’s shared slice and can therefore affect a replacement instance after its lease is released. **Smallest fix:** keep pending stops unreleasable until the resize finishes, with generations both on and off. Extend the witness to exceed `opJoinBound`, run the pending-stop sweep, and verify that the lease remains held.
+
+2. **P1 — Plan 3, Task 6: the prescribed control interface lacks the resize methods.** [Sizes:325](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/docs/superpowers/plans/2026-10-21-instance-policy-3-sizes.md:325) routes `ResizeLive` and `ReadSize` through `genControl()`. That returns `GenControl` ([containment.go:277](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/internal/activator/containment.go:277)), whose declaration contains neither `Size` nor `ReadSize` ([control.go:38](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/internal/activator/control.go:38)). Task 5 adds methods to the concrete client, but no task extends this interface or supplies another accessor. Calls through it cannot compile. **Smallest fix:** specify a size-control interface and accessor, or extend `GenControl` and its affected fakes.
+
+3. **P2 — Plan 1, Task 9: closing the database cannot test mandatory-audit failure.** [Foundation:866](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/docs/superpowers/plans/2026-10-21-instance-policy-1-foundation.md:866) proposes closing the fixture’s database before the PUT. Earlier authorization and account reads use that same database ([apiSystemRoller:149](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/internal/console/apisystemupdate.go:149), [AccountBySqid:1064](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-egress-sizes-328-329/internal/console/store.go:1064)), so the request never reaches the audit write. **Smallest fix:** fail only the `audit` insertion—for example, with an aborting SQLite trigger—and assert the audit-specific refusal, unchanged policy object, and unchanged recorded revision.
+
+REJECT
\ No newline at end of file