Skip to main content

Bounded, auditable action governor for LLM agents — a policy gate between an MCP client and its servers.

Project description

wardn

ci

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

On PyPIpip install wardn. Release history: CHANGELOG.md; vulnerability reports: SECURITY.md.

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 is final as of 0.2.0, and the repo practices what it preaches — CI 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.

The roadmap, locked decisions, and spike record live in 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 (wardn report) → 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:

{
  "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"
    }
  ]
}

You don't have to write it from scratch: wardn init --out policy.json -- npx … scaffolds a starter policy from the server's own tools/list — one rule per tool, effects declared from the annotations (frozen into declarations a later lie cannot loosen), default deny — and wardn check --policy policy.json validates any policy offline, printing the rule summary and the policy hash.

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

{
  "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:

$ 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:

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

Family

Sibling to agentrec (records what models say) and 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wardn-0.3.0.tar.gz (180.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wardn-0.3.0-py3-none-any.whl (40.3 kB view details)

Uploaded Python 3

File details

Details for the file wardn-0.3.0.tar.gz.

File metadata

  • Download URL: wardn-0.3.0.tar.gz
  • Upload date:
  • Size: 180.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for wardn-0.3.0.tar.gz
Algorithm Hash digest
SHA256 17c623c31a9f7f217bb5c7b0a67006b1cd66a53291ab3206a26bdc7b749d1969
MD5 82edecd70dfee3b3d872116bd20f50cd
BLAKE2b-256 30432f9bea16a2b7c53ea2d328fdf96276e5f971e13b47c86967d77747e25fd0

See more details on using hashes here.

File details

Details for the file wardn-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: wardn-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 40.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.7

File hashes

Hashes for wardn-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 10731982f29c24d96f5212e69f72dd3b97a214b76e0283a115ceabf18b651bb7
MD5 7f29bd256bb56236a0d4fcd4290a16a9
BLAKE2b-256 abe85d80a6bf0fdeb572802564f14f82cbbd045d415f8a1ce30fb109aedb05e4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page