Metadata-Version: 2.5
Name: agentmoat
Version: 0.1.1
Summary: Enforcement and audit layer for AI agents — argument-level tool firewall, trust scoring, tamper-evident audit log, and prompt-injection detection across the Anthropic & OpenAI SDKs, LangGraph, and MCP.
Project-URL: Homepage, https://github.com/Shashank-016/agentmoat
Project-URL: Repository, https://github.com/Shashank-016/agentmoat
Project-URL: Documentation, https://github.com/Shashank-016/agentmoat#readme
Project-URL: Issues, https://github.com/Shashank-016/agentmoat/issues
Project-URL: Changelog, https://github.com/Shashank-016/agentmoat/blob/master/CHANGELOG.md
Author: Shashank Nambiar
License: MIT
License-File: LICENSE
Keywords: agentic,agents,ai,ai-security,anthropic,firewall,guardrails,langgraph,llm,llm-security,mcp,observability,openai,prompt-injection,security
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: aiosqlite<1.0,>=0.20.0
Requires-Dist: anthropic<1.0,>=0.40.0
Requires-Dist: anyio<5.0,>=4.0.0
Requires-Dist: click<9.0,>=8.1.0
Requires-Dist: fastapi<1.0,>=0.115.0
Requires-Dist: httpx<1.0,>=0.27.0
Requires-Dist: pydantic<3.0,>=2.7.0
Requires-Dist: pyyaml<7.0,>=6.0.1
Requires-Dist: sqlalchemy<3.0,>=2.0.35
Requires-Dist: uvicorn[standard]<1.0,>=0.32.0
Provides-Extra: all
Requires-Dist: build; extra == 'all'
Requires-Dist: langchain-anthropic<1.0,>=0.2.0; extra == 'all'
Requires-Dist: langchain-core<1.0,>=0.3.0; extra == 'all'
Requires-Dist: langgraph<1.0,>=0.2.0; extra == 'all'
Requires-Dist: mypy>=1.11.0; extra == 'all'
Requires-Dist: openai<2.0,>=1.0.0; extra == 'all'
Requires-Dist: pytest-asyncio<1.0,>=0.24.0; extra == 'all'
Requires-Dist: pytest-cov<6.0,>=5.0.0; extra == 'all'
Requires-Dist: pytest<9.0,>=8.3.0; extra == 'all'
Requires-Dist: ruff>=0.7.0; extra == 'all'
Requires-Dist: sentence-transformers<4.0,>=3.2.0; extra == 'all'
Provides-Extra: benchmarks
Requires-Dist: agentdojo<1.0,>=0.1.0; extra == 'benchmarks'
Requires-Dist: datasets<4.0,>=2.14; extra == 'benchmarks'
Requires-Dist: langdetect<2.0,>=1.0.9; extra == 'benchmarks'
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: mypy>=1.11.0; extra == 'dev'
Requires-Dist: pytest-asyncio<1.0,>=0.24.0; extra == 'dev'
Requires-Dist: pytest-cov<6.0,>=5.0.0; extra == 'dev'
Requires-Dist: pytest<9.0,>=8.3.0; extra == 'dev'
Requires-Dist: ruff>=0.7.0; extra == 'dev'
Provides-Extra: embeddings
Requires-Dist: sentence-transformers<4.0,>=3.2.0; extra == 'embeddings'
Provides-Extra: langgraph
Requires-Dist: langchain-anthropic<1.0,>=0.2.0; extra == 'langgraph'
Requires-Dist: langchain-core<1.0,>=0.3.0; extra == 'langgraph'
Requires-Dist: langgraph<1.0,>=0.2.0; extra == 'langgraph'
Provides-Extra: openai
Requires-Dist: openai<2.0,>=1.0.0; extra == 'openai'
Description-Content-Type: text/markdown

# AgentMoat

[![CI](https://github.com/Shashank-016/agentmoat/actions/workflows/ci.yml/badge.svg)](https://github.com/Shashank-016/agentmoat/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
[![MCP](https://img.shields.io/badge/MCP-compatible-6e56cf.svg)](https://modelcontextprotocol.io/)

**An enforcement layer for AI agents.** AgentMoat sits between your agent's decisions and the
actions it takes — inspecting tool-call arguments and blocking dangerous ones before they run,
with a verifiable audit trail and an instant kill switch. Works across the Anthropic & OpenAI
SDKs, LangGraph, and any MCP server; one line to drop in.

![AgentMoat blocking a prompt-injection attack at the tool layer](docs/demo.gif)

## Why

Autonomous agents don't just talk — they *act*: read documents, call tools, write files, hit
APIs. The dangerous moment isn't the model saying something wrong, it's the model *doing*
something wrong — and by the time a bad tool call reaches your filesystem or an internal API,
detecting the prompt that caused it is too late.

AgentMoat is the enforcement layer in between. It checks the **actual arguments** of every tool
call against your policy — path allow/deny lists, SSRF and path-traversal guards, rate limits —
and blocks the call *before it runs*. It records every decision in a **hash-chained audit log**
that can be independently verified line-by-line. It tracks how far each session has **drifted
from trusted input** across agent hops. And it gives you a **one-call kill switch** for any
session, or all of them.

Catching the injection that *tried* to trigger a bad action is useful, but it's a probabilistic
heuristic attackers paraphrase around. So AgentMoat treats injection detection as one
defense-in-depth signal layered on top of enforcement that **doesn't depend on catching the
prompt** — the controls that actually block an action work whether or not the injection was
recognized.

## Benchmarks

Measured against **public** datasets, one command, numbers quoted verbatim — including the
unflattering ones. Full methodology, caveats, and the exact derived policy are in
[`benchmarks/`](benchmarks/); machine-readable output with the commit hash is in
[`benchmarks/results/latest.json`](benchmarks/results/latest.json).

The benchmark dependencies are kept out of the base install — install the extra, then run:

```bash
pip install "agentmoat[benchmarks]"            # datasets, agentdojo, langdetect
python benchmarks/run.py                        # add --embeddings for the embedding rows
# the embedding pass also needs the [embeddings] extra:
# pip install "agentmoat[benchmarks,embeddings]" && python benchmarks/run.py --embeddings
```

**The moat — tool firewall evaluated against [AgentDojo](https://github.com/ethz-spylab/agentdojo) attack cases (v1, 609 indirect-injection security cases):**

This is an **offline replay**: each AgentDojo attack task's declared tool-call sequence is
checked statically against the firewall layer. We do *not* run live agents or measure
AgentDojo's task-utility metric — so read this as "evaluated against AgentDojo's attack cases",
not "benchmarked on AgentDojo".

| Metric | Result |
|--------|--------|
| Attack catch rate | **86.0%** (524 / 609) |
| &nbsp;&nbsp;— by least-privilege tool policy (only) | 524 |
| &nbsp;&nbsp;— by argument constraints (only) | 0 |
| &nbsp;&nbsp;— by both | 0 |
| False-positive rate (on 339 benign tool calls) | **0.0%** |
| Per-call evaluation latency | **p50 0.10 ms · p95 0.23 ms** (excludes the LLM/API call) |

For each user task we allow exactly the tools its *benign* ground-truth run uses and deny the
rest, then measure what that blocks on the paired attacks. Because the policy is derived from
the same task suite it's later measured on, the benign-call false-positive rate is favorable
**by construction** — read the 0.0% FPR as *"simulates a well-configured least-privilege
deployment"*, not a claim about arbitrary hand-written policies.

**Constraint contribution, as measured: 0.** All 524 catches come from least-privilege tool
policy denying an out-of-scope tool; the always-on argument constraints (path traversal, SSRF,
shell, sensitive paths) fire on none of these cases, because AgentDojo's attacks are
financial/messaging exfiltration, not the filesystem/network class those detectors target
(they're exercised in `tests/test_constraints.py` instead). The 85 uncaught cases (14%) all
reuse a tool the benign task itself uses — semantic misuse the firewall doesn't model — and
break down as **41 financial-transaction, 16 send/message, 16 write, 12 read**
(full list in [`benchmarks/results/uncaught_cases.json`](benchmarks/results/uncaught_cases.json)).

**Defense-in-depth, *not* the security boundary — injection-text detector vs. [deepset/prompt-injections](https://huggingface.co/datasets/deepset/prompt-injections) (263 injection / 399 benign):**

| Path | Catch rate | FPR | Latency |
|------|-----------|-----|---------|
| Rule-based — overall | 5.3% (14 / 263) | 0.0% | p50 0.10 ms |
| Rule-based — English only | 8.9% (14 / 157) | 0.0% | — |
| Rule-based — non-English | 0.0% (0 / 106) | 0.0% | — |
| + Embeddings (opt-in) | **+0** catches over rules | 0.0% | p50 27 ms |

The rule patterns are English-only, so splitting by detected language (via `langdetect`) tells
the real story: **8.9% on English attacks, 0.0% on the non-English ~40% of the corpus** — the
English regexes cannot see non-English injection at all. The opt-in embedding pass added
**zero** catches over the rules here — the highest cosine similarity any attack reached against
a known-attack phrase was **0.629, below the 0.82 flag threshold** — while costing hundreds of
times the latency (p50 27 ms vs 0.10 ms).

These unflattering numbers are in the README on purpose, because they *are* the thesis:
**regex/embedding injection detection is bypassable and is not the security boundary.** It's a
cheap signal layered on top; the boundary that actually stops a bad action is the deterministic
tool-argument enforcement above, which blocks the call whether or not detection recognized the
prompt. See [THREAT_MODEL.md](THREAT_MODEL.md) for exactly where detection helps and where it
doesn't.

> Generated with `python benchmarks/run.py --embeddings` against `deepset/prompt-injections`
> and AgentDojo `v1`. Reproduce with the steps in [`benchmarks/README.md`](benchmarks/README.md);
> the exact commit is recorded in `results/latest.json`.

## "Can't I just not bind the tool?"

The strongest objection to AgentMoat is that this is already solved: bind each agent only the
tools it needs at the framework level and you're done. It's a fair point, and our own benchmark
makes it — all 524 firewall catches (the full 86.0%) were out-of-scope-tool *policy* catches,
denying a tool the task never needed. Careful tool binding would have prevented every one. Read
the 86.0% as evidence for least-privilege as a principle, not for this proxy as the only place to
implement it.

What binding *cannot* do, and where the proxy earns its place:

- **Argument-level constraints on tools the agent must have.** Binding is binary — the tool is in
  or out — but the risk usually lives in the arguments: which path, which URL, which recipient.
- **Enforcement from outside the agent's codebase.** One choke point a platform owner controls,
  instead of trusting every agent's code to stay correctly configured — the same reason you run a
  network firewall even when every host already has its own rules.
- **A runtime control plane binding doesn't give you:** tamper-evident audit of the calls you
  *allowed*, human-in-the-loop approval, rate limits, and a kill switch.

See [THREAT_MODEL.md](THREAT_MODEL.md) for exactly where each of these holds and where it doesn't.

## Performance overhead

The per-call cost AgentMoat adds is sub-millisecond and dwarfed by the LLM/API round-trip it
sits in front of (which it does not change). Measured with `python benchmarks/overhead.py`:

| What | p50 | p95 |
|------|-----|-----|
| Injection scan per message (rule mode) | ~0.09 ms | ~0.15 ms |
| Argument-constraint check per tool call | ~0.01 ms | ~0.02 ms |
| End-to-end MCP proxy overhead per intercepted call | ~0.10 ms | ~0.16 ms |

The MCP-proxy row is the delta between a full proxied `tools/call` (request parse + injection +
argument constraints + tool policy + trust + event emission) and a direct upstream call, using a
zero-cost mock upstream — so it reflects AgentMoat's interception cost alone, not real
tool-server or transport latency. The injection-scan figure uses benign inputs, which are the
regex **worst case** (every pattern is scanned and none short-circuits on an early match), so the
latency is conservative — a real injection tends to match sooner and cost less. Numbers measured
on an AMD Ryzen 5 5600H (CPython 3.10, Windows); they exclude the LLM/API call and the ~80 MB
optional embedding model (off by default).

## Install

```bash
pip install agentmoat

# with extras
pip install "agentmoat[langgraph,openai]"
```

For development (editable install from source):

```bash
git clone https://github.com/Shashank-016/agentmoat
cd agentmoat
pip install -e ".[langgraph,openai]"   # extras optional; base install works on its own
```

## Quick start (30 seconds)

```python
import anthropic
from agentmoat import GuardedClient

# Wrap your existing client — same interface as anthropic.Anthropic()
client = GuardedClient(
    anthropic.Anthropic(),
    agent_id="researcher",
    policy_path="policy.yaml",   # optional
    mode="observe",              # "observe" | "enforce" | "interactive"
)

# Use it exactly as before. AgentMoat scans inputs, checks tool calls,
# logs every event, and (in enforce mode) blocks dangerous actions.
resp = client.messages.create(
    model="claude-haiku-4-5-20251001",
    max_tokens=512,
    messages=[{"role": "user", "content": "Summarize this document..."}],
)
```

See it catch a real attack:

```bash
python examples/mcp_proxy_demo.py
# An agent reads a poisoned document and tries a privileged write —
# AgentMoat blocks it at the tool layer and prints a session report.
```

## Add AgentMoat to your own agent

One line at the point you create your client or graph. Everything downstream is instrumented.

**Anthropic SDK**
```python
from anthropic import Anthropic
from agentmoat import GuardedClient

client = GuardedClient(Anthropic(), agent_id="my-agent", policy_path="policy.yaml", mode="enforce")
```

**OpenAI SDK**
```python
from openai import OpenAI
from agentmoat import GuardedOpenAI

client = GuardedOpenAI(OpenAI(), agent_id="my-agent", policy_path="policy.yaml", mode="enforce")
```

**LangGraph** — attach the callback to any graph/runnable:
```python
from agentmoat import AgentMoatCallback

graph.invoke(state, config={"callbacks": [AgentMoatCallback(session_id="run-1")]})
```

**Any MCP tool server** — run AgentMoat as a transparent proxy, no agent code change at all.
Point your MCP client at AgentMoat instead of the real server:
```bash
agentmoat mcp proxy stdio \
  --upstream-cmd "npx -y @modelcontextprotocol/server-filesystem /data" \
  --agent-id my-agent \
  --policy policy.yaml \
  --mode enforce
```

Async variants (`AsyncGuardedClient`, `AsyncGuardedOpenAI`) and streaming are supported with the
same interface. Events flow to an in-memory bus, an optional SQLite store, and a hash-chained
JSONL audit log; view them via the bundled FastAPI service and React dashboard (see below).

---

## Policy File

```yaml
version: "1"
agents:
  researcher:
    allowed_tools: [web_search, read_file]
    denied_tools:  [write_file, execute_code]
    rate_limits:
      web_search: 10/minute

  writer:
    allowed_tools: [write_file, read_file]
    denied_tools:  [web_search, execute_code]
```

### Argument constraints

Tool *names* are only half the story — `write_file("/etc/crontab", payload)` passes a
name-level check for any agent allowed to use `write_file`. `ToolPolicyEngine.check_arguments()`
inspects the *arguments* of a tool call, combining always-on built-in detectors with
per-tool rules declared in the policy file:

```yaml
agents:
  writer:
    tool_constraints:
      write_file:
        path_allowlist: ["/tmp/**", "./output/**"]   # only these globs are permitted
        path_denylist:  ["/etc/**", "~/.ssh/**"]      # these are always blocked
        max_arg_length: 10000                         # flag oversized argument values
      fetch:
        url_denylist: ["169.254.169.254", "localhost", "10.*"]
        # url_allowlist, arg_denylist also supported
```

Built-in detectors run on every tool call regardless of configuration:

| Detector | Flag | Triggers on |
|----------|------|-------------|
| Path traversal | `constraint:path_traversal` | `../`, `..\`, or URL-encoded `%2e%2e` in any argument |
| SSRF targets | `constraint:ssrf_target` | URLs/hosts pointing at `169.254.169.254`, `localhost`, `127.0.0.1`, RFC-1918 ranges, `metadata.google.internal` |
| Shell metacharacters | `constraint:shell_metachar` | `;`, `\|`, `&&`, `` ` ``, `$(`, `>`, `<` in arguments to tools whose name suggests command execution (`exec`, `shell`, `command`, `run`, `bash`, `sh`) |
| Sensitive path access | `constraint:sensitive_path` | `/etc/`, `/root/`, `~/.ssh`, `id_rsa`, `.env`, `credentials`, `/proc/` |

Violations are emitted as `policy_violation` events with `severity="critical"` and raise
`AgentMoatException` in `enforce` mode — both from the SDK wrappers (checked against the
arguments the model produced, before the agent runtime executes the tool) and from the MCP
proxy (checked before the call is forwarded upstream, where blocking actually prevents execution).

---

## What Gets Detected (defense-in-depth signal)

The table below is the **detection** layer — heuristics that flag suspicious input. Treat it
as a signal, **not** the security boundary. Regex and embedding detection are pattern-based and
bypassable (base64, homoglyphs, zero-width characters, paraphrasing, other languages — see
[THREAT_MODEL.md](THREAT_MODEL.md)), so a determined injection *will* eventually slip past them.
The boundary that actually stops a bad action is the deterministic tool-argument enforcement
above (allow/deny policy + argument constraints), which blocks the call whether or not detection
recognized the prompt that produced it.

| Threat | Detection Method | Default Severity |
|--------|-----------------|-----------------|
| Jailbreak attempt | Regex: "ignore previous instructions", "you are now DAN" | Critical |
| System prompt exfiltration | Regex: "print your system prompt", "repeat everything above" | Critical |
| Role override | Regex: "act as if you have no restrictions" | Critical |
| Tool abuse via injection | Regex: "call the write_file tool" | Critical |
| Indirect injection (docs, web) | Regex + embedding similarity | Warning/Critical |
| Tool policy violation | YAML policy engine | Critical |
| Rate limit exceeded | Sliding window counter | Critical |
| Low-trust agent calling sensitive tools | Trust score degradation | Warning |
| Multi-agent trust chain poisoning | Multiplicative provenance tracking | Warning |

---

## OWASP Top 10 for Agentic Applications (2026)

How AgentMoat's controls line up against the
[OWASP Top 10 for Agentic Applications](https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/)
(OWASP GenAI Security Project, published 9 Dec 2025). We're deliberate about not overstating this:
AgentMoat is a *tool-action enforcement and audit* layer, so it directly **covers** tool misuse and
is **partial** on most of the rest — shrinking blast radius and adding visibility rather than fully
preventing the root cause. Full rationale per item is in [`docs/owasp-mapping.md`](docs/owasp-mapping.md).

| OWASP ID | Risk | Status | AgentMoat control |
|----------|------|--------|-------------------|
| ASI01 | Agent Goal Hijack | 🟡 Partial | Injection detection (heuristic) + trust provenance; downstream tool calls still enforced |
| ASI02 | Tool Misuse and Exploitation | 🟢 Covered | Policy engine + argument-level firewall, enforced at SDK wrappers **and** MCP proxy |
| ASI03 | Identity and Privilege Abuse | 🟡 Partial | Per-agent tool least-privilege; secret redaction in logs (no credential/identity management) |
| ASI04 | Agentic Supply Chain Vulnerabilities | ⚪ Out of scope | Forensic visibility via audit log only |
| ASI05 | Unexpected Code Execution | 🟡 Partial | Shell-metachar constraints, deny exec-style tools (no sandbox) |
| ASI06 | Memory and Context Poisoning | 🟡 Partial | Trust provenance + indirect-injection detection (no memory-store integrity) |
| ASI07 | Insecure Inter-Agent Communication | 🟡 Partial | Trust propagation + audit of handoffs (no transport auth/encryption) |
| ASI08 | Cascading Failures | 🟡 Partial | Kill switch, rate limits, bounded state, fail-closed engine |
| ASI09 | Human-Agent Trust Exploitation | 🟡 Partial | Approval gate with full call context + verifiable audit trail |
| ASI10 | Rogue Agents | 🟡 Partial | Tool/argument enforcement, trust flags, kill switch, audit log |

**1 covered · 8 partial · 1 out of scope.** AgentMoat is one layer — strongest at constraining and
recording tool actions — meant to sit alongside identity, sandboxing, and supply-chain controls.

---

## Response modes

Every guarded client, callback, and the MCP proxy take a `mode`:

| Mode | Behavior |
|------|----------|
| `"observe"` (default) | Detect and log everything — never interrupts the agent. |
| `"enforce"` | Raise `AgentMoatException` (or return a JSON-RPC error from the MCP proxy) on any hard violation. A fixed, pre-decided policy. |
| `"interactive"` | Route violations to a human (or programmatic approver) for a real-time decision via `ApprovalGate`. A "deny" blocks the call just like enforce mode; an "approve" lets it through. |

`"interactive"` mode is for situations where a blanket policy is too coarse — let
a human apply judgment to a specific borderline case instead of pre-encoding
every exception:

```python
from agentmoat import GuardedClient, ApprovalGate
from agentmoat.control import ApprovalRequest, ApprovalDecision

def slack_approval_handler(request: ApprovalRequest) -> ApprovalDecision:
    # Post to Slack, wait for a thumbs-up/thumbs-down reaction, etc.
    ...
    return "approve"  # or "deny"

client = GuardedClient(
    anthropic.Anthropic(),
    agent_id="researcher",
    mode="interactive",
    approval_gate=ApprovalGate(handler=slack_approval_handler),
)
```

Each request emits `approval_required`, then `approval_granted` or
`approval_denied`, so the full decision trail lands in the audit log. The
default handler (when no `approval_gate=` is supplied) prompts on the CLI with
a y/N confirmation — fine for local development, but register your own handler
(Slack, a web UI, a queue) for anything running unattended. A misbehaving or
exception-raising handler defaults to `"deny"` — approval gates fail closed.

Note: `trust_flag` warnings never hard-block in `enforce` mode (a low trust
score alone shouldn't halt an agent), but in `interactive` mode they still
route through the approval gate — a human's explicit "deny" blocks the call.
This gives interactive mode finer-grained control than a blanket policy.

### Kill switch

Independent of `mode`, any session — or every session in the process — can be
halted immediately via `KillSwitch`:

```python
from agentmoat.control import get_default_kill_switch

switch = get_default_kill_switch()
switch.kill_session("session-123")   # halt one session
switch.kill_all()                    # halt every session in this process
switch.revive_session("session-123") # restore it
switch.status()                      # {"global": False, "killed_sessions": [...]}
```

A killed session's next intercepted action raises `AgentMoatKilled` (a subclass
of `AgentMoatException`) — or, for the MCP proxy, returns a JSON-RPC error
(`AGENTMOAT_SESSION_KILLED`) — *before* any API call or tool execution happens.
A critical `session_end` event with `flags=["kill:tripped"]` is emitted first,
so the halt is visible in the audit trail.

The same switch is reachable over HTTP once the audit API is running. The
`/control` routes are behind the **same** `AGENTMOAT_API_KEY` authentication as
the rest of the audit API (see below) — so set that key, then present it:

```bash
export AGENTMOAT_API_KEY=secret-123
curl -X POST http://localhost:8000/control/kill/session-123 -H "X-API-Key: $AGENTMOAT_API_KEY"
curl -X POST http://localhost:8000/control/kill-all        -H "X-API-Key: $AGENTMOAT_API_KEY"
curl -X POST http://localhost:8000/control/revive/session-123 -H "X-API-Key: $AGENTMOAT_API_KEY"
curl http://localhost:8000/control/status                  -H "X-API-Key: $AGENTMOAT_API_KEY"
```

These endpoints affect sessions in the API process only — a multi-process
deployment needs a shared backing store (see Roadmap) for one trip to halt
every worker. Authentication is enforced only when `AGENTMOAT_API_KEY` is set:
if you leave it unset the API (including the kill switch) stays open for
backward compatibility and logs a warning — **always set it before exposing the
API**, so an unauthenticated `kill-all` can't be turned into a denial-of-service
against your own agents.

---

## Tamper-evident audit log

`AuditLogger` (passed via `audit_log=` to any guarded client/callback) writes one JSON
object per line to a durable JSONL file. By default (`chained=True`) every record also
carries `prev_hash` — the SHA-256 `record_hash` of the previous line, with a genesis value
of 64 zeros for the first line in a fresh file — and its own `record_hash`, a digest over
the record's canonical JSON plus `prev_hash`. Editing or deleting any line breaks the link
to the next record, so tampering is always detectable, not just guessable. The chain
survives process restarts (it resumes from the last line on disk) and rotations (the new
file's first record continues from the rotated file's last hash).

```bash
agentmoat audit verify agentmoat_audit.jsonl
# ✓ Chain intact — 1,432 records verified
#   (or, if a line was edited or removed:)
# ✗ Chain broken at line 87 — record was modified or a prior line was deleted

agentmoat audit tail agentmoat_audit.jsonl -n 50
agentmoat audit stats agentmoat_audit.jsonl   # counts by event_type and severity
```

This gives you a forensic trail suitable for SOC 2 / ISO 27001 evidence: an auditor (or an
incident responder) can independently confirm that the log they're looking at is the
complete, unaltered record AgentMoat produced — not a reconstruction. It does not, by
itself, prove *who* tampered with a file; pair it with filesystem-level access controls and
off-host replication for full chain-of-custody guarantees.

---

## Secret & PII redaction

AgentMoat observes raw LLM messages, tool arguments, and responses — which routinely carry
secrets you don't want copied into the audit trail or shown on the dashboard. Every event
payload is therefore **redacted before it is persisted**: `redact()` runs inside
`make_payload()`, so the SQLite store, the JSONL audit log, and the dashboard only ever see
masked values. It walks nested dicts/lists and replaces recognizable secrets/PII with a
`«REDACTED:<kind>»` placeholder, and runs *before* truncation so a secret can't dodge the
pattern by landing on the truncation boundary.

What it catches out of the box: OpenAI-style API keys, AWS access key IDs, GitHub tokens,
JWTs, PEM private-key blocks, and email addresses.

```python
from agentmoat.events import make_payload

make_payload(message="my key is sk-ABCDEFGHIJ1234567890abcdef")
# {'message': 'my key is «REDACTED:openai_api_key»'}
```

Redaction is **on by default**. Disable it for local debugging via the environment —
`AGENTMOAT_REDACT=0` (also accepts `false`/`no`/`off`) — or programmatically, which takes
precedence over the env var:

```python
from agentmoat.redaction import set_redaction_enabled

set_redaction_enabled(False)  # raw payloads; True to force on, None to fall back to the env var
```

---

## Running the API + Dashboard

<!-- TODO(Shashank): add docs/dashboard.png before launch. Screenshot the live event
     feed with a few critical/warning events visible (run examples/langgraph_demo.py first
     to populate it). ~1600px wide works well; see the instructions printed at the end of
     the launch-prep run. This <img> intentionally 404s until that file is committed. -->
<!-- <img src="docs/dashboard.png" alt="AgentMoat dashboard — live security event feed with severity badges and a per-session timeline" width="900"> -->

```bash
# 1. Install
pip install -e ".[langgraph]"

# 2. Start the audit API
uvicorn api.main:app --reload

# 3. Start the dashboard
cd dashboard
npm install
npm run dev
# → http://localhost:5173

# 4. Run the demo
python examples/langgraph_demo.py
```

See [`dashboard/README.md`](dashboard/README.md) for dashboard-specific setup, including how to
authenticate against an API started with `AGENTMOAT_API_KEY` set.

---

## Running Tests

```bash
pytest
```

---

## Trust Scoring

AgentMoat tracks *information provenance* across agent hops. When a session processes external content (a file, a web page, a user upload), its trust score degrades:

```
Initial:          1.0  (TRUSTED  — human instructions)
After file read:  0.3  (EXTERNAL — external content processed)
After handoff:    0.21 (EXTERNAL — downstream agent inherits low trust)
After injection:  0.0  (UNTRUSTED — flagged)
```

When trust drops below 0.5, any attempt to call a sensitive tool (write, execute, send, delete) emits a `trust_flag` warning even if the tool is otherwise policy-allowed.

---

## How AgentMoat compares

The agent-guardrail space is active and several of these projects are more mature or better
funded than AgentMoat. They mostly solve a *different* part of the problem — detection quality,
output validation, conversational rails — and AgentMoat is complementary to all of them.
AgentMoat's specific angle is **deterministic enforcement at the tool boundary**, delivered as
SDK wrappers *and* a transparent MCP proxy in one package, with a hash-chained tamper-evident
audit log, a human-in-the-loop interactive mode, and no cloud dependency (the default rule path
runs fully local, no external model calls).

| Project | Primary focus | Does better than AgentMoat | Where AgentMoat differs |
|---------|---------------|----------------------------|-------------------------|
| **[LlamaFirewall](https://github.com/meta-llama/PurpleLlama) (Meta)** | ML-based guardrails — prompt-injection/jailbreak detection (PromptGuard), agent-alignment auditing (AlignmentCheck), insecure-code scanning (CodeShield) | Far stronger, model-based detection and alignment auditing; backed by Meta | AgentMoat's boundary is deterministic tool-argument enforcement, not detection; ships a transparent MCP proxy and tamper-evident audit log |
| **[NeMo Guardrails](https://github.com/NVIDIA/NeMo-Guardrails) (NVIDIA)** | Programmable conversational rails via the Colang DSL — topical, dialog, input/output rails | Mature, broad integrations, rich conversation-flow control; NVIDIA-backed | AgentMoat enforces at the tool-call layer (arguments, policy) rather than the conversation layer, and works transparently over MCP with no app rewrite |
| **[Guardrails AI](https://github.com/guardrails-ai/guardrails)** | LLM output validation — a large hub of validators (structure, PII, toxicity, format) | Much larger validator ecosystem; strong structured-output/validation story | AgentMoat targets *actions* (what a tool call does) rather than validating model text output, and adds MCP-proxy enforcement + audit integrity |
| **[Invariant Labs](https://github.com/invariantlabs-ai)** | Agent security tooling — a policy/analysis language for agent traces and MCP-focused security (e.g. MCP scanning, a guardrailing gateway) | Deeper trace-analysis policy language and dedicated MCP vulnerability scanning; the closest overlap on MCP | AgentMoat bundles SDK wrappers + proxy enforcement + hash-chained audit + interactive approval in one local package with no cloud/Explorer component |

Honest summary: if you want the strongest *detection*, pair AgentMoat with LlamaFirewall's
models; if you need conversational rails, NeMo is purpose-built; for output-schema validation,
Guardrails AI has the ecosystem. AgentMoat's bet is that the durable security control is
*stopping the action at the tool boundary and being able to prove what happened* — which is why
it leads with enforcement and a verifiable audit log rather than detection. Capabilities of the
other projects move fast; check their current docs before relying on any specific feature above.

---

## Roadmap

- [x] **OpenAI SDK support** — `GuardedOpenAI` / `AsyncGuardedOpenAI` wrap `openai.OpenAI` / `AsyncOpenAI`
- [x] **Async GuardedClient** — `AsyncGuardedClient` wraps `AsyncAnthropic` for async codebases
- [x] **Streaming support** — `GuardedStream` / `AsyncGuardedStream` intercept `messages.stream()`
- [x] **MCP server integration** — transparent stdio + SSE proxy for Model Context Protocol
- [x] **Tamper-evident audit log** — SHA-256 hash-chained JSONL with `agentmoat audit verify`
- [x] **Human-in-the-loop approval** — `mode="interactive"` routes violations through `ApprovalGate`
- [x] **Kill switch** — halt any session (or every session) immediately, programmatically or via `/control`
- [ ] **OpenTelemetry export** — emit spans/traces to any OTEL-compatible backend
- [ ] **Multi-process bus** — Redis-backed EventBus for distributed agent deployments
- [ ] **Slack/PagerDuty alerting** — push critical events to on-call channels
- [ ] **SARIF export** — machine-readable security findings for CI integration
- [ ] **Policy hot-reload** — watch policy.yaml for changes without restart

---

## License

MIT — see [LICENSE](LICENSE)
