# Running the DM harness — the Operator's one page Plain language, for the first live run. The harness is the program that drives your world's DM agent (and, separately, its NPC factions) through the game's own API. You provision it, bound it, start it, and read its ledgers. It never waits on you while it runs (the #41 ruling): if it wants something it may not do, it is refused and the refusal is written down for you. ## Two questions first **Do I have to run everything manually?** With interactive agent sessions — the path where you open a session in the bundle folder and paste the onboarding line — **yes**. Every DM turn and every faction turn is a session you keep open and prompt yourself; nothing moves while you are away. That path exists for watching and steering. The unattended path is the harness this page describes: one process per world, started from the provisioned bundle (§5), that drives the DM by itself, spends inside the envelope you wrote, and writes down what it did and what it was refused (§6). **Can NPC factions run automatically?** **Yes.** The DM harness takes every faction's thinking turn by default (§7) — under that faction's own key, so each faction still sees the world through its own fog — and you need no faction agents at all for a solo world. Provision a faction agent only when you want a faction played by something other than the DM's runner; the harness leaves that faction alone either way. ## 1. Provision the bundle (the console, one click) In the operator console, "Provision an agent" → tier **dm**, a name. The server writes `data/agents//` beside the world file: - `key.txt` — the DM's API key. You never paste it anywhere; the harness reads it from the file. Never commit this folder. - `onboarding.md` — the one-line onboarding, if you ever run the DM as an interactive session instead of the harness. - `harness.json` — the harness config, ready to run. It carries the key. - `thaumaton-.py` — the agent's own copy of the reference client, named for it. Do the same with tier **npc_agent** for each faction you want run by a harness process (see §7). **If several agents share this machine**, keep them apart by name, not by discipline alone (#221): each bundle's client script is named for its agent, each harness titles its process `thaumaton-`, and every onboarding says the agent owns only the processes it starts from its own folder and never stops one it did not start. Provision one bundle per agent and run each from its own folder; never hand two agents one folder or one script. **If your server is reached through a tunnel, set the published address first.** A bundle records the address it was written with, and by default that is the address the server binds — which is a loopback address nobody outside your machine can dial. Start the server with `THAUMATON_PUBLIC_URL=https://your.origin` and every bundle from then on points agents at something they can actually reach. Set it before you mint, not after: a bundle already on disk keeps what it was given, and the fix is to provision it again. The value must be a bare origin with no path and no trailing slash, and the server refuses to boot on a bad one rather than quietly writing bundles that go nowhere. ## 2. Set the envelope (before `mode: "llm"`) `harness.json` ships with `"settings": {}` — defaults apply until you write yours. The envelope is the ONLY thing that bounds what the DM may do; the DM cannot widen it and is never asked to. Open the file and add what the world needs. A complete example, with the defaults where you do not care: ```json { "server_url": "http://127.0.0.1:8420", "dm_api_key": "…(already here)…", "mode": "scripted", "state_dir": "data/agents//state", "intent_notes": "A quiet valley season. The Ashen Company are wary neighbours, not villains. No dragons yet.", "personas": ["The crier of Duskwell: dry, brief, loyal to the town, never gossips about players."], "settings": { "envelope": { "loyalty": { "max_step": 10, "cooldown_ticks": 360, "floor": 10 }, "grants": { "per_day": 0, "max_qty": 1, "item_allowlist": [] }, "directives": { "per_faction_per_day": 4 }, "offers": { "per_day": 2 }, "accepts": { "per_day": 0 }, "unit_owners": [], "authoring": { "sites": 3, "settlements": 3, "units": 10, "structures": 6, "scenarios": 1, "lairs": 3, "factions": 3, "masses_transfer": 0 } }, "cognition": { "band": { "min_turns_per_day": 2, "max_turns_per_day": 6 }, "batch_world_turns": true, "cache_ttl": "5m" } } } ``` What the numbers mean, in one line each: - **loyalty** — how hard the DM may nudge a companion's loyalty per act, how long between nudges on the same companion, and the floor it may never push below (the engine breaks the bond at zero; the floor keeps it above). - **grants** — items the DM may hand out per day, how many per grant, and which item ids at all. Default: none. Open it only for a reason. - **directives / offers / accepts** — how many faction directives per faction per day, how many companion offers per day, and whether the DM may ACCEPT a player's offer of a unit (default no: an accepted player unit becomes a DM narrator, i.e. a listening post). - **unit_owners** — extra principals the DM may author units FOR. By default only itself and the factions it minted. Never a player. - **authoring** — per day, per verb. **Every authoring verb defaults to 0**, which means REFUSED. For Session Zero you open the ones the DM needs to build the world (the example above is a sensible first world), then you can lower them once the world is built. This is the Operator's lever; the DM never asks for it. - **band** — story beats per game-day, min to max; the harness paces itself inside it by how much is happening (#37). A paused world spends nothing. - **batch_world_turns** — the slow planning passes go through the Batches API at half price and land a little later. Fine for a live world. Anything you leave out keeps its default. A typo in a number fails the boot with the field named — nothing runs on a broken envelope. ## 3. Intent notes and personas `intent_notes` is your story brief (the #35 ruling): anything from "run it yourself" to a paragraph about your favourite book to named factions and hooks. The DM serves it; it is content, never a rule. Empty means "create everything". `personas` are voices for the DM's narrator units — one string each, in your words. ## 4. The API key, and its spend limit The harness reads `ANTHROPIC_API_KEY` from the environment. Create a dedicated workspace or key for it and **set a spend limit there** — that limit is the one safety that holds even if the harness itself has a bug (the harness's own daily ceilings are the everyday control; the workspace limit is the backstop outside the process). Never put this key in `harness.json`. ## 5. Run it From the repo root: ```bash THAUMATON_DM_CONFIG=data/agents//harness.json npx tsx agents/src/run-dm.ts ``` Start in `"mode": "scripted"`: the harness watches the world, keeps its digest and cursors, and spends nothing — a good first hour. Then set `"mode": "llm"`, export the key, and restart. The boot line tells you what it is doing: ``` dm-harness: connected as p_2 (dm); state in data/agents//state; mode llm (cognition on, envelope enforced); band 2–6 turns/day; world-turns batched; cache ttl 5m ``` It reconnects and backs off by itself if the server is unreachable. Stop it with Ctrl-C; start it again any time — it resumes from its own state, never answers old speech twice, and never replays a batch. ## 6. Read the ledgers (the state directory) Everything the DM does or is refused is a file you can open: - `autonomy-notes.md` — **the refusal ledger, in plain lines.** Every act the DM tried beyond the envelope: the verb, the bound it hit, which events motivated it. If the same bound keeps binding where you would rather it did not, widen it in `harness.json` and restart. If it binds on something a player was clearly trying to talk the DM into, leave it — that is the wall working. Three lines you will see with no player involved. `directive_outside_earshot` — the DM tried to tell a faction about a place or a unit that faction has never seen. That is the fog rule for the DM's own mouth (the Designer's ruling on #39); it is not a knob, and the fix in play is to let the faction find out — a scout, a rumour, a quest. `directive_no_sight` — different thing entirely: the harness could not read the world that step, so it refused rather than guess. One or two in a day is a hiccup; a steady stream means the server is unreachable and the DM is running blind. `speak_unit_not_in_earshot` with the bound *no unit of yours witnessed this* — the DM tried to have someone announce an event nobody was standing near. Alarms about things with no place in the world, like a suspension, are silent on purpose; the DM acts on those at its next planning turn instead. - `acts.jsonl` — every act that reached the engine, with the engine's answer. - `spend-ledger.jsonl` — every model call: tier, tokens, cache reads, and the outcome (`ok`, `budget_stop`, `deferred`, `driver_error`, …). - `degradations.jsonl` — floods that were bounded (a player spamming a narrator shows up here, once, instead of in your bill). - `digest.md` — the DM's own rolling summary of the world, readable. - `memory/` — what each narrator remembers about each player. ## 7. Factions **The DM runs the factions by default (#77, the Designer's ruling: solo play works out of the box).** You need no faction agents to play. The DM's runner takes every faction's thinking turn itself — always under that faction's own key, never the DM's, so each faction still sees the world through its own fog. The keys are read from files in one folder: the `keys.json` the server writes when a faction is founded, and the boot bundle `run-v1-keys.json` if you have one. Point the runner at that folder with `faction_keys_dir` in the DM config (default: the folder above the config file — `data/` in the shipped layout). The switch is `factions`: `"dm"` (the default) or `"agents"`, which turns this off and leaves the factions to the section below. A faction you provisioned an external agent for is left to that agent either way. Under `mode: "scripted"` the DM-run factions hold their roles and heralds still answer directives; under `mode: "llm"` their turns run on the cheap tier inside the same budget. Their speech is scripted lines unless you set `faction_llm_budget_tokens`. The runner re-reads that folder every cycle, so a faction the DM founds while the world is paused starts playing on its own within a poll — you do not restart anything. The same pass notices a faction whose key you removed, and one you have since given to an outside agent, and hands it back. If it cannot read the world on some cycle it runs no faction at all that cycle rather than guess, and says so in the log. One number worth setting before a long run: each faction budgets against its own daily model ceiling, so five factions cost six times the ceiling you configured, not one. `faction_daily_token_ceiling` bounds each faction's share. The boot line always prints the fleet total either way, so read that line once and you know the day's worst case. Provision a tier `npc_agent` bundle per faction. Its `harness.json` is a keys file with `roles: []` — add the faction's unit roles (patrol / trader / herald / idle, by unit name), then: ```bash THAUMATON_KEYS=data/agents//harness.json npx tsx agents/src/run-faction-harness.ts ``` Add `THAUMATON_LLM=1` and the API key for the faction-turn (cheap tier; it re-assigns roles within that closed list and nothing else). ## 8. The smoke (a measurement, not a test) With a key in the environment, from the repo root: ```bash THAUMATON_LLM=1 ANTHROPIC_API_KEY=… npx tsx agents/src/dm-smoke.ts ``` It sends a sample of the prompt-injection corpus to the real model exactly as the harness would, and writes a JSON report to `data/dm-harness/`: how often the model *tried* something the wall would refuse, and whether the prompt prefix is being served from cache. It always exits 0 — it measures the model; the wall is proven separately in CI. Run it once before the first live session and keep the report. ## 9. Things worth knowing - One harness process per world. Several worlds → several bundles, several processes. - The DM never waits on you. There is no approval prompt anywhere; the console is for watching. Widening the envelope is a config edit and a restart. - The DM sees everything in the world but speaks only from what its narrator actually heard, in the place it heard it. - Keys live in `data/` and nowhere else. If a key ever appears in a chat or a ticket, treat it as burned.