AgentFence architecture
June 14, 2026 · View on GitHub
AgentFence operates in two modes:
- Batch
checkmode — evaluate a JSONL stream of recorded tool calls offline. Does not execute tool calls; produces decisions + audit events. - MCP proxy mode (
agentfence proxy) — live policy gate that sits between an agent and an MCP tool server, interceptingtools/callrequests in real time. Does not execute tool calls itself; it forwards allowed calls to the downstream MCP server and synthesizes JSON-RPC error responses for denied ones.
Neither mode is a sandbox: AgentFence enforces policy before a tool call executes; it does not contain a tool call that has already been forwarded.
Batch (check) architecture
AgentFence works as a local policy evaluator and audit logger:
- Load YAML policy.
- Read JSONL tool-call records.
- Evaluate each call against policy rules. Malformed lines (invalid JSON or missing required fields) emit a synthetic
denyaudit event and processing continues — no subsequent calls are silently skipped. - Produce a decision (
allow,deny,ask) and reason. - Redact sensitive-looking values in arguments.
- Emit audit events as JSONL.
Fail-safe behavior: a single malformed line never aborts evaluation of remaining calls. An all-malformed input (every line fails to parse) returns a non-zero exit code.
MCP proxy architecture
AgentFence can also run as an MCP-aware stdio proxy between an agent and
its tool servers (agentfence proxy --policy <file> -- <command> [args...]).
See integration-guide.md for end-to-end
configuration examples.
- Agent → AgentFence proxy → MCP server/tools.
- The proxy spawns the MCP server as a subprocess and relays newline- delimited JSON-RPC messages in both directions.
- Every
tools/callrequest is parsed (internal/mcp) and converted to apolicy.ToolCall, then evaluated by the engine. Non-tools/callmessages (initialize, ping, notifications) are forwarded untouched. - On
allowthe original request is forwarded to the subprocess. - On
denythe proxy answers the agent with a JSON-RPC error response (code-32001, "blocked by AgentFence policy:") and the subprocess never sees the request. - On
aska pluggableApprover(the sharedapproval.Approvercontract, used by both proxies andcheck) decides at runtime: by default an interactiveTTYApproverprompts the operator, bounded by--approval-timeout;--no-interactivesubstitutes the fail-closedDenyAllApprover. An approved call is forwarded, a denied one becomes the same blocked-by-policy response, and the audit event records the resolved decision. - Every evaluated request produces one audit event using the same
audit.Writerascheck, so--tamper-evidentandagentfence audit verifywork identically against proxy logs.
Streamable-HTTP proxy
agentfence proxy-http --upstream <url> (package internal/httpproxy) is
the HTTP/SSE counterpart: an http.Handler that reverse-proxies to a remote
MCP server. It parses each POST body, evaluates a single tools/call exactly
as the stdio path does, and relays the upstream response — including streamed
text/event-stream bodies, copied with a flush per chunk. Non-tools/call
requests, GETs (the SSE channel), and anything that is not a JSON-RPC request
are forwarded transparently. JSON-RPC batch bodies are forwarded ungated.
See threat-model.md for the
HTTP-specific surface.
Session-scoped taint tracking
Both proxies evaluate through an engine.Session rather than the stateless
Engine. When a policy enables taint:, the session remembers the text of
tool results it relays (internal/taint) and escalates a later call whose
argument is derived from that untrusted output — the confused-deputy
detection described in threat-model.md.
The stateless check/explain paths use Engine directly and are
unaffected.
Policy evaluation flow
- Rule lookup uses the following precedence (highest first):
- Exact match — tool name matches a key in
toolsexactly. - Group match — tool name matches a member pattern of a named group in
groupsthat also has atoolsentry. Groups are checked in alphabetical name order; within a group, member patterns are checked in declaration order. - Wildcard match — tool name matches a glob pattern key in
toolsthat is not a group name. Patterns are checked in alphabetical order for determinism. - Default —
defaults.decisionapplies.
- Exact match — tool name matches a key in
- Path constraints are checked when present.
- Argument value constraints (
constraints.args) are checked after path constraints. - URL constraints (
constraints.urls) are checked for browser/HTTP tools. - Command constraints (
constraints.command) are checked for shell/terminal tools. - Deny patterns/rules are evaluated before allow patterns.
- Any denied constraint returns
denyimmediately.
Audit flow
- Every evaluated call creates one audit event.
- Each event carries a stable schema (
schema_version), a per-runsession_id, a monotonicseqnumber, plustimestamp,call_id,tool,decision,reason, and (optionally) redacted arguments. - Events are encoded as JSONL for easy ingestion.
--tamper-evidentenables a SHA-256 hash chain: each event records its ownhash; all but the first chained event record the previous event'shashinprev_hash. The chain can be verified after the fact withagentfence audit verify --log <file>. Seethreat-model.mdfor what this does and does not protect against.
Redaction flow
- Redaction patterns are loaded from policy regex rules.
- Arguments are recursively traversed.
- String values matching any configured secret pattern are replaced with
[REDACTED:<pattern_name>]. - Redaction occurs before arguments are written into audit logs.
Enforcement modes
AgentFence defines four enforcement modes (see modes.md for
details): detection, prevention, audit-only, and dry-run. The
modes differ in whether decisions are enforced, whether the operator is
prompted, and how the exit code is propagated. The canonical definitions and
command-to-mode mapping live in modes.md; the threat model
references those modes when describing what each enforcement boundary
guarantees (see threat-model.md).