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>
3 files changed,
+61
−10
docs/superpowers/plans/2026-10-21-instance-policy-1-foundation.md+18 −4docs/superpowers/plans/2026-10-21-instance-policy-3-sizes.md+33 −6docs/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 |