Skip to content

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 NovarchKillError

Your 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_cited is assigned in code, by combine(), 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 a hold or kill, 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_cited is 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 kill with 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 kill Decision 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

TermMeaning
ActionOne 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 statusWhat 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.
ToolCallOne 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.
SessionOne 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.
RuleA 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 engineThe 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 reviewThe 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.
DecisionThe 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 ackThe 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 decisionWhat 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 switchThe 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-closedUncertainty (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.
PostureWhat 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 verdictBuyer-facing labelWhat it means
pass(none; the write runs)No rule fired; the action proceeds.
holdPAUSEDA human decides before the write executes.
killBLOCKEDThe 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:

FrameworkWhat you register
LangChain (create_agent)NovarchGateMiddleware in middleware=[...]
LangGraphNovarchToolGate on your ToolNode
Claude Agent SDKNovarchHookGate 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 verdict

kind="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