Skip to main content
Version: 1.8.0

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.

Enterprise only

Not included in DVARA Open Source.

Which controls are available on each plane?​

TrafficWhat DVARA governsAvailability
Model callsPolicy, PII, guardrails, cost, audit, correlation through X-Session-Id, and, from 1.8.5, session killsEnterprise only
MCP tool callsTool policy, PII, sessions, loop detection, session kills, human approval, and tool-call evidenceEnterprise only
A2A hopsPeer policy, PII, sessions, loop detection, session kills, human approval, and a separate hop audit chainEnterprise 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.

CheckDefault
Same server and tool called consecutivelyBlock the fifth call
Repeating cycleBlock a cycle of 2–4 calls after 3 repetitions
Call rateBlock when the session exceeds 60 calls in 60 seconds
Stored historyMost recent 100 calls per session
Kill the session after detectionOff

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 settingWhat it changes
agentic.loop-detection.enabledTurns MCP loop detection on or off for that workspace; it inherits true
agentic.loop-detection.repetition-thresholdChanges the consecutive-call threshold
agentic.loop-detection.max-calls-per-minuteChanges the one-minute rate threshold
agentic.loop-detection.auto-killDecides 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.

SettingDefaultPurpose
approval.required-toolsEmptyComma-separated tool-name globs, such as delete_*,payment_refund
approval.required-serversEmptyComma-separated server IDs
approval.timeout-seconds300How long the call waits for a decision
approval.default-actiondenyWhat 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:

  1. DVARA writes MCP_APPROVAL_REQUESTED, stores a pending approval, and pauses the call.
  2. A reviewer opens Agents → Approvals in the Flightdeck.
  3. Approve releases the call and writes MCP_APPROVAL_GRANTED.
  4. Deny refuses it with HTTP 403 and writes MCP_APPROVAL_DENIED.
  5. No answer within the configured window applies approval.default-action. The default refusal returns HTTP 408 and writes MCP_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.

Keep timeout behavior fail-closed

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:

EventMeaning
MCP_TOOL_INTENT / MCP_TOOL_RESULTA governed tool call entered and completed the execution path
AGENT_LOOP_DETECTEDThe MCP loop detector refused a call
MCP_SESSION_KILLEDA person killed an MCP session
MCP_APPROVAL_REQUESTEDA tool call paused for review
MCP_APPROVAL_GRANTED / MCP_APPROVAL_DENIEDA reviewer resolved it
MCP_APPROVAL_TIMEOUT / MCP_APPROVAL_UNAVAILABLENo 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 + skill per 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-agents matches exact target-agent IDs. approval.required-skills accepts 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 the A2A_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?​

  1. Send one stable X-Session-Id across the whole workflow.
  2. Keep loop detection enabled and test each threshold against a non-production tool.
  3. Leave automatic session kills off until you have tested false positives.
  4. Require approval only for tools and agents with meaningful external effects.
  5. Keep the approval timeout action set to deny.
  6. Test operator-issued session kills on every data-plane pod, on a model call, a tool call and a hop.
  7. Confirm loop, approval, and kill events appear in the audit trail and your SIEM.