Article I Preamble
1.1
What the instrument is
ian-operator is a single Node process that speaks the Model Context Protocol over
loopback HTTP. It holds one person’s operating state — the day’s tasks, the decisions awaiting a
verdict, the audit trail, the record of what each autonomous tick attempted and what came of it —
and it exposes that state to any MCP client as a compact context packet.
It also holds something rarer: a gate. Before an autonomous agent may send, spend, publish, delete or telephone, it must first record an intent. Recording is not doing. The intent passes through a pure function that stamps it allowed, requires approval or blocked, and that stamp is the law of the rest of the system.
1.2
The governing doctrine
The model proposes, code disposes. lib/comms.js:10
Every safety property in this repository descends from that one sentence. A language model may compose an email, choose a task, argue for a purchase, or draft a note. It may never be the thing that decides whether the email leaves the building. That decision is made by deterministic code the model cannot reach, cannot read the internals of, and cannot forge a token for.
The kernel modules state the same constraint from the other side: lib/safety-compose.js
describes itself as “deterministic code the brain can never
reach,” pure, with no LLM, no I/O and no database access.1
1.3
The chain of custody for a single act
Five parties handle any consequential action, and the boundaries between them are enforced by different mechanisms: a bearer token at the network edge, a pure policy function at the gate, an approver allowlist bound to a content hash at the approval step, and a compare-and-set claim at the executor.
operator_record_action_intent. Everything to
the right of the first dashed line is deterministic. The amber path is the sole route by which an
intent that requires approval can become executable, and it does not exist on the MCP face at all.1.4
Fault isolation as a design premise
The service is deliberately a sibling and not a subsystem. It runs in its own process, opens its
own SQLite file at ~/.ian-operator/operator.db, listens on its own loopback port
(127.0.0.1:3401 by default), carries its own bearer token, and — the comment is emphatic —
“never imports” the codebase it was forked from.2
The database module says the same thing in different words: it is “deliberately not imported from zschool-mcp — fault isolation requires a wholly separate DB.” The connection is opened with WAL journaling, a five-second busy timeout, and foreign keys on.
| Route | Auth | Behaviour |
|---|---|---|
| GET /health | none | Probes the database with SELECT 1; answers 200 or 503 with service, version, profile id, timezone, database path and uptime. Performs no inference. |
| POST /mcp | Bearer | Stateless streamable-HTTP transport; a fresh McpServer is built per request and closed when the response closes. |
| anything else | — | 404 {"error":"not found"} |
1.5
Powers withheld
A large part of the design is negative space — capabilities that were deliberately not built, or were built and then removed. These are stated as invariants in the source, each with the reasoning that produced it.
- no approval toolThe MCP face has 24 tools and none of them can approve.
operator_update_action_intentthrows if asked to setstatus:'approved';operator_update_decisionthrows onapprovedoraccepted. - no runner approverAny actor subject matching the runner pattern — the fast tick, the slow tick, the brain, the executor, a relay — can never be treated as a human approver, even if mistakenly added to the allowlist.
- no auto-purchaseEven an approved spend intent is not executed. The spend handler notifies the owner with the full details and marks the intent a terminal human hand-off.
- no arbitrary toolsThe executor may call exactly four downstream tools; a handler requesting anything else throws before any side effect.
- dry-run by defaultThe executor runs in dry-run unless the environment explicitly sets the string
0. The shipped unit file pins it to1and the timer is not enabled.
1.6
Composition
Three runtime dependencies, no build step, no bundler, no transpiler. The package is
"type": "module" throughout; tests run on the Node built-in runner.
- @modelcontextprotocol/sdk ^1.26.0
- Server and streamable-HTTP transport for the MCP face.
- better-sqlite3 ^12.8.0
- Synchronous SQLite. The whole state layer is prepared statements against one file.
- zod ^4.0.0
- Argument schemas for the tool definitions.
- engines: node >=20.19.0 <21
- Pinned to a single major line.
Two npm scripts: npm start runs node server.js; npm test
runs node --test.
1.7
Status of this document
This is a description of a private repository’s capabilities, prepared while assessing it for possible public release. It documents mechanisms, invariants and interfaces. It contains no task content, no personal data, no credentials, no host or channel identifiers, and no operating figures.3 Line references point at the source as read; quoted text is quoted exactly.
Notes
lib/safety-compose.js:3–7. The module additionally forbids itself from importingoperator.jsor the runner library, so purity is structural rather than conventional. ↩server.js:3–5. The header comment describes the process as a “fault-isolated sibling”. ↩- The repository does contain deployment paths, a channel identifier used as a fallback default, and business-specific planning notes. None of it appears on this site. See Article VII §7.6. ↩