diff --git a/docs/GENESIS-RUNBOOK.md b/docs/GENESIS-RUNBOOK.md index 9da6d414..85365518 100644 --- a/docs/GENESIS-RUNBOOK.md +++ b/docs/GENESIS-RUNBOOK.md @@ -133,18 +133,21 @@ Confirm `GET /health` reports `"env":"production"`. Bare `wrangler deploy` witho Gate evidence: [PRODUCTION-GENESIS-GATE.md](PRODUCTION-GENESIS-GATE.md). -## Successor world (RFC-0121) — not production this landing +## Successor world (RFC-0121) -Rehearse locally (preview, or preview + activate). `--successor --activate` stops after activation; it does not inhabit. Agent ENTER is covered by unit tests. Flipping local `DEFAULT_WORLD_ID` is a later step. +Local rehearsal (preview, or preview + activate). `--successor --activate` stops after activation; it does not inhabit. The rehearsal script still refuses `https://noema.guru`. ```bash ADMIN_TOKEN=… BASE=http://127.0.0.1:8787 ./scripts/genesis_rehearsal.sh --successor -# explicit local activate only; never against https://noema.guru -# does not ENTER / LOOK — successor PLAY is not this script ADMIN_TOKEN=… BASE=http://127.0.0.1:8787 ./scripts/genesis_rehearsal.sh --successor --activate ``` -Later production cutover (human gate, separate campaign): preview successor on production (override deny lifted then), compare genesis_id ≠ `genesis.ef578f4ffceeccd0` and room_count 10, `confirm: true` activate on `world.perihelion-reach-2`, set `DEFAULT_WORLD_ID=world.perihelion-reach-2`, deploy. Do not `force`. Do not reseed `genesis.ef578f4ffceeccd0`. +Production cutover (human-gated, two deploys): + +1. Deploy Worker that allows Admin `world_id: world.perihelion-reach-2` on preview/activate. Keep `DEFAULT_WORLD_ID=world-01`. `force` and reseed stay `POLICY_DENIED`. Omitted `world_id` still targets the live 5-room DO. +2. Admin preview successor on production. Require `genesis_id ≠ genesis.ef578f4ffceeccd0` and `room_count: 10`. +3. Admin `confirm: true` activate on `world.perihelion-reach-2`. No `force`. Do not reseed `genesis.ef578f4ffceeccd0`. +4. Set production `DEFAULT_WORLD_ID=world.perihelion-reach-2` and deploy. PLAY then uses the successor DO. Do not add PLAY to the old DO. ## Recovery diff --git a/workers/noema/src/genesis.ts b/workers/noema/src/genesis.ts index 4d43200b..0d0e3b21 100644 --- a/workers/noema/src/genesis.ts +++ b/workers/noema/src/genesis.ts @@ -17,10 +17,6 @@ export function resolveAdminGenesisWorldId( const fallback = String(env.DEFAULT_WORLD_ID || "world-01").trim() || "world-01"; const value = String(requested || "").trim(); if (!value) return { ok: true, world_id: fallback }; - const envName = (env.NOEMA_ENV || "local").toLowerCase(); - if (envName === "production") { - return { ok: false, code: "POLICY_DENIED", message: "world_id override forbidden in production" }; - } if (value !== SUCCESSOR_WORLD_ID) { return { ok: false, code: "INVALID_REQUEST", message: "world_id override this campaign must be world.perihelion-reach-2" }; } diff --git a/workers/noema/test/genesis-admin-world-id.test.ts b/workers/noema/test/genesis-admin-world-id.test.ts index 68325fab..3b3bd7be 100644 --- a/workers/noema/test/genesis-admin-world-id.test.ts +++ b/workers/noema/test/genesis-admin-world-id.test.ts @@ -16,12 +16,21 @@ describe("resolveAdminGenesisWorldId", () => { expect(r).toEqual({ ok: true, world_id: "world.perihelion-reach-2" }); }); - it("production denies any override", () => { + it("production allows successor override", () => { const r = resolveAdminGenesisWorldId("world.perihelion-reach-2", { NOEMA_ENV: "production", - DEFAULT_WORLD_ID: "world.perihelion-reach", + DEFAULT_WORLD_ID: "world-01", }); - expect(r).toEqual({ ok: false, code: "POLICY_DENIED", message: "world_id override forbidden in production" }); + expect(r).toEqual({ ok: true, world_id: "world.perihelion-reach-2" }); + }); + + it("production still rejects other explicit ids", () => { + const r = resolveAdminGenesisWorldId("world.other", { + NOEMA_ENV: "production", + DEFAULT_WORLD_ID: "world-01", + }); + expect(r.ok).toBe(false); + if (!r.ok) expect(r.code).toBe("INVALID_REQUEST"); }); it("rejects other explicit ids", () => { @@ -151,14 +160,15 @@ describe("POST /v1/admin/genesis/preview world_id routing", () => { expect(fetches.every((c) => c.headers?.["x-noema-world-id"] === SUCCESSOR_WORLD_ID)).toBe(true); }); - it("production denies any override before DO lookup", async () => { + it("production routes successor preview to successor DO", async () => { const calls: DoCall[] = []; const res = await adminPost("/v1/admin/genesis/preview", SUCCESSOR_PREVIEW, calls, "production"); - expect(res.status).toBe(403); - const data = (await res.json()) as { error: { code: string; message: string } }; - expect(data.error.code).toBe("POLICY_DENIED"); - expect(data.error.message).toBe("world_id override forbidden in production"); - expect(calls.some((c) => c.op === "idFromName")).toBe(false); + expect(res.status).toBe(200); + const data = (await res.json()) as { result: { world_id: string; genesis_id: string } }; + expect(data.result.world_id).toBe(SUCCESSOR_WORLD_ID); + expect(data.result.genesis_id).not.toBe(FROZEN_GENESIS_ID); + expect(calls.filter((c) => c.op === "idFromName").map((c) => c.name)).toEqual([SUCCESSOR_WORLD_ID]); + expect(calls.filter((c) => c.op === "fetch").every((c) => c.headers?.["x-noema-world-id"] === SUCCESSOR_WORLD_ID)).toBe(true); }); it("rejects other explicit ids", async () => { @@ -190,7 +200,7 @@ describe("POST /v1/admin/genesis/activate world_id routing", () => { expect(act?.body?.world_id).toBe(SUCCESSOR_WORLD_ID); }); - it("production denies activate override", async () => { + it("production routes successor activate to successor DO", async () => { const calls: DoCall[] = []; const res = await adminPost( "/v1/admin/genesis/activate", @@ -198,10 +208,11 @@ describe("POST /v1/admin/genesis/activate world_id routing", () => { calls, "production", ); - expect(res.status).toBe(403); - const data = (await res.json()) as { error: { code: string } }; - expect(data.error.code).toBe("POLICY_DENIED"); - expect(calls.some((c) => c.op === "idFromName")).toBe(false); + expect(res.status).toBe(200); + expect(calls.filter((c) => c.op === "idFromName").map((c) => c.name)).toEqual([SUCCESSOR_WORLD_ID]); + const act = calls.find((c) => String(c.url).includes("/genesis-activate")); + expect(act?.headers?.["x-noema-world-id"]).toBe(SUCCESSOR_WORLD_ID); + expect(act?.body?.world_id).toBe(SUCCESSOR_WORLD_ID); }); it("omitted activate uses DEFAULT_WORLD_ID and does not send DO world_id", async () => {