ian-operator test/ 32 files · 272 declared tests node:test · node:assert/strict Folio VII of VII

Record of verification

An invariant that is only written in a comment is a wish. This article records which of the preceding articles’ claims are enforced by code, which are proven by an adversarial test, where the documentation has drifted from the implementation, and what this description leaves out.

Article VII — Of proof, of drift, and of the scope of disclosure

Article VII Verification

7.1

Shape of the suite

Thirty-two test files, two hundred and seventy-two declared test() calls. Six of those are parameterised inside loops, so the runtime count is higher. There is no test framework: node:test and node:assert/strict throughout, invoked by node --test.

The suite’s conventions are unusually consistent. Files isolate their database into a temporary file before importing anything, so no test can touch live state. Several point both the state layer and the runner library at the same temporary file, because the two resolve their path from different environment variables and a split would make the assertions meaningless — a hazard that is itself the subject of two dedicated tests.

Where a test drives real runner code rather than a helper, dependencies are injected as explicit seams: the model call, the notification path, the outward tool call and even file existence are all replaceable, so the full path — model decision, gate, coarsened feedback, stored row — can be exercised with no network and no MCP transport.

7.2

The pure kernel, run

The safety-composition module has no database, no environment and no model dependency, so it runs on its own with nothing installed. The output below is a real run of that file.

terminal · the precedence lattice under testtest/safety_compose.test.js
$ node --test test/safety_compose.test.js
 TERMINAL_INTENT_STATUSES mirrors operator.js set (0.812166ms)
 T1 P0 blocked policy -> halt_blocked, no escalation/memory, not sanctioned (0.220666ms)
 T2 P0 terminal intentStatus=sent -> halt_blocked (0.077250ms)

 MF2 sanctioned+web -> web_readonly label (most-restrictive), flags preserved (0.028666ms)
 MF4 P3 fail-closed: webAutoDispatch="descriptor" -> web_readonly, readOnly (0.017291ms)
 MF3 composeRestrictions forces taint consequence on malformed input (0.046500ms)
 MF-A composeRestrictions binds halt-tier label to its consequence flags (0.056458ms)
 MF-A2 P0 case-variant policy.decision=bLoCkEd -> halt_blocked (0.014875ms)
 tests 45
 suites 0
 pass 45
 fail 0
 cancelled 0
 skipped 0
 todo 0
 duration_ms 62.270708

Forty-five runtime tests from twenty-seven declared calls; the difference is four parameterised loops. Run outside the pinned Node line — the module’s purity is what makes that possible. The rest of the suite requires the pinned runtime, because the SQLite binding is native.

7.3

The adversarial file

One file is written from the attacker’s side. Each of its six tests is titled as an attack and asserts that it fails.

  • Attack 1Self-approval. Every runner subject and every non-allowlisted subject is rejected; after each attempt the intent is asserted still not executable.
  • Attack 2Replay. A decision approving one intent is pointed at another, and separately the approved intent’s recipient is mutated after approval. Both are rejected — the second by hash mismatch.
  • Attack 3Record-time linkage. An approval identifier supplied when the intent is created is stripped, and the intent is not executable.
  • Attack 4Terminal transitions. A sent or failed intent cannot move again.
  • Attack 5The stuck claim. A dead claim is routed to needs_human; a fresh one is left alone.
  • Attack 6Header injection. Multi-recipient and CR/LF-bearing addresses are refused by the mail handler.

A second file extends the same posture across process boundaries: a cross-midnight approval is executed by today’s pass; a stale claim from yesterday is reaped; a runner-authored comment is not treated as authoritative human signal; and an untrusted inbound message does not raise goal confidence, because “taint ≠ Ian signal”.

7.4

Register of invariants

The claims made in Articles I to VI, with where each is enforced and where each is proven.

Invariant · enforcement · proof
InvariantEnforced byProven in
No MCP tool can grant an approvalExplicit throws in both mutation pathsslice5_2_critique · slice8_1_security
A runner is never a human approverSubject pattern, tested before the allowlistslice8_1_security · slice8_executor
One approval binds one intent and its contentintent_id + action_hash, re-derived on every readslice8_1_security
A blocked intent is never executableFirst branch of the executability predicateslice4_gate · slice8_executor
Terminal statuses never transitionGuard before every lifecycle writeslice8_1_security
An external send needs approval, tier-independentlyExecutor tier floor, released back to the prior status on refusalslice8_executor
Approved spending is never auto-purchasedSpend handler notifies and hands offslice8_executor
A tainted tick writes no durable memoryP1 of the precedence lattice, checked before each writesafety_compose · step1a
A caller can never un-taint an observationForced at the sink; no field on the tool schemastep1b
Gate feedback to the model is coarseCoarsening applied at four call sitesstep2 (four tests)
Success labels come from the verifier, not self-reportEffect–action binding, then a world checkphaseA_learning · step2
Learning never auto-promotesCandidate status only; minting off by defaultphaseA_learning
Exactly one active goal modelPartial unique index — a second is an insert errorslice8_2_crosscut · slice8_3_version
The computed priority cannot forge board churnExcluded from the state hash by an explicit field liststep1a
Pruning never starves the streak windowRetention window plus a minimum-rows floorhardening_retention (three tests)
The runners and the gate share one databaseBoth resolve the same override variablehardening_dbpath
Silence is the default for interruptionShort-circuit gate, atomic budget claimstep4 · step4_wiring
An identical proposal produces one row and one pingDerived idempotency key plus a created flagslice2_critique

7.5

Drift observed on reading

Three places where the prose in the repository no longer matches the code beneath it. None is a defect in behaviour; each is a hazard for the next reader, and two of them concern security-relevant descriptions.

  • schema headerThe core schema section is labelled “8 tables”; the function creates fifteen.
  • approval hashThe comment above the approval function lists fewer hashed fields than the implementation actually hashes. The code is the stricter of the two.
  • stale test headerOne cross-cutting file’s header describes five fixes; the file contains seven plus an additional whole-system test.

One structural coupling is documented but not machine-checked: the terminal-status set is deliberately duplicated between the state layer and the pure kernel module to preserve the latter’s purity. A test asserts the two are equal, which converts an ordinary duplication hazard into a checked one — a pattern worth copying rather than criticising.

One test in the suite hard-codes an absolute path belonging to a specific deployment and cannot run elsewhere. For a public release that would need parameterising.

7.6

Scope of disclosure

This description was prepared from a private repository under assessment for public release. It documents mechanisms and interfaces only. The following categories were found in the source and are deliberately absent from every page of this document, including its diagrams and terminal renderings:

  • personal contentThe operator’s own goal model, task titles, decision log entries and the profile document describing the person the system serves.
  • channel identifiersA messaging channel identifier that appears as a hard-coded fallback default in two runner files, and the messaging bot’s handle.
  • deployment topologyAbsolute installation paths, the service account name, the host name of the machine the units target, and the URL of a private operator console.
  • inference endpointsDefault model identifiers and the loopback addresses and ports of adjacent private services.
  • business specificsNamed commercial goals, counterparties and targets appearing in the planning documents and in one audit runner’s list of stale phrases.

No credential, token or secret value was found in the tracked source; the ignore file excludes environment files, databases, logs and anything matching a secrets pattern, and the runners read every secret from files outside the repository at run time. The one hard-coded default worth flagging is the channel identifier noted above, which is not a secret but is personal.

Two mechanisms in the codebase reduce the risk of such material leaking through the system’s own outputs, and both are worth noting in an assessment: text and metadata are redacted against three token patterns before storage, and the corpus tooling applies its own redaction plus a sensitive-path exclusion covering environment files, key material and databases before any transcript is read.

7.7

Standing observations

Three properties of this codebase are unusual enough to record plainly, because they are the ones a reader would most want to know are genuine.

The dangerous path is the narrow one

There is exactly one function that can make an action executable, it is not exported to the network interface, and four independent mechanisms guard it: a subject pattern, an allowlist, a binding to the intent, and a binding to the intent’s content. Removing any one of them still leaves three.

Defaults are refusals

Dry-run is on unless a variable holds one exact string. The executor’s timer is not enabled. The attention sweep is inert unless armed. Learning mints nothing unless switched on and above a threshold. Confidence is never hand-written by a model. The pattern is consistent enough to read as a policy rather than a habit.

The system is instrumented to notice its own failure

A no-progress streak, a board-unchanged backstop, a stall monitor outside the loop, a dead-man’s-switch heartbeat on runs, a reaper for stuck claims, an interrupt-precision scorer, and a verifier that is not permitted to accept the model’s account of its own success. Much of the code exists to make dishonesty about progress structurally difficult.