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"]
env: # optional, added to mesh7's own environment
NODE_OPTIONS: "--max-old-space-size=512"
- name: remote-service
transport: sse
url: "https://mcp-server.example.com/sse"
headers: # ${VAR} expanded from mesh7's environment
Authorization: "Bearer ${REMOTE_TOKEN}"
- name: memory
transport: streamable-http
url: "http://localhost:9070/mcp"
forward_identity: true # send the calling agent in tools/call _meta
headers:
Authorization: "Bearer ${MEM7_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.
env applies to stdio servers: the child inherits mesh7's environment plus
these entries. Values are taken literally (no ${VAR} expansion).
An upstream that cannot be reached when mesh7 starts is retried in the
background (after 5 s, then twice as long each time, up to 5 min) until it
answers; meanwhile GET /mcp-servers lists it as retrying with its last
error, and its tools appear as soon as it connects. Clients connected to the
mesh over MCP see them on their next tools/list.
headers values go through ${VAR} expansion, so a bearer token stays in the
service's environment file instead of this YAML. memory.token is expanded the
same way.
Every tools/call sent to an MCP upstream carries the W3C traceparent of the
governed call in its _meta, the field the MCP specification reserves for
request metadata. With forward_identity: true, it also carries the agent the
mesh authenticated (art.flux7/agent). Leave it off for remote servers: they
have no use for your agent names. mem7 uses both to sign what it stores and to
scope what each agent reads (see mem7 provenance and scopes).
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). They get the same policy and the 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"]
- name: make
bin: make
working_dir: /srv/project # cwd of the process (default: mesh7's cwd)
env: # added to a minimal PATH/HOME/LANG environment
CI: "true"
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).
A CLI process does not inherit mesh7's environment: it gets PATH, HOME, LANG=en_US.UTF-8 and the entries of env, nothing else. Secrets present in the daemon's environment therefore do not leak to wrapped binaries unless you list them.
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. Therefore a glob such as terraform.*: allow cannot hand over destroy along with plan. See CLI Tools.
Policies
Policies are YAML-based and first-match-wins, with 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 but does not
make 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, without a restart. 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 # MCP agents block on human_approval until resolved
expose_content: false # redact raw params → structural metadata
supervisor_agents: # agent IDs (glob) allowed to see approval and grant tools
- "supervisor-*"
The operator tools exposed over MCP (approval.resolve, approval.pending, grant.create, grant.revoke) are visible and callable only by agents matching supervisor_agents, whether or not enabled is set. Any other agent gets them neither in tools/list nor on call, and resolves approvals through the admin-gated HTTP API or the mesh CLI. grant.list and mesh.catalog stay visible to everyone.
enabled changes what an MCP agent experiences on human_approval: instead of receiving an immediate "pending" result, the call blocks until a supervisor (or a human) resolves it, then returns the outcome. See Supervisor Protocol.
This enables a Managed Agent (e.g. Claude via MCP Streamable HTTP) to act as a cloud supervisor, connecting to POST /mcp with an identity that matches supervisor_agents and resolving approvals with Claude's judgment.
Memory integration
Persist approval decisions as queryable facts in mem7. The write is fire-and-forget, so 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|timeout, <tool>, by:<human|supervisor|mem7|system>, agent:<id>].
Auto-approve from past decisions: when memory.url is set, mesh7 asks mem7 before submitting to the approval queue. A tool that reads, with at least min_approvals human approvals of exactly this tool and agent and no refusal, is approved at once (traced as supervisor:mem7). Approvals by a supervisor or by auto-approval itself never count, and writes are never approved from precedents unless auto_approve_writes is set.
supervisor:
auto_approve: true # default true when memory.url is set
min_approvals: 3 # human approvals needed (default 3)
auto_approve_writes: false # default: precedents approve reads only
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 rather than blocked. See Auto-approve from flux7-memory for why these limits exist and a step-by-step example. These settings can be changed at runtime: Approval flow.
Authentication
The auth block guards two planes:
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 data-plane access: POST /tool/*, POST /decide, POST/DELETE /mcp, and /tools, /mcp-servers enumeration all return 401 without a credential |
Anonymous allowed, governed by policy (fails closed) |
The data plane (tool calls, /decide, /mcp, /health) is never gated by admin_token. Details: control-plane auth and JWT authentication.
Tool catalogue
pin_tools: true # fingerprint upstream MCP tools; hold back new and changed ones
hide_denied_tools: true # leave out of tools/list what the policy can only deny
Both are off by default: on a machine where the operator is also the user, the full list and no pinning is usually what is wanted.
pin_tools keeps a fingerprint of every upstream MCP tool (description, parameter schemas, annotations) in storage_path. A server seen for the first time is trusted and pinned as is; afterwards a tool it adds is denied and a tool that changed asks for approval, whatever the policy says, until it is accepted. Without storage_path the pins live in memory and are re-trusted at every start.
hide_denied_tools removes from an MCP client's tools/list every tool whose every path through the policy ends in deny for that agent, so the model never sees it. A tool with an approval or a conditional allow stays listed, and calls are enforced either way.
Details: Tool Classification.
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, hash-chained (MESH_TRACE_KEY → HMAC)
otel_endpoint: /path/to/traces-otel.jsonl # or "stdout" or "http://localhost:4318"
otel_headers: # sent on every OTLP/HTTP request, ${VAR} expanded
Authorization: "Bearer ${OTLP_TOKEN}"
otel_ca_cert: /etc/mesh7/collector-ca.pem # PEM appended to the system roots
otel_insecure_skip_verify: false # self-signed local collector only
approval:
timeout_seconds: 300 # approval TTL (default 5 min)
notify_url: https://hooks.slack.com/... # webhook on new pending approval
channel: tty-fallback # queue | tty | tty-fallback (default)
wait_seconds: 0 # MCP: wait for a supervisor's decision before answering (0 = none, max 10)
tls: # optional in-binary TLS, both fields required
cert_file: /etc/mesh7/tls.crt
key_file: /etc/mesh7/tls.key
approval.channel routes human_approval for MCP tool calls: queue always enqueues (daemons, supervisor setups), tty requires the interactive /dev/tty prompt and denies when none is available, tty-fallback tries the prompt and falls back to the queue. Any other value is a config error. REST calls (POST /tool/*) always use the queue.
tls serves HTTPS directly when both cert_file and key_file are set. Without it mesh7 serves plaintext and logs a warning; the recommended deployment keeps it on loopback or an internal network behind an ingress that terminates TLS.
Every line of trace_file is chained to the previous one; set MESH_TRACE_KEY in the service environment to make it an HMAC chain, and check it with mesh7 trace verify or GET /traces/verify. See Trace Integrity and Observability for OTLP delivery (batches, retries, trace context).