Metadata-Version: 2.4
Name: wardn
Version: 0.2.0
Summary: Bounded, auditable action governor for LLM agents — a policy gate between an MCP client and its servers.
Project-URL: Repository, https://github.com/Pi-Wi/warden
Project-URL: Documentation, https://github.com/Pi-Wi/warden/tree/main/docs
Author: Pi-Wi
License-Expression: MIT
License-File: LICENSE
Keywords: agents,audit,governance,ledger,llm,mcp,model-context-protocol,policy
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: System :: Logging
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# wardn

[![ci](https://github.com/Pi-Wi/warden/actions/workflows/ci.yml/badge.svg)](https://github.com/Pi-Wi/warden/actions/workflows/ci.yml)

Bounded, auditable action governor for LLM agents — a policy gate between an
MCP client and its servers. Every tool call is allowed, denied, or
quarantined by a rule you declared; run-wide budgets cap total side-effects
with loud refusals; and every attempt lands in an append-only,
timestamp-free, byte-deterministic ledger that replays offline and diffs in
CI.

wardn bounds and audits. It does not sandbox, and it is not a content-safety
product.

## Status

**0.2.0 on [PyPI](https://pypi.org/project/wardn/)** — `pip install wardn`.

M2 (adoptable, CI-integrable) gave the surface: the proxy governs one or
more real MCP servers over stdio; `wardn review` decides quarantined calls
from any terminal; `wardn replay` reproduces every decision offline;
`wardn revert` undoes declared-reversible effects from the ledger file
alone; and `wardn diff` turns two ledgers into a CI verdict — exit 4 the
moment governed activity drifts.

M3 (harden the claim) is done: the determinism, replay, and revert
promises now hold under adversarial input, and that is tested, not
asserted. Property-based suites (hypothesis) drive random policies against
random request streams — byte-identical repeat runs, exact replay from any
crash prefix, bounds never exceeded, revert round-trips. Hostile servers
(lying annotations, garbage on the pipe, tool names that collide
mid-session), malformed JSON-RPC, megabyte arguments, lone surrogates,
clients that vanish mid-quarantine, reviewers who never come: none of it
can corrupt the ledger, dodge a declared bound, or make replay lie. The
[ledger format](docs/ledger-format.md) is final as of this release, and
the repo practices what it preaches — [CI](.github/workflows/ci.yml) runs
a zero-dep core leg, an ubuntu 3.10–3.13 matrix, a Windows leg, packaging
checks, and wardn's own `diff` gate on the repo's
[blessed example ledger](examples/ci-gate/).

The roadmap, locked decisions, and spike record live in [TODO.md](TODO.md).

## The gap

Permission systems exist (per-client allow/ask/deny rules), approval
services exist, security gateways exist, eval-time diffs exist. But none of
them owns the middle: side-effect governance that is *declared* (a policy
file, not scattered client settings), *bounded* ("at most 2 writes", not
just "ask"), and *auditable* — emitted as a single deterministic artifact a
CI job can diff with an exit code.

That artifact is the whole point of wardn. A permission prompt leaves no
record. An observability stream isn't deterministic. wardn's ledger is
both: the decision log and the drift detector, one file.

## The core idea: one ledger, three uses

Every attempt — allowed, denied, quarantined, refused by a budget — is one
recorded event stream: what was attempted, which rule matched, how the call
was classified (and by what authority), what was decided, why, and what came
back.

- **Read it forward** → the audit report. Every side-effect, explained.
- **Replay it** (`wardn replay`) → the decisions, reproduced offline from
  policy + file alone. A tampered ledger or a swapped policy refuses loudly.
- **Replay it inverted** (`wardn revert`) → the undo. Declared-reversible
  effects are reverted from the file alone — the undo call was resolved and
  recorded at capture time, so revert needs no policy, only the ledger and a
  live server. If the world drifted since the run, revert refuses rather
  than guesses.

And because the file is byte-deterministic — no timestamps, no PIDs, no
nonces — `wardn diff yesterday.jsonl today.jsonl` is a CI gate: same
policy, same attempts, same decisions, or exit 4.

## Quick look

A policy is an ordered list of rules, first match wins, with a mandatory
explicit default — a policy that doesn't say what happens to unmatched
calls is invalid:

```json
{
  "format": "wardn-policy/1",
  "default": "deny",
  "budget": {"max_calls": 20, "max_writes": 5, "max_spend": 1.0},
  "approval_timeout_s": 300,
  "rules": [
    {
      "id": "allow-reads",
      "match": {"server": "filesystem", "tool": "read_text_file"},
      "effect": "read",
      "disposition": "allow",
      "reason": "reads are harmless here"
    },
    {
      "id": "bound-writes",
      "match": {"server": "filesystem", "tool": "write_file",
                "args": {"path": {"path_prefix": "/work/out/"}}},
      "effect": "write",
      "disposition": "allow",
      "bound": {"max_calls": 2},
      "cost_per_call": 0.1,
      "capture": {"tool": "read_text_file", "args_from": {"path": "path"}},
      "inverse": {"tool": "write_file",
                  "args": {"path": "$args.path", "content": "$capture.value"},
                  "expect": "$args.content"},
      "reason": "writes capped at 2, under out/ only, pre-state captured"
    },
    {
      "id": "quarantine-moves",
      "match": {"server": "filesystem", "tool": "move_file"},
      "effect": "destructive",
      "disposition": "quarantine",
      "reason": "moves need a human"
    }
  ]
}
```

Put wardn between your client and the server — the client config points at
wardn, wardn spawns the real server:

```json
{
  "mcpServers": {
    "filesystem": {
      "command": "wardn",
      "args": ["proxy", "--policy", "policy.json", "--ledger", "run.jsonl",
               "--", "npx", "-y", "@modelcontextprotocol/server-filesystem",
               "/work"]
    }
  }
}
```

The agent now reads freely, gets its third write refused loudly (an error
result naming the rule and the bound — the agent can read why and adapt),
and blocks on `move_file` until someone, in any terminal, runs:

```console
$ wardn review list --queue run.jsonl.queue.json
q6  move_file  {"source":"/work/report.txt","destination":"/work/old.txt"}  rule=quarantine-moves  — moves need a human

$ wardn review approve q6 --queue run.jsonl.queue.json --by phiwir
q6: approve by phiwir
```

Afterwards, the run is a file:

```console
$ wardn replay --policy policy.json --ledger run.jsonl
replayed 8 attempt(s) — every decision reproduced by this policy

$ wardn revert --ledger run.jsonl -- npx -y @modelcontextprotocol/server-filesystem /work
applied 2 undo(s) — revert ledger: run.jsonl.revert.jsonl

$ wardn diff approved/run.jsonl run.jsonl && echo "no drift"
```

Several servers can sit behind one gate — `wardn proxy --servers
servers.json` takes a file in the same shape as an MCP client config, and
rules match on the server's name. See [docs/concepts.md](docs/concepts.md)
for what multi-server mode honestly does and doesn't proxy.

## What wardn is not

- **Not a sandbox.** wardn is a policy gate in front of cooperative
  transports. A hostile server, or a client that bypasses the proxy, is out
  of scope — and argument constraints are string constraints:
  `path_prefix: "out/"` happily matches `out/../secret.txt`. wardn does not
  resolve paths; that is a sandbox's job. Never treat a wardn policy as an
  isolation boundary.
- **Not content safety.** wardn governs side-effects, not text. Prompt
  injection defense is a different layer.
- **No LLM anywhere in the enforcement path.** A decision is a pure
  function of policy + request + recorded approvals. Ever.
- **No measured spend.** `max_spend` refuses on arithmetic over *declared*
  costs (`cost_per_call`), never on billing APIs — wardn cannot observe
  real spend and doesn't pretend to.

## Docs

- [docs/concepts.md](docs/concepts.md) — rules, dispositions, bounds,
  classification, quarantine, capture/revert, and the determinism claim
  stated precisely (including its honest asterisks).
- [docs/policy-format.md](docs/policy-format.md) — `wardn-policy/1`,
  normative.
- [docs/ledger-format.md](docs/ledger-format.md) — `wardn-ledger/1`, the
  compatibility contract. The ledger format and the proxy CLI are the two
  things wardn promises stability for.
- [docs/ci.md](docs/ci.md) — the CI story: budgets as drift tripwires,
  `wardn diff` as the gate.
- [docs/embedding.md](docs/embedding.md) — the engine API for non-MCP
  embeddings: `govern_call` in front of any tool-calling boundary.
- [docs/launch-post.md](docs/launch-post.md) — the launch post: the gap,
  the family, the honest limits.
- [examples/ci-gate/](examples/ci-gate/) — the drift gate this repo runs
  on itself; [examples/flagship/](examples/flagship/) — the two-act demo
  against the real filesystem server.

## Family

Sibling to [agentrec](https://github.com/Pi-Wi/agent-rec) (records what
models *say*) and [mendr](https://github.com/Pi-Wi/mendr) (repairs what data
*got wrong*) — wardn governs what agents *do*. Same ethos: declared,
bounded, recorded; the record is the product; no LLM anywhere in the
enforcement path.
