carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 0ef3e715af049e3f6f17f41bc0ec2d29176629da parent e47d04e
2 files changed, +51 −6
  • docs/superpowers/specs/2026-10-08-seeded-instances-design.md +35 −6
  • docs/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