Skip to content

Commit 7982a5f

Browse files
committed
feat: add tool safety guard
1 parent 73655ab commit 7982a5f

27 files changed

Lines changed: 2685 additions & 0 deletions

examples/tool_safety/README.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# Tool Safety Guard 示例
2+
3+
本示例说明 Tool Safety Guard 的设计目标、使用方式和交付物。它用于在工具调用、代码执行、技能执行、脚本扫描等入口执行确定性的安全检查,并输出结构化结果、Telemetry 属性和审计记录。
4+
5+
## 背景与设计目标
6+
7+
Agent 工具通常可以执行文件操作、Shell 命令、网络请求或依赖安装。此类能力很有用,但也会带来误删文件、泄露密钥、访问非预期网络、无限循环或资源滥用等风险。
8+
9+
Tool Safety Guard 的目标是:
10+
11+
- 在高风险操作执行前给出确定性判断。
12+
- 让工具调用、CodeExecutor、Skill 执行和 CLI 扫描复用同一套审查逻辑。
13+
- 为 CI 和开发流程提供结构化输出与明确退出码。
14+
- 记录可观测属性和审计事件,便于排查与合规留痕。
15+
- 保持轻量:不替代沙箱,不引入新的 Telemetry 框架。
16+
17+
## 整体架构
18+
19+
```text
20+
SafetyReviewer
21+
22+
Rule
23+
24+
Policy
25+
26+
ToolSafetyFilter
27+
CodeExecutor Wrapper
28+
Skill Wrapper
29+
30+
Telemetry
31+
Audit
32+
```
33+
34+
- `SafetyReviewer` 是统一入口,接收待检查文本、动作类型和工具名,返回结构化 review。
35+
- `Rule` 提供确定性模式匹配。
36+
- `Policy` 提供 allowlist、blocked path 和风险等级配置。
37+
- `ToolSafetyFilter` 用于已有工具过滤器链。
38+
- `CodeExecutor Wrapper``Skill Wrapper` 用于没有 Filter 能力的执行入口。
39+
- Telemetry 将安全判断写入当前 OpenTelemetry span。
40+
- Audit 用于保存离线审计记录。
41+
42+
## Rule 分类
43+
44+
### 文件操作
45+
46+
文件类规则关注破坏性删除、敏感路径读取和大文件写入。例如删除目录、访问 `.env`、访问 SSH 私钥路径,或写入异常大的文件内容。
47+
48+
### 网络访问
49+
50+
网络类规则关注直接访问外部域名、使用非 allowlist 域名、`wget`、原始 socket、`aiohttp` 客户端等行为。允许访问的域名应通过 Policy 显式配置。
51+
52+
### 系统命令
53+
54+
系统命令类规则关注 `os.system`、Python 子进程调用、Shell 管道、命令串联、`sudo``systemctl`、部署或生产环境关键字等高风险模式。
55+
56+
### 依赖安装
57+
58+
依赖安装类规则关注 `pip install``npm install``apt install` 等会修改环境的命令。默认结果通常是 `needs_human_review`,由人确认是否允许继续。
59+
60+
### 资源滥用
61+
62+
资源类规则关注无限循环、过高并发、过大文件写入、递归进程生成等可能导致资源耗尽的行为。
63+
64+
### 敏感信息泄露
65+
66+
敏感信息类规则关注打印环境变量、token、password、secret、api key 等内容,避免工具输出把凭据带入模型上下文、日志或审计系统。
67+
68+
## Policy 配置说明
69+
70+
示例 Policy 位于 [tool_safety_policy.yaml](./tool_safety_policy.yaml)
71+
72+
常用字段:
73+
74+
- `allowed_domains`:允许访问的网络域名。域名会做规范化处理,子域名可被匹配。
75+
- `blocked_paths`:按规则 ID 配置禁止读取或访问的路径片段。
76+
- `allowed_commands`:保留给调用方或上层执行器使用的命令 allowlist。
77+
- `max_timeout`:保留给调用方或上层执行器使用的最大超时配置。
78+
- `max_output_size`:保留给调用方或上层执行器使用的最大输出大小配置。
79+
- `risk_levels`:按规则 ID 覆盖风险等级。
80+
81+
最小示例:
82+
83+
```yaml
84+
allowed_domains:
85+
- api.example.com
86+
87+
blocked_paths:
88+
read_dotenv:
89+
- ".env"
90+
read_ssh:
91+
- "~/.ssh"
92+
93+
risk_levels:
94+
network_not_allowlisted: critical
95+
```
96+
97+
## CLI 使用示例
98+
99+
独立扫描命令位于 `scripts/tool_safety_check.py`。
100+
101+
扫描 Python 脚本:
102+
103+
```bash
104+
python scripts/tool_safety_check.py example.py
105+
```
106+
107+
扫描 Bash 脚本:
108+
109+
```bash
110+
python scripts/tool_safety_check.py example.sh
111+
```
112+
113+
指定 Policy:
114+
115+
```bash
116+
python scripts/tool_safety_check.py example.sh --policy examples/tool_safety/tool_safety_policy.yaml
117+
```
118+
119+
输出 text 格式:
120+
121+
```bash
122+
python scripts/tool_safety_check.py example.sh --format text
123+
```
124+
125+
写入 JSON report 文件:
126+
127+
```bash
128+
python scripts/tool_safety_check.py example.sh --output tool_safety_report.json
129+
```
130+
131+
退出码约定:
132+
133+
| Decision | Exit Code | 含义 |
134+
| --- | ---: | --- |
135+
| `allow` | 0 | 可继续执行 |
136+
| `deny` | 1 | 阻断,CI 应失败 |
137+
| `needs_human_review` | 2 | 需要人工审核 |
138+
139+
## 如何新增 Rule
140+
141+
新增 Rule 时应保持规则小而明确:
142+
143+
1. 明确风险场景和期望 decision。
144+
2. 添加稳定的 `rule_id`、`finding`、`recommendation` 和匹配模式。
145+
3. 在 Policy 的 `risk_levels` 中补充默认风险等级。
146+
4. 增加 allow、deny 或 `needs_human_review` 的单元测试。
147+
5. 确认 CLI、Filter、Wrapper 都通过 `SafetyReviewer` 自动复用该规则。
148+
149+
Rule 不应承担执行隔离职责,也不应读取系统状态。它只做输入文本审查。
150+
151+
## Tool Filter 与 Wrapper 的区别
152+
153+
`ToolSafetyFilter` 用于已经接入框架工具过滤器链的 `BaseTool`。它在工具执行前运行,命中阻断时返回结构化工具错误。
154+
155+
Wrapper 用于没有 Filter 能力的入口,例如直接调用 `CodeExecutor.execute_code()`,或直接运行某个 Skill runner。Wrapper 通过组合方式包住原执行入口,不改变底层执行器。
156+
157+
两者的共同点:
158+
159+
- 都复用 `SafetyReviewer`。
160+
- 都复用同一套 Rule 和 Policy。
161+
- 都输出相同风格的安全 decision。
162+
- 都写入相同的 Telemetry attributes。
163+
164+
## Telemetry
165+
166+
安全检查完成后,会向当前 OpenTelemetry span 写入以下 attributes:
167+
168+
- `tool.safety.decision`
169+
- `tool.safety.risk_level`
170+
- `tool.safety.rule_id`
171+
172+
如果当前环境未启用 OpenTelemetry,写入会退化为 no-op,不影响工具执行或 CLI 扫描。
173+
174+
## Audit
175+
176+
示例审计文件位于 [tool_safety_audit.jsonl](./tool_safety_audit.jsonl)。每行是一条 JSON 记录,便于流式写入和日志系统采集。
177+
178+
稳定字段包括:
179+
180+
- `tool_name`
181+
- `decision`
182+
- `risk_level`
183+
- `rule_id`
184+
- `blocked`
185+
- `latency`
186+
- `timestamp`
187+
- `input_sha256`
188+
189+
示例 report 位于 [tool_safety_report.json](./tool_safety_report.json),包含 `allow`、`deny`、`needs_human_review` 三类结果,可作为 README 或 Issue 的结构化输出示例。
190+
191+
## 已知限制
192+
193+
### 误报
194+
195+
规则基于确定性模式匹配,可能把安全的命令片段判为高风险。例如文档中展示的危险命令、测试字符串或被转义的示例代码。
196+
197+
### 漏报
198+
199+
规则无法覆盖所有语言语义、动态拼接、编码混淆、间接调用或运行时生成命令。复杂攻击可能绕过静态文本匹配。
200+
201+
### 绕过风险
202+
203+
模型或用户可以尝试通过变量拼接、base64、下载后执行、跨文件组合等方式绕过规则。Policy allowlist 也可能因配置过宽降低防护效果。
204+
205+
## 为什么 Safety Guard 不能替代 Sandbox
206+
207+
Safety Guard 是执行前的静态审查层,适合快速阻断明显风险和输出可观测证据。它不能提供进程隔离、文件系统隔离、网络隔离、权限隔离或资源配额。
208+
209+
生产环境仍应使用 Sandbox、容器、只读挂载、网络策略、最小权限凭据、资源限制和人工审核流程。Safety Guard 应作为 Sandbox 之前的一层防线,而不是 Sandbox 的替代品。
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print('hello from tool safety')
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Inert sample: rm -rf /tmp/demo
2+
printf '%s\n' 'destructive delete sample is intentionally not executed'
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Inert sample: npm install left-pad
2+
printf '%s\n' 'dependency install sample is intentionally not executed'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{"action_type": "python", "allowed_domains": [], "blocked": false, "case": "allow_python", "decision": "allow", "desensitized": false, "input_sha256": "7523adf6df9ff2c18a6116b069e77f4fe8d273a980a0a4610f904e4809ddffa3", "latency": 3.5e-05, "risk_level": "none", "rule_id": "safe_python", "rules_evaluated": ["safe_python", "network_allowlist", "network_not_allowlisted", "read_dotenv", "read_ssh", "dangerous_delete", "subprocess_execution", "os_system_execution", "package_install", "npm_install", "apt_install", "infinite_loop", "sensitive_output", "wget_network", "aiohttp_network", "socket_network", "fork_bomb", "bash_pipe", "shell_injection", "excessive_concurrency", "large_file_write", "human_review_required"], "timestamp": "2026-07-01T00:00:00Z", "tool_name": "tool_safety_check"}
2+
{"action_type": "bash", "allowed_domains": [], "blocked": true, "case": "deny_bash", "decision": "deny", "desensitized": false, "input_sha256": "6ac440d686cb1abc7d1be8778126fcd67d01fc279fa5ce75a90065778209adc1", "latency": 0.000153, "risk_level": "critical", "rule_id": "dangerous_delete", "rules_evaluated": ["safe_python", "network_allowlist", "network_not_allowlisted", "read_dotenv", "read_ssh", "dangerous_delete", "subprocess_execution", "os_system_execution", "package_install", "npm_install", "apt_install", "infinite_loop", "sensitive_output", "wget_network", "aiohttp_network", "socket_network", "fork_bomb", "bash_pipe", "shell_injection", "excessive_concurrency", "large_file_write", "human_review_required"], "timestamp": "2026-07-01T00:00:01Z", "tool_name": "tool_safety_check"}
3+
{"action_type": "bash", "allowed_domains": [], "blocked": true, "case": "needs_human_review_bash", "decision": "needs_human_review", "desensitized": false, "input_sha256": "e5e6076a68ab5cc084360b7b1d92875e83d64bbf48090db0eb7c3e6ea0e03b74", "latency": 2e-05, "risk_level": "medium", "rule_id": "npm_install", "rules_evaluated": ["safe_python", "network_allowlist", "network_not_allowlisted", "read_dotenv", "read_ssh", "dangerous_delete", "subprocess_execution", "os_system_execution", "package_install", "npm_install", "apt_install", "infinite_loop", "sensitive_output", "wget_network", "aiohttp_network", "socket_network", "fork_bomb", "bash_pipe", "shell_injection", "excessive_concurrency", "large_file_write", "human_review_required"], "timestamp": "2026-07-01T00:00:02Z", "tool_name": "tool_safety_check"}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
allowed_domains:
2+
- api.example.com
3+
4+
blocked_paths:
5+
read_dotenv:
6+
- ".env"
7+
- ".env.local"
8+
read_ssh:
9+
- "~/.ssh"
10+
- ".ssh/"
11+
12+
allowed_commands:
13+
- bash
14+
- echo
15+
- python
16+
- python3
17+
18+
max_timeout: 60
19+
max_output_size: 10000
20+
21+
risk_levels:
22+
safe_python: none
23+
dangerous_delete: critical
24+
read_dotenv: high
25+
read_ssh: critical
26+
subprocess_execution: high
27+
os_system_execution: high
28+
package_install: medium
29+
npm_install: medium
30+
apt_install: medium
31+
infinite_loop: high
32+
sensitive_output: high
33+
wget_network: high
34+
aiohttp_network: high
35+
socket_network: high
36+
fork_bomb: critical
37+
bash_pipe: medium
38+
shell_injection: medium
39+
excessive_concurrency: high
40+
large_file_write: high
41+
human_review_required: medium
42+
network_allowlist: none
43+
network_not_allowlisted: high
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"generated_by": "SafetyReviewer + scripts/tool_safety_check.py report schema",
3+
"reports": [
4+
{
5+
"action_type": "python",
6+
"case": "allow_python",
7+
"decision": "allow",
8+
"evidence": "",
9+
"finding": "No risky code or command patterns detected.",
10+
"path": "examples/tool_safety/samples/allow.py",
11+
"recommendation": "Proceed with normal execution.",
12+
"risk_level": "none",
13+
"rule_id": "safe_python"
14+
},
15+
{
16+
"action_type": "bash",
17+
"case": "deny_bash",
18+
"decision": "deny",
19+
"evidence": "rm -rf",
20+
"finding": "Destructive delete operation detected.",
21+
"path": "examples/tool_safety/samples/deny.sh",
22+
"recommendation": "Do not run destructive deletes without explicit user approval and scoped paths.",
23+
"risk_level": "critical",
24+
"rule_id": "dangerous_delete"
25+
},
26+
{
27+
"action_type": "bash",
28+
"case": "needs_human_review_bash",
29+
"decision": "needs_human_review",
30+
"evidence": "npm install",
31+
"finding": "NPM package installation command detected.",
32+
"path": "examples/tool_safety/samples/needs_human_review.sh",
33+
"recommendation": "Send npm dependency installation through human review before mutating the environment.",
34+
"risk_level": "medium",
35+
"rule_id": "npm_install"
36+
}
37+
],
38+
"schema_version": 1
39+
}

0 commit comments

Comments
 (0)