9.1 KiB
Tool Execution Governance
This document describes how CyberStrikeAI governs long-running tools, blocking MCP calls, oversized outputs, cancellation, and context restore. The goal is to preserve standard Agent/Eino tool semantics while preventing tool hangs, context blowups, oversized database records, and unsafe resume behavior.
Goals
- Keep the agent runner responsive: tools may run for a long time, but the current runner waits only for a bounded interval.
- Allow long tasks to continue: timeout returns an
execution_id; later turns can callwait_tool_execution. - Support cancellation: users and agents can cancel a running execution.
- Keep DB and agent views identical: the database stores the same canonical capped result returned to the agent.
- Protect resume paths: resume uses model-facing traces and caps historical oversized tool traces.
- Isolate external MCP failures: external MCP servers are protected by per-server concurrency limits, global concurrency limits, and circuit breakers.
Execution Model
Tool calls still appear to Eino/Agent as standard tool invocations, but the blocking work runs in a worker:
Agent calls tool
-> ExecutionService creates execution
-> worker runs the real MCP/tool call
-> Agent bounded wait
-> completed: return tool result
-> still running: return execution_id, worker continues in background
This prevents MCP servers, exec, sqlmap, nmap, nuclei, and similar long-running tools from binding the current runner indefinitely.
Execution Statuses
| Status | Meaning |
|---|---|
queued |
Execution exists and is waiting for a worker or concurrency slot |
running |
Worker is executing |
background_running |
UI display state: agent stopped waiting, background worker continues |
completed |
This tool call completed |
failed |
The tool actually failed |
cancelled |
User, agent, or session cleanup cancelled the execution |
hard_timeout |
The tool exceeded its hard timeout |
orphaned |
A persisted running execution no longer has a runtime worker |
Important: when wait_tool_execution reaches timeout_seconds and the target execution is still running, the wait call itself is a completed observation, not a failed tool execution.
Control Tools
| Tool | Purpose |
|---|---|
get_tool_execution |
Read current execution state |
wait_tool_execution |
Wait for a selected execution for a bounded interval |
cancel_tool_execution |
Cancel a selected execution |
get_tool_execution and wait_tool_execution can include a live output preview:
include_partial_output: whether to return partial output, defaulttrue.partial_output_max_bytes: tail preview limit for this call, default4096, maximum65536.
Partial output is a bounded preview of output produced so far, not the final result. The canonical result is still written only when the tool finishes. Tools that do not support streaming output simply omit partial fields.
Typical flow:
1. Call a long-running tool such as exec/sqlmap/nmap
2. After tool_wait_timeout_seconds, receive execution_id
3. Agent can continue reasoning, use other tools, or call wait_tool_execution
4. If still incomplete, continue waiting or call cancel_tool_execution
tool_wait_timeout_seconds applies to internal MCP tools, external MCP tools, and Eino filesystem's streaming execute. Eino's non-streaming filesystem tools such as ls/read_file/write_file/edit_file/glob/grep are recorded in execution monitoring, but they are not converted into resumable background workers.
Cancellation and Session Cleanup
- User stop cancels running tools for the current conversation.
- Normal session end cancels remaining running tools for the current conversation.
- Interrupt-and-continue style flows do not mass-cancel tools.
- Conversation-scoped cancellation avoids killing tools from other conversations.
External MCP Isolation
External MCP servers can hang, disconnect, or return failures. CyberStrikeAI uses three protections:
| Capability | Config | Description |
|---|---|---|
| Per-server concurrency | external_mcp_max_concurrent_per_server |
Max simultaneous calls for one external MCP server |
| Global concurrency | external_mcp_max_concurrent_total |
Max simultaneous external MCP calls across all servers |
| Circuit breaker | external_mcp_circuit_failure_threshold / external_mcp_circuit_cooldown_seconds |
Temporarily fast-fails a server after repeated failures |
Recommended defaults:
agent:
external_mcp_max_concurrent_per_server: 2
external_mcp_max_concurrent_total: 16
external_mcp_circuit_failure_threshold: 3
external_mcp_circuit_cooldown_seconds: 60
Output Governance
CyberStrikeAI uses multi_agent.eino_middleware.reduction_max_length_for_trunc as the unified tool result cap. The example configuration uses 50000 bytes.
multi_agent:
eino_middleware:
reduction_enable: true
reduction_max_length_for_trunc: 50000
Coverage:
| Channel | Behavior |
|---|---|
| Agent-facing tool result | Uses the canonical capped result |
| DB / monitor storage | Stores the same canonical result |
get_tool_execution / wait_tool_execution |
Reads the same canonical result |
Eino execute / filesystem monitor records |
Capped before completion is persisted |
Non-streaming exec stdout/stderr |
Source-side bounded buffer |
Streaming exec stdout/stderr |
Streamed UI output is also bounded |
| PTY execution path | Uses the same output cap |
| Frontend detail modal | Has an additional UI display cap |
When the cap is reached, the full output is written to a local trunc file and the agent-facing payload becomes a <persisted-output> notice (with absolute path) that fits inside the configured budget.
Example:
<persisted-output>
Output too large (200000). Full output saved to: /path/to/tmp/reduction/conversations/<id>/trunc/<execution_id>
Use read_file with offset/limit to read parts of the file.
Preview (first …):
…
Preview (last …):
…
</persisted-output>
The current strategy is “spill full text to disk + bounded preview in context.” Agents can recover the original via read_file.
Database and Resume Context
New results are written through this path:
tool completes
-> NormalizeToolResultForStorage
-> update in-memory execution
-> persist to DB
-> return to Agent
So, under normal operation, the DB stores exactly the result returned to the agent.
Resume uses LastAgentTraceInput, which is the model-facing trace that actually reached ChatModel, not raw event accumulation. The restore path also caps historical tool content to prevent context blowups from:
- pre-upgrade DB records that contain raw large output,
- manual imports or migrations,
- lowering the configured cap from a larger value,
- future bypasses that accidentally skip canonicalization.
Recommended Configuration
For long-running security tasks:
agent:
max_iterations: 800
tool_timeout_minutes: 60
tool_wait_timeout_seconds: 30
external_mcp_max_concurrent_per_server: 2
external_mcp_max_concurrent_total: 16
external_mcp_circuit_failure_threshold: 3
external_mcp_circuit_cooldown_seconds: 60
shell_no_output_timeout_seconds: 1200
multi_agent:
eino_middleware:
reduction_enable: true
reduction_max_length_for_trunc: 50000
| Parameter | Recommended | Notes |
|---|---|---|
max_iterations |
300-1000 |
Very large values weaken loop protection |
tool_timeout_minutes |
60 |
Hard timeout for tools such as sqlmap |
tool_wait_timeout_seconds |
30-60 |
Agent wait bound before returning execution_id |
shell_no_output_timeout_seconds |
600-1200 |
Kills silent hangs |
reduction_max_length_for_trunc |
50000 |
Unified tool result cap |
Do not make tool_wait_timeout_seconds very large by default. Long tasks should continue in workers and be observed by execution_id, rather than blocking one turn for several minutes.
Testing
Long-task test prompt:
Call exec to run sleep 120. If it is not done after 10 seconds, do not keep waiting; report execution_id, call wait_tool_execution for 5 seconds, then cancel_tool_execution if still incomplete and report final status.
Large-output test prompt:
Call exec to run: python3 - <<'PY'
print("A" * 200000)
PY
Then show the tool result length and whether it contains the truncation marker.
Expected behavior:
- The initial long task returns an
execution_idand statusrunningor UIbackground_running. wait_tool_executiontiming out while the target is still running is not displayed as a tool failure.- Large output never exceeds
reduction_max_length_for_trunc. - DB, monitor details, and agent continuation use the same capped result.
Boundaries
- CyberStrikeAI cannot control how a remote external MCP server collects output internally; it caps results after they enter CyberStrikeAI and protects calls with concurrency limits and circuit breakers.
- Oversized tool output is spilled to local
tmp/reduction/.../trunc/<id>(orreduction_root_dir) before truncation; the bounded result includes an absolute path forread_file.