Skip to content

Agent Security

Tools give an AI agent the ability to act. Once an agent can write a file, send an email, call an API, or run a command, the relevant question is no longer "is the model good?" but "what can this agent do, and who said it could?". flux7-mesh answers that question at the tool-call layer, and it also keeps the answer honest under attack.

This page maps the agent-security threat model to the mechanisms mesh7 provides, and records how those mechanisms were hardened by auditing the mesh against itself.

The threat model

An agent runtime is an attacker-influenced system. The model's input includes web pages, file contents, tool results, and other agents' output, and any of these can carry instructions. Therefore the following threats are real:

Threat What it looks like OWASP
Excessive agency An agent calls a tool it should never have been allowed to LLM06
Privilege escalation A constrained agent grants itself, or assumes, more rights ASI03
Indirect prompt injection A poisoned web page / file makes the agent run a dangerous tool call LLM01
Human-in-the-loop bypass The "a human must approve" gate is circumvented ASI09
Identity spoofing An agent claims to be a different, more privileged agent ASI03
Supply-chain / tool poisoning A compromised upstream tool server redirects or exfiltrates traffic ASI04
Cascading failure / DoS One agent exhausts shared resources and takes down governance for all ASI08

mesh7 does not try to make the model trustworthy. It assumes the model can be manipulated and constrains what its tool calls can achieve.

The mechanisms

Every tool call, whether over MCP stdio, MCP Streamable HTTP, or the REST API, travels the same pipeline: identity → rate limit → policy → (grant) → (auto-approve) → (human approval) → forward → trace. Each stage answers one question.

Mechanism Defends against Page
Per-agent policy, first-match, fail-closed excessive agency, unauthorized tools Writing Policies
human_approval action (blocking) high-stakes actions without oversight Approval Flow
JWT identity (strict) identity spoofing JWT Authentication
Control-plane auth (admin token / loopback) tampering with traces, grants, approvals Control Plane Auth
Injection detection before auto-approve indirect prompt injection Memory Integration
Rate limiting + loop detection runaway agents, resource exhaustion Writing Policies
MCP session binding session hijack by a caller holding another agent's Mcp-Session-Id this page
Dispatcher floor (default_action) a broad glob handing over undeclared CLI subcommands Configuration
Harness hook (mesh7-hook) built-in harness tools (Bash, Write, WebFetch) escaping policy Python SDK
SSRF-guarded egress tool/spec poisoning, metadata theft this page
Full JSONL trace, hash-chained (HMAC with MESH_TRACE_KEY) "who approved that?", audit, forensics, silent edits of the trace file Trace Integrity, Observability

Two principles run through all of them:

  • Fail closed. No matching policy means deny. Missing credentials mean anonymous, and anonymous has no rights unless you grant them. The default is always the safe one.
  • The mesh governs the mesh. Actions that modify governance (minting a grant, resolving an approval, reading the trace history) are themselves privileged operations, not free side-doors.

Hardening: auditing the mesh against itself

A governance tool that can be bypassed is theatre. To check that mesh7 is not theatre, we pointed audit7 (a RAG-powered audit agent that runs through the mesh and stores findings in flux7-memory) at flux7-mesh's own source, with the OWASP LLM and Agentic corpora as its knowledge base.

It found real issues. The interesting ones were structural:

Self-escalation via grants. The virtual grant.create tool was dispatched before policy evaluation, so any agent could mint itself a * grant and bypass its own human_approval rules. A grant is sudo; minting one had no guard. Fixed: grant mutation is now reserved to declared supervisor agents.

Its twin: self-approval. A second audit pass found the sibling we had missed: approval.resolve was callable by any agent in the default mode, so an agent could approve its own pending request. It was the same class of bug, through another door to the same room. The lesson: when you guard one privileged primitive, look for its twin. Both are now gated identically.

Identity spoofing past JWT. The legacy Bearer agent:<name> form was checked before JWT validation, so it worked even with JWT configured. Hence the cryptographic check was decorative. Fixed: with JWT on, plaintext identity claims are rejected (strict by default).

Injection slipping through one transport. The injection check that gates auto-approval lived only on the HTTP path; the MCP path (the busiest one, used by Claude Code and Cursor) had none. A poisoned tool call could be silently auto-approved. Fixed: the guard now lives in one shared function both paths call.

SSRF via callbacks and spec URLs. Agent-supplied callback URLs and OpenAPI spec URLs were fetched without protection, vulnerable to DNS rebinding and cloud metadata theft (169.254.169.254). Fixed: outbound fetches go through an SSRF-guarded client that checks every resolved IP at dial time.

MCP session riding. A later review found that on the Streamable HTTP transport the Mcp-Session-Id was treated as enough: identity was resolved when the session opened, and any later request carrying the ID inherited that agent's policy. Fixed: identity is resolved on every request and must match the agent (and user) that opened the session, otherwise the call gets 403. Closing a session with DELETE /mcp requires the same ownership, and session IDs are always minted server-side, never adopted from the client on initialize.

Every fix followed the same shape, which is the single most useful pattern for agent security work:

A safe, explicit default plus an opt-in escape hatch. Control plane → loopback-only by default, admin_token to open it. Identity → JWT-strict by default, allow_legacy to relax it. Transport → plaintext with a warning by default, tls.cert_file to encrypt. Data plane → anonymous-allowed (policy-governed) by default, require_authentication to lock it.

The default never silently exposes anything; the capability remains available when the deployment needs it.

Production hardening checklist

For any deployment where the mesh is reachable beyond loopback:

  • [ ] Identity: configure auth.jwt and leave allow_legacy off, so agent identity is cryptographically proven.
  • [ ] Control plane: set auth.admin_token (or MESH_ADMIN_TOKEN), and never rely on loopback when the port is exposed.
  • [ ] Transport: terminate TLS at your ingress, or set tls.cert_file / tls.key_file for standalone hosts. Don't expose the port in plaintext.
  • [ ] Data plane: set auth.require_authentication: true to reject anonymous callers with 401 on POST /tool/*, POST /decide, POST/DELETE /mcp, and to stop registry enumeration (/tools, /mcp-servers).
  • [ ] Supervisors: declare supervisor.supervisor_agents explicitly. Only those agents can mint grants or resolve approvals over MCP.
  • [ ] Policies: prefer strict: true for CLI tools, write glob policies (terraform.*) so undeclared commands are covered, and keep a final "*" → deny so nothing slips through.
  • [ ] Limits: set per-agent rate_limit so a runaway or hostile agent can't exhaust shared resources.
  • [ ] Traces: set MESH_TRACE_KEY in the service environment so the trace chain is an HMAC, and check it with mesh7 trace verify or GET /traces/verify. See Trace Integrity.

See also