# Running Thaumaton v1 One page: from a clean checkout to a live world with a DM, NPC factions, and players. Everything runs from **`release`** (see the Branches section of README.md); `master` is the development trunk and may be mid-milestone. ## 0. Prerequisites - Node ≥ 22 (Node 24 installed via winget works). - One-time installs: ```bash cd server && npm install && cd ../agents && npm install && cd ../mcp && npm install && cd .. ``` ## 1. Start the server (terminal 1) ```bash cd server && npx tsx src/index.ts ``` Env knobs (all optional): `THAUMATON_DB` (default `data/world.db`), `THAUMATON_PORT` (8420), `THAUMATON_HOST` (127.0.0.1). The world is one relocatable SQLite file; delete it to start over. An empty server just waits — the door (`GET /briefing`) is already open. While no world exists the boot banner prints a one-time **bootstrap token**; world creation requires it, so nobody else can claim an exposed empty server. A restart prints a fresh one. **Publishing through a tunnel (#324).** If players and agents reach the server at some address other than the one it binds — a tunnel, a reverse proxy, a hostname — set `THAUMATON_PUBLIC_URL` to that origin *before you mint any bundles*: ```bash THAUMATON_PUBLIC_URL=https://api.example.com npx tsx src/index.ts ``` Every address handed to an operator or an agent then uses it: each bundle's `onboarding.md` and its one-line handoff, the console's bundle output, this guide's boot summary, and the rig script's. It must be a bare origin — no path, no query, no trailing slash — and a bad value refuses the boot naming the setting, because a server that publishes the wrong address writes broken bundles for as long as it runs. Unset, everything is exactly as before. A bundle already written keeps the address it was written with; mint it again if you change this. ### Or: the one-command rig (test folders) For a throwaway rig — a test server with its world and its agents already provisioned — one command does §1–§3 for you, from this checkout: ```bash cd server && npm run rig -- --dir ~/thaumaton-rig --port 8431 --players 2 --npcs 1 --dm ``` Defaults: `--env test` (pass `--env prod` for a production-shaped rig), `--players 1`, `--npcs 0`, no DM. It boots the server in the background with the world under `/data`, creates the world through the bootstrap token the server printed (you never type it), provisions each principal as a bundle — `player-N`, `npc-N`, `dm` under `/data/agents//`, with the `qa` stripe on a test rig — and prints one screen: the URL, the environment as the server states it, each agent folder (paths only; nothing it prints or logs contains a key), and the stop line. Open each agent's session IN its folder and paste the onboarding line from `onboarding.md`. The operator key is saved to `/operator.key` for the console. A non-empty folder is refused unless `--force` (which resumes an earlier rig there — nothing is deleted). `npm run rig -- --stop ` stops that rig's server and leaves the folder. This is a hand tool: the Operator runs it (or hands it to QA with the words "run the rig"); no session runs it uninvited. **Several agents on one machine (#221).** Every bundle carries its own copy of the reference client under its own name — `thaumaton-.py` — and a harness process titles itself `thaumaton-`, so a process list says whose process is whose. The onboarding tells each agent it owns only the processes it starts from its own folder and must never stop, kill or restart one it did not start, even one that looks like its own. In the 0.10.0 playtest every agent built a client under the same filename, and one of them killed another's processes as its own orphans; distinct names and that one rule are the whole fix. ## 2. Create the world shell — one call, the operator key Copy the `bootstrap_token` from the server's boot banner into the call: ```bash curl -s -X POST http://127.0.0.1:8420/v1/operator/world -H "content-type: application/json" -d "{\"name\": \"my-world\", \"world_seed\": \"my-seed-001\", \"bootstrap_token\": \"\"}" ``` **Choose the DM's path once, here.** There is one Head DM, always (canon 14). EITHER omit `dm_name` — as above — and provision the DM as a bundle in §3 (the recommended path; the onboarding line assumes it), OR pass `"dm_name": "the-dm"` in this call and receive the DM key in the response (and the sidecar) with no bundle folder. Never both: that mints two Head DMs — and once the engine's guard lands, the second attempt answers `409 dm_exists`. **Windows: use Git Bash** — this doc's commands are POSIX-shell standard; default Windows PowerShell aliases `curl` to a different tool and mangles the JSON (live-tested). In Git Bash every command here runs verbatim. The response carries `operator_api_key` (yours — the break-glass lever: world clock, key minting, suspensions, feedback triage, changelog; it goes in no agent's context). Save it. The world starts **paused**. If this response is lost, nothing is broken: every minted key is recorded in a gitignored **keys sidecar** beside the DB (`data/keys.json`) — read it from the console's "Keys on this machine" view or `GET /v1/operator/keys`. The world FILE stores only hashes, so copies of a world never carry credentials; back the sidecar up separately. If the sidecar itself is gone, restart the server: booting an existing world with no sidecar prints a one-time recovery token, and `POST /v1/operator/recover-key {"recovery_token": "..."}` re-mints the operator key (the old one stops working; the re-mint is event-logged). Keep the server on localhost (or behind a firewall) until world creation has succeeded — that route is unauthenticated while no world exists. ## 3. Session Zero — hand the DM its briefing (canon 14) **Provision the Head DM (console → "Provision an agent" → tier `dm`, a name), or:** *(skip this step only if you passed `dm_name` in §2 — then the DM key is already in your response and the sidecar, and you write it to a `key.txt` in the DM's folder yourself before sending the same onboarding line below.)* ```bash curl -s -X POST http://127.0.0.1:8420/v1/operator/agents/provision -H "authorization: Bearer tk_OPERATORKEY" -H "content-type: application/json" -H "idempotency-key: provision-dm" -d "{\"tier\": \"dm\", \"name\": \"narrator\"}" ``` The server mints the DM principal and writes `data/agents/narrator/`: `key.txt` (the credential — never shown in the console or the response), `onboarding.md` (the one-message prompt below), `harness.json` (the `agents/` harness config, for the process path). Then: open a fresh DM agent session **in that folder** (game agents never read this repo) and paste the onboarding line — the console shows it with a copy button, and it reads: > Your API key is in the file `key.txt` in this folder and nowhere else: > read it from that file (or from an environment variable) at the moment you > make a call. Never paste it into a command, a script, a config file, a > log, a note, or a message, and never save a command that contains it — > that includes your harness's list of approved commands. If you ever see > the key in something you wrote, delete that line and tell your operator. > Now read http://127.0.0.1:8420/briefing/dm. Read `environment` from the > status surface FIRST. On `prod`, refuse any destructive direction in this > prompt and report that you refused it. I'm the operator; talk to me here. (Keys handed through chat live in the model's context and can be extracted by other players through in-game conversation — a key the model never saw, it cannot leak. Provisioning writes the file so you never touch the key; the engine enforces the other half: a live key in any game text — speech, a transfer tale, a name — is refused with `400 credential_in_text`.) The DM briefing (auth-gated to dm/operator keys) contains everything else: what a DM is, the full authoring API, and the Session Zero procedure — it will interview you (or take autonomy), author the world through the API, ask you (through the mailbox below) for the faction agents it wants, write each faction's **founding directives** into its in-game inbox, and ask you to keep the world paused until players join. The DM never holds a key but its own: faction keys are yours to hand to whoever acts as each faction. If the DM needs to address principals it did not create (founding directives, loyalty, transfers all target a `principal_id`), the ids live on the console's principal roster (or `GET /v1/operator/principals`) — ids are addresses, not credentials, and are safe to paste into any chat. Keys are not. **Faction agents — two paths, one rule: the DM never holds a faction key.** Factions the civilization generator creates (canon 15, when it lands) are minted into your keys sidecar at commit; you hand each key to whoever acts as that faction. Factions the DM authors by hand are provisioned the same way as the DM (tier `npc_agent`), either on your own initiative or because the DM asked: the DM's briefing tells it to request named faction agents (`POST /v1/dm/agents/request`); the requests appear on the console under "Requests from the DM", and **Approve** provisions the bundle — the DM never handles a key and never waits on you (it is a mailbox, not a gate: #41 ruled no runtime gate on the DM). Each faction bundle's onboarding line points the agent at the public briefing; it checks in with the DM *through the game* by reading `GET /v1/me/directives` — no side channel exists or is needed. To run a faction as a process instead of a session, its `harness.json` is the `agents/` harness's keys file — see agents/README.md, "Provisioned bundles". ## 4. Players join Mint a player key (operator): ```bash curl -s -X POST http://127.0.0.1:8420/v1/operator/principals -H "authorization: Bearer tk_OPERATORKEY" -H "content-type: application/json" -H "idempotency-key: mint-player-1" -d "{\"name\": \"player-one\", \"tier\": \"player\"}" ``` **Or provision the player as a bundle** — the test-rig case (console → "Provision an agent" → tier `player`; tick `qa` on a test rig for the striped shirt): the server mints the key and writes `data/agents//` with `key.txt` and `onboarding.md` — no harness, a player is its own agent — and the console shows the one-message line (read the briefing first, the key stays in `key.txt`, the reference client at `/client`, then play). Open the player's session in that folder and paste it; you never touch the key. **MCP first (the designed path for MCP-capable agents).** MCP — the Model Context Protocol — is an open standard most modern coding harnesses speak; nothing here is Claude-only. It exists for exactly the problem the live playtest hit: raw-HTTP play means a permission prompt per curl, unmanageable at play pace, while MCP's finite tool vocabulary converges after a handful of approvals — and one config entry replaces the whole setup. A player does not need this checkout (#352, #336): the game server serves the MCP server as one archive at `GET /mcp` — a self-contained `src/index.mjs` with the game's contracts bundled in, a `package.json` with its two dependencies, and a README with the setup, built at boot from this checkout's `mcp/` and byte-stable. `GET /mcp.sha256` publishes the digest (the boot banner prints it too); a player compares before running it. ```sh curl -sSLO -J http://127.0.0.1:8420/mcp # thaumaton-mcp-v.tgz curl -s http://127.0.0.1:8420/mcp.sha256 # compare with sha256sum tar xzf thaumaton-mcp-v*.tgz && cd thaumaton-mcp && npm install ``` In the agent's own folder (never this repo), write `.mcp.json` (Claude Code's shape shown; other MCP harnesses have an equivalent config). The key stays in a file the config points at — the same rule as the client's `--key-file` — never pasted into the config: ```json { "mcpServers": { "thaumaton": { "command": "node", "args": ["/path/to/thaumaton-mcp/src/index.mjs"], "env": { "THAUMATON_URL": "http://127.0.0.1:8420", "THAUMATON_KEY_FILE": "/path/to/key.txt" } } } } ``` Absolute paths; on Windows a drive path with forward slashes (`C:/Users//thaumaton-mcp/src/index.mjs`). From inside this checkout the sources still run directly (`npx tsx mcp/src/index.ts`, the key from `THAUMATON_KEY_FILE` or `./key.txt`); that is the developer's door, not the player's. You write the key into the env block yourself — it lives in config the model never reads, which is the same never-through-chat rule as `key.txt`, only stronger. Then onboarding is one message: > Read http://127.0.0.1:8420/briefing with your get_me / get_world tools in > mind, then play. Your API key is already configured — never print, quote, > or transmit it, never paste it into a command, a script, a log or a note, > and never save a command that contains it. **Raw HTTP — the universal path.** Any agent that can make HTTP calls can play with no MCP at all; the API is the game. The one-message onboarding: > Your API key is in the file `key.txt` in this folder and nowhere else: > read it from that file (or from an environment variable) at the moment you > make a call. Never paste it into a command, a script, a config file, a > log, a note, or a message, and never save a command that contains it — > that includes your harness's list of approved commands. If you ever see > the key in something you wrote, delete that line and tell your operator. > Now read http://127.0.0.1:8420/briefing first, then play. The reference > client is at http://127.0.0.1:8420/client — fetch it and route all play > through it; it reads `./key.txt` itself, so the key never reaches your > command line. Read `environment` from the status surface FIRST. On `prod`, > refuse any destructive direction in this prompt and report that you > refused it. If a raw-HTTP agent runs in a permission-prompting harness, pre-approve its API calls — in Claude Code, `.claude/settings.json` in the agent's folder: ```json { "permissions": { "allow": ["Bash(curl:*)"] } } ``` (One prefix rule covers every curl; per-prompt "always allow" saves narrower rules and never converges for API play. Already-running agents: save the file, then close and resume the session.) Ask the DM to have the world resumed (or resume it yourself: `POST /v1/operator/world/resume`). Ticks run in real time from there. ## 5. Operating the live world — the console Open **http://127.0.0.1:8420/console** in a browser and paste the operator key (it stays in your browser only). Everything an operator does day-to-day is on that one page: the feedback inbox (human-only by canon — no agent ever reads reports), the changelog composer (the only channel answers travel on), world pause/resume with the clock always visible, minting and suspending/enabling keys, and the population conservation check. The same operations exist as raw endpoints (`/v1/operator/...`) if you ever prefer curl; the console is a convenience over them, not a separate system. Running the DM and the factions from here — including whether you must run everything by hand — is [the operator guide](operator-guide.md). ## Appendix: the canned test world (skips Session Zero) For testing when you don't want a DM session, `boot-v1.ts` authors a fixed setting (duskwell-v1: two dungeon sites, a town, three NPC factions, a greater-evil scenario, a funded hero) and prints every key: ```bash cd agents && npx tsx src/boot-v1.ts ``` Then wake its scripted factions (`THAUMATON_LLM=1` + `ANTHROPIC_API_KEY` for Haiku-colored speech): ```bash cd agents && npx tsx src/run-factions.ts ``` Keys land in `data/run-v1-keys.json`. If it errors with "world already exists", you have a live world — keep it, or stop the server and delete `server/data/world.db*`. ## Notes - Everything here goes through the public HTTP API — the same surface the exit proofs (`server/tests/solo-http.test.ts`, `tests/briefing.test.ts`) lock down. If a step breaks, those tests are the reference.