Configuration
Every feature is declared in a single YAML file. This page is the complete reference; Getting Started shows the minimal config, Writing Policies goes deep on rules.
All features are declared in a single YAML config.
MCP servers
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>"
- name: huggingface
transport: streamable-http
url: "https://huggingface.co/mcp"
streamable-http is the transport hosted MCP servers now default to. It holds
no long-lived stream: each JSON-RPC request is a POST whose response carries
the answer, either as JSON or as an event stream. Sessions (Mcp-Session-Id)
are handled transparently, and cross-origin redirects are refused so a
compromised upstream cannot relay your tool calls elsewhere.
OpenAPI specs
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:3001
Each endpoint becomes a tool (e.g. get_public_holidays). Same policy, same traces as MCP tools.
CLI 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"]
- name: jq # binary without subcommands
bin: jq
default_action: allow
bare:
allowed_args: ["-r", "--compact-output"]
Agents call CLI tools like any MCP tool — terraform.plan, kubectl.get, gh.pr. A bare binary registers a single <name>.run tool. Every CLI tool accepts an optional stdin param, piped to the process as data (never shell-interpreted).
default_action is the floor for the dynamic dispatcher (<name>.__dispatch), which is where undeclared subcommands land. It can only restrict, never widen, and defaults to deny — so a glob such as terraform.*: allow cannot hand over destroy along with plan. See docs/cli-tools.md.
Policies
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: "amount" # the path starts at the tool's arguments
operator: "<"
value: 500
# Conditions read strings too, so a rule can look inside the call
# rather than only at its name. A list means "any of these".
- tools: ["Bash"]
action: deny
condition:
field: "command"
operator: "contains"
value: ["mkfs", "> /dev/sd", "/etc/sudoers"]
- tools: ["*"]
action: deny
Operators: < <= > >= == != on numbers, == != contains
not_contains starts_with not_starts_with on strings. String matching is
literal and case-sensitive: it raises the floor against accidents, it is not a
sandbox. See writing-policies.md.
| 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.
Per-agent policy files
One file per agent, drop-in/drop-out:
# policies/scout7.yaml
name: scout7
agent: "scout7"
rate_limit:
max_per_minute: 30
rules:
- tools: ["searxng.*", "fetch.*", "ollama.*", "memory.*"]
action: allow
- tools: ["*"]
action: deny
Files are loaded alphabetically after inline policies:. Duplicate names produce an error.
Policy hot-reload
Policies are reloaded automatically when files change — no restart required. The daemon watches:
config.yaml(inlinepolicies:section)policy_dir/(all*.yamlfiles)
Changes are debounced (200ms) and validated before applying. If the new YAML is invalid, the current policies are kept and the error is logged. Rate limits defined in policies are also reloaded.
# Add a new agent policy at runtime — takes effect in <1s
echo 'name: temp-agent
agent: "temp"
rules:
- tools: ["weather.*"]
action: allow' > policies/temp.yaml
# Remove it — reverts immediately
rm policies/temp.yaml
Hot-reload covers policies and rate limits only. Changes to MCP servers, CLI tools, or OpenAPI specs require a restart.
Supervisor mode
supervisor:
enabled: true # hide approval tools from agents
expose_content: false # redact raw params → structural metadata
supervisor_agents: # agent IDs (glob) allowed to see approval tools
- "supervisor-*"
When enabled, approval.resolve and approval.pending are hidden from agents — only an external supervisor can resolve approvals. See docs/supervisor-protocol.md.
Agents matching supervisor_agents globs are whitelisted: they see and can call approval tools even in supervisor mode. This enables a Managed Agent (e.g. Claude via MCP Streamable HTTP) to act as a cloud supervisor — connecting to POST /mcp with Authorization: Bearer agent:supervisor-claude and resolving approvals with Claude's judgment.
Memory integration
Persist approval decisions as queryable facts in mem7. Fire-and-forget — a failing mem7 never blocks approvals.
When configured, every approval resolve (approve, deny, timeout) is written to mem7 as a fact with tags [decision, approved|denied, <tool>, agent:<id>].
Auto-approve from past decisions — when memory.url is set, mesh7 queries mem7 before submitting to the approval queue. If a tool+agent pattern has 3+ consistent approvals with 0 rejections, it is auto-approved (traced as supervisor:mem7). Governance gets less intrusive over time without getting less safe.
supervisor:
auto_approve: true # default true when memory.url is set
min_approvals: 3 # threshold for auto-approve (default 3)
The auto-approve is a pre-filter (Level 1). If it can't resolve, the request proceeds to the external supervisor (if running) or human. If mem7 is down, the request is escalated — never blocked. See docs/mem7-auto-approve.md for a step-by-step example.
Authentication
Two planes, two guards:
auth:
# Control plane (traces, grants, approvals, policies, sessions, metrics).
# When set, these endpoints require `Authorization: Bearer <token>`.
# When empty, they are restricted to loopback callers only.
# MESH_ADMIN_TOKEN env overrides this value.
admin_token: "a-long-random-secret"
# Data plane: validate agent identity via JWT against an external IdP.
jwt:
jwks_url: https://idp.example.com/.well-known/jwks.json
issuer: https://idp.example.com # optional
audience: mesh7 # optional
agent_claim: sub # claim used as agent id (default: sub)
user_claim: "" # claim naming the human the agent acts for (default: off)
allow_legacy: false # keep plaintext "agent:<id>" off when JWT is on
# Reject data-plane requests with no credentials (401) instead of letting
# them resolve to "anonymous" and fail closed at the policy engine.
require_authentication: false
| Setting | Guards | Default behavior when unset |
|---|---|---|
admin_token |
Control plane — a caller here can mint grants and resolve approvals, overriding what policies enforce | Loopback-only |
jwt |
Data-plane identity — cryptographic agent id instead of the spoofable agent:<id> header. With user_claim set, a delegation-shaped token also carries the human the agent acts for, recorded on every trace (user_id) and OTel span (enduser.id) |
Plaintext identity accepted |
require_authentication |
Anonymous access to /tools and /mcp-servers enumeration |
Anonymous allowed, governed by policy |
The data plane (tool calls, /decide, /mcp, /health) is never gated by admin_token. Details: control-plane auth and JWT authentication.
Other settings
port: 9090 # HTTP port (default 9090)
storage_path: state.db # SQLite durable state (approvals, grants survive restarts)
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 approval