Guardrail for AI agents. Open-source sidecar proxy between AI agents and their tools — policy, human approval, and tracing without changing agent code.
One binary. One YAML config. Fail closed by default.
Works with Claude Code, Cursor, LangChain, CrewAI, or any agent that uses HTTP, MCP, or CLI tools.
- Architecture
- Install
- Quick start
- Config reference — MCP servers, OpenAPI, CLI tools, policies, supervisor
- Features — approval, grants, rate limiting, tracing, OTEL, supervisor
- Commands & flags
- API
- Project structure
- Tests
- Roadmap
flowchart LR
subgraph Agents["Agents"]
A1["Claude Code / Cursor"]
A2["LangChain / CrewAI"]
A3["Any HTTP agent"]
end
subgraph Mesh["agent-mesh (sidecar proxy)"]
direction TB
REG["Registry<br/>(tools)"]
RL["Rate limiter<br/>+ loop detect"]
POL["Policy engine<br/>(glob, conditions)"]
FWD["Forward"]
APP["Approval store"]
GRT["Grant store<br/>(sudo for agents)"]
TRC["Trace store<br/>(JSONL + sessions)"]
OTEL["OTEL exporter<br/>(file / stdout / OTLP)"]
REG --> RL --> POL --> FWD
POL -.approval.-> APP
POL -.bypass.-> GRT
FWD --> TRC
TRC --> OTEL
end
subgraph Upstream["Upstream tools"]
U1["MCP servers<br/>(stdio + SSE)"]
U2["REST APIs<br/>(OpenAPI specs)"]
U3["CLI binaries<br/>(terraform, gh, docker)"]
end
subgraph Observability["Observability"]
O1["Jaeger / Tempo /<br/>Datadog / OTLP HTTP"]
O2["traces-otel.jsonl"]
end
A1 -- MCP --> Mesh
A2 -- HTTP --> Mesh
A3 -- HTTP --> Mesh
FWD --> U1
FWD --> U2
FWD --> U3
OTEL --> O1
OTEL --> O2
Import: OpenAPI specs (URL or file) · MCP servers (stdio + SSE) · CLI binaries
Export: MCP server (stdio) · HTTP proxy (:port) · OTLP traces
When you connect tools directly to an AI agent, the agent gets unguarded access — no policy, no trace, no control.
Put Agent Mesh between the agent and its tools:
claude mcp add agent-mesh -- ./agent-mesh --mcp --config config.yamlThe agent sees a normal tool surface. Agent Mesh enforces policy and records traces on every call.
VERSION=$(curl -s https://api.github.com/repos/KTCrisis/agent-mesh/releases/latest | grep tag_name | cut -d '"' -f4)
# Linux amd64
curl -L "https://github.com/KTCrisis/agent-mesh/releases/download/${VERSION}/agent-mesh_${VERSION#v}_linux_amd64.tar.gz" | tar xz
sudo mv agent-mesh /usr/local/bin/
# macOS Apple Silicon
curl -L "https://github.com/KTCrisis/agent-mesh/releases/download/${VERSION}/agent-mesh_${VERSION#v}_darwin_arm64.tar.gz" | tar xz
sudo mv agent-mesh /usr/local/bin/All releases: github.com/KTCrisis/agent-mesh/releases
Requires Go 1.24+:
git clone https://github.com/KTCrisis/agent-mesh.git
cd agent-mesh
make install # builds to ~/go/bin/agent-mesh with version metadata
agent-mesh --version
# agent-mesh v0.8.5 (bee2a9e) built 2026-04-16T07:11:16Z# config.yaml
mcp_servers:
- name: filesystem
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/me/projects"]
policies:
- name: claude
agent: "claude"
rules:
- tools: ["filesystem.read_*", "filesystem.list_*", "filesystem.search_*"]
action: allow
- tools: ["filesystem.write_file", "filesystem.edit_file"]
action: human_approval
- tools: ["filesystem.*"]
action: deny
- name: default
agent: "*"
rules:
- tools: ["*"]
action: denyOr auto-generate one:
agent-mesh discover --config config.yaml --generate-policy
agent-mesh discover --openapi https://petstore.swagger.io/v2/swagger.json --generate-policyclaude mcp add agent-mesh -- agent-mesh --mcp --config config.yamlRestart Claude Code. The agent sees the tools. Agent Mesh enforces the rules. Every call is traced.
All features are declared in a single YAML config.
mcp_servers:
- name: filesystem
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/me"]
- name: remote-service
transport: sse
url: "https://mcp-server.example.com/sse"
headers:
Authorization: "Bearer <token>"Import REST APIs as governed tools — persisted across restarts.
openapi:
# From URL
- url: https://date.nager.at/swagger/v3/swagger.json
# From local file
- file: ./specs/internal-api.json
backend_url: http://localhost:3001Each endpoint becomes a tool (e.g. get_public_holidays). Same policy, same traces as MCP tools.
Wrap any CLI binary behind policy, approval, and tracing:
cli_tools:
- name: gh
bin: gh
default_action: allow
- name: terraform
bin: terraform
default_action: human_approval
commands:
plan:
timeout: 120s
- name: kubectl
bin: kubectl
strict: true # only declared commands, everything else denied
commands:
get:
allowed_args: ["-n", "--namespace", "-o"]Agents call CLI tools like any MCP tool — terraform.plan, kubectl.get, gh.pr. See docs/cli-tools.md.
YAML-based, first-match-wins, glob patterns for agents and tools.
policies:
- name: support-agent
agent: "support-*"
rate_limit:
max_per_minute: 30
max_total: 1000
rules:
- tools: ["*.read_*", "*.list_*", "*.get_*"]
action: allow
- tools: ["create_refund"]
action: allow
condition:
field: "params.amount"
operator: "<"
value: 500
- tools: ["*"]
action: deny| Action | Behavior |
|---|---|
allow |
Forward to backend, return result |
deny |
Block the call, return denial |
human_approval |
Require human approval before forwarding |
Fail closed: no matching rule = deny.
One file per agent, drop-in/drop-out:
# config.yaml
policy_dir: ./policies # load all *.yaml from this directory# policies/scout7.yaml
name: scout7
agent: "scout7"
rate_limit:
max_per_minute: 30
rules:
- tools: ["searxng.*", "fetch.*", "ollama.*", "memory.*"]
action: allow
- tools: ["*"]
action: denyFiles are loaded alphabetically after inline policies:. Duplicate names produce an error.
supervisor:
enabled: true # hide approval tools from agents
expose_content: false # redact raw params → structural metadataWhen enabled, approval.resolve and approval.pending are hidden from agents — only an external supervisor can resolve approvals. See docs/supervisor-protocol.md.
port: 9090 # HTTP port (default 9090)
trace_file: traces.jsonl # JSONL persistence
otel_endpoint: /path/to/traces-otel.jsonl # or "stdout" or "http://localhost:4318"
approval:
timeout_seconds: 300 # approval TTL (default 5 min)
notify_url: https://hooks.slack.com/... # webhook on new pending approvalWhen a policy requires human_approval, the flow is non-blocking:
Claude calls filesystem.write_file
→ agent-mesh returns: "Approval required (id: a1b2c3d4)"
→ Claude calls approval.resolve(id: a1b2c3d4, decision: approve)
→ agent-mesh replays the original tool call
→ Result returned to Claude
Virtual MCP tools: approval.resolve, approval.pending.
Also via CLI (mesh approve <id>) or HTTP API (POST /approvals/{id}/approve).
Like sudo for agents — temporary override for repeated approvals:
"Grant filesystem.write_* for 30 minutes"
→ grant.create {tools: "filesystem.write_*", duration: "30m"}
→ All filesystem.write_* calls bypass approval for 30m
→ Traced as "grant:a1b2c3d4"
Virtual MCP tools: grant.create, grant.list, grant.revoke.
Grants only bypass human_approval. Tools marked deny remain blocked — policy edit required.
Per-agent call limits with automatic loop detection:
| Protection | What it stops |
|---|---|
max_per_minute |
Runaway loops |
max_total |
Budget exhaustion |
| Loop detection | Same tool + same params > 3x in 10s |
Every tool call is logged: agent, tool, params, policy decision, latency, approval metadata.
curl http://localhost:9090/traces?agent=claude&tool=filesystem.write_file
curl http://localhost:9090/sessions # list sessions
curl http://localhost:9090/sessions/abc123 # session detailSession IDs are propagated via X-Session-Id header or --mcp-session-id flag.
otel_endpoint: /path/to/traces-otel.jsonl # file
otel_endpoint: stdout # debug
otel_endpoint: http://localhost:4318 # Jaeger, Tempo, DatadogEach span includes agent.id, tool.name, policy.action, approval.*, and llm.token.* attributes. See docs/otel.md.
External supervisor agents can poll GET /approvals?status=pending, evaluate with full context (recent traces, active grants, injection risk), and resolve with structured verdicts (reasoning, confidence). See docs/supervisor-protocol.md.
agent-mesh [flags] # run proxy (HTTP or MCP mode)
agent-mesh discover [flags] # discover tools + generate policy
agent-mesh --version # print version| Flag | Default | Description |
|---|---|---|
--config |
config.yaml |
Path to YAML config |
--openapi |
OpenAPI spec URL (ephemeral, for quick tests) | |
--backend |
Backend base URL override | |
--port |
from config or 9090 |
Port override |
--mcp |
false |
MCP mode (stdio JSON-RPC instead of HTTP) |
--mcp-agent |
claude |
Agent ID for MCP-mode policy evaluation |
--mcp-session-id |
auto-generated | Session ID for MCP traces |
discover flags: --openapi <url>, --config <path>, --generate-policy, --backend <url>.
mesh pending # list pending approvals
mesh show <id> # full details
mesh approve <id> # approve
mesh deny <id> # deny
mesh watch # interactive poll + promptSet MESH_URL to override the default http://localhost:9090.
| Method | Path | Description |
|---|---|---|
POST |
/tool/{name} |
Proxy a tool call through policy |
GET |
/tools |
List all registered tools |
GET |
/mcp-servers |
List connected MCP servers |
GET |
/traces |
Query traces (?agent=...&tool=...) |
GET |
/sessions |
List sessions (id, agent, event count, timespan) |
GET |
/sessions/{id} |
Session detail |
GET |
/otel-traces |
OTLP JSON spans (?agent=...&tool=...&limit=...) |
GET |
/approvals |
List approvals (?status=pending&tool=filesystem.*) |
GET |
/approvals/{id} |
Approval detail with context |
POST |
/approvals/{id}/approve |
Approve (optional: reasoning, confidence) |
POST |
/approvals/{id}/deny |
Deny (optional: reasoning, confidence) |
GET |
/grants |
List active grants |
POST |
/grants |
Create a grant |
DELETE |
/grants/{id} |
Revoke a grant |
GET |
/health |
Health check and stats |
GET |
/version |
Version info |
agent-mesh/
├── cmd/
│ ├── agent-mesh/ # Main binary (entry point, wiring)
│ └── mesh/ # Approval CLI (pending/approve/deny/watch)
├── config/ # YAML config parsing + validation
├── registry/ # Tool registry (OpenAPI + MCP + CLI imports)
├── policy/ # Rule evaluation (globs, conditions, fail-closed)
├── proxy/ # HTTP handler (auth → rate limit → policy → forward → trace)
├── mcp/ # MCP client/server/transport (stdio + SSE)
├── approval/ # Channel-based approval store with timeout
├── grant/ # Temporal grants (TTL-based sudo)
├── ratelimit/ # Sliding window + loop detection
├── supervisor/ # Content isolation + injection detection
├── exec/ # Secure CLI execution (no shell, arg validation)
├── trace/ # In-memory + JSONL + OTEL export
├── policies/ # Per-agent policy files (used with policy_dir)
├── examples/ # Example configs (filesystem, petstore, travel, langchain)
└── docs/ # CLI tools guide, OTEL guide, supervisor protocol
go test ./... # all tests
go test ./... -race # with race detector207 tests across 13 packages covering config parsing, policy evaluation, HTTP/MCP proxy flows, approval lifecycle, CLI execution security, rate limiting, tracing, OTEL export, supervisor content isolation, and injection detection.
- Import OpenAPI (URL + file), MCP (stdio + SSE), CLI binaries
- Policy engine with glob patterns + conditions
- Human approval (non-blocking, virtual MCP tools, CLI, HTTP)
- Temporal grants (sudo for agents)
- Rate limiting + loop detection
- Trace store + JSONL + OTEL export
- Per-agent policy files + specificity sort
- Session tracking
- Supervisor protocol (content isolation, injection detection)
- CLI tool governance (3 modes, secure exec)
- OpenAPI config field (persistent import)
- Dashboard UI (via agent7)
- Durable state (approvals, grants, rate limits persisted across restarts)
-
agent-mesh servedaemon mode (persistent, multi-client) - Operator auth (separate identity from agent Bearer)
- Session log durable +
wake(sessionId)recovery - Policy hot-reload
- Condition engine v2 (AND/OR/nested)
The same way Envoy sits between microservices and adds observability, auth, and rate limiting without changing service code — Agent Mesh sits between AI agents and their tools.
Agents don't know the proxy exists. They call tools, get results. The governance layer is invisible to the agent, visible to the operator.
Apache 2.0