spec: instance-bound tokens rev 2, answering Astra round 1; round 2 brief
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+333
−60
docs/superpowers/specs/2026-10-16-instance-bound-tokens-design.md+191 −60docs/superpowers/specs/CODEX-FINDINGS-INSTANCE-BOUND-TOKENS-R1.md+123 −0docs/superpowers/specs/CODEX-REVIEW-BRIEF-INSTANCE-BOUND-TOKENS-R2.md+19 −0
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.011s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 285.337s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.986s
ok github.com/carlosframework/platform/cmd/moveprobe 93.286s
ok github.com/carlosframework/platform/cmd/zddprobe 0.710s
ok github.com/carlosframework/platform/internal/activator 249.475s
ok github.com/carlosframework/platform/internal/agentsock 0.039s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.027s
ok github.com/carlosframework/platform/internal/appconfig 2.514s
ok github.com/carlosframework/platform/internal/apperr 0.119s
ok github.com/carlosframework/platform/internal/applog 0.165s
ok github.com/carlosframework/platform/internal/awsinv 0.003s
ok github.com/carlosframework/platform/internal/boxchoice 0.005s
ok github.com/carlosframework/platform/internal/boxid 0.548s
? 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.014s
ok github.com/carlosframework/platform/internal/certwarm 0.006s
ok github.com/carlosframework/platform/internal/cgroup 0.724s
ok github.com/carlosframework/platform/internal/changelog 0.315s
ok github.com/carlosframework/platform/internal/clamd 0.056s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 387.410s
? 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 5.677s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.010s
ok github.com/carlosframework/platform/internal/dbverify 0.004s
ok github.com/carlosframework/platform/internal/dnsrec 0.003s
ok github.com/carlosframework/platform/internal/docsite 0.005s
ok github.com/carlosframework/platform/internal/domains 2.393s
ok github.com/carlosframework/platform/internal/doorkey 0.006s
ok github.com/carlosframework/platform/internal/economics 0.004s
ok github.com/carlosframework/platform/internal/edge 26.504s
? 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.139s
ok github.com/carlosframework/platform/internal/gate 1.664s
ok github.com/carlosframework/platform/internal/genselect 0.015s
ok github.com/carlosframework/platform/internal/geo 0.010s
ok github.com/carlosframework/platform/internal/geoip 0.035s
ok github.com/carlosframework/platform/internal/host 160.752s
ok github.com/carlosframework/platform/internal/hostactors 0.444s
ok github.com/carlosframework/platform/internal/hostbox 2.538s
ok github.com/carlosframework/platform/internal/hostclaim 0.527s
ok github.com/carlosframework/platform/internal/hostevidence 0.008s
ok github.com/carlosframework/platform/internal/hostexec 4.723s
ok github.com/carlosframework/platform/internal/hostfence 0.035s
ok github.com/carlosframework/platform/internal/hostfiles 10.395s
ok github.com/carlosframework/platform/internal/hostfloor 0.012s
ok github.com/carlosframework/platform/internal/hostguard 0.977s
ok github.com/carlosframework/platform/internal/hostops 1.622s
? 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.006s
ok github.com/carlosframework/platform/internal/hostrelocate 0.454s
ok github.com/carlosframework/platform/internal/hostresolve 0.164s
ok github.com/carlosframework/platform/internal/hostwrite 0.004s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 4.174s
ok github.com/carlosframework/platform/internal/instanceexec 2.711s
ok github.com/carlosframework/platform/internal/instancefs 0.005s
ok github.com/carlosframework/platform/internal/instancekey 0.002s
ok github.com/carlosframework/platform/internal/instanceupdates 0.010s
ok github.com/carlosframework/platform/internal/layout 1.541s
ok github.com/carlosframework/platform/internal/lease 9.801s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.007s
ok github.com/carlosframework/platform/internal/ledger 0.016s
ok github.com/carlosframework/platform/internal/mail 0.166s
ok github.com/carlosframework/platform/internal/maintenance 0.075s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.516s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.071s
ok github.com/carlosframework/platform/internal/metrics 0.006s
ok github.com/carlosframework/platform/internal/moves 20.990s
ok github.com/carlosframework/platform/internal/moves/archive 0.801s
ok github.com/carlosframework/platform/internal/objstore 0.165s
ok github.com/carlosframework/platform/internal/ops 0.169s
ok github.com/carlosframework/platform/internal/park 0.199s
ok github.com/carlosframework/platform/internal/privatefile 0.003s
ok github.com/carlosframework/platform/internal/provision 10.917s
ok github.com/carlosframework/platform/internal/publicrelease 0.006s
? github.com/carlosframework/platform/internal/regionname [no test files]
ok github.com/carlosframework/platform/internal/registry 4.872s
ok github.com/carlosframework/platform/internal/relay 7.218s
ok github.com/carlosframework/platform/internal/release 19.352s
ok github.com/carlosframework/platform/internal/rollout 3.608s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/rootparticipation 0.008s
ok github.com/carlosframework/platform/internal/rootstate 0.025s
ok github.com/carlosframework/platform/internal/runs 0.584s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.477s
ok github.com/carlosframework/platform/internal/seeds 0.012s
ok github.com/carlosframework/platform/internal/sim/hostlife 8.512s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 3.622s
ok github.com/carlosframework/platform/internal/status 0.037s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.203s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.003s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.004s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.536s
=== 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: 47055010 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.022s
=== RUN TestT4StorageProbeMinio
2026/10/16 02:56:56 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEE31D351AAFC6, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.00s)
PASS
ok github.com/carlosframework/platform/internal/host 0.021s
minio contract: PASS
diff --git a/docs/superpowers/specs/2026-10-16-instance-bound-tokens-design.md b/docs/superpowers/specs/2026-10-16-instance-bound-tokens-design.md| index 7f5f77f6e..4e4b54f6b 100644 |
| --- a/docs/superpowers/specs/2026-10-16-instance-bound-tokens-design.md |
| +++ b/docs/superpowers/specs/2026-10-16-instance-bound-tokens-design.md |
| @@ -1,6 +1,6 @@ |
| # Instance-bound tokens (#155, half 1) |
| -Status: draft rev 1, 2026-10-16. Discussion: amadan carlos/platform #155. |
| +Status: draft rev 2, 2026-10-16 (Astra round 1 answered, §9). Discussion: amadan carlos/platform #155. |
| ## 1. The problem |
| @@ -93,10 +93,23 @@ empty for every other kind. |
| `All()` does not list it and `Parse("self")` is false. So no service |
| credential can be minted with it, the service mint door and form never |
| offer it, and a `kind='service'` row naming it fails to resolve. It is |
| -what a `kind='instance'` row resolves to and nothing else. `Allow(Self)` |
| -is accepted explicitly (Allow's unknown-role panic checks the catalogue |
| -plus Self). Admin is added to every Allow set as today, which is harmless: |
| -an Admin principal is app-wide and passes no host check (§4.1). |
| +what a `kind='instance'` row resolves to and nothing else. |
| + |
| +`Allow` refuses Self (it panics, as for any role outside the catalogue), |
| +because `Allow` adds Admin to every set and Admin is app-wide: a door |
| +opened to Self through `Allow` would open to every Admin credential too, |
| +and Admin has no host to check (codex 2). Self is admitted only through a |
| +second constructor: |
| + |
| +```go |
| +// AllowSelf is a Set for a door that also admits Self. base is the door's |
| +// existing Set, unchanged: AllowSelf(Set{}) admits Self and nothing else, |
| +// not Admin; AllowSelf(Allow(Instance)) admits Instance, Admin and Self. |
| +func AllowSelf(base Set) Set |
| +``` |
| + |
| +So the update doors, closed to every service credential today, become |
| +`AllowSelf(Set{})`: people and a bound Self principal, and still no Admin. |
| `Self` is never `Instance`. `TestInstanceRoleMatchesTodaysAppTokenReach` |
| keeps pinning the app token's reach unchanged. |
| @@ -105,12 +118,21 @@ keeps pinning the app token's reach unchanged. |
| A token is bound to one incarnation of its host: |
| -- **Every Self door compares `rec.CreatedAt.UnixNano()` with |
| - `p.InstanceBorn`** after reading the record and answers the canonical |
| - 404 on a mismatch. A host that is deleted and created again, by any |
| - path, gets a new `CreatedAt`, so the old tenant's token dies with the |
| - old record even if no revoke ran. This is the guarantee; the revokes |
| - below are tidiness. |
| +- **Every read of the record on behalf of a Self principal compares |
| + `rec.CreatedAt.UnixNano()` with `p.InstanceBorn`** and answers the |
| + canonical 404 on a mismatch, before anything that depends on the record |
| + is answered (§4.3). A host that is deleted and created again gets a new |
| + `CreatedAt`, so the old tenant's token dies with the old record even if |
| + no revoke ran. This is the guarantee; the revokes below are tidiness. |
| +- **`provision.CreateRecord` enforces a fresh `CreatedAt`** (codex 10). A |
| + zero `CreatedAt` is stamped with the clock; any other value outside |
| + [now − 15 min, now + 2 min] is refused with a new `ErrStaleCreatedAt`. |
| + It is the one boundary every record creation crosses (the console create, |
| + seed, import and alias paths and `provision.replica.go`), and each of them |
| + stamps the clock just before the write today, so none is refused. A |
| + record replayed from saved data therefore cannot carry its old |
| + incarnation. The seed path's longest gap between stamping and writing is |
| + its store checks, well inside the window. |
| - `handleAPIInstanceDelete` revokes every live `kind='instance'` row for |
| (account, app, host) **before** deleting the record. If the revoke |
| fails, the delete answers 500 and deletes nothing. A delete that fails |
| @@ -120,22 +142,35 @@ A token is bound to one incarnation of its host: |
| account-move and rename statements (`store.go:1928`, `:1970`, `:2074`, |
| `:2089`) add `cliKindInstance` to their `kind IN (…)` lists, so an |
| instance token follows the same rules as the app's other credentials. |
| +- Rows an older console left behind (§6) are swept by one statement at |
| + console start: `DELETE FROM cli_tokens WHERE kind='instance' AND NOT |
| + EXISTS (a live app row for its account and app)`. One indexed delete, |
| + bounded by the table, not by the bucket. |
| - A record with a zero `CreatedAt` cannot be minted for (§5): there is |
| nothing to bind to. |
| -### 3.5 Every place that branches on Name |
| - |
| -`principalKey` and `auditActor` read `p.Name == ""` as "legacy app row". |
| -An instance row also has an empty name, so both gain an `Instance` branch |
| -**before** the Name check: |
| - |
| -- `principalKey`: `"instance:<account id>/<app>/<host>"`. Per host, so one |
| - customer's instance cannot spend a sibling's budget. |
| -- `auditActor`: `"instance:<sqid>/<app>/<host>"`. |
| - |
| -`serviceBinding` already accepts a principal with an app. Every other |
| -`p.Name == ""` and `p.App == ""` read in `internal/console` is audited in |
| -the plan, and each one is listed with its answer for an instance row. |
| +### 3.5 Every place that branches on Name or kind |
| + |
| +Several places read `p.Name == ""` or "not a service row" as "legacy app |
| +row". An instance row also has an empty name, so each gains an explicit |
| +instance branch **before** that test: |
| + |
| +| Place | For an instance row | |
| +|---|---| |
| +| `principalKey` | `"instance:<account id>/<app>/<host>"` | |
| +| `auditActor` | `"instance:<sqid>/<app>/<host>"` | |
| +| `instanceGate`'s limiter (`apiinstances.go:87`) | keyed on `principalKey` for an instance principal, so every token for one host shares one budget and a sibling's is separate (codex 8); token hash for every other caller, unchanged | |
| +| `CLIToken` (`authstore.go`) | gains `Host`; `IsInstance()`; `NeverExpires()` true (codex 9) | |
| +| `scanCLITokenRows` and both listing queries | read `host`; role shown as `self` | |
| +| `serviceRowOf` / `/auth/cli/tokens/revoke` (`authcli.go:1543`, `:1618`) | takes the app-token branch: current membership of the row's account is required, then the account-scoped revoke. A former member cannot revoke it (codex 4) | |
| +| `serviceWhat` (`apiservices.go:441`) | names the host (copy review) | |
| +| the services API projection (`apiservices.go:311`) | adds `"host"` | |
| +| `templates/settings.html` | an instance row is labelled as bound to its host (copy review) | |
| + |
| +`serviceBinding` already accepts a principal with an app. The plan's first |
| +task greps `internal/console` for every `Name == ""`, `IsApp()`, |
| +`IsService()`, `cliKindApp` and `kind IN` and lists each with its answer |
| +here before any code changes. |
| ## 4. The gate |
| @@ -172,40 +207,85 @@ Exactly these, pinned by a new `TestSelfRoleDoors`: |
| | Door | Allow set (before → after) | Self may | |
| |---|---|---| |
| -| `GET …/instances/{host}` | Instance → Instance, Self | read its own record | |
| -| `PATCH …/instances/{host}` | Instance → Instance, Self | repoint its own upstreams and probe | |
| -| `GET …/instances/{host}/move` | Instance → Instance, Self | read its own move status | |
| -| `GET …/instances/{host}/updates` | closed → Self | read its own settings | |
| -| `PUT …/instances/{host}/updates` | closed → Self | set settings named in `instance_control` | |
| -| `POST …/instances/{host}/updates/install` | closed → Self | install now, if `updates` is in `instance_control`; never fast track | |
| +| `GET …/instances/{host}` | `Allow(Instance)` → `AllowSelf(Allow(Instance))` | read its own record | |
| +| `PATCH …/instances/{host}` | `Allow(Instance)` → `AllowSelf(Allow(Instance))` | `addrs` and `probe` only, under every rail a service principal already meets | |
| +| `GET …/instances/{host}/move` | `Allow(Instance)` → `AllowSelf(Allow(Instance))` | read its own move status | |
| +| `GET …/instances/{host}/updates` | `Set{}` → `AllowSelf(Set{})` | read its own settings | |
| +| `PUT …/instances/{host}/updates` | `Set{}` → `AllowSelf(Set{})` | set settings named in `instance_control` | |
| +| `POST …/instances/{host}/updates/install` | `Set{}` → `AllowSelf(Set{})` | install now, if `updates` is in `instance_control`; never fast track | |
| -The update doors stay closed to `Instance`: the app-wide token still |
| -cannot set a sibling's pin, which is why they were closed. |
| +The update doors still refuse Instance and Admin: an app-wide credential |
| +still cannot set a sibling's pin, which is why they were closed. |
| With `CARLOS_MOVES_PHASE1` on, the move mutations are operator-only and |
| Self never reaches them; the status read stays as above. |
| -### 4.3 Handler checks |
| - |
| -Each Self door, after `instanceGate` and after reading the record: |
| - |
| -1. `p.Instance != ""` and `rec.CreatedAt.UnixNano() != p.InstanceBorn` → |
| - `http.NotFound`. |
| -2. PUT updates: every key in the request that changes a setting must be |
| - in the app policy's `instance_control`; otherwise 403 with the approved |
| - string `settings.console.app-keeps` (`"%s is kept by the app for |
| - itself. Ask someone who can change the app's settings"`), naming the |
| - first such key in request order. Keys sent with the record's current |
| - value are not changes and are not refused, so a client can PUT back |
| - what it read. |
| -3. POST install: refused with the same string naming `updates` unless |
| - `updates` is in `instance_control`. `fast_track: true` from Self is |
| - refused with the existing `settings.console.fast-track-owner`, before |
| - `s.store.Role` is consulted (that call takes an actor string and must |
| - never be asked about an instance actor). |
| -4. `channel` stays refused on the PUT for everyone |
| +### 4.3 The incarnation check lives where the record is read |
| + |
| +A check once in the handler is not enough (codex 3): `requestInstall` |
| +re-reads the record on every retry, and `moveGate` answers kind and |
| +placement refusals from the record before the handler runs. So the binding |
| +travels as a value and is checked at every read: |
| + |
| +```go |
| +// selfBinding is the instance a Self principal is bound to; the zero |
| +// value (every other caller) binds nothing and every check passes. |
| +type selfBinding struct{ Host string; Born int64 } |
| + |
| +func bindingOf(p Principal) selfBinding |
| +// refuses reports whether rec is not the incarnation b names. |
| +func (b selfBinding) refuses(rec *provision.Record) bool |
| +``` |
| + |
| +- `readUpdatesTarget` takes a `selfBinding` and answers the canonical 404 |
| + when it refuses, straight after the record read and before |
| + `shapeRefusal` or anything else that reads the record. Every update |
| + path (GET, PUT and each `requestInstall` attempt) goes through it, so a |
| + retry that finds a replacement record stops there. |
| +- `requestInstall` and `applyUpdates` take the binding and the policy read |
| + on the same attempt, and re-check `instance_control` against that policy |
| + on every attempt. Each write stays conditional on the checked record's |
| + ETag, as today. |
| +- `moveGate` takes the binding and checks it immediately after its |
| + record read, before any record-dependent refusal. |
| +- `handleAPIInstanceGet` and `handleAPIInstanceUpdate` check it |
| + immediately after their record read. PATCH's write is conditional on |
| + the checked ETag, as today. |
| + |
| +### 4.4 What Self may change |
| + |
| +1. **PATCH:** a Self principal may send `addrs` and `probe` only. Any of |
| + `environment`, `steering` or `gate` present in the body is refused with |
| + 403 before any side effect (codex 1: `environment` alone would bind |
| + the instance to a sibling's environment bundle and its secrets, and |
| + clear that store's seed marker, `seedenvbind.go:64`). The refusal is a |
| + new string (copy review). Every existing rail still applies, including |
| + the operator-only upstream-address rail and |
| + `refuseServiceControlPlane`. |
| +2. **PUT updates.** A key is a *change* when it is present and its |
| + normalized value differs from the record's **explicit** value |
| + (`rec.Updates`, `rec.Major`, `rec.Pin`), not from the effective value |
| + GET reports. Echoing an inherited effective value back is therefore a |
| + change: it would freeze the app's default into an instance override |
| + (codex 6). Every change must be named in the app policy's |
| + `instance_control`; otherwise 403 with the approved string |
| + `settings.console.app-keeps` (`"%s is kept by the app for itself. Ask |
| + someone who can change the app's settings"`). Keys are checked in the |
| + fixed order `updates`, `major`, `pin`, and the first refused key is |
| + named (codex 7). Duplicate JSON keys are refused as today's decoder |
| + refuses them, or, if it does not, last-wins as `encoding/json` does; |
| + the plan pins which. |
| +3. **GET then PUT:** a client that wants to send back what it read sends |
| + GET's explicit values (`settings`), never its effective ones. `channel` |
| + stays refused on the PUT for everyone, even when unchanged |
| (`settings.console.channel-elsewhere`); the channel door is not a Self |
| door in half 1 (§8). |
| +4. **POST install:** refused with `settings.console.app-keeps` naming |
| + `updates` unless `updates` is in `instance_control` (as read on that |
| + attempt). `fast_track: true` from Self is refused with the existing |
| + `settings.console.fast-track-owner` before `s.store.Role` is consulted, |
| + since that call takes an actor string and must never be asked about an |
| + instance actor. |
| Audit lines name the actor `instance:<sqid>/<app>/<host>` through |
| `auditActor`, unchanged otherwise. |
| @@ -240,6 +320,17 @@ docs page section are copy review. |
| - Rolled-back console: kind `instance` does not resolve, so every |
| instance token is a 401. Nothing widens. |
| +- What an older console does to these rows while it runs (codex 5): its |
| + purge, rename and account-move statements skip them, and its personal |
| + revoke path hard-deletes one for the address that minted it, member or |
| + not. None of these widens anything: a skipped row is bound to a record |
| + incarnation that the purge removed, so it can only 404, and a revoke by |
| + a former member only takes a token away. On roll-forward, the start-up |
| + sweep (§3.4) deletes rows whose app is gone, so a stranded row cannot |
| + block a later rename's stale-credential check. The rollback runbook |
| + line: "rolling back below #155 half 1 leaves instance tokens unusable |
| + until roll-forward; nothing to do by hand." |
| + |
| - Old CLIs never send these tokens. The app token's reach is unchanged. |
| - The two `ALTER`s join authstore's tolerant list (a duplicate-column |
| error is ignored), and both default so old rows read unchanged. |
| @@ -249,23 +340,46 @@ docs page section are copy review. |
| - `CLIPrincipal`: an instance row resolves to Self with host and born; a |
| row with an empty app, a bad host, a zero born, or any role, name or |
| rungs does not resolve; a revoked row does not. |
| -- `svcrole`: `Parse("self")` is false, `All()` omits Self, `Allow(Self)` |
| - does not panic, the service mint door and form refuse role `self`. |
| +- `svcrole`: `Parse("self")` is false, `All()` omits Self, `Allow(Self)` panics, |
| + `AllowSelf(Set{})` admits Self and not Admin, and the service mint door |
| + and form refuse role `self`. |
| - Containment, one table over every door registered in the console: for |
| an instance token bound to host A, every door outside §4.2 answers its |
| role refusal (403), every §4.2 door answers the canonical 404 for |
| - sibling B and for an unknown host, and answers for A. |
| + sibling B and for an unknown host, and answers for A. Each case also |
| + asserts B's record, A's record and the bucket are unchanged, not only |
| + the status and bytes. |
| +- Admin and Instance credentials still get 403 at the three update doors |
| + (codex 2). |
| +- PATCH from Self with `environment`, `steering` or `gate` is refused, and |
| + A's record, B's environment bundle and the store's seed marker are |
| + unchanged afterwards. |
| - Incarnation: delete A and create A again; the old token gets the |
| canonical 404 on every §4.2 door, including when the revoke on delete |
| - is skipped (test hook). |
| + is skipped (test hook). Also: A is replaced between the install door's |
| + first read and its retry (store hook forcing one CAS conflict), and the |
| + install lands on neither record; A is replaced by a route of a |
| + different kind before a move-status read, and the answer is the |
| + canonical 404, not the kind refusal. |
| +- `CreateRecord` stamps a zero `CreatedAt`, refuses one older than 15 |
| + minutes, and every production caller passes. |
| +- Update settings: Self may change exactly `instance_control`'s keys; the |
| + refusal names the first key in fixed order whatever the raw JSON order; |
| + echoing GET's explicit values passes; echoing an inherited effective |
| + value is a change; an unchanged `channel` is still refused; install |
| + needs `updates`; fast track is refused without a role lookup. |
| +- Rate limit: two tokens for A share one budget on a real door; B's is |
| + separate. |
| +- Personal revoke: a former member who minted an instance token cannot |
| + revoke it from `/settings`; a current member can. |
| +- Old-console SQL: the previous release's purge, rename and move |
| + statements, run against a database holding instance rows, leave rows |
| + the new console's start-up sweep removes, and a rename afterwards is |
| + not refused. |
| - Delete revokes; a revoke failure aborts the delete. |
| - Purge, rename and account-move delete or carry instance rows the way |
| they do app rows. |
| -- Update settings: Self may change exactly `instance_control`'s keys; the |
| - refusal names the key; a no-op PUT of current values passes; install |
| - needs `updates`; fast track is refused without a role lookup. |
| -- Rate limit and audit keys are per host and never collapse onto the |
| - `app:` key. |
| +- Audit keys are per host and never collapse onto the `app:` key. |
| - Mint door: people only (403 for every role, 401 inner wall), |
| members only, absent host 404, zero `CreatedAt` 409, token shown once. |
| @@ -278,3 +392,20 @@ docs page section are copy review. |
| - A host filter on logs, errors and analytics so an instance can read its |
| own lines. |
| - An in-app helper in rastrillo. Its shape follows delivery. |
| + |
| +## 9. Astra round 1 (CODEX-FINDINGS-INSTANCE-BOUND-TOKENS-R1.md) |
| + |
| +Every finding was checked against the code and every citation held. |
| + |
| +| # | Sev | Disposition | |
| +|---|---|---| |
| +| 1 | P0 | Accepted. PATCH from Self takes `addrs` and `probe` only (§4.4.1). | |
| +| 2 | P0 | Accepted. `Allow` refuses Self; `AllowSelf` admits it without Admin on the update doors (§3.3, §4.2). | |
| +| 3 | P0 | Accepted. The binding travels into `readUpdatesTarget`, `requestInstall`, `applyUpdates` and `moveGate` and is checked at each read (§4.3). | |
| +| 4 | P0 | Accepted. Instance rows take the app-token branch of the personal revoke, which requires current membership (§3.5). | |
| +| 5 | P1 | Accepted in part. An older console's writes are stated and shown not to widen; a start-up sweep reconciles on roll-forward (§3.4, §6). Refusing old writers outright is not possible: an old binary cannot be taught a rule after the fact. | |
| +| 6 | P1 | Accepted. Changes compare explicit values; the round trip is defined on GET's explicit settings (§4.4.2, §4.4.3). | |
| +| 7 | P2 | Accepted. Fixed order `updates`, `major`, `pin` (§4.4.2). | |
| +| 8 | P2 | Accepted. `instanceGate`'s limiter keys an instance principal on `principalKey` (§3.5). | |
| +| 9 | P2 | Accepted. Every consumer is listed in §3.5's table; the plan's first task re-greps. | |
| +| 10 | P2 | Accepted. `CreateRecord` stamps or refuses `CreatedAt` (§3.4). | |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-INSTANCE-BOUND-TOKENS-R1.md b/docs/superpowers/specs/CODEX-FINDINGS-INSTANCE-BOUND-TOKENS-R1.md| new file mode 100644 |
| index 000000000..e8029d50c |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-INSTANCE-BOUND-TOKENS-R1.md |
| @@ -0,0 +1,123 @@ |
| +**Review: instance-bound tokens — commit `54521e81`, branch `instance-bound-tokens-155`** |
| + |
| +**Changes required.** The proposed host gate is sound, but permitted handlers and existing token-management paths introduce authority beyond the intended scope. |
| + |
| +This is a static review of the design against code at the specified commit. The feature is not implemented there. No files were changed and no tests were run. The owner’s rulings are treated as fixed. |
| + |
| +**1. P0 — PATCH lets Self select a sibling’s environment** |
| + |
| +A token for A could PATCH A with `{"environment":"customer-b"}`. The path and incarnation checks would pass, but A would then receive customer B’s environment configuration, including secrets delivered through that bundle. |
| + |
| +The handler accepts `environment`, `steering`, and `gate` alongside upstreams and probes, and applies the environment without a Self-specific restriction. See [apiinstances.go:1944](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiinstances.go:1944) and [apiinstances.go:2004](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiinstances.go:2004). Reconciliation applies that binding, and config loading merges the selected environment: [reconcile.go:716](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/provision/reconcile.go:716), [environments.go:198](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/appconfig/environments.go:198). |
| + |
| +There is also an immediate effect outside A’s record: changing environment can clear the destination store’s seed ownership marker. See [seedenvbind.go:64](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/seedenvbind.go:64). |
| + |
| +**Fix:** Give Self an explicit PATCH field allowlist matching §4.2: upstreams and probe only. Reject environment, steering, and gate changes before side effects. Preserve the existing upstream-address authorization rail. |
| + |
| +**2. P0 — `Allow(Self)` opens the update doors to app-wide Admin credentials** |
| + |
| +The draft calls Admin inclusion harmless, but these three doors currently refuse **every** service role. `Allow(Self)` automatically adds Admin, whose principal has no instance binding. Admin therefore skips the proposed host and incarnation checks and reaches sibling update settings. |
| + |
| +The automatic grant is at [svcrole.go:126](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/svcrole/svcrole.go:126); the currently closed registrations are at [authcli.go:657](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/authcli.go:657). PUT and ordinary install contain no further person-only check: [apiupdates.go:114](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiupdates.go:114), [apiupdates.go:246](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiupdates.go:246). |
| + |
| +Keeping the legacy Instance role unchanged does not catch this widening. |
| + |
| +**Fix:** Express “people or a bound Self principal” explicitly for these doors, refusing other service principals before scope lookup. Preserve Admin’s existing behavior elsewhere. |
| + |
| +**3. P0 — Handler-only lifetime checks miss helper reads and install retries** |
| + |
| +A Self install request can validate A’s original record, then encounter deletion and recreation before `requestInstall` reads it. Alternatively, its first write can lose a conflict and retry against the replacement. The helper accepts only an actor string, rereads the record on every attempt, discards the policy, and retries without retaining the authorized incarnation. See [updatescore.go:200](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/updatescore.go:200). A check performed once in the HTTP handler would let that request install on the replacement tenant. |
| + |
| +There is also an earlier-response hazard in move status: `moveGate` reads the record and can return its kind or placement refusal before control returns to the handler. A stale token could learn facts about a replacement record before a handler-level incarnation check. See [apiinstancemove.go:64](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiinstancemove.go:64). |
| + |
| +**Fix:** Pass typed authorization into the record-reading helpers. Check incarnation immediately after each read, before record-dependent responses, and on every install retry. Check the freshly read install policy there too. Keep each write conditional on the checked record’s ETag. |
| + |
| +**4. P0 — A removed member can revoke instance tokens through personal settings** |
| + |
| +An instance row is neither `IsService()` nor `IsApp()`. Consequently, a member who minted it can leave the account and still revoke it through `/auth/cli/tokens/revoke`. |
| + |
| +`serviceRowOf` classifies only `kind='service'`: [authcli.go:1543](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/authcli.go:1543). The subsequent membership check applies only to `svc.IsApp()`: [authcli.go:1618](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/authcli.go:1618). An instance row falls through to a hard delete scoped only by its minting address and listing ID: [authstore.go:647](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/authstore.go:647). |
| + |
| +Fresh authentication proves who the former member is; it does not restore account membership. |
| + |
| +**Fix:** Classify instance credentials explicitly in this path. Either require current account membership, as for app tokens, or route revocation through account credential management. Test revocation after member removal and account transfer. |
| + |
| +**5. P1 — Rollback fails closed for authentication, but not for lifecycle maintenance** |
| + |
| +An old console sharing the upgraded database ignores instance rows when moving or purging an app. A later new console then sees stranded credentials, and its expanded stale-token checks can refuse a destination name because the old purge left those rows behind. |
| + |
| +The old kind-filtered operations are visible at [store.go:1735](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/store.go:1735), [store.go:1927](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/store.go:1927), [store.go:1969](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/store.go:1969), and [store.go:2088](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/store.go:2088). Claim-aware purge has the same filter at [claim_app_identity.go:182](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/claim_app_identity.go:182). |
| + |
| +Updating these statements in the new binary does not change old writers. Old consoles also retain finding 4’s personal-revoke behavior. |
| + |
| +**Fix:** Specify a mixed-version and rollback protocol covering writes, not just token resolution. Prevent incompatible old writers from operating while instance rows exist, or provide compatible cleanup before rollback. Exercise old lifecycle SQL against a database containing new rows. |
| + |
| +**6. P1 — The promised GET-to-PUT no-op does not match the existing API** |
| + |
| +GET returns both effective settings and explicit record values. An inherited setting can therefore appear as `"updates":"manual"` while the record stores `""`. See [apiupdates.go:70](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiupdates.go:70) and [updates.go:29](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/provision/updates.go:29). |
| + |
| +Sending the effective value back is an explicit override, not a no-op: `applyUpdates` assigns it to the record and compares explicit values. See [updatescore.go:105](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/updatescore.go:105). Literal enforcement of §4.3 would refuse that request when `updates` is app-controlled. Comparing only effective values instead would risk silently freezing an app-controlled default into an instance override. |
| + |
| +Both returned settings structures also include `channel`, whose presence is rejected even when unchanged: [apiupdates.go:123](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiupdates.go:123). |
| + |
| +**Fix:** Define the round-trip contract precisely: writable explicit fields, excluding channel, with normalized explicit-value comparisons. If effective-value resubmission is accepted, preserve inheritance rather than writing an override. Test inherited updates, inherited major, clears, and unchanged channel. |
| + |
| +**7. P2 — “First refused key in request order” is lost during decoding** |
| + |
| +PUT decodes directly into `map[string]any`, which preserves neither JSON member order nor repeated occurrences. See [apiupdates.go:119](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiupdates.go:119). The existing core processes settings in a fixed order: updates, major, pin. |
| + |
| +Thus the specified refusal ordering cannot be implemented by iterating the current request representation. |
| + |
| +**Fix:** Preserve ordered members during decoding and define duplicate-key handling, or specify a deterministic canonical order. Test raw JSON with reversed key orders; marshalled-map fixtures do not establish this property. |
| + |
| +**8. P2 — Changing `principalKey` does not make the six doors rate-limit per host** |
| + |
| +All six doors pass through `instanceGate`, whose limiter uses `hashToken(bearer(r))`, not `principalKey`. See [apiinstances.go:87](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiinstances.go:87). |
| + |
| +Several live tokens for one host therefore retain separate budgets. A unit test proving that `principalKey` contains the host would pass while the actual endpoints remain limited per token. |
| + |
| +**Fix:** Use the instance principal’s host key in the actual instance limiter, preserving existing behavior for other credential kinds. Test two tokens for A sharing a budget while B remains independent. |
| + |
| +**9. P2 — Other token-kind consumers still misclassify instance rows** |
| + |
| +Beyond `principalKey` and `auditActor`: |
| + |
| +| Consumer | Result without an instance branch | |
| +|---|---| |
| +| [authstore.go:138](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/authstore.go:138), `NeverExpires` | Treats the epoch expiry as a date rather than “never.” | |
| +| [templates/settings.html:17](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/templates/settings.html:17) | Presents the row without its app/service classification. | |
| +| [apiservices.go:441](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiservices.go:441), `serviceWhat` | Calls every nameless row an app credential in revocation audit descriptions. | |
| +| [apiservices.go:311](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiservices.go:311) | The explicit API projection carries app scope but no host binding. | |
| + |
| +**Fix:** Carry host identity through the token model and both listing queries, then update API projection, expiry classification, personal settings, and revocation audit rendering. Keep the real app binding separate from the displayed host scope. Listing tests should cover custom labels that do not identify the host. |
| + |
| +**10. P2 — Fresh `CreatedAt` on every recreation is a caller convention** |
| + |
| +The ordinary console creation paths stamp fresh time, but `provision.CreateRecord` accepts and serializes the caller’s `CreatedAt` unchanged: [provision.go:882](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/provision/provision.go:882). On an ungated store, deleting a record and recreating it from saved record data preserves the timestamp and revives any unrevoked token bound to it. |
| + |
| +This is a gap in the draft’s “by any path” guarantee, not a demonstrated exploit through the ordinary console create endpoint. Claim-aware creation is stronger: `PrepareInstance` refuses a supplied timestamp before stamping its own at [record.go:159](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/hostops/record.go:159). |
| + |
| +**Fix:** Enforce fresh incarnation allocation at every supported creation boundary. Define whether replaying a retired record is forbidden or must allocate a new incarnation. Test recreation through the production writer, including replayed creation metadata. |
| + |
| +**Areas that are sound** |
| + |
| +- **Direct account/app/host containment:** Service account resolution compares the bound account’s sqid, and app resolution checks `p.App` before existence lookup. See [apicli.go:228](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apicli.go:228) and [apicli.go:76](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apicli.go:76). Combined with exact host comparison, these prevent direct sibling and foreign-app requests. |
| +- **Path handling:** A valid nonempty host binding cannot equal an encoded traversal segment. A pattern without `{host}` supplies an empty `PathValue`, so the proposed comparison fails closed. The six handlers obtain their target host from the path; none substitutes a body/query host. Move `detail=1` additionally requires a person’s operator token at [apiinstancemove.go:468](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/apiinstancemove.go:468). |
| +- **Credential-kind separation:** Keeping Self outside `Parse` prevents minting it as an ordinary service role. Old `CLIPrincipal` rejects unknown kinds at [svcprincipal.go:149](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/svcprincipal.go:149); `CLITokenAddress` also refuses non-user kinds. Authentication rollback itself does not widen authority. |
| +- **Ordinary lifecycle behavior:** Creation stamps time; seed/import creation uses the newly prepared destination record. In-place mutations protect `CreatedAt` at [maintenance.go:81](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/provision/maintenance.go:81), and region moves preserve it. Non-console deletion paths, including seed cleanup at [seedfinish.go:108](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/instance-bound-tokens-155/internal/console/seedfinish.go:108), leave no record to authorize against. |
| +- **Existing secondary restrictions:** Self remains a service principal, so control-plane PATCH restrictions and the operator-only upstream-address rail still apply. “Repoint upstreams” therefore does not currently include adding arbitrary new addresses. |
| + |
| +**Tests in §7 that could pass while their property is false** |
| + |
| +| Proposed test | Missing case | |
| +|---|---| |
| +| Every door refuses sibling B | PATCH A to B’s environment; inspect delivered configuration and store side effects. | |
| +| Legacy Instance reach remains unchanged | Admin gains the three update doors. | |
| +| Delete/recreate before calling each door | Recreate between install reads or during a conflict retry; replace A with a different route shape before move status. | |
| +| People-only mint and account-pane revoke | Former minter revokes through personal settings. | |
| +| Purge/rename/move carry or delete rows | An old console performs the operation against the upgraded database. | |
| +| No-op PUT passes | Inherited values, explicit clears, and channel returned by GET. | |
| +| Per-host rate-limit key | Exercise actual endpoints using two tokens for one host. | |
| +| New `CreatedAt` after recreation | Replay the old creation metadata through a supported writer. | |
| + |
| +The containment tests should assert resulting records and side effects, as well as response status and canonical 404 bytes. |
| \ No newline at end of file |
diff --git a/docs/superpowers/specs/CODEX-REVIEW-BRIEF-INSTANCE-BOUND-TOKENS-R2.md b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-INSTANCE-BOUND-TOKENS-R2.md| new file mode 100644 |
| index 000000000..41c4835f9 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-INSTANCE-BOUND-TOKENS-R2.md |
| @@ -0,0 +1,19 @@ |
| +# Review brief, round 2: instance-bound tokens (#155, half 1) |
| + |
| +Same rules as `CODEX-REVIEW-BRIEF-INSTANCE-BOUND-TOKENS.md` (read it |
| +first): read-only, do not reopen §2's rulings, file:line evidence, P0/P1/P2. |
| + |
| +The spec is now rev 2. Your round 1 findings are in |
| +`CODEX-FINDINGS-INSTANCE-BOUND-TOKENS-R1.md`, and the spec's §9 records a |
| +disposition for each. |
| + |
| +1. First, a table: for each round 1 finding, whether rev 2's answer |
| + actually closes it (closed / partly / not), with one line of reason. |
| +2. Then only NEW findings: anything rev 2's changes break or introduce |
| + (AllowSelf, the selfBinding plumbing, the PATCH field refusal, the |
| + CreateRecord freshness rule and its callers and tests, the start-up |
| + sweep, the personal revoke branch, the limiter key), and anything round |
| + 1 missed. |
| + |
| +Say plainly if you find nothing new at P0 or P1. Leave GOCACHE unset; do |
| +not run the full test suite. |