Agentic-DART runs in two modes:
| Mode | Claude? | Network? | Purpose |
|---|---|---|---|
deterministic |
No (scripted) | No | Reproducible demo, accuracy metric, CI |
live |
Yes (Anthropic API) | Yes | Real investigation against real evidence |
live mode is what you use when a real case comes in.
┌────────────────────────┐ ┌──────────────────────────┐
│ dart_agent │ MCP over stdio │ dart_mcp.server_stdio │
│ (Anthropic API │ ◄───────────────►│ (subprocess; typed │
│ tool-use loop) │ JSON-RPC │ forensic functions — │
│ │ │ native + SIFT adapters) │
└──────────┬─────────────┘ └────────────┬─────────────┘
│ │
│ HTTPS │ file read
▼ ▼
api.anthropic.com EVIDENCE_ROOT (read-only)
The agent:
- Spawns
python -m dart_mcp.server_stdioas a subprocess - Completes the MCP initialize handshake
- Calls
list_tools()— sees exactly the full registered forensic function set - Hands that tool list (converted to Anthropic's tool-use schema) to Claude
- Enters a loop: ask Claude → receive tool_use blocks → route each via MCP session → feed results back → repeat until Claude stops or max-iter hits
Claude cannot see anything beyond the typed MCP surface. Not because we told it not to, but because the MCP server does not expose anything else.
The documented live-mode credential path is an Anthropic API key:
export ANTHROPIC_API_KEY=sk-ant-...
export DART_EVIDENCE_ROOT=/path/to/evidence
export PYTHONPATH="$PWD/dart_audit/src:$PWD/dart_mcp/src:$PWD/dart_agent/src:$PWD/dart_corr/src"
python3 -m dart_agent --mode live \
--case my-case \
--out /tmp/my-case-out \
--prompt "Investigate evidence root for IP-KVM insider pattern. Report findings with audit IDs." \
--model claude-haiku-4-5-20251001 \
--max-iterations 10Override the model with --model or DART_MODEL when you need a different
currently-supported Claude model.
Pass --dry-run. Everything runs the same — MCP subprocess, stdio handshake,
real tool calls — except Claude is replaced with a scripted mock that walks a
plausible tool-call sequence. Useful for:
- CI pipelines where no credentials should live
- Verifying the MCP plumbing without spending tokens
- Running the same plumbing Claude will use in a deterministic test
python3 -m dart_agent --mode live --case test --out /tmp/out --dry-runLive mode writes three files to --out:
| File | Contents |
|---|---|
live_summary.json |
case id, mode, iterations, tool_call_count, final findings |
live_tool_calls.jsonl |
one line per MCP call: iteration, tool, input, output preview |
live_transcript.txt |
the assistant's final text (or the mock transcript) |
These sit alongside the existing audit.jsonl / progress.jsonl /
report.json from deterministic mode — same evidence tree, same run
directory, same audit-chain discipline.
Compare two hypothetical designs for a DFIR agent:
# Anti-pattern — do NOT do this
def execute_shell(cmd: str) -> str:
"""The LLM has read our system prompt saying 'only read evidence'."""
return subprocess.run(cmd, shell=True, capture_output=True).stdoutOne prompt injection in a document, one hallucinated command, one model update that changes the alignment, and the LLM can do anything.
# dart-mcp registers ONLY this interface
@tool(name="extract_mft_timeline", schema=...)
def extract_mft_timeline(mft_path, start, end): ...The LLM can no more call execute_shell than it can call delete_evidence
— those names do not resolve to anything on the server. It is not a
policy. It is an absence.
The MCP protocol is the enforcement point. test_live_mcp.py asserts this
with a real call_tool("execute_shell", ...) over the wire — the
call is blocked by KeyError: ToolNotFound at the protocol layer, not
by any prompt.
# End-to-end: agent subprocess spawns MCP subprocess, 73-tool handshake,
# real tool calls over stdio, guardrail-over-wire verification.
python3 tests/test_live_mcp.pyFour tests, all run in under 10 seconds, no API key required.