Concepts
Novarch is the enforcement layer for AI agents in production. It sits in the path of an agent’s actions: before a consequential write executes, Novarch checks it against rules your team wrote in plain English, then blocks clear violations, pauses borderline ones for a human to decide, or lets the rest through.
The flow
agent (your code, instrumented with the SDK) │ read tools buffer; the first write drains the buffer ▼Novarch deployment │ stores the Action, runs the Novarch engine ▼Novarch engine (signals → rule review on flag → grounding) │ computes deterministic evidence, applies your rules, │ emits a grounded Decision ▼ ├─ pass → the write executes ├─ hold → the action is PAUSED; an operator decides └─ kill → the write is BLOCKED; the SDK raises NovarchKillErrorYour agent calls read tools to gather context (look up a vendor, validate
an invoice, compute a total). Those read calls buffer: nothing is
gated yet. When the agent calls its first write tool, the moment it
commits to a consequential action, the SDK drains the buffer, packages
everything into an Action, and sends it to your Novarch deployment.
The deployment runs the Novarch engine over the action and its session
history. The engine evaluates every applicable rule against deterministic
signals — code, a connector lookup, or a narrow LLM question over one tool
result — computed as named evidence. A rule whose signals are all plain code
makes no LLM call to pass; a rule that includes a narrow LLM signal (to catch
something only language carries, like urgency in a note) makes that one call
on every crossing it applies to, whether or not the rule ends up flagging. If
one or more rules flag or can’t be settled by their signals, a rule
review reads only those rules — the rule’s text,
its signal results, and the agent’s reasoning — and a grounding step
requires the verdict to cite the rule and signals it relied on before
returning a Decision. If the decision is pass, the write runs. If it’s
hold, the action is paused and surfaced to an operator, who approves or
denies it. If it’s kill, the SDK raises NovarchKillError and the
write never executes.
The engine reasons across the agent’s entire session, not just the single write in isolation — the signals that walk the session and the rule review’s read of the whole trajectory both see it. It can catch patterns that no per-action check would see (a re-submitted invoice, reasoning that drifted across steps, a sequence that only looks wrong in aggregate).
Grounding
A verdict from the Novarch engine is not free-text model output. Before a hold
or kill is returned, it has to clear a grounding stage that constrains
what a rule review, when one runs, is allowed to say:
- It must name a rule.
rule_id_citedis assigned in code, bycombine(), from the rules that fired, held, or could not be evaluated — never freely generated by a model. A rule whose signals leave it unsettled holds, citing its own id, so there is always a rule behind aholdorkill, including one whose signals settled it with no review at all. - It may only cite evidence that exists. The deterministic extractors
compute every signal available for the action (
signals_computed);signals_citedis computed in code as the subset of the CITED RULE’s own results that came back true — never a model’s free choice of what to name. A citation cannot point at evidence that does not exist, by construction. - The verdict is structured, not prose. It comes back as a validated
Decision(forced tool-use where a model ran, pinned models, pinned prompt templates), not a paragraph a downstream system has to parse. - Uncertainty fails closed. An engine error or a timeout resolves to a
killwith the__FAIL_CLOSED__sentinel. That is the default. A deployment can set a posture that releases the write instead, and the release is recorded as one. See NovarchKillError.
Because the deterministic trace (signals_computed) is recorded alongside what
a rule review invoked, when one ran, (signals_cited), every citation is
checkable against the evidence that actually existed. That separation is what
makes the audit trail credible: an auditor sees the deterministic facts, the
rule that fired, and the exact signals the engine relied on.
This is what separates the engine from an LLM-as-judge: the verdict is constrained, evidence-backed, and auditable, not a model’s unverified opinion.
The kill switch
Blocking is per-action; the kill switch is session-grain. An operator can kill a live session from the portal (the session-detail header, or directly on the triage card) behind a two-second armed window — click again or press Escape to cancel. Killing a session does two things:
- denies every waiting hold in that session, and
- flips the session to KILLED — from that moment, every write the agent
attempts is refused without evaluation. No engine call, no waiting: the
server records a synthetic
killDecision with an[operator_kill]rationale and returns it on the normal wire.
Your agent code does not change: a refused write raises the same
NovarchKillError as any blocked action, carrying the
[operator_kill] prefix so you can tell a session kill apart from a rule kill
or a fail-close. Every refused attempt is still recorded as an action, so the
audit trail shows exactly what the agent tried after the switch was thrown.
A killed session is terminal: the agent’s own session exit cannot flip it back to completed, the idle reaper leaves it alone, and re-killing is a safe no-op. In the portal and the audit document these refusals cite KILL SWITCH — a deliberate operator decision, never presented as a system fail-close.
Operators can also revoke a single paused action out-of-band — a deny without waiting for triage — leaving the rest of the session running.
The resumable gate
PAUSED means a human decides before the write executes. It does not have to mean your process stands still while they do.
A nightly batch runs at 2am and the people who decide arrive at 8. A run that has declared it can park gets a paused action back as an exception rather than a block: it records the item as deferred and exits. The decision happens in the morning against a live queue. A later run re-enters the same session, presents the same continuation key, and the gate hands back the standing verdict instead of judging again. The write runs then, once.
Four things hold that together:
- The continuation key is yours.
(session_id, action_id), both derived from your own work item. Novarch keeps no list of pending work and offers no way to discover one, so a run that opens a new session is a new commitment. - The approval is bound to the content. A fingerprint of the action type and the context binds the verdict to what the write will do, so a resume presenting a different amount or a different payee is refused rather than executed on somebody’s signature.
- An approval is consumed once. The SDK acknowledges the write after it
runs, and a later run presenting the same key is refused. A second write is a
second commitment under a new
action_id. - Nobody claims to know what the gate cannot see. If a process dies between claiming the write and acknowledging it, the record says the execution outcome is unconfirmed and an operator settles it. Novarch never asserts that a write landed.
While the decision is outstanding the action is a pending hold and sits in TRIAGE, the same as any other. Between the approval and the write it moves to MONITOR’s AWAITING EXECUTION ledger, where an operator can see which vendor and which amount are waiting and withdraw the authorization if the answer has gone stale. An action a person has approved and no run has executed does not disappear from view.
Details and code: augur decorators
and the TypeScript SDK.
Terminology
| Term | Meaning |
|---|---|
| Action | One write commitment by your agent, the moment it crosses its trust boundary into the outside world. Carries the read tools it called and the reasoning it recorded. The unit the Novarch engine evaluates. |
| Reasoning status | What an action’s silence means, recorded beside the reasoning itself: provided (the agent explained itself, and a blank counts), unavailable (this agent produces no reasoning), or missing (this agent reasons and this action carried none). The session declares which of the last two an absence is. See Declaring that your agent has no reasoning. |
| ToolCall | One read invocation the agent made to inform an action (a lookup, validation, or computation). Audit surface only: surfaced to the operator in the timeline drill-down; not cited directly in the verdict. |
| Session | One instrumented agent run. Every action inside it shares a session_id, tenant_id, team_id, and agent_id. The Novarch engine sees the whole session, not just the action under review. |
| Rule | A plain-English statement your admin authors and confirms in the dashboard. Compiled into signals and a condition the engine evaluates in code, and applied to every action whose action_type matches the rule’s applies_to. A rule with no applies_to applies to every action; a rule with no confirmed configuration pauses every action it applies to, citing itself, until confirmed. See Rules. |
| Novarch engine | The component that evaluates each action. It computes deterministic signals as evidence against your rules, runs an LLM rule review only over the rules a crossing’s signals flag or can’t settle, and grounds the verdict (every hold / kill must cite a rule and signals). The review is one stage inside it, run only when needed — most crossings never reach it. |
| Rule review | The engine’s LLM stage: narrow, per-rule questions over the rules your signals flagged, never a single call reasoning freely over everything active. Samples multiple times with an exact escalation table; runs with structured output and a quoted span requirement. A rule’s author decides, per rule, whether the review may clear a flag at all. |
| Decision | The Novarch engine’s verdict: { action, rule_id_cited, signals_cited, signals_computed, rationale_text, decision_id, model_sha, prompt_template_version }. signals_computed is the full deterministic-evidence trace; signals_cited is the subset the rule review invoked, when one ran, and must be a subset of it, or the verdict is rejected. A cited rule is required on every hold and kill the engine emits; fail-closed and operator-kill refusals are the exception — they carry the __FAIL_CLOSED__ sentinel because no rule was evaluated. |
| Hold (internal) / Paused (buyer-facing) | The Novarch engine said “a human should decide before this executes.” The SDK pauses the action and polls until an operator commits a verdict, or fail-closes if the wait exceeds the configured timeout. A run that declared it can park exits instead of polling, and a later run collects the answer — see The resumable gate. |
| Continuation key | (session_id, action_id), both supplied by you. It is what a later run presents to collect a decision made while it was not running. There is no third identifier and no new wire field. |
| Execution ack | The SDK claims the right to execute a write, runs it, then tells the gate it ran. Until that acknowledgement the authorization is unspent, which is what stops one approval being spent twice. |
| Operator decision | What an operator does to a paused action: Approve / Deny. (Killed actions get an Acknowledge for the audit trail.) The decision and the operator’s identity are recorded in an append-only audit row. |
| Kill switch | The operator’s session-grain stop. Kills a live session: waiting holds are denied, the session is surfaced as KILLED, and every later write is refused without evaluation. Refusals cite KILL SWITCH and raise NovarchKillError with an [operator_kill] rationale prefix. |
| Fail-closed | Uncertainty (the engine erroring, the engine timing out, operator timeout, a server restart that outlasts a pending hold) resolves to a kill. The SDK raises NovarchKillError with a [fail_closed_*] rationale prefix. See NovarchKillError. |
| Posture | What the deployment does when it cannot get a judgment. fail_closed refuses the write and is the default; fail_open and fail_open_declared release it and record that nobody decided it. Set by a customer admin, deployment-wide and per action type. See The fail-closed posture. |
Verdict language
Internally the Novarch engine emits pass, hold, or kill. The product
surfaces buyer-facing labels for the gated outcomes:
| Internal verdict | Buyer-facing label | What it means |
|---|---|---|
pass | (none; the write runs) | No rule fired; the action proceeds. |
hold | PAUSED | A human decides before the write executes. |
kill | BLOCKED | The write is stopped; the SDK raises NovarchKillError. |
One more answer can reach the SDK, and it is not a verdict: unjudged says no
judgment exists, because the engine could not answer and the deployment’s
posture released the action. It is never counted as CLEARED and
never carries a rule, since nothing evaluated the action.
How you instrument
There are two ways to put Novarch in the path of your agent’s writes. Both drive the same engine and produce the same audit trail; they differ only in where the gate attaches.
Register one gate at your framework’s tool seam. If your agent runs on a framework with a blocking tool-execution hook, you declare which tool names are reads and which are gated writes, and register a single gate. Every tool the agent has is then gated — including tools you didn’t write, like a library’s or an MCP server’s. Nothing in your tool bodies changes. This is the recommended path where it exists:
| Framework | What you register |
|---|---|
LangChain (create_agent) | NovarchGateMiddleware in middleware=[...] |
| LangGraph | NovarchToolGate on your ToolNode |
| Claude Agent SDK | NovarchHookGate via ClaudeAgentOptions(hooks=...) |
Or decorate your tools. If you drive the model in your own loop — no
framework, or a framework without a usable seam — you mark each tool with
the augur decorators instead. This path works everywhere, which is why
it stays the floor: no agent is un-instrumentable.
The decorators
The decorator namespace comes from the SDK:
from novarch import augur
@augur.session(tenant_id="acme", team_id="ap", agent_id="invoice-bot")def run(): @augur.tool(kind="read") def lookup_vendor(vendor_id): ...
augur.set_reasoning("Vendor is in the master DB; domain matches.")
@augur.tool(kind="write") def submit_payment(payment): ... # gated on the Novarch engine's verdictkind="read" tools buffer. kind="write" tools drain the buffer,
materialize an Action, and gate on the Novarch engine’s Decision. Every
write crosses the gate and is judged and recorded — that is the product.
Whether a verdict is enforced or only recorded is a deployment setting
(observe mode), and the record says which.
The wire-contract schema (Action, ToolCall, Decision,
SessionLog) ships in the public package:
from novarch.schema import Action, ToolCall, Decision, SessionLog