Martello-Systems/agent-firewall
GitHub: Martello-Systems/agent-firewall
专为 AI 编程代理设计的 dry-run 防火墙,在工具调用执行前拦截、审计并按策略决定放行或拒绝。
Stars: 1 | Forks: 0
# agent-firewall
[](LICENSE) [](https://martellosystems.com)
**专为编程代理设计的试运行 (dry-run) 防火墙。**
编程代理(如 Claude Code、MCP 服务器、自主工具调用器)会执行真实的现实副作用:它们会写入文件、运行 shell 命令并发起 HTTP 请求。`agent-firewall` 位于这些调用的**前端**。在工具调用接触到真实世界之前,它会执行以下操作:
1. **总结副作用:** 为文件写入提供统一的 diff,为 shell 提供确切的命令,为 HTTP 提供方法 + URL + body。
2. **应用允许 / 拒绝 / 询问策略:** 基于工具名称和参数模式(glob / regex / substring)匹配的有序规则,首个匹配项生效。
3. **审计决策:** 每次调用都会被追加到可重放的 SQLite 日志中。
它被有意设计得小巧、轻依赖,并经过了全面测试(包含 141 个测试,其中包括一个可生成真实下游服务器的实时端到端 MCP-proxy 测试)。
```
tool call ──▶ [ policy engine ] ──▶ allow / deny / ask
│ │
├─▶ [ summarizer ] └─▶ (ask) interactive hold
│ (diff / cmd / http) a/d/persist-rule
└─▶ [ audit log ] (sqlite, queryable)
```
将其置于代理前端有两种方式:
- **Claude Code `PreToolUse` hook:** 拦截每一次 Claude Code 工具调用。
- **MCP stdio proxy:** 位于任何 MCP 服务器前端并拦截 `tools/call`。
**看看它是如何阻止破坏性调用的。** 无需设置,这是真实的 `check` 输出:
```
$ echo '{"tool":"Bash","args":{"command":"rm -rf /"}}' > call.json
$ agent-firewall check call.json
● DENY block rm -rf on absolute roots
Shell command
rm -rf /
```
## 安装
```
npm install # from a clone
# 或者,发布后:
# npm install -g agent-firewall
```
需要 **Node 18+**。仅支持 ESM。
## 将其作为 PreToolUse hook 接入 Claude Code
`agent-firewall hook` 读取 **stdin** 上的 Claude Code `PreToolUse` 事件,并在 **stdout** 上打印 Claude Code 期望的权限决策 JSON。将其添加到你的 Claude Code 设置中(`~/.claude/settings.json` 或项目中的 `.claude/settings.json`):
```
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node /absolute/path/to/agent-firewall/bin/agent-firewall.js --config /absolute/path/to/firewall.config.json hook"
}
]
}
]
}
}
```
该 hook 会输出如下内容,例如:
```
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "[agent-firewall] DENY: block rm -rf on absolute roots"
}
}
```
### 我们实现所依据的 hook I/O 契约
已根据官方 Claude Code hooks 文档进行验证
(,确认于 2026-06-23):
- **stdin:** Claude Code 会写入一个 JSON 事件:`{ session_id, transcript_path, cwd, permission_mode, hook_event_name: "PreToolUse", tool_name, tool_input }`。
- **stdout (exit 0):** 对于 `PreToolUse`,决策位于 `hookSpecificOutput`(camelCase)之下,**而不是**顶级的 `decision` 字段:`permissionDecision` ∈ `allow | deny | ask`,并附带 `permissionDecisionReason`(当决策为 `deny` 时**必填**)。
- **Exit codes:** 在 exit `0` 时,stdout 的 JSON 会被执行;在 exit `2` 时,JSON 会被*忽略*,而 stderr 会作为阻断性错误返回。因此我们**始终 exit 0**,并完全通过 `permissionDecision` 来表达决策。
`permissionDecision` 直接映射自你的策略。遇到无法解析的输入时,hook 会默认降级为 `ask`,因此它永远不会导致代理严重崩溃。
## 将其接入到 MCP 服务器前端(stdio proxy)
`agent-firewall mcp -- [args...]` 会生成一个下游 MCP 服务器,并在你的 MCP 客户端与该服务器之间代理以换行符分隔的 JSON-RPC stdio 流。每条消息都会**原样**转发,但 `tools/call` 请求除外,该请求会经过相同的策略引擎处理:
- **allow** → 转发给服务器,由其正常执行。
- **deny** → 在 proxy 处被拦截;客户端会收到一个 JSON-RPC 错误(`code -32001`),而服务器根本看不到该调用。
- **ask** → 被挂起;默认情况下会向客户端返回拒绝(使用 `--allow-holds` 可让被挂起的调用通过)。
```
# 与其将你的 MCP client 指向: node ./my-mcp-server.js
# 不如将其指向:
agent-firewall mcp -- node ./my-mcp-server.js
```
帧边界处理非常正确:跨流数据块拆分的消息会被重新组装,每个数据块中的多个消息会被拆分,而非 JSON 行会被原样放行,因此协议流绝不会损坏。
## 策略:`firewall.config.json`
防火墙会在工作目录中查找 `firewall.config.json`(可通过 `--config ` 或 `AGENT_FIREWALL_CONFIG` 环境变量覆盖)。如果未找到,则会使用安全的内置默认配置(允许只读工具;拒绝 `rm -rf /` 和写入 `.env`;其他所有操作均设为 `ask`)。
```
{
"policy": {
"default": "ask",
"rules": [
{
"action": "allow",
"tool": ["Read", "Glob", "Grep", "LS"],
"description": "read-only tools are always allowed"
},
{
"action": "deny",
"tool": "Bash",
"match": { "command": "regex:rm\\s+-rf\\s+/" },
"description": "block rm -rf on absolute roots"
},
{
"action": "deny",
"tool": ["Write", "Edit", "MultiEdit"],
"match": { "file_path": "glob:**/.env" },
"description": "never write to .env files"
},
{
"action": "ask",
"tool": ["Write", "Edit", "MultiEdit"],
"description": "review all other file writes"
}
]
},
"audit": { "db": ".agent-firewall/audit.sqlite" }
}
```
### 规则语义
- **有序,首个匹配项生效。** 请将特定的拒绝规则放在宽泛的允许规则之上。
- **`action`**(必填):`allow` | `deny` | `ask`。
- **`tool`**(可选):一个名称、名称数组,或 `"*"`(默认)。不区分大小写。
- **`match`**(可选):一个 `argPath → matcher` 的对象。**所有**条目都必须匹配。参数路径支持点号访问(`"options.danger"`)。
- **`default`**(可选,顶级):当没有规则匹配时的操作。默认为 `ask`。
### 匹配器
| 形式 | 含义 |
|---|---|
| `"glob:**/.env"` | 针对字符串化参数值进行 glob 匹配(`*` 仅在路径段内生效,`**` 可跨越路径段) |
| `"regex:rm\\s+-rf"` | 正则表达式,默认不区分大小写 |
| `"equals:exact"` | 严格相等 |
| `"sudo"` (纯字符串) | 不区分大小写的子字符串包含匹配 |
| `{ "glob": "..." }` / `{ "regex": "...", "flags": "i" }` / `{ "equals": "..." }` / `{ "contains": "..." }` | 显式对象形式 |
### 网络出口允许列表
工具级别的权限只能控制*运行哪个*工具,但无法控制被允许的工具在网络上的*访问位置*。在策略中添加一个可选的 `egress` 块,为出站 HTTP(S) 目标设置允许列表。任何针对不在列表中的主机发起的 `WebFetch`/`fetch`/`http` 调用都会被拒绝(或针对 `ask` 被挂起),这与处理其他所有操作的决策 + 审计机制相同。该允许列表**也**适用于 shell 命令:从 `curl`/`wget`/`nc`/`ssh`/`scp` 中提取的目标主机(任何纯粹的 `scheme://host` URL,以及纯主机名、IPv4 字面量如 `1.2.3.4`,以及传递给这些工具的括号括起来的 IPv6 字面量)都会受到相同列表的限制,因此 shell 调用无法悄无声息地绕过它:
```
{
"policy": {
"egress": {
"allow": ["api.github.com", "*.openai.com", ".internal.example.com"],
"action": "deny"
},
"rules": [ ... ]
}
}
```
- **`allow`**: 主机列表。条目可以是确切的主机(`api.github.com`)、glob(`*.openai.com`)、同样匹配根域名的前导点后缀(`.example.com` 匹配 `example.com` 及任何子域),或者是 `*`(允许全部)。
- **`action`**(可选):针对不在允许列表中的主机执行 `deny`(默认)或 `ask`。
- 主机无法被解析的请求将被视为违规。
- 该配置是**可选启用**的:如果没有 `egress` 键,网络调用将仅受你的常规规则约束。
- **Shell 覆盖范围是尽力而为的。** 主机是通过文本扫描从命令字符串中提取出来的:包括任何位置显式的 `scheme://host` URL,加上纯主机名、IPv4 字面量(`1.2.3.4`)以及传递给受支持网络工具的括号括起来的 IPv6 字面量(`[2001:db8::1]`)。这**不是**一个完整的 shell 解析器:在运行时组装的目标(变量展开、命令替换、base64/`eval` 混淆、十进制/八进制编码的 IP,或者是它无法识别的网络工具)仍然有可能让 shell 命令绕过允许列表。请将 shell 出口控制视为兜底方案,并针对不受信任的工作负载将其与操作系统级别的网络控制结合使用。
## CLI
```
# Claude Code PreToolUse hook (stdin event JSON -> stdout decision JSON)
agent-firewall hook
# 根据策略评估单个 tool call (dry run)
agent-firewall check call.json # human-readable + diff
agent-firewall check call.json --json # machine-readable decision
agent-firewall check call.json --interactive # on 'ask', prompt a/d/persist
# call.json 可以是 {"tool":"Write","args":{...}} 或者是一个 PreToolUse event
# exit code: 0 = allow, 1 = ask, 2 = deny
# 检查 audit log (最新记录优先)
agent-firewall log
agent-firewall log -n 50 --decision deny --tool Bash
agent-firewall log --json
# 位于任何 MCP server 前的 MCP stdio proxy
agent-firewall mcp -- node ./some-mcp-server.js
agent-firewall mcp --allow-holds -- node ./some-mcp-server.js
```
### 交互式 `ask` 流程
当决策为 `ask` 时,`--interactive` 会挂起调用,打印副作用摘要,并等待单次按键:
```
● ASK no rule matched, default action "ask"
File write: /proj/server.js
--- /proj/server.js current
+++ /proj/server.js proposed
@@ ... @@
+app.listen(3000)
[a]llow once [d]eny [p]ersist allow rule ?
```
- **`a`** / **`y`**: 允许本次调用。
- **`d`** / **`n`**: 拒绝它。
- **`p`**: 允许它**并且**在 `firewall.config.json` 顶部持久化保存一个精确的 `allow` 规则(范围限定为确切的 tool + command/file/url),以便下次自动允许相同的调用。
交互层采用了可注入的提示/渲染 IO,因此无需 TTY 即可完全进行单元测试。
### 示例:`check`
```
$ echo '{"tool":"Write","args":{"file_path":"/proj/.env","content":"API_KEY=__placeholder__"}}' > call.json
$ agent-firewall check call.json
● DENY never write to .env files
File write: /proj/.env
--- /proj/.env (new file) (absent)
+++ /proj/.env (new file) proposed
@@ -0,0 +1,1 @@
+API_KEY=__placeholder__
```
### 示例:审计日志
每一次决策(来自 hook、proxy 或 `check`)都会追加到一个 SQLite 日志中,供你日后查询:
```
$ agent-firewall log
2026-06-23T09:32:36.665Z ASK Write File write: /p/x.js
2026-06-23T09:32:36.573Z DENY Bash Shell command
2026-06-23T09:32:36.465Z ALLOW Read Tool call: Read
3 of 3 record(s)
```
```
$ agent-firewall log --json --decision deny
[
{
"id": 2,
"ts": "2026-06-23T09:32:36.573Z",
"source": "check",
"tool": "Bash",
"decision": "deny",
"kind": "shell",
"summary": "Shell command\nrm -rf /",
"reason": "block rm -rf on absolute roots",
"ruleIndex": 1,
"args": { "command": "rm -rf /" }
}
]
```
该日志由 `better-sqlite3` 提供支持,**全面采用了参数化查询**(没有使用字符串插值的 SQL),因此工具名称或过滤值永远不会引发注入。存储的参数在**写入磁盘前会被脱敏**:提供商密钥前缀、`Authorization: Bearer`/`Basic` token、以 secret 命名的赋值、`scheme://user:pass@host` URL 密码,以及包含敏感信息的 URL 查询参数都会被替换为 `[REDACTED]`,因此嵌入在 shell 命令或 URL 中的 token 绝不会原样持久化到审计数据库中。
## 结构说明
| 模块 | 职责 | 已测试 |
|---|---|---|
| `src/policy.js` | 规则评估,glob/regex/equals/contains 匹配,排序,默认值 | ✅ |
| `src/summarize.js` | 副作用摘要(文件 diff / shell / http / 通用) | ✅ |
| `src/audit.js` | 追加 + 查询 SQLite 审计日志 | ✅ |
| `src/hook-adapter.js` | Claude Code PreToolUse 事件 ⇄ 决策 JSON 映射 | ✅ |
| `src/mcp-proxy.js` | MCP `tools/call` 拦截 + 实时 stdio proxy + 帧边界处理 | ✅ (包含 e2e) |
| `src/interactive.js` | 交互式 `ask` 挂起(允许 / 拒绝 / 持久化规则) | ✅ |
| `src/secret-guard.js` | 阻止提交包含明文 secret 的写入操作(覆盖策略) | ✅ |
| `src/egress-guard.js` | 根据允许列表拦截出站 HTTP(S) 目标 | ✅ |
| `src/engine.js` | 共享接缝:每次调用中的 secret-guard + egress-guard + policy + summarize + audit | ✅ (直接测试 + 通过适配器测试) |
| `src/config.js` | 加载 + 验证 `firewall.config.json`;持久化规则 | ✅ |
| `bin/agent-firewall.js` | CLI | ✅ (派生集成测试) |
运行测试套件和 lint:
```
npm test # node --test, 141 tests, incl. a live MCP-proxy e2e
npm run lint # eslint, zero warnings
```
## 局限性
`agent-firewall` 是一个**安全网,而不是沙箱。** 在将其信任地置于自主代理前端之前,请阅读以下内容:
- 它只能看到已接入其前端的调用。绕过 hook/proxy 的代码路径(例如 proxy 未拦截的工具,或是派生自身子进程的 shell 子进程)**不会**被拦截。对于不受信任的工作负载,请将其与操作系统级别的沙箱结合使用。
- MCP proxy 仅拦截 `tools/call`;按照设计,所有其他 JSON-RPC 流量均被原样转发。
- 在 stdio 上,MCP 的 `ask` 没有交互式提示:被挂起的调用默认会被拒绝(或者通过 `--allow-holds` 放行)。交互式的允许/拒绝/持久化流程可以通过 `agent-firewall check -i` 和 Claude Code hook 原生的 `ask` 对话框来使用。
- 副作用摘要是尽力而为的:文件 diff 是通过从磁盘读取当前文件(一次试运行)计算得出的,摘要生成器能识别常见的工具形态,但不会对每种可能的参数布局进行深度解析。
- secret 检测采用的是启发式算法(已知的密钥前缀 + 针对非占位符值的 secret 命名赋值);它是一种兜底机制,而不是绝对的。
## 安全说明
内置的 **secret guard**(`src/secret-guard.js`)会在每条路径(包括 Claude Code hook、MCP proxy 和 `check` CLI,因为它位于共享的 `src/engine.js` 接缝中)上的策略执行之前运行:任何试图提交明文凭据(提供商密钥前缀、`*_live_*` 密钥、私钥块、JWT,或针对非占位符值的 secret 命名赋值)的 `Write`/`Edit` 操作都会被无条件拒绝,且拒绝原因绝不会回显该 secret。环境变量引用(`${VAR}`)、`` 以及 `changeme` 风格的值会被允许通过。切勿将真实的 secret 提交到 `firewall.config.json` 或你的工具调用测试用例中:请使用占位符和环境变量。
## License
Apache-2.0 © 2026 Martello Systems。请参阅 [LICENSE](./LICENSE)。
由 **Martello Systems** 构建。我们设计并交付 AI 驱动的软件。Martello 开源开发工具家族的一部分。
## 由 Martello Systems 构建
`agent-firewall` 是 **[Martello Systems](https://martellosystems.com)** 开源工具包的一部分。我们交付 AI 构建的软件,从规范到交付只需几天。如果这为您节省了时间,请[来看看我们做什么](https://martellosystems.com)。
采用 [Apache License 2.0](LICENSE) 授权。
标签:AI代理, GNU通用公共许可证, MCP, MITM代理, Node.js, SOC Prime, Streamlit, 审计日志, 开发工具, 数据可视化, 自定义脚本, 访问控制, 防火墙