Metadata-Version: 2.4
Name: novarch
Version: 0.1.3
Summary: Runtime enforcement layer for AI agents in production — client SDK.
Project-URL: Homepage, https://novarch.ai
Project-URL: Documentation, https://docs.novarch.ai
Author-email: Novarch <support@novarch.ai>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agent,ai-safety,governance,kill-switch,llm,session
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: httpx>=0.26.0
Requires-Dist: pydantic<3.0,>=2.9.0
Provides-Extra: adk
Requires-Dist: claude-agent-sdk<1,>=0.2; extra == 'adk'
Provides-Extra: langchain
Requires-Dist: langchain-anthropic<2,>=1; extra == 'langchain'
Requires-Dist: langchain-core<2,>=1; extra == 'langchain'
Provides-Extra: langgraph
Requires-Dist: langgraph-prebuilt<2,>=1.0.2; extra == 'langgraph'
Requires-Dist: langgraph<2,>=1.0; extra == 'langgraph'
Description-Content-Type: text/markdown

# Novarch

**The runtime enforcement layer for AI agents.** Novarch sits in the path of
an agent's actions and, *before an irreversible write executes*, checks it
against rules your team wrote in plain English — **blocking** clear violations
and **pausing** borderline ones for a human to decide.

Per-action guardrails only see one call at a time. Novarch judges the agent's
**whole trajectory** — every lookup it ran, the reasoning it gave, and the
write it's about to commit — so it catches things a single-call check can't: a
payment to a vendor whose bank details changed three days ago, a refund that's
the customer's fourth this week, an off-policy promise with no exception
ticket. When the gate fires, you get an auditable record citing the exact rule
and the signals behind the decision.

This package is the **client SDK** — a thin, two-dependency library
(`httpx` + `pydantic`) that talks to a Novarch deployment. The judge, the
rules engine, and the operator triage dashboard run on the server side, hosted
by us or in your VPC. To get an endpoint and an SDK key, reach
<support@novarch.ai> or visit <https://novarch.ai>.

## How it works

Three moving parts, one mental model:

1. **Read tools run freely** and are *buffered* as evidence — the lookups and
   checks your agent does to inform a decision.
2. **A write tool is the gated moment** — the instant the agent crosses its
   trust boundary. Novarch bundles the buffered reads + the agent's stated
   reasoning into an `Action` and submits it to the judge.
3. **The judge applies your plain-English rules** and returns one of three
   verdicts: `pass` (write runs), `hold` (pauses for a human operator), or
   `kill` (blocked — the write never runs). Any uncertainty **fails closed**.

Your agent code catches exactly one exception, `NovarchKillError`, for every
way a write can be stopped.

## Install

```bash
pip install novarch
```

Optional framework adapters ship as extras (see [Framework
adapters](#framework-adapters)):

```bash
pip install "novarch[langgraph]"    # LangGraph — one native seam, no per-tool wiring
pip install "novarch[langchain]"    # LangChain @tool composition
pip install "novarch[adk]"          # Claude Agent SDK (async tools)
```

## Quickstart

Point the SDK at your deployment, then wrap your tools and your agent's entry
point. This example uses `GenericContext` — the minimal, vertical-agnostic
evidence shape (an id, a summary, an amount, plus any extra fields you want the
judge to see).

```bash
export NOVARCH_SERVER_URL=https://<your-deployment>
export NOVARCH_SDK_KEY=<your-sdk-key>
```

```python
from novarch import augur, NovarchKillError
from novarch.verticals.generic import GenericContext

# Reads run normally — the SDK buffers them as evidence for the judge.
@augur.tool(kind="read")
def lookup_customer(customer_id: str) -> dict:
    return crm.get(customer_id)

# The write is the gated moment. `context_builder` maps THIS call's arguments
# into the evidence your rules will be judged against.
@augur.tool(
    kind="write",
    action_type="issue_refund",
    context_builder=lambda customer_id, amount: GenericContext(
        record_id=customer_id,
        summary=f"Refund ${amount:.2f} to {customer_id}",
        amount=amount,
    ),
)
def issue_refund(customer_id: str, amount: float) -> str:
    return payments.refund(customer_id, amount)

# One @augur.session wraps the agent's entry point. Everything inside it —
# reads, reasoning, the write — is one governed trajectory.
@augur.session(tenant_id="acme", team_id="support", agent_id="refund-bot")
def handle_ticket(customer_id: str, amount: float) -> str:
    lookup_customer(customer_id)                      # buffered as evidence
    augur.set_reasoning("Customer reported a double charge; verified in CRM.")
    return issue_refund(customer_id, amount)          # ← judged right here

try:
    handle_ticket("cust-4821", 240.00)
except NovarchKillError as e:
    # Blocked by a rule, denied by an operator, or failed closed.
    log.warning("refund stopped at the gate: %s", e)
```

That's the whole surface: `@augur.tool(kind="read")`, `@augur.tool(kind="write", ...)`,
`@augur.session(...)`, and `augur.set_reasoning(...)`. Sync and `async def` tools
both work — the decorators dispatch on the wrapped function. In the decorator
form, `context_builder` receives the **write tool's own arguments** (here,
`customer_id` and `amount`).

## The rules see your evidence

Your rules live on the server and are written in plain English — that's the
product. Whatever you put in the context is what they get to reason over.
Suppose a rule reads:

> *"Block any refund over $500 for a customer with an open dispute."*

`GenericContext` accepts arbitrary extra fields, so add the dispute count and
the judge can key off it — no schema change:

```python
context_builder=lambda customer_id, amount: GenericContext(
    record_id=customer_id,
    summary=f"Refund ${amount:.2f} to {customer_id}",
    amount=amount,
    open_dispute_count=disputes.count_open(customer_id),  # extra → visible to the rule
)
```

The judge sees this context **plus** every buffered read and your
`set_reasoning(...)` text — the whole trajectory, not just the write's
arguments.

## Already using LangGraph?

Don't decorate every tool. Register **one hook** at LangGraph's native
tool-execution seam and it gates the whole `ToolNode`:

```python
from novarch import augur
from novarch.integrations.langgraph import NovarchToolGate, WriteSpec
from novarch.verticals.generic import GenericContext
from langgraph.prebuilt import ToolNode

@augur.session(tenant_id="acme", team_id="support", agent_id="refund-bot")
def run(question: str):
    gate = NovarchToolGate(
        # Classify each tool once. Reads buffer; writes gate.
        tool_kinds={"lookup_customer": "read", "issue_refund": "write"},
        write_specs={
            "issue_refund": WriteSpec(
                action_type="issue_refund",
                context_builder=lambda args: GenericContext(
                    record_id=args["customer_id"],
                    amount=args["amount"],
                ),
            ),
        },
    )
    node = ToolNode(tools, wrap_tool_call=gate.wrap())
    # ...build and invoke your graph with this node...
```

One difference to note: the `WriteSpec` `context_builder` receives a **single
`args` dict** (the tool call's arguments), whereas the decorator form receives
the tool's arguments directly.

At the seam, a `pass` executes, a `kill` raises `NovarchKillError` to halt the
graph, an operator **deny** injects a blocked `ToolMessage` so the agent can
recover, and any fail-closed case raises. Async graphs use
`awrap_tool_call=gate.awrap()` and `await gate.aclose()` at teardown.

> **Keep the kill switch intact.** LangGraph's *default* error handling
> re-raises, so a `kill` correctly halts the graph. But if you set a custom
> `handle_tool_errors` on the `ToolNode`, it will turn a BLOCKED verdict into an
> ordinary error message and the agent continues past it. If you set it at all,
> use `kill_safe_handle_tool_errors()` from the same module — it re-raises
> Novarch's halt signal and applies your fallback to everything else.

## What happens at the gate

The table below is the decorator path. (Buyer-facing labels are what an
operator sees in the dashboard.)

| Verdict | Buyer-facing | Behavior |
|---|---|---|
| `pass` | — | The write executes. |
| `hold` | **PAUSED** | The call **blocks** while a human operator approves or denies in the triage dashboard. Approve → the write runs; deny or timeout → `NovarchKillError`. |
| `kill` | **BLOCKED** | `NovarchKillError` immediately; the write never runs. |

Under the LangGraph seam the only difference is `hold` → **deny**: instead of
raising, it injects a blocked `ToolMessage` so the agent can recover and try
something else. `kill` and every fail-closed case still raise.

**Fail closed.** Any uncertainty — judge error, judge timeout, operator
timeout, a server restart past the grace window — raises `NovarchKillError`
with a `[fail_closed_*]` prefix in the message. Your agent never proceeds on a
write Novarch couldn't confidently clear.

Two practical notes: every gated write is a **blocking round-trip to the
judge** (one LLM call), and a `hold` blocks the caller until an operator
decides or the poll timeout elapses (default 5 minutes — tune with
`@augur.session(poll_timeout_s=...)`). `NovarchKillError` carries `verdict`,
`rule_id_cited`, `rationale`, `action_id`, and `session_id` for your alerting
and audit paths.

## Framework adapters

All optional; the core SDK is framework-agnostic.

| Extra | Import | Use |
|---|---|---|
| `novarch[langgraph]` | `from novarch.integrations.langgraph import NovarchToolGate, WriteSpec` | One native seam gates a whole `ToolNode` (shown above). |
| `novarch[langchain]` | `from novarch.integrations.langchain import augur_tool` | Wrap an `@augur.tool` function as a LangChain `BaseTool`. |
| `novarch[adk]` | `from novarch.integrations.claude_agent_sdk import augur_async_tool` | One decorator that stacks the Claude Agent SDK `@tool` over `@augur.tool` for async tools. |

## Typed vertical contexts

`GenericContext` covers any workflow. For domains with a rich, fixed signal
set, typed contexts give stricter validation and turn known fields into
first-class evidence:

- `from novarch.verticals.ap import APContext` — accounts payable / vendor payments
- `from novarch.verticals.cs import CSContext` — customer support / refunds

Reach for these when your rules key off vertical-specific signals (bank-change
recency, dispute counts, exception-ticket references). Otherwise `GenericContext`
plus a few extra keyword fields is enough — the judge sees any extras you pass.

## Docs & license

Full documentation: <https://docs.novarch.ai> · Product: <https://novarch.ai>

This client SDK is licensed **Apache-2.0**. The Novarch platform — judge, rules
engine, operator triage — is separately licensed and available as a managed
service or a private deployment.
