Stop runaway agents and approve risky actions
An agent can repeat a tool call, spend without limit, or take an action that should have required a person. DVARA puts policy, session tracking, loop detection, approval, and audit controls on the calls an agent makes. Your agent framework still plans and runs the workflow; DVARA governs each call that crosses a gateway.
Not included in DVARA Open Source.
Which controls are available on each plane?
| Traffic | What DVARA governs | Availability |
|---|---|---|
| Model calls | Policy, PII, guardrails, cost, audit, correlation through X-Session-Id, and, from 1.8.5, session kills | Enterprise only |
| MCP tool calls | Tool policy, PII, sessions, loop detection, session kills, human approval, and tool-call evidence | Enterprise only |
| A2A hops | Peer policy, PII, sessions, loop detection, session kills, human approval, and a separate hop audit chain | Enterprise only |
Give one workflow one session ID
Send the same X-Session-Id value on every model call, MCP tool call, and A2A
hop that belongs to one agent run. DVARA attaches it to logs, traces, audit
events, and MCP tool-call records where those records are produced.
X-Session-Id: invoice-triage-2026-09-11-001
Without this header, each call still passes through its configured governance controls, but DVARA cannot join the calls into an MCP session or detect a loop across them. From 1.8.5 a session kill uses the same ID on every plane, so one kill stops the workflow's model calls, tool calls and hops.
A session ID is supplied by the caller; it is not an identity credential. A session kill blocks that ID only inside the workspace that issued the kill; the same ID in another workspace remains active. A caller can still start a new ID. Revoke the API key or disable the workspace when you need to contain an actor rather than one workflow.
Detect repeated MCP tool calls
MCP loop detection is on by default on the Enterprise MCP plane. It checks the history for the supplied session ID before the tool reaches its upstream server.
| Check | Default |
|---|---|
| Same server and tool called consecutively | Block the fifth call |
| Repeating cycle | Block a cycle of 2–4 calls after 3 repetitions |
| Call rate | Block when the session exceeds 60 calls in 60 seconds |
| Stored history | Most recent 100 calls per session |
| Kill the session after detection | Off |
The history is held by the pod that receives the calls. Keep a session on one pod when you need exact loop detection across a multi-pod MCP deployment. Audit and tool-call records can still be stored centrally; that does not make the in-flight loop history fleet-wide.
Test the repetition rule
This example assumes that you registered an MCP server named crm, exposed a
tool named lookup_invoice, and created a workspace API key. Replace the key
before running it:
for attempt in 1 2 3 4 5; do
curl -sS -w '\nHTTP %{http_code}\n' \
http://localhost:8080/mcp/crm/tools/call \
-H 'Authorization: Bearer <workspace-api-key>' \
-H 'Content-Type: application/json' \
-H 'X-Session-Id: invoice-triage-2026-09-11-001' \
-d '{
"name": "lookup_invoice",
"arguments": {"invoice_id": "INV-1042"}
}'
done
With no stricter policy or approval rule, the first four calls reach the
registered server. The fifth call is rejected before dispatch with HTTP 429:
{
"error": {
"message": "Tool 'crm::lookup_invoice' called 5 times consecutively",
"type": "mcp_error",
"code": "mcp_agent_loop_detected",
"trace_id": "7cc59f7d2e934318aeb3a16792d97225"
}
}
The rejection writes AGENT_LOOP_DETECTED with the loop type, session, server,
tool, trace ID, and reason. Because auto-kill defaults to false, the session
is not killed; only the detected call is refused.
Tune the MCP detector
Configure install-wide defaults under dvara.mcp-gateway.agentic.loop-detection:
dvara:
mcp-gateway:
agentic:
loop-detection:
enabled: true
repetition-threshold: 5
cycle-max-length: 4
cycle-repetitions: 3
max-calls-per-minute: 60
auto-kill: false
history-size: 100
Override the settings that vary by workspace from its agentic-governance settings:
| Workspace setting | What it changes |
|---|---|
agentic.loop-detection.enabled | Turns MCP loop detection on or off for that workspace; it inherits true |
agentic.loop-detection.repetition-threshold | Changes the consecutive-call threshold |
agentic.loop-detection.max-calls-per-minute | Changes the one-minute rate threshold |
agentic.loop-detection.auto-kill | Decides whether a detection also kills the session; it inherits false |
Cycle length, cycle repetitions, and history size are install-wide settings in 1.8.
From 1.8.5 the MCP and A2A loop detectors see a session's calls on every
Gateway. Before, each Gateway kept its own history, so with calls spread over
several Gateways a loop was caught later, or not at all. Now Gateways in one
cluster share the history, and two Gateways refuse a loop at the same call as
one does. A check waits at most 50 ms
(dvara.agentic.loop-detection.cluster-timeout). When the shared history does
not answer, the Gateway judges the call on its own history and counts
dvara_loop_detector_fallback_total. A loop check never fails a call.
Stop an agent session
Open Agents → MCP Sessions in the DVARA Flightdeck, select the session, and choose Kill session. A workspace administrator or developer can do the same for its own session in the workspace-scoped Flightdeck pages.
From 1.8.5 a kill stops the session on every plane: its model calls, its tool calls and its A2A hops, on every Gateway, for 24 hours. Before 1.8.5 each plane read only its own kills, so a session killed from the MCP sessions page could go on delegating to peers and calling models.
A session seen only on the LLM plane has no MCP or A2A row to kill it from. For
that case, and for a session the Gateways have not reported yet, use the Kill
a session by id form at the top of the sessions page. Pick the workspace,
enter the X-Session-Id, and choose Kill session. The Flightdeck form is for
owner and policy-admin; the Flightdeck form is for admin and developer, in
their own workspace. It writes AGENT_SESSION_KILLED.
The action writes MCP_SESSION_KILLED. Each kill event carries
scope: all_planes. Later MCP calls carrying that session ID are rejected with
HTTP 403:
{
"error": {
"message": "Agent session has been killed: invoice-triage-2026-09-11-001",
"type": "mcp_error",
"code": "mcp_session_killed",
"trace_id": "5b8e4c0291ed4ad4a4faf55a92c79efa"
}
}
From 1.8.5 a model call on /v1 with the X-Session-Id of a killed session
is refused with HTTP 403:
{
"error": {
"message": "Agent session has been killed: invoice-triage-2026-09-11-001. Start a new session.",
"type": "session_killed",
"code": "session_killed",
"trace_id": "5b8e4c0291ed4ad4a4faf55a92c79efa"
}
}
The status is 403, not 429, so clients do not retry. GET /v1/models and
calls with no session ID are not checked. A model stream already running when
the kill lands finishes; the next call is refused. A hop on a killed session is
refused with A2A_SESSION_KILLED. A running A2A stream ends with one
a2a_session_killed error event. A tool call or hop held for approval is
denied at its next check, not left to wait for the approval timeout.
Every Gateway pod receives operator-issued kills through its signed revocation channel from Flightdeck. From 1.8.5 Flightdeck publishes a kill the moment it is saved and Gateways poll every 5 seconds, so a Flightdeck kill reaches every Gateway within 5 seconds. Before 1.8.5 it took up to about 15 seconds. The Flightdeck connection and the revocation refresh must still be healthy on every pod. A pod that has never reached the channel logs a warning and cannot enforce the kill.
When auto-kill: true, the pod that detects a loop rejects the current call and
marks the session killed. From 1.8.5 Gateways in one cluster pass that kill to
each other within milliseconds, without waiting for Flightdeck. Gateways that
are not in one cluster still get it from the revocation list, more slowly.
The first refusal of a killed session on each plane of each Gateway writes one
SESSION_KILL_ENFORCED event. Every refusal counts on
dvara_session_kill_enforced_total{plane,source}.
A kill stops a session, not an agent. An agent can start a new session ID. To stop an agent, revoke its API key.
Require a person to approve a tool call
The MCP approval engine is enabled by default, but no call pauses until a workspace has at least one required tool or server rule.
| Setting | Default | Purpose |
|---|---|---|
approval.required-tools | Empty | Comma-separated tool-name globs, such as delete_*,payment_refund |
approval.required-servers | Empty | Comma-separated server IDs |
approval.timeout-seconds | 300 | How long the call waits for a decision |
approval.default-action | deny | What happens when the wait expires |
Set these values in the workspace's approval settings. Use a narrow rule for actions with external effects—for example:
approval.required-tools: "delete_*,payment_refund"
approval.required-servers: "production-payments"
approval.timeout-seconds: 600
approval.default-action: "deny"
A match follows this flow:
- DVARA writes
MCP_APPROVAL_REQUESTED, stores a pending approval, and pauses the call. - A reviewer opens Agents → Approvals in the Flightdeck.
- Approve releases the call and writes
MCP_APPROVAL_GRANTED. - Deny refuses it with HTTP
403and writesMCP_APPROVAL_DENIED. - No answer within the configured window applies
approval.default-action. The default refusal returns HTTP408and writesMCP_APPROVAL_TIMEOUT.
Pending approvals are held by Flightdeck, and each Gateway pod asks Flightdeck
for the decision over its /internal channel, so a decision made through one
Flightdeck instance releases the waiting call on whichever pod holds it.
The data-plane call checks for a decision every second by default.
Changing approval.default-action to approve allows the action when no person
responds or when the approval service becomes unavailable. Keep deny unless
your risk owner has explicitly accepted that failure mode.
If the approval service is unavailable, DVARA applies the configured default
immediately instead of pretending a reviewer can act. It records
MCP_APPROVAL_UNAVAILABLE, which is different from an ordinary timeout. From
1.8.4, when the default action is deny, the caller receives HTTP 403
mcp_approval_unavailable (-32001 on native /mcp). Before 1.8.4 it was a
502, which clients retry against a control plane that is already down.
A client on native /mcp can see that its call is waiting. From 1.8.4 a
tools/call that carries a progressToken in _meta receives an MCP
notifications/progress as soon as it is held and every 5 seconds after, naming
the approval ID and its expiry. tools/list marks each tool the workspace holds
for approval with _meta["dvara/approval_required"]: true. See
See progress while a call waits for approval.
Review what the agent did
Open Agents → MCP Tool Calls to filter calls by workspace, server, and tool.
From 1.8.4 the list, and the session timeline, include calls a policy denied,
with decision DENY and status 403.
Open Agents → MCP Sessions to see a session summary and its ordered tool-call
timeline. A session is reconstructed from stored tool-call records; it is not
marked inactive merely because 60 minutes passed. You can export a session and
its timeline as JSON from the session detail page.
Use the audit guide for signed governance evidence. The events most useful during an agent incident are:
| Event | Meaning |
|---|---|
MCP_TOOL_INTENT / MCP_TOOL_RESULT | A governed tool call entered and completed the execution path |
AGENT_LOOP_DETECTED | The MCP loop detector refused a call |
MCP_SESSION_KILLED | A person killed an MCP session |
MCP_APPROVAL_REQUESTED | A tool call paused for review |
MCP_APPROVAL_GRANTED / MCP_APPROVAL_DENIED | A reviewer resolved it |
MCP_APPROVAL_TIMEOUT / MCP_APPROVAL_UNAVAILABLE | No decision arrived, or no reviewer could act |
Prometheus exposes mcp_agent_loop_detected_total,
mcp_agent_sessions_killed_total, and the four mcp_approval_* counters. See
Observability for scrape authentication and labels.
Govern agent-to-agent hops
The Enterprise A2A plane applies the same intent to a different action: one agent calling another. It governs one hop at a time; it does not orchestrate the larger workflow.
- A2A loop detection tracks
target agent + skillper session. Its 1.8 thresholds are fixed at the same 5 repetitions, 2–4 call cycle repeated 3 times, 60 hops per minute, and 100-entry history. They cannot be tuned per workspace in 1.8. From 1.8.5 the history is shared across Gateways in one cluster. - From 1.8.5 parallel calls from one delegation chain are not a loop. Five
independent calls to the same agent and skill from the same chain count as
one, so they are no longer refused with
A2A_LOOP_DETECTED. Real recursion is refused at the same hop as before. The 60-hops-a-minute rate now has its own code,429 A2A_SESSION_RATE_LIMITED, and does not kill the session. See the A2A delegation guard. approval.required-agentsmatches exact target-agent IDs.approval.required-skillsaccepts comma-separated skill globs. The timeout and default action use the same workspace approval settings as MCP.- A2A writes its own tamper-evident audit chain. Events include
A2A_HOP_INTENT,A2A_HOP_RESULT,A2A_HOP_DENIED,A2A_LOOP_DETECTED, and theA2A_APPROVAL_*decisions.
See A2A Gateway for registration, request shapes, policy, and streaming behavior. From 1.8.3, the A2A delegation guard refuses a delegation that loops back or goes too deep on its first offending hop, before the loop detector would see it.
What should you verify before production?
- Send one stable
X-Session-Idacross the whole workflow. - Keep loop detection enabled and test each threshold against a non-production tool.
- Leave automatic session kills off until you have tested false positives.
- Require approval only for tools and agents with meaningful external effects.
- Keep the approval timeout action set to
deny. - Test operator-issued session kills on every data-plane pod, on a model call, a tool call and a hop.
- Confirm loop, approval, and kill events appear in the audit trail and your SIEM.