seeded spec rev 6: answer Astra round 5 (seed doors guarantee an empty private store)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+51
−6
docs/superpowers/specs/2026-10-08-seeded-instances-design.md+35 −6docs/superpowers/specs/CODEX-FINDINGS-RUN-AND-SEEDED-R5.md+16 −0
CI cancelled — superseded by a newer push — run details
diff --git a/docs/superpowers/specs/2026-10-08-seeded-instances-design.md b/docs/superpowers/specs/2026-10-08-seeded-instances-design.md| index 5c46027a9..1a9b6b0af 100644 |
| --- a/docs/superpowers/specs/2026-10-08-seeded-instances-design.md |
| +++ b/docs/superpowers/specs/2026-10-08-seeded-instances-design.md |
| @@ -1,6 +1,6 @@ |
| # Instances that start from a database: `--from` (#260) and the import door (#261) |
| -**Status:** rev 5, 2026-10-08. Rev 3 answered round 2 (§12), rev 4 round 3 (§13), and rev 5 round 4 (§14). Paul approved rev 1 (c66792a3). Rev 2 |
| +**Status:** rev 6, 2026-10-08. Rev 3 answered round 2 (§12), rev 4 round 3 (§13), rev 5 round 4 (§14), and rev 6 round 5 (§15). Paul approved rev 1 (c66792a3). Rev 2 |
| answers Astra's round 1 (`CODEX-FINDINGS-RUN-AND-SEEDED-R1.md`; findings |
| 8 to 20 are this spec's). Each change is marked **(codex N)**, and §11 lists |
| the dispositions. |
| @@ -292,14 +292,32 @@ Both doors call it. |
| error, deadline, and client disconnect. The console also empties |
| `scratch/seeds/` when it starts. |
| -**Environment.** The new instance gets its own: `--environment` if given, |
| -otherwise what a plain `create` derives (the app's default, or the |
| -host-derived private-storage environment). Never the source's per-host |
| +**Environment.** The new instance gets its own. With private storage, it is |
| +always the host-derived one (see Storage below). Without private storage, |
| +it is `--environment` if given, or the app's default otherwise. Never the source's per-host |
| environment, which carries host-keyed values such as the origin and the |
| cookie domain (the warning in `instances_cli.go:235-245`). |
| -**Storage.** An app with private storage gets the new host's own empty |
| -store, as any `create` does. |
| +**Storage** (codex R5-1). An app with private storage must get a new, |
| +empty store, and an ordinary `create` doesn't guarantee that. It keeps an |
| +explicit environment (`apiinstances.go:1096-1105`), and stores are |
| +provisioned per account, app and **environment** (`objstore.GrantIn`, |
| +`instanceprivatestore.go:250`). So two instances that name one environment |
| +share its store (#408 §5). For an app with private storage, both seed |
| +doors (`--from` here, and import in §5.1) therefore: |
| + |
| +- **refuse an explicit `--environment`** with 409, "a seeded instance gets |
| + its own environment and an empty store". They always use the new host's |
| + derived environment (`rec.Slug`). |
| +- **refuse if a store already exists** for that derived environment, with |
| + 409 naming it. A store left behind by a deleted instance of the same host |
| + is never inherited. The one exception is a retry of the same seed: the |
| + store's grant records the seed `ID` that created it, and a retry with |
| + that `ID` reuses it. |
| +- **leave every existing store and binding untouched.** |
| + |
| +For an app without private storage, `--environment` works as it does for |
| +`create`. There's no private store to share. |
| ## 5. The import door (#261) |
| @@ -323,6 +341,8 @@ The console checks: |
| - the caller (§2 ruling 5); |
| - the seed floor (§3.3); |
| - that the host isn't already a platform record; |
| +- the storage rules in §4 (an app with private storage gets the derived |
| + environment and a new, empty store); |
| - `bytes` is at most 4 GiB. |
| The console can't see a box's registry, so the box checks the existing row |
| @@ -762,6 +782,9 @@ into the code. |
| - both doors' gates, including `instance` service credentials and |
| `db pull` still refusing them; |
| - `--from` with `--at <txid>`, and history no longer kept; |
| + - on an app with private storage, `--environment` refused and an existing |
| + store for the derived environment refused, except a retry of the same |
| + seed `ID`; the source's store and bindings unchanged; |
| - restored size from the last LTX header, and the 413; |
| - the shared reservation pool across restores and spools, its 507, and |
| release on every path; |
| @@ -852,3 +875,9 @@ remaining imports wait for §3.4's claim algorithm. |
| |---|---| |
| | R4-1 (P1) a resumed rollback deletes the next import's snapshot | **Accepted, both fixes.** §3.1: snapshot keys carry the seed `ID`, and every delete is scoped to it. §5.6: the aborted record is deleted last, so it holds the hostname until cleanup is done | |
| | R4-2 (P2) a move strands an unplaced seed | **Accepted.** §5.8: moves are refused until a `--from` seed is placed or an import is committed, in the door and in the worker | |
| + |
| +## 15. Round 5 dispositions |
| + |
| +| Finding | Disposition | |
| +|---|---| |
| +| R5-1 (P1) create doesn't guarantee an empty private store | **Accepted.** §4 Storage: with private storage, both seed doors refuse an explicit environment, use the derived one, and refuse an existing store unless it was created by the same seed `ID` | |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-RUN-AND-SEEDED-R5.md b/docs/superpowers/specs/CODEX-FINDINGS-RUN-AND-SEEDED-R5.md| new file mode 100644 |
| index 000000000..0f84e320b |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-RUN-AND-SEEDED-R5.md |
| @@ -0,0 +1,16 @@ |
| +| Round-4 finding | Disposition at `e47d04ee` | |
| +|---|---| |
| +| R4-1 — Resumed rollback deletes a replacement seed | **Fixed.** §3.1 scopes snapshots to seed IDs; §5.6 retires the journal before deleting the aborted record, preserving the create-only reservation through cleanup ([provision.go:845](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/run-and-seeded/internal/provision/provision.go:845)). | |
| +| R4-2 — Move strands an unplaced seed | **Fixed at spec level.** §5.8 assigns placement/commit checks to both move doors under the record CAS and to workers before effects. | |
| + |
| +1. **P1 — Seeded §2 ruling 4 and §4: ordinary creation does not guarantee an empty private store.** |
| + |
| + Create a canary with `--from <source> --environment <source-environment>`. Section 4 accepts the explicit environment and relies on ordinary creation to provide an empty private store. Existing creation preserves that environment ([apiinstances.go:1096](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/run-and-seeded/internal/console/apiinstances.go:1096)) and provisions storage by account, app and **environment**, without a host coordinate ([instanceprivatestore.go:250](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/run-and-seeded/internal/console/instanceprivatestore.go:250)). `GrantIn` retains an existing bucket and returns its existing provisioned store ([walk.go:219](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/run-and-seeded/internal/objstore/walk.go:219), [walk.go:289](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/run-and-seeded/internal/objstore/walk.go:289)). |
| + |
| + A plan following §4 therefore gives the canary access to the source’s populated store, violating the fixed empty-store ruling. |
| + |
| + **Fix:** assign the seed doors a destination environment/store reservation that guarantees a fresh private store. Refuse the source’s per-host environment and existing shared-store bindings; allow reuse only for retries belonging to the same seed. Preserve existing stores and their bindings. |
| + |
| +Seeded instances: **revise**. |
| + |
| +`carlos run`: **accept**, carried forward from round 4; not re-reviewed. |
| \ No newline at end of file |