Auditing agents¶
See also: the canonical design reference at docs/PROJECT_REFERENCE_WARDEN.md.
Warden — Project Reference & Design Document¶
Least-privilege auditor for AI agents. Scan the assembly.
Part of the Bulwark suite (see BULWARK.md). Warden reuses bulwark-core for findings,
severity, the rule engine, reports, and the optional AI layer. This document defines what is
Warden-specific. It is the source of truth for Warden's design.
Status (v0.1, shipped): the AgentSpec IR, capability lexicon, capability graph, agency score,
A1–A10, the least-privilege recommender (--recommend), all five importers (manifest, MCP config,
OpenAI Assistants, LangChain/LangGraph, CrewAI), the Airlock bridge (--scan-parts), and the optional
AI layer are all built and tested green. The sections below describe the design; the "Later"/roadmap
markers are retained as history but every listed capability is complete.
1. Thesis & positioning¶
Airlock answers "is this component malicious?" But a system built entirely from benign parts can still be dangerous because of how they were wired together. An agent given a read-secrets tool and a send-email tool has an exfiltration path neither tool has alone. An agent with a shell tool, web browsing, and no human gate can be driven to run arbitrary commands via a poisoned web page.
Warden audits the composition. It answers: does this assembled agent hold more capability than its purpose requires (excessive agency), and do its tools combine into dangerous paths (toxic combinations)? Then it proposes a least-privilege version.
1.1 Prior art / positioning¶
- Cloud IAM least-privilege tools exist for humans/services, but not for agent tool-sets.
- Excessive Agency is OWASP LLM06 — named as a top risk, but tooling to measure it in a concrete agent assembly is essentially absent.
- Warden's edge: a normalized Agent Spec across frameworks, a capability graph that finds source→sink toxic combinations, an agency score, and an actionable least-privilege recommendation — plus native composition with Airlock (parts) and Manifest (inventory).
2. What Warden ingests¶
The challenge is that agents are configured many ways. Warden solves this with an importer →
normalized IR pattern (mirrors Airlock's loader). Importers convert real configs into one
AgentSpec; all analysis runs on the IR, so adding a framework never touches the analysis engine.
Importers (all shipped; auto-detected, in priority order — warden/importers/):
1. MCP client configs (mcp_config.py) — .mcp.json, claude_desktop_config.json (which
servers/tools are wired).
2. Generic agent manifest (manifest_yaml.py) — a documented YAML/JSON schema describing tools,
scopes, system prompt, model, data sources, gates, and limits. Also the canonical fallback for an
agent Warden can't otherwise parse; its detect defers to the specific shapes below so it never
steals a framework config.
3. OpenAI Assistants (openai_assistant.py) — an Assistants API config
(instructions → system prompt; function/code_interpreter/file_search tools).
4. LangChain / LangGraph (langchain.py) — best-effort static regex parse of a .py file
(Tool(...) name+description, the model, the system prompt). Never executed or imported.
5. CrewAI (crewai.py) — an agents.yaml crew (role/goal/backstory/tools/delegation), aggregated
into one AgentSpec.
warden import <config> prints the normalized AgentSpec so you can see exactly what each importer
resolved.
3. Threat taxonomy (A-codes)¶
Each finding maps to one category. Default severities are starting points; the rule packs set the exact values and can be tuned.
A1 — Excessive tool scope. (MEDIUM–HIGH) A tool granted broader capability than needed —
filesystem root, unrestricted shell, wildcard network, * resource scopes. Detect: capability
tags + wildcard/breadth analysis in analysis/scopes.py. Ref: OWASP LLM06, CWE-269.
A2 — Dangerous tool combination (toxic combination). (HIGH–CRITICAL) Two or more tools that are
individually acceptable but together form a harmful path — classically a sensitive source (read
files/secrets/DB/context) reachable to an egress sink (network/email/write-external). The flagship
check. Detect: capability graph source→sink reachability in analysis/graph.py. Ref: OWASP
LLM06/LLM02, confused-deputy.
Attacker-triggerable variants (stronger A2). A plain pairing says two capabilities could be
chained; it becomes a real kill chain when the assembly also ingests untrusted external content
(BROWSE/NET_IN — the indirect-prompt-injection surface). graph.py emits two escalated signals:
agent.injectable_toxic_flow (CRITICAL — untrusted input + a crown-jewel source + an egress sink =
inject → read secret → exfiltrate, fully attacker-controllable) and agent.injectable_action
(untrusted input + an ungated high-impact tool = injected content can trigger a dangerous action). This
mirrors the "toxic flow" emphasis in the MCP-security literature. Ref: OWASP LLM01 (Prompt
Injection) → LLM06.
A3 — Missing human-in-the-loop on high-impact actions. (HIGH) Irreversible/destructive/financial
/external-communication tools with no confirmation gate. Detect: high-impact capability without a
declared gate in analysis/limits.py. Ref: OWASP LLM06.
A4 — Over-broad system-prompt authority / weak guardrails. (MEDIUM–HIGH) System prompt grants
open-ended autonomy ("do whatever it takes", "you may access anything"), lacks refusal/limits, or is
itself injectable/ambiguous. Detect: authority + injectability heuristics in analysis/prompt.py
(AI-enrichable). Ref: OWASP LLM01/LLM06.
A5 — Unrestricted egress / exfiltration surface. (HIGH) Agent can reach arbitrary URLs or send data outward without allow-listing. Detect: network capability breadth + presence of any sensitive source. Ref: OWASP LLM02.
A6 — Secrets/credentials in the assembly. (HIGH–CRITICAL) API keys/tokens embedded in configs or broadly injected into many tools' environments. Detect: secret signatures over config (reuse core secret rules). Ref: CWE-798, OWASP LLM02.
A7 — Excessive data/memory access. (MEDIUM–HIGH) Agent has read access to more data (files, DBs, long-lived memory, whole-drive context) than its stated purpose. Detect: declared data sources vs purpose; broad/rooty data scopes. Ref: OWASP LLM06, least-privilege.
A8 — Unsandboxed code/shell execution. (HIGH) Code-exec or shell tools without a declared sandbox
/ isolation boundary. Detect: exec capability without sandbox attribute. Ref: CWE-250.
A9 — Untrusted/unscanned parts wired in. (MEDIUM) The assembly references MCP servers/models that
haven't passed Airlock. Detect: cross-check wired components; with --scan-parts, invoke Airlock
and merge. Ref: OWASP LLM05.
A10 — No runaway guards. (MEDIUM) Autonomous/looping agent with no iteration cap, budget, or
timeout. Detect: missing limits in analysis/limits.py. Ref: OWASP LLM06.
3.1 Mapping to the OWASP Top 10 for Agentic Applications (2026)¶
Warden's A-codes predate but align cleanly with the emerging OWASP Top 10 for Agents (2026), which elevates agent-specific risks beyond the LLM Top 10:
| Warden | Agentic-Top-10 theme |
|---|---|
| A1, A7 | Excessive permissions / excessive functionality |
| A2, A5 | Tool misuse & unexpected data flows (toxic combinations) |
| A3 | Insufficient human-in-the-loop / oversight |
| A4 | Agent instruction / goal manipulation surface |
| A6 | Identity & secret management |
| A8 | Unsafe code/tool execution |
| A9 | Supply-chain of tools & components |
| A10 | Insufficient guardrails / autonomy limits |
This is advisory alignment (the framework is still stabilizing), not a conformance claim.
4. AgentSpec IR (Warden-specific data model)¶
Findings/severity come from bulwark_core. Warden adds the IR:
class Capability(str, Enum):
FS_READ="fs_read"; FS_WRITE="fs_write"; SHELL="shell"; CODE_EXEC="code_exec"
NET_OUT="net_out"; NET_IN="net_in"; SECRET_READ="secret_read"; DB_READ="db_read"
DB_WRITE="db_write"; EMAIL_SEND="email_send"; FINANCIAL="financial"; DESTRUCTIVE="destructive"
BROWSE="browse"; MEMORY_WRITE="memory_write"; UNKNOWN="unknown"
class Gate(str, Enum):
NONE="none"; CONFIRM="confirm"; APPROVAL="approval"; DRY_RUN="dry_run"
class Tool(BaseModel):
name: str
source: str | None = None # which MCP server / plugin provided it
description: str | None = None
scopes: list[str] = [] # raw scope strings from config
capabilities: set[Capability] = set() # filled by normalize.py
sandboxed: bool | None = None
gate: Gate = Gate.NONE
class DataSource(BaseModel):
name: str; kind: str # files | db | memory | context | env
scope: str | None = None; sensitive: bool = False
class Limits(BaseModel):
max_iterations: int | None = None
budget: float | None = None
timeout_s: int | None = None
class AgentSpec(BaseModel):
name: str
model: str | None = None
system_prompt: str | None = None
tools: list[Tool] = []
data_sources: list[DataSource] = []
mcp_servers: list[str] = [] # references (paths/urls) → A9 / --scan-parts
limits: Limits = Limits()
autonomy: Literal["manual","assisted","autonomous"] = "assisted"
normalize.py maps raw tool names/scopes/descriptions to Capability sets using a keyword lexicon,
so classification improves by adding patterns rather than editing logic. (The lexicon is currently a
Python dict; moving it to a YAML pack loaded like the rule packs — so it can ship through the
community feed and be extended per project — is tracked as future work.)
Two patterns were narrowed after they proved noisy in practice, and the reasoning generalises:
\bformat\b classified format_response as DESTRUCTIVE (hence high-impact, hence a spurious
missing-gate finding and +10 agency score on a text formatter), and \bsandbox\b classified
"runs in a sandbox" as CODE_EXEC — inverted, since the word usually appears because the author is
reassuring you. Both now require corroborating context. A capability pattern should require a
verb and a domain noun, or it will fire on ordinary documentation.
5. Capability graph & the agency score¶
- Graph: nodes = tools, data sources, and external sinks; edges = "can supply data to" / "can act
on". A sensitive source → egress sink reachable path is a toxic combination (A2/A5). Within a
single agent every tool is reachable from every other — the agent can call them in sequence and no
privilege boundary separates them — so the "graph" is correctly a cross-product rather than a
search. Pairings are capped at
MAX_PAIRSwith a roll-up finding naming the total, so a large tool-set produces a readable report instead of hundreds of near-duplicates. - A5 egress: a network-out tool counts as constrained when its scope names something concrete —
an allow-list, a URL prefix, a hostname, or a CIDR. Matching only the literal word
allowlistpreviously reportedhttps://api.example.com/**as unrestricted, a false positive on exactly the configuration the finding asks the user to adopt. - Agency score (0–100): a transparent weighted sum over breadth of capabilities, count of high-impact tools, ungated high-impact actions, exfil paths, and missing limits. Documented formula (no black box). Displayed in the report header; great demo headline ("Agency score: 82/100 — HIGH").
- The score is advisory context, never a substitute for the itemized findings.
6. Least-privilege recommendation¶
recommend/least_privilege.py produces a minimized AgentSpec plus a human-readable diff:
- Drop scopes/capabilities with no evidence of need (heuristic: not referenced by purpose/task).
- Add gate: confirm/approval to high-impact tools (A3).
- Break toxic pairs (A2): suggest splitting into separate agents, removing one capability, or adding a
mediation/allow-list boundary.
- Add limits (A10) and sandboxed: true for exec tools (A8).
- Allow-list egress instead of open net_out (A5).
Output: warden audit ... --recommend prints before/after and can write a agentspec.hardened.yaml.
The split between applied changes and advisories is the design: mechanical hardening that preserves intent is applied (gates, sandboxes, an allow-list placeholder, runaway limits), while anything that changes what the agent does — breaking a toxic pair, restricting egress — is raised for a human decision and never silently rewritten. A "hardened" spec that no longer performs its job is worse than none. A property test asserts that the recommendation measurably lowers the agency score, so the recommender and the analyzer cannot drift apart while encoding the same threat model.
7. Reports¶
Reuse bulwark_core.report: terminal (rich, grouped by A-code, header shows agency score + worst
severity), JSON (ScanResult + the AgentSpec + score), HTML, SARIF (ruleId = A-code). --fail-on
sets the CI gate.
8. AI enrichment (optional; reuse bulwark_core.ai)¶
Off by default; enabled AND --ai; capped by max_findings_to_enrich; local Ollama default. Uses:
1. System-prompt analysis (A4): judge whether the prompt is over-permissive or injectable; strict
JSON verdict {weak: bool, confidence, reason}.
2. Non-obvious toxic combinations (A2): reason over the capability set for harmful paths rules
didn't encode; must reference concrete tools, else discard.
3. Attack-path prose: explain a flagged source→sink path for the report.
4. Remediation phrasing: turn a minimized spec diff into a short rationale.
AI output is tagged source="ai", never downgrades a deterministic finding, degrades gracefully on
error.
9. CLI¶
warden audit <path|config|manifest.yaml> [--format terminal|json|html|sarif]
[--fail-on SEV] [--recommend] [--scan-parts] [--ai]
warden import <path> # show the normalized AgentSpec (debug importers)
warden rules list|lint
warden version
10. Testing & fixtures¶
- Benign, inert fixtures only. An "exfil" fixture wires a fake read-notes tool and a fake post-webhook tool that does nothing real — enough to trip A2 without any harmful behavior.
- Fixtures:
over_privileged/(basic A1, exfil A2/A5, ungated-destructive A3, runaway A10) andleast_privilege/(clean controls). - Tests assert on
category+severity+ presence of the recommendation, not on prose. - Golden-file tests for JSON/SARIF and for the hardened-spec diff.
11. Release & community¶
README with a one-glance "before/after" (an over-privileged agent → the hardened spec Warden
suggests), the agency-score demo, the A-code table, and a GitHub Action. The "it rewrote my agent to
least-privilege" moment is the shareable hook. CONTRIBUTING.md invites capability-lexicon and rule
PRs.
12. Roadmap¶
- ✅ v0.1 — IR, MCP-config + manifest importers, capability graph, A1–A10, agency score.
- ✅ v0.2 — least-privilege recommendation, SARIF/HTML,
--fail-onCI gate. - ✅ v0.3 — framework importers (OpenAI Assistants, LangChain/LangGraph, CrewAI), the Airlock
bridge /
--scan-parts, A9 promoted from advisory to concrete part-level findings. - ✅ v0.4 — optional AI enrichment (
bulwark_core.ai). - ⏭️ v0.5+ — richer lexicon, policy profiles (e.g. "strict", "balanced"), org policy files.
13. Research / talk angle¶
"We normalized N public agent configs and found X% contain at least one toxic tool combination; Y% expose exec/shell without a sandbox." A concrete, reproducible measurement of excessive agency in the wild — a strong workshop paper or conference talk, and a natural companion to Airlock's corpus study.
14. References (verify current versions when building)¶
OWASP Top 10 for LLM Applications (esp. LLM06 Excessive Agency, LLM01, LLM02, LLM05); MITRE ATLAS; Model Context Protocol spec (tool/scope schema); least-privilege / CWE-269, CWE-250, CWE-798; cloud IAM least-privilege analyzers as conceptual prior art (applied here to agent tool-sets, not cloud roles).