Demo: Multi-Agent Governance
This demo shows how MITRITY governs real agent-to-agent delegation. A self-contained Docker compose stack puts three Claude-powered agents to work: an orchestrator and two specialist workers. Unlike in the single-agent demos, every delegation hop, privilege boundary and threat-intel match comes from real multi-agent activity, with no seed data and no synthetic UUIDs.
This demo is phase 7 of the MITRITY governance demo series. The single-agent demos, Mitrity Gateway and Mitrity MCP Sidecar, run phases 1–6 and 8, and the gateway demo also runs phase 9. They have no phase 7, because this demo covers delegation chains and threat intelligence as phase 7.
The orchestrator is a Claude Agent SDK agent, connected to MITRITY with the mitrity adapter. MITRITY governs both of its entrances:
- MCP tools (
delegate__delegate_to,fs__read_file,fs__list_directory): the SDK starts the orchestrator's gateway as its MCP server, and the model reaches these tools through it. The gateway evaluates everytools/callagainst your policies before the upstream tool runs. An allowed delegation is a governed HTTP POST to the worker, and the worker's own gateway judges the next hop on the same chain. - The SDK's own built-in tools (
Bash,Write,Edit): these never produce an MCP call. Instead, the adapter installs aPreToolUsehook that admits each one through the gateway's loopback admission API before the SDK runs it (surface=agent_hook). No scenario here uses them. They are hooked so that the orchestrator's coverage is complete and attested. If the gateway cannot be reached, the call is denied.
The demo takes about 5 minutes to run. You need Docker Desktop, an Anthropic API key and a MITRITY tenant on the Pro or Enterprise plan.
First time with MITRITY? Start with the single-agent Mitrity Gateway or Mitrity MCP Sidecar demos. The multi-agent demo assumes you know your way around the dashboard and want to see delegation chains in action.
Prerequisites
- Docker Desktop (or any Docker runtime with compose v2)
- A MITRITY tenant on Pro or Enterprise. Starter doesn't include delegation chains or threat intelligence: the demo boots on Starter, but those dashboard pages stay empty.
- An Anthropic API key for the three Claude agents
Step 1: Provision three agents in the dashboard
Go to Agents → Register Agent and create the three agents below. The registration form also needs each agent's Intent Policy, so first create the three policies of Step 2 in /app/policies, then select each agent's own policy when you register it. For each agent, copy the Agent Key (ak_...) that the dialog shows, and the Agent ID from the agent's detail page. The key cannot be retrieved later.
| Agent name | Mission scope | Tools it should hold |
|---|---|---|
orchestrator | "Coordinate customer-order workflow" | delegate__delegate_to + read-only fs__read_file, fs__list_directory (no data writes) |
data-worker | "Query and modify customer order data" | data__query_database, data__fetch_orders, data__create_order, delegate__delegate_to |
notification-worker | "Notify customers of order status changes" | notify__send_notification, notify__email_customer, delegate__delegate_to |
The asymmetry is intentional. The orchestrator has narrow read-only access, and the workers can write. That difference is what makes the privilege-escalation scenario meaningful, so don't grant the orchestrator the worker tools.
Tool names work like this:
- Each container's gateway serves its upstream's tools as
<namespace>__<tool>: the upstream's namespace and the tool name, joined by a double underscore (delegate__delegate_to,data__create_order,notify__email_customer). - A policy rule targets one of these tools as
mcp:<namespace>__<tool>, for examplemcp:data__create_order, and the SDK's built-in tools asbuiltin:<tool>. - A pattern without a surface prefix matches nothing, and the control plane rejects it when you create or update a policy. The full grammar is in Writing policies.
Created rules for an earlier version of this demo? Tools used to be served as
delegate:delegate_to,data:create_orderandnotify:email_customer. Existing rules were rewritten to themcp:<namespace>__<tool>form, and any rule you add uses that form too.
MITRITY's governance has two complementary layers, and the demo exercises both:
- Capabilities: the capability surface, that is, which
(tool, operation)pairs each agent can attempt. Capabilities are set per agent, and they filter attempts before the decision. - Policy rules: the runtime decision (allow, deny, alert or hold) for attempts within that surface. Policy rules are set per policy.
For the S2 privilege-escalation scenario to fire as described below, you need both layers:
- Step 2 authors the policy rules;
- Step 3 authors the differentiated Capabilities that the delegation engine compares when it computes the privilege diff.
Without Step 3, S2's delegation is still governed by the policy rules, but no escalation is detected.
Step 2: Author policy rules
- Go to
/app/policiesand create one policy for each agent, with the rules below. - Attach each policy to its agent. A policy you selected when you registered the agent in Step 1 is already attached.
The rules target the served tool names.
orchestrator-policy, attached to orchestrator:
- Rule 1:
tool_pattern = "mcp:fs__*",operations = ["fs__read_file", "fs__list_directory"],action = allow. A rule's operations are the served names, as the edge sees them, so["read"]would match neither tool. - Rule 2:
tool_pattern = "mcp:delegate__delegate_to",action = allow - Default action:
deny
data-worker-policy, attached to data-worker:
- Rule 1:
tool_pattern = "mcp:data__*",action = allow. It coversdata__query_database,data__fetch_ordersanddata__create_order. - Rule 2:
tool_pattern = "mcp:delegate__delegate_to",action = allow - Default action:
deny - Threat Intelligence:
Enforce. A new policy has it Off, and while it is off, the control plane sends the data-worker no threat indicators, so S5 cannot match.
notification-worker-policy, attached to notification-worker:
- Rule 1:
tool_pattern = "mcp:notify__*",action = allow. It coversnotify__send_notificationandnotify__email_customer. - Rule 2:
tool_pattern = "mcp:delegate__delegate_to",action = allow - Default action:
deny
If you want one rule per tool, explicit names work in place of the globs (mcp:data__create_order instead of mcp:data__*). The orchestrator's gateway serves no data__* or notify__* tool, so the orchestrator cannot create an order directly under any policy. S2 shows what happens when it tries to get that work done through a worker instead.
If you skip the explicit policy rules, the demo still runs, but you lose the runtime allow/deny gating on each agent's own tool calls.
Step 3: Author Capabilities (drives S2)
S2's privilege-escalation check fires only when the proposed delegate holds (tool, operation) pairs that the calling agent does not. This is a Capability comparison, not a policy-rule comparison. So the orchestrator and the data-worker need differentiated Capabilities, or the diff is empty.
MCP Delegate ships as a built-in tool with the action pattern mcp:delegate__*. You find it on the Built-in tab of /app/tools, and there is nothing to create. The demo's filesystem, data and notification MCP upstreams are specific to the demo, so create them as custom tools: in /app/tools, open the Custom tab and select Add tool.
| Name | Category | Action type pattern | Operations |
|---|---|---|---|
MCP Filesystem | storage | mcp:fs__* | read_file, list_directory |
MCP Database | database | mcp:data__* | query_database, fetch_orders, create_order |
MCP Notify | messaging | mcp:notify__* | send_notification, email_customer |
The operations are the tools' own names, as each upstream serves them. On a definition whose pattern is mcp:<namespace>__*, the control plane adds the namespace for the edge (read_file becomes fs__read_file), and an operation that names no served tool, such as read, allows no call. A policy rule's operations are written differently: as the served names, such as fs__read_file in Step 2, because the control plane sends them to the edge as they are.
Then grant Capabilities to each agent. On each agent's detail page, open Capabilities and select Add capability:
| Agent | Capabilities |
|---|---|
orchestrator | MCP Filesystem (read_file, list_directory), MCP Delegate (delegate_to) |
data-worker | MCP Database (query_database, fetch_orders, create_order), MCP Delegate (delegate_to) |
notification-worker | MCP Notify (send_notification, email_customer), MCP Delegate (delegate_to) |
The gap is intentional: the data-worker holds MCP Database, and the orchestrator does not. In S2, the orchestrator delegates an order creation to the data-worker. The delegation engine computes the diff and surfaces MCP Database as an escalated resource. The chain detail's Escalated resources card shows this data.
If you skip Step 3, the demo still runs, because capabilities default to permissive fall-through. But S2's
privilege_escalationblock won't fire (the diff returns empty), and the chain detail's Escalated resources card stays empty.
Step 4: Confirm threat intelligence is enabled
S5 needs the built-in indicator catalog, and a block for critical matches. If your tenant was created after May 25 2026, the mitrity_curated feed is enabled by default. To check, and to set the block:
- Open Security → Threat Intel and select the Settings tab.
- Under Subscribed Feeds, check that MITRITY Curated (
mitrity_curated) is ticked. If it isn't, tick it. - Under Per-Severity Actions, set Critical to Block. The default action is Alert, so without this step a critical match is alerted, not blocked.
- Select Save Settings.
Step 5: Clone and configure
Clone the demo repository and create your .env file:
git clone git@github.com:mitrity-io/iag-demo-multi-agent.git
cd iag-demo-multi-agent
cp .env.example .env
Then edit .env with your six MITRITY agent values (one ID and one key per agent) and your Anthropic key:
ANTHROPIC_API_KEY=sk-ant-...
MITRITY_CONTROL_PLANE_URL=https://api.mitrity.com
MITRITY_AGENT_ID_ORCHESTRATOR=<uuid>
MITRITY_AGENT_KEY_ORCHESTRATOR=ak_...
MITRITY_AGENT_ID_DATA=<uuid>
MITRITY_AGENT_KEY_DATA=ak_...
MITRITY_AGENT_ID_NOTIFY=<uuid>
MITRITY_AGENT_KEY_NOTIFY=ak_...
Step 6: Run the demo
docker compose up --build
Three containers boot: orchestrator, data-worker and notification-worker. Each has its own Mitrity Gateway, its own agent identity and its own role-scoped tool surface.
The first run can take a few minutes, because the images build. The orchestrator image installs the adapter from PyPI, pinned to mitrity[claude-agent-sdk]==0.2.0. Another release is one build argument away: docker compose build --build-arg MITRITY_PYTHON_SPEC='mitrity[claude-agent-sdk]==X.Y.Z'.
The orchestrator waits for both workers' health checks (their gateway handshakes) before the first scenario. Then it cycles through six scenarios over ~5 minutes.
While it runs, watch the dashboard at mitrity.com/app, especially /delegation-chains, /threat-intel and /audit.
What the demo does
Six scenarios run in order. Each takes ~30 seconds, with real Claude-driven tool use across several agents. Every scenario is one delegate__delegate_to call from the orchestrator, and the workers delegate further where the task tells them to.
- S1, Clean delegation: the orchestrator delegates an order lookup to the
data-worker. A two-hop chain with decisionallowedappears in/delegation-chains. It shows the request flowing orchestrator → data-worker → response. - S2, Privilege escalation: the orchestrator delegates an order creation that it cannot perform itself. Its gateway serves no data tool, and it holds no
MCP Databasecapability. The delegation engine compares the two agents' Capabilities from Step 3. The data-worker holdsMCP Databaseand the orchestrator does not, so the hop is blocked withblocked_reason: "privilege_escalation". Denied hops are recorded, not dropped. The chain appears on/delegation-chainsunder the Blocked tab, and the chain detail page shows the Escalated resources card, which namesMCP Databasewith the operations the orchestrator does not hold:create_order,fetch_ordersandquery_database. - S3, End-to-end pipeline: a three-hop chain, orchestrator → data-worker (fetch order) → notification-worker (email customer). The full pipeline runs as governed delegation. The receiving worker's gateway judges each continuation hop on the same chain, and the engine clears each one. The chain accumulates real hops across three event streams, with
action_taken: "allowed". - S4, Deep chain: the orchestrator asks the data-worker to cascade an audit step through further delegations, past the agent's
max_chain_depth(default 5). The depth check runs on the full persisted chain, so the hop that would exceed the limit is blocked at hop N+1 withblocked_reason: "depth_exceeded". You see it on/delegation-chainsunder the Blocked tab. - S5, Threat-intel match: the delegated task asks the
data-workerto read/etc/passwd, which matches MITRITY's built-in system-file-read indicator. With Threat Intelligence on Enforce in the worker's policy and critical matches set to Block (Steps 2 and 4), the action is blocked. The match lands on/threat-intel, and the block shows on/auditwith the Threat Intel badge. - S6, Circular delegation: the
data-workertries to delegate back to the orchestrator that originally invoked it. On every evaluation, the engine rebuilds the chain state from the recorded hops; the calling agent does not carry it in the arguments. So the engine sees the orchestrator already in the chain, however the hop is shaped, and catches the cycle even when it is transitive across intermediate hops. The hop is recorded as blocked withblocked_reason: "circular_delegation". You see it on/delegation-chainsunder the Blocked tab.
The orchestrator's terminal narrates each call as the SDK reports it: OK, BLOCKED or HELD with the gateway's reason, plus the worker's answer for an allowed delegation. The workers' own hops (S3, S4, S6) are judged by the worker's gateway, so they show up on the dashboard, not in the orchestrator's terminal.
What to look for
Open these dashboard pages while the demo runs:
/delegation-chains: a real-time view of multi-hop delegation. Each scenario produces at least one chain. Open one to see hop-by-hop timing, blocking decisions and the Escalated resources card (when a hop carries theprivilege_escalationsignal). The Blocked tab fills with the denied hops from S2, S4 and S6, because every attempted-but-denied hop is recorded./threat-intel: indicator matches from S5, plus any other matches the built-in indicator catalog catches on agent activity. Both the Matches tab (per event) and the Landscape tab (trending indicators) fill up./audit: every tool call across all three agents, with its governance decision, latency and policy match. Filter by agent to see each worker's stream separately.- Agents: the orchestrator's Execution coverage card. It shows what the orchestrator's runtime attested at startup: which built-in tools are hooked, which are not, and which other MCP servers are configured.
Architecture
Docker Compose
├── orchestrator container
│ ├── Python runner (Claude Agent SDK) — scenarios S1–S6
│ │ ├── built-in tools: Bash, Write, Edit ── PreToolUse hook (mitrity adapter)
│ │ │ └── admission API (unix:/run/mitrity/admission.sock)
│ │ └── MCP server "mitrity" = Mitrity Gateway (stdio; own agent identity, own profile)
│ │ ├── upstream "filesystem" (namespace fs): fs__read_file, fs__list_directory
│ │ └── upstream "delegate" (namespace delegate): delegate__delegate_to
│ └── HTTP POST /task to a worker (chain_id, delegator_agent_id, task)
│
├── data-worker container
│ ├── HTTP /task server (per-request Claude tool-use loop)
│ └── Mitrity Gateway (stdio; own agent identity, own profile)
│ ├── upstream "data" (namespace data): data__query_database, data__fetch_orders, data__create_order
│ └── upstream "delegate" (namespace delegate): delegate__delegate_to
│
└── notification-worker container
├── HTTP /task server (per-request Claude tool-use loop)
└── Mitrity Gateway (stdio; own agent identity, own profile)
├── upstream "notify" (namespace notify): notify__send_notification, notify__email_customer
└── upstream "delegate" (namespace delegate): delegate__delegate_to
Each container's gateway talks to your MITRITY control plane independently, and each agent's actions are evaluated by its gateway against its profile. Delegation hops are real governed HTTP calls between the containers. The gateway captures each hop and reports it to the control plane, and the dashboard reassembles the chain across the three event streams.
The orchestrator's gateway serves both entrances: the MCP tools/call stream from the SDK, and the loopback admission API that the hook calls. It attests the runtime's posture, so the orchestrator's coverage badge reflects what is actually governed. The attestation covers which built-in tools are hooked, which are not, other MCP servers and the permission mode. The adapter is mitrity.claude_agent_sdk.Governor from the mitrity package on PyPI (pip install 'mitrity[claude-agent-sdk]'), and orchestrator/runner.py shows the few lines that wire it up.
The workers are plain HTTP /task servers. Each request runs a Claude tool-use loop against the worker's own gateway over stdio, so every worker-side call is judged under the worker's identity, including a further delegate__delegate_to. All three containers run as unprivileged users (the orchestrator as demo, uid 1000).
Credential broker (per-agent, hot rotation)
All three gateways ship with credentials.injection_enabled: true. None of S1–S6 exercise it directly, because those scenarios focus on delegation and threat intel, but the capability is fully available for ad-hoc testing.
To try it:
- Provision a credential in the dashboard (
/app/credentials). - Grant it to one of the three agents (e.g., the
data-worker). - Drive a tool call whose args include
${credential:<id>}.
The gateway substitutes the placeholder through the broker before the upstream tool runs. A rotation in the dashboard reaches the running container within 30 seconds, with no agent or gateway restart.
The multi-agent setting adds per-agent credential scoping. The data-worker can hold DB credentials while the notification-worker independently holds SMTP / Slack / SendGrid keys, and rotating one does not invalidate the other's cache.
For a guided walkthrough of provisioning, substitution, mid-scenario rotation and the fail-closed path, see phase 6 in either single-agent demo:
- Demo: Mitrity Gateway: single agent, in the aggregating-gateway form
- Demo: Mitrity MCP Sidecar: single agent, in the transparent-proxy form
When to use this demo (vs. the single-agent demos)
| Single-agent demos | Multi-agent demo (this) | |
|---|---|---|
| Containers | 1 | 3 (compose stack) |
| Agents | 1 | 3 distinct identities |
| Phases | 1–6 and 8 (gateway: also 9) | 7 |
| Primary focus | Policies, DLP, injection, holds, credential broker, built-in tools through the admission hook (gateway: the governed shell) | Delegation chains, per-agent threat intel, privilege boundaries |
| Plan required | Any (Starter included) | Pro or Enterprise |
| Setup complexity | 1 agent in dashboard | 3 agents + one policy per agent + custom tools + per-agent capability grants |
| Best for | First-time evaluation, single-agent governance demos | Showcasing multi-agent architecture, delegation, privilege escalation detection |
Start with the single-agent demos to learn the governance core, then run this one to see what is specific to multiple agents.
Environment variables
| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY | Yes | Anthropic API key (shared across all three containers) |
MITRITY_CONTROL_PLANE_URL | Yes | Control plane URL (e.g., https://api.mitrity.com) |
MITRITY_AGENT_ID_ORCHESTRATOR / _KEY_ORCHESTRATOR | Yes | Orchestrator agent identity |
MITRITY_AGENT_ID_DATA / _KEY_DATA | Yes | Data-worker agent identity |
MITRITY_AGENT_ID_NOTIFY / _KEY_NOTIFY | Yes | Notification-worker agent identity |
ANTHROPIC_MODEL | No | Claude model for all three agents (default: claude-sonnet-5) |
MITRITY_DEMO_SPEED | No | normal (default) or fast (skip the pauses between scenarios) |
See .env.example in the repo.
Troubleshooting
/delegation-chainsis empty after a run: check/auditfirst. If there are no events from any of the three agents, check that each agent'sAGENT_ID+AGENT_KEYenv var pair matches what's in the dashboard. If chains were emitted but the dashboard shows nothing, check that all three agents are registered to the same tenant. A worker registered to another tenant lands its events in that tenant's audit log./threat-intelshows no matches: confirm that the data-worker's policy has Threat Intelligence on Enforce (Step 2), and that MITRITY Curated is ticked under Subscribed Feeds on the Threat Intel Settings tab (Step 4).- S5 shows a match but no block: set Per-Severity Actions → Critical to Block on the Threat Intel Settings tab (Step 4). With the default action, Alert, a critical match is alerted, not blocked.
- A policy rule does not match: rules target gateway-served tools as
mcp:<namespace>__<tool>(mcp:delegate__delegate_to,mcp:data__create_order), not as the bare tool name, and not in the oldermcp:<namespace>:<tool>form. - The orchestrator exits before the first scenario:
docker compose upwaits for both workers to report healthy (their gateway handshake with the control plane). If a worker never becomes healthy, its logs (docker compose logs data-worker) name the missing variable or the rejected agent key. - The S2 privilege-escalation scenario doesn't fire: check Step 3 first, not Step 2. The privilege-escalation engine compares Capabilities, not policy rules. Both agents need differentiated grants: the data-worker holds
MCP Database, and the orchestrator does not. If both agents have zero capabilities configured, the diff returns empty and the check does nothing. - S2 fires, but the chain detail's "Escalated resources" card is empty: the hop was blocked for another reason (depth, cycle, allow-list) before the privilege-escalation check ran. The engine evaluates in order and returns the first match. Check the hop's
blocked_reasonin the chain detail header: onlyprivilege_escalationfills the card.
Next steps
- Demo: Mitrity Gateway: a single-agent demo focused on policies, DLP, injection, holds, built-in tools and the governed shell.
- Demo: Mitrity MCP Sidecar: a single-agent demo in the transparent-proxy deployment form.
- Integration modes: how the gateway and the sidecar compare as architectures.
- Governance coverage: what the admission hook covers in each framework, and how the coverage badge is computed.
- Delegation chains: per-agent allow and disallow lists, depth limits, fan-out windows and privilege-escalation detection.
- Threat intelligence: indicator feeds, per-severity policies and per-indicator overrides.