Skip to content

feat(safety): 构建 Tool 执行脚本安全检查器,支持 Filter 拦截与监控#210

Closed
Stelquis wants to merge 0 commit into
trpc-group:mainfrom
Stelquis:main
Closed

feat(safety): 构建 Tool 执行脚本安全检查器,支持 Filter 拦截与监控#210
Stelquis wants to merge 0 commit into
trpc-group:mainfrom
Stelquis:main

Conversation

@Stelquis

Copy link
Copy Markdown

实现可插拔的脚本安全扫描系统,覆盖 6 类风险类型,
支持 Python (AST) 和 Bash (正则) 扫描,三级决策,
Filter/Wrapper 双接入方式,含审计日志与 OTel 埋点。

Fixes #90

@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

确认:wrapper 仅检查 is_blocked (第 145 行);needs_review 会落入 execute_fn (第 164 行)。Filter (第 158 行) 也是如此 —— needs_review 分支仅记录日志。现在我已经确认了所有问题,让我完成最终总结。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_safety_filter.py:158-164trpc_agent_sdk/tools/safety/_wrapper.py:145-164:NEEDS_HUMAN_REVIEW 实际不拦截,高危资源滥用脚本仍被执行
    • _determine_decision 将 MEDIUM 命中(如 R006 while True、fork bomb,policy 中 resource_abuse 为 medium)归为 NEEDS_HUMAN_REVIEW,但 SafetyFilter._beforeelif report.needs_review 分支只打 info 日志、不置 is_continue=FalseSafetyWrapper.run_safe 同样只判断 report.is_blockedneeds_review 直接落到 await execute_fn()。结果是 README 宣称"需人工审核"的脚本会被静默放行执行,资源滥用类风险形同未检测。建议:对 needs_review 显式拦截或提供可配置的拦截开关(默认拦截更安全),而非仅记录日志。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_safety_filter.py:86-93trpc_agent_sdk/tools/safety/_wrapper.py:82-86:策略文件缺失时静默回退到空策略,等于关闭防护

    • 默认策略文件不存在时仅打印 warning 并 SafetyPolicy()(无规则、default_decision=NEEDS_HUMAN_REVIEW),结合上一条 needs_review 不拦截的行为,意味着任何脚本都不会被 DENY,安全保护被静默降级。建议在文件缺失时抛错或至少让默认决策为 DENY,避免"装了 filter 却无防护"。
  • trpc_agent_sdk/tools/safety/_wrapper.py:24 与 README 示例:await execute_fn() 调用同步 lambda 会运行时报错

    • docstring/README 示例写成 execute_fn=lambda: run_bash_command("rm -rf /"),但 run_safe_wrapper.py:164 执行 result = await execute_fn(),对返回普通字符串的同步 lambda await 会抛 TypeError: object str can't be used in 'await' expression。示例会误导用户写出不可运行的代码;应改为 execute_fn=asyncio.coroutine(...) 或 async 函数并更新示例。
  • tests/tools/safety/test_scanner.py:620-627tests/tools/safety/test_scanner.py:568-573:高风险规则测试无有效断言

    • test_sensitive_info_leak_detectedprint 不 assert(实际 R007 会命中但未被验证);test_bash_pipe_sensitivelen(sensitive) > 0 or report.decision == DENY 的弱 or,即使 R002 未命中只要决策是 DENY 就通过,无法验证敏感文件读取检测本身。建议补齐对 R007/R002 命中的强制断言。

💡 Suggestion

总结

存在一个 Critical:MEDIUM/needs_review 类风险(含 R006 资源滥用)在 Filter 与 Wrapper 中均不拦截、被静默执行,安全防线存在缺口;另有策略缺失静默降级、示例代码 await 同步 lambda、测试断言缺失等问题建议一并修复。核心拦截逻辑(DENY 路径)本身可用。

测试建议

  • 补充 NEEDS_HUMAN_REVIEW 路径的端到端测试:验证 MEDIUM 风险脚本(如 while True)在 SafetyFilter/SafetyWrapper 下的实际拦截行为是否符合预期安全策略。
  • 为 R007(Python AST)、R002(bash 敏感文件读取)补强制命中断言,替换当前 print/弱 or 断言。

@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

已确认。现在开始撰写审查报告。

发现的问题

🚨 Critical

  • scripts/tool_safety_check.py:48--version 导入不存在的 __version__ 导致命令直接抛 ImportError
    • from trpc_agent_sdk.tools.safety import __version__ as ver 会失败,因为 trpc_agent_sdk/tools/safety/__init__.py 并未定义 __version__(已确认全文无此符号)。执行 python scripts/tool_safety_check.py --version 会直接崩溃而非打印版本。应在 __init__.py 中定义 __version__(或改从 trpc_agent_sdk.version 导入),与 CLI 用法保持一致。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_safety_filter.py:79-95:策略文件缺失时静默降级为空策略,过滤等于失效

    • _DEFAULT_POLICY_PATH 不存在时只打印 warning 并使用 SafetyPolicy()rules={},无任何规则)。此时 SafetyFilter 不会拦截任何危险脚本,且仍以 safety_filter 名义注册生效,造成"已启用安全防护"的虚假安全感。建议缺失时直接抛异常拒绝初始化,或至少在 _before 中对非空脚本一律阻断。
  • trpc_agent_sdk/tools/safety/_policy.py:159-195allowed_commands / trigger_commands / is_command_allowed 为死代码,命令白名单形同虚设

    • is_command_allowedallowed_commands 在整个 scanner/filter 链路中从未被调用(仅 policy 自身定义),trigger_commands 也未用于触发域名检查;而默认白名单还包含 rmchmod 等危险命令。这会让运维误以为存在命令白名单防护。建议要么接入 BashScanner 的命令解析逻辑,要么从策略与文档中移除以免误导。
  • trpc_agent_sdk/tools/safety/_types.py:283-290:OTel 属性用 str(bool) 输出 "True"/"False",不符合 OTel 规范

    • to_otel_attributes()blocked/masked 转为 Python str(bool) 得到大写的 "True"/"False",测试还将其固化为该值。OTel 布尔属性应为原生 bool(或小写 "true"/"false"),当前值会被下游当作普通字符串消费,影响监控告警判定。建议直接传 bool
  • trpc_agent_sdk/tools/safety/tool_safety_policy.yaml:118process_executionsu 模式(带空格)会产生误判

    • re.search("su ", line) 会匹配任何含 su 子串的行,如 result = issue_logconsole.log("...") 之外含 "su " 的合法命令都会被标记为 HIGH/DENY。属于高风险误报,建议改为更精确的边界匹配(如 (^|[\s;&|])su(\s|$))。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:166-175:网络外连命中时 line_number 固定为 0,丢失定位信息

    • _check_network_egress 返回的 RuleMatch 一律 line_number=0,未传入实际行号;审计与报告无法定位到具体行,且 Python 侧 _add_match 的去重逻辑依赖 (rule_id, line_number),多行违规可能被误去重。建议传入真实 line_no
  • trpc_agent_sdk/tools/safety/_policy.py:104-131from_dict 缺少类型校验,结构异常的策略文件会引发运行时错误

    • allowed_domains/forbidden_paths/patterns 等若被写成字符串或标量,会在 is_domain_allowedre.searchfor pattern in ... 处抛 TypeError,而非给出可读的配置错误。建议对关键字段做 isinstance(..., list) 校验并显式报错。
  • tests/tools/safety/test_filter.py:128-140test_wrapper_allows_safe 用空规则策略断言"放行",实际掩盖了"无匹配→NEEDS_HUMAN_REVIEW"的语义

    • 空规则时 default_decision=NEEDS_HUMAN_REVIEWis_blocked 固然为 False,但断言并未校验 decision,无法区分 ALLOW 与 NEEDS_HUMAN_REVIEW,测试无法真正验证放行路径。建议显式断言 decision in (ALLOW, NEEDS_HUMAN_REVIEW) 或构造明确 ALLOW 的策略。

💡 Suggestion

总结

整体实现完整、测试覆盖较广,但存在一个必须修复的 Critical:CLI --version 因导入不存在的 __version__ 直接崩溃;其余主要是安全防护"静默失效"风险(空策略降级、命令白名单死代码)与策略误报/OTel 数据规范问题,建议在合入前修复 Critical 并至少处理空策略降级。

测试建议

  • 补充 SafetyFilter 在策略文件缺失/rules={} 时的行为断言(当前会放行 rm -rf /,应明确是否可接受)。
  • 补充 CLI --version 的执行测试,防止再次回归。

Comment thread scripts/tool_safety_check.py Outdated
parser.add_argument("--version", "-v", action="store_true", help="Show version")

args = parser.parse_args()

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.

--version 导入不存在的 version 导致崩溃

from trpc_agent_sdk.tools.safety import __version__ 会失败,因为 safety 包的 init.py 未定义该符号,执行 --version 直接抛 ImportError 而非打印版本。建议在 init.py 中定义 version,或改从 trpc_agent_sdk.version 导入。

@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我已经掌握了足够的信息。让我来整理最终的审查报告。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_python_scanner.py:108-115:R003 网络外连检测对非字符串字面量 URL 完全失效,存在安全绕过
    • _get_string_arg 仅当第一个参数是 ast.Constant 字符串字面量时才返回值;对于变量、f-string、requests.get(url)os.environ["URL"] 等动态 URL,url_argNone,整个 R003 检查被静默跳过。恶意脚本只需把 URL 存入变量即可绕过非白名单域名拦截。结合 _check_network_egress 只在 check_domains 规则上触发的逻辑,动态构造的请求不会被任何规则命中。建议:对命中的网络函数调用,当无法静态解析 URL 时降级为 NEEDS_HUMAN_REVIEW(R003 medium)或至少记录一条告警,而不是放行。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_scanner.py:90-94:UNKNOWN 脚本类型“先 Python 后 Bash”回退会丢弃 Bash 命中,导致漏报

    • 对未知类型先跑 PythonScanner,仅当“无任何匹配”时才跑 BashScanner。一段 bash 脚本若恰好不含 Python 可解析的危险 AST(绝大多数纯 bash 都会 ast.parse 抛 SyntaxError 触发 _text_scan 走 Bash),但如果脚本可被 Python AST 解析(如全是注释/字符串/合法表达式),则不会回退到 Bash 扫描,bash 规则(R001/R003/R005)全部漏掉。建议改为合并两个扫描器的结果,而非短路覆盖。
  • trpc_agent_sdk/tools/safety/_python_scanner.py:144-148tool_safety_policy.yaml 中 R006 patterns: ["while True", "while :"]while True 检测双轨且 AST 路径漏报 while 1/while 1 == 1

    • AST 检测仅匹配 ast.Constantvalue is True,但 while 1:Constant int)、while 1 == 1:while not False: 等等价无限循环均不触发 R006;同时 BashScanner 又用正则 while True 匹配,对 Python 代码(注释里写 while True 也会误报)规则不一致。建议统一为“条件为常量真值(1True、非常量表达式)”的判定,或明确文档化已知漏报。
  • trpc_agent_sdk/tools/safety/_python_scanner.py:163-176 + tests/tools/safety/test_scanner.pytest_sensitive_info_leak_detected:R007 的敏感信息泄漏检测对样例实际不命中,测试未做断言

    • 样例 leak_api_key.pyapi_key = "sk-123456789012345678901234"_is_sensitive_valuere.match 匹配 sk-[A-Za-z0-9]{20,} —— "sk-123456789012345678901234"sk- 后是 24 位数字,应能命中;但测试函数 test_sensitive_info_leak_detectedprint 了结果,没有任何 assert。这意味着即使 R007 逻辑回归(例如有人把 re.match 改错),测试也会静默通过。该测试对高风险规则零保护,需补 assert len(r007) > 0
  • trpc_agent_sdk/tools/safety/_safety_filter.py 整体(__init__ + _before)+ trpc_agent_sdk/tools/__init__.py:README 宣称的 BashTool(filters_name=["safety_filter"]) 开箱即用,但 safety 模块未被 tools/__init__.py 导入,过滤器未注册

    • @register_tool_filter("safety_filter") 装饰器仅在显式 import trpc_agent_sdk.tools.safety 时执行。BaseTool.__init___init_filtersget_filter(FilterType.TOOL, "safety_filter") 会返回 None 并抛 ValueError("Filter safety_filter not found")。用户照 README 直接用会构造失败。建议在 tools/__init__.py(或框架入口)导入 tools.safety 以触发注册,或在文档中明确要求先导入。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:147-163:R003 非白名单域名 line_number=0,审计/定位信息缺失

    • _check_network_egress 生成的 RuleMatch 写死 line_number=0,尽管该方法在 _scan_line 中按行调用、已知 line_no。该参数未被传入,导致报告里所有 R003 命中行号都是 0,运维无法定位。修复:把 line_no 透传进 _check_network_egress
  • trpc_agent_sdk/tools/safety/_policy.py:206-215_glob_to_regex)+ forbidden_paths: [".env", "~/.ssh/*"]is_path_forbiddenre.search 子串匹配,导致广泛误报

    • _glob_to_regex(".env") 生成正则 .env. 未转义为任意字符),re.search 会在任何含 env 前任意字符的路径上命中,例如 /home/user/xenv.txt/data/develop/app.py(含 .env 子串 developevel 不命中,但 dotenvxenv 会命中)。同样 /.ssh/ 会匹配任意含 .ssh/ 的路径。这会让 R002 在合法路径上误报为 DENY/HIGH,阻断正常脚本。建议:对无通配符的条目用 re.fullmatch 或精确匹配,且正确转义 .

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_python_scanner.py:117-119socket.connect/socket.create_connection 只要出现就报 R003,无域名判定,所有合法 socket 用法都会被标 HIGH/DENY;建议至少尝试解析目标 host 再判定,或在策略中调整为 MEDIUM/review。

总结

整体风险偏高:作为安全护栏模块,存在可被绕过的检测盲区(动态 URL 不触发 R003、UNKNOWN 类型回退丢命中)以及会导致误拦截的路径匹配过宽问题,外加一个高风险规则(R007)的测试零断言。Critical 项(R003 动态 URL 绕过)建议合并前修复;Warning 项中测试断言缺失与 safety_filter 未注册两类问题也建议本次解决,否则核心功能与文档承诺不一致。

测试建议

  • 为 R003 补充动态 URL 场景:url = "http://evil.com"; requests.get(url) 应被拦截或至少标为需人工审核,断言 decision != ALLOW
  • 修复 test_sensitive_info_leak_detected:补 assert len(r007) > 0,并新增 R007 漏报/误报边界(如非敏感普通字符串不应命中)。
  • is_path_forbidden 增加误报用例:/home/user/develop/app.py/tmp/xenv.txt 应返回 False.env/~/.ssh/id_rsa 应返回 True

"httpx.get",
"httpx.post",
"httpx.put",
"httpx.delete",

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.

R003 动态 URL 绕过:非字符串字面量 URL 静默放行

_get_string_arg 仅在参数为字符串字面量时返回值,变量、f-string、requests.get(url) 等动态 URL 会被解析为 None,导致 R003 整体跳过,恶意脚本只需将 URL 存入变量即可绕过非白名单域名拦截。建议对无法静态解析 URL 的网络函数调用降级为 NEEDS_HUMAN_REVIEW 或记录告警,而非放行。

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
trpc_agent_sdk/tools/safety/_python_scanner.py 77.38693% 45 Missing ⚠️
trpc_agent_sdk/tools/safety/_audit.py 74.19355% 16 Missing ⚠️
trpc_agent_sdk/tools/safety/_policy.py 88.54167% 11 Missing ⚠️
trpc_agent_sdk/tools/safety/_safety_filter.py 89.24731% 10 Missing ⚠️
trpc_agent_sdk/tools/safety/_wrapper.py 82.60870% 8 Missing ⚠️
trpc_agent_sdk/tools/safety/_bash_scanner.py 94.02985% 4 Missing ⚠️
trpc_agent_sdk/tools/safety/_scanner.py 94.87179% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #210   +/-   ##
==========================================
  Coverage        ?   87.88992%           
==========================================
  Files           ?         488           
  Lines           ?       45747           
  Branches        ?           0           
==========================================
  Hits            ?       40207           
  Misses          ?        5540           
  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.

@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我已经完成了审查,让我整理一下结论。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_audit.py:76-77log_report_emit_otel(event, report) 调用参数顺序与定义不符

    • 静态方法 _emit_otel(report, event) 的形参顺序是先 reportevent,但 log_report_audit.py:76 处以 self._emit_otel(event, report) 顺序传入,导致 OTel 上报时 report 实际收到的是 AuditEventevent 收到的是 SafetyReport,后续 set_attributes(event.to_otel_attributes()) 调用的是 SafetyReport.to_otel_attributes()SafetyReport 没有该方法)会抛 AttributeError,被 except Exception 吞掉,安全决策属性无法上报。修复为 self._emit_otel(report, event)
    ...
    # 当前:self._emit_otel(event, report)
    # _emit_otel 签名:(report, event)
    ...
  • scripts/tool_safety_check.py:88-95--json 输出模式缺少退出码语义,且与文档/默认模式不一致

    • 当使用 --json 时,无论 report.is_blocked 还是 needs_review 都只 print(...) 后正常 return(退出码 0),而默认模式对 blocked 退出 2、needs_review 退出 1。这会让依赖退出码做拦截的 CI/调用方在 --json 模式下完全失效,高危脚本(如 rm -rf /)被判定为 DENY 却返回成功。建议 --json 分支同样根据 is_blocked/needs_review 设置退出码。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_scanner.py:158-186_determine_decision 完全忽略 RuleConfig.decision

    • 策略文件中每条规则都显式配置了 decision(如 R006 decision: deny),但总体决策只根据 risk_level 高低推导,未参考单条规则的 decision。这意味着任何将某规则 decision 改为 allow 的用户配置都不会生效,与"修改策略文件即可生效"的设计承诺不符。建议至少在规则 decision=DENYALLOW 时覆盖基于 risk_level 的推导。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:103-130:R007 在 Bash 扫描中会被双重匹配

    • _scan_line 先在通用 for rule_name, rule_cfg 循环里对 sensitive_info_leak 规则用其 patterns(如 API_KEYpassword)做正则匹配并 append 一条 R007(masked=False),随后第 127-131 行又调用 _check_sensitive_leak 再 append 一条 R007(masked=True)。同一行同一规则产生两条语义冲突的 match(一个标记为未脱敏、一个标记为脱敏),会让审计/日志产生重复且不一致的记录。建议在通用循环里跳过 sensitive_info_leak,或去掉独立的 _check_sensitive_leak 分支。
  • trpc_agent_sdk/tools/safety/_policy.py:1819-1945is_path_forbidden):使用 re.search 进行 glob 匹配存在路径穿越误判/漏判风险

    • _glob_to_regex 生成的是子串正则且用 re.searchforbidden_paths 中的 .env 会匹配任何含 .env 的路径(如 my.env.backup),而 /etc/shadow 这类无通配符模式本应精确匹配却会匹配 /etc/shadow.bak。对安全敏感的路径判定而言,这种子串匹配既可能漏判也可能误判。建议改为锚定匹配(re.fullmatch 或对非 glob 模式做完整路径比较/os.path 规范化后再比较)。
  • trpc_agent_sdk/tools/safety/_python_scanner.py:2268-2276_is_domain_allowed_in_url 在无法提取域名时默认放行

    • 当 URL 无法被正则匹配出域名时(如 http://1.2.3.4http://[::1]、或非常规格式),函数返回 True(放行),这意味着对 IP 形式或畸形 URL 的网络外连不会触发 R003,绕过白名单。建议提取失败时按非白名单处理(返回 False)或对 IP/localhost 单独策略化。
  • trpc_agent_sdk/tools/safety/_wrapper.py:80-82:策略文件缺失时回退到空 SafetyPolicy()default_decisionNEEDS_HUMAN_REVIEW 会拦截一切

    • SafetyFilter(缺失策略直接 raise FileNotFoundError)不同,SafetyWrapper 在默认策略文件不存在时静默使用空 SafetyPolicy(),其 default_decision=NEEDS_HUMAN_REVIEW 会让任何脚本(包括无害的 ls -la)都被 run_safe 当作 blocked 返回(见 run_safe 第 116-122 行 is_blocked or needs_review 分支)。这一行为既与 Filter 不一致,也让"未配置策略"难以被察觉。建议要么抛异常,要么在空策略时把默认决策改为 ALLOW。
  • tests/tools/safety/test_filter.py:411-416test_filter_allows_safe_command):测试名/注释与断言矛盾,掩盖了"安全命令也被拦截"的真实行为

    • 测试名为 allows_safe_command、注释为 "should allow safe commands",但实际断言 rsp.is_continue is False,即 ls -la 在默认策略下也被拦截。这并非测试有效性的覆盖,而是用相反的语义"修绿"了一个反直觉的设计(默认 NEEDS_HUMAN_REVIEW 即拦截)。建议要么修正测试命名与期望、要么显式断言默认策略下安全命令确实走人工审核,避免误导后续维护者。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:1638-1668_check_network_egress 对一行内多个域名只返回第一个非白名单域名便 return,后续域名不再检查;同时 network_egress 规则即便无 patterns 也会因 check_domains 触发,可考虑合并循环并补全多域名场景。

  • trpc_agent_sdk/tools/safety/_policy.py:1792-1801RuleConfig 文档声称有 trigger_commands 字段且 YAML 中 network_egress 配置了 trigger_commands,但 RuleConfigfrom_dict 均未解析该字段,属于文档/配置与实现不一致,建议补齐字段或在文档中移除。

总结

整体实现结构清晰,但存在一处必须修复的 OTel 参数顺序错误(安全属性无法上报被静默吞掉)和一处 --json 模式退出码缺失导致 CI 拦截失效的问题;此外决策逻辑忽略规则级 decision、路径匹配用子串正则、域名提取失败默认放行等 Warning 级问题会影响安全判定的准确性,建议在合入前处理。

测试建议

  • 补充用例覆盖 _emit_otel 参数顺序:构造一个 recording 的 OTel span,断言 log_report 后 span 上确实写入了 tool.safety.* 属性。
  • 补充 --json 模式下对 rm -rf /(DENY)退出码为 2 的断言,避免 CI 误判通过。
  • 补充 _is_domain_allowed_in_url 对 IP(http://1.2.3.4)和提取失败 URL 的期望(应触发 R003 而非放行)。

Comment thread trpc_agent_sdk/tools/safety/_audit.py Outdated

# Emit telemetry events
if self._enable_otel:
self._emit_otel(event, report)

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.

_emit_otel 调用参数顺序与定义不符

log_report 以 self._emit_otel(event, report) 调用,但静态方法签名是 _emit_otel(report, event),导致 OTel 上报时 report/event 错位,SafetyReport.to_otel_attributes() 抛 AttributeError 被 except 吞掉,安全决策属性无法上报。修复为 self._emit_otel(report, event)。

Comment thread scripts/tool_safety_check.py Outdated
)
report = scanner.scan(scan_input)

if args.json:

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.

--json 模式缺少退出码语义

--json 分支只 print 后正常 return(退出码 0),未像默认模式对 blocked 退出 2、needs_review 退出 1。依赖退出码拦截的 CI 在 --json 模式下完全失效,rm -rf / 被 DENY 却返回成功。建议该分支同样根据 is_blocked/needs_review 设置退出码。

@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我已经审查了 pr.diff(新增 trpc_agent_sdk/tools/safety/ 安全扫描模块、测试、CLI 及示例),并对照了仓库中 BaseFilter/FilterResult/BashTool 等上下游调用关系。下面是结论。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_scanner.py:189-203:安全报告的 script_summary 原样泄露脚本中的敏感信息

    • _build_summary 仅做空格压缩和截断,不脱敏;SafetyReport.to_dict()_types.py:201-229)会把它输出。被 R007 命中的硬编码密钥(如 api_key = "sk-...")会原样出现在 script_summary 中,并通过 SafetyWrapper.run_safe 返回值 / CLI --json 输出暴露——安全扫描器反而成为敏感信息泄露通道。建议对命中的敏感片段做掩码或不在 summary 中包含原始脚本。
  • trpc_agent_sdk/tools/safety/_safety_filter.py:151-165:默认策略下 safety_filter 会拦截所有未命中规则的(安全)命令

    • _beforeneeds_review 也置 is_continue=False,而默认 default_decision=NEEDS_HUMAN_REVIEWtool_safety_policy.yaml:17_scanner.py:179-180)。结果:ls -lagit status 这类无匹配命令都会被拦。README 说“高危脚本被拦截”暗示安全命令放行,与实际行为矛盾,直接启用 BashTool(filters_name=["safety_filter"]) 会让该工具几乎不可用。建议默认放行无匹配脚本,或显式文档化 fail-closed 语义。
  • scripts/tool_safety_check.py:44:CLI 默认 --policy 路径在仓库根目录下不存在

    • 默认值 tool_safety_policy.yaml 相对当前工作目录解析,但实际策略文件位于 trpc_agent_sdk/tools/safety/tool_safety_policy.yaml。从仓库根目录运行 python scripts/tool_safety_check.py xxx.sh 会直接 FileNotFoundError 退出。建议默认指向模块自带策略(复用 _DEFAULT_POLICY_PATH)。
  • trpc_agent_sdk/tools/safety/_wrapper.py:159run_safeexecute_fnawait,但文档/README 示例传入同步 lambda

    • 模块 docstring(_wrapper.py:20-25)与 README.md:52 都示例 execute_fn=lambda: run_command(...)(同步),而代码 result = await execute_fn() 对同步返回值 await 会抛 TypeError。类型标注 Callable[[], Any] 也未要求返回 coroutine。建议统一为 Awaitable 并修正示例,或兼容同步 callable。
  • trpc_agent_sdk/tools/safety/_safety_filter.py:181-185_extract_script_content 只扫描顶层字符串参数,嵌套脚本内容被绕过

    • 仅检查 command/content/script/code/cmd/body 顶层键;若工具将脚本放在 args["input"]["code"] 等嵌套结构中,则提取不到、直接放行,安全扫描被旁路。建议至少对常见嵌套结构做递归提取,或在未提取到时记录告警。

💡 Suggestion

  • tests/tools/safety/test_filter.py:77test_filter_allows_safe_command 名称与断言相反

    • 测试名声称“允许安全命令”,但断言 rsp.is_continue is False(实际因默认 needs_review 被拦)。建议改名为 test_filter_blocks_unmatched_command 以免误导后续维护者。
  • trpc_agent_sdk/tools/safety/_policy.py:67tool_safety_policy.yaml:9allowed_commands 在文档/注释中声明但从未加载

    • SafetyPolicy 注释和 yaml 注释都提到“允许命令(Bash 白名单)”,但 from_dict 并未解析该字段,运行时无任何效果。建议实现该字段或移除文档声明。

总结

整体为 fail-closed 的静态安全扫描器,核心拦截链路与 BaseFilter 集成正确、测试覆盖了 7 条规则;但存在若干需修复的问题:报告 summary 会泄露敏感信息、默认策略会拦截所有安全命令、CLI 默认策略路径不可用、Wrapper 示例与实现不符、嵌套脚本可绕过扫描。无明确 Critical 阻塞问题,上述 Warning 建议在合入前处理。

测试建议

  • 补充用例:脚本含硬编码密钥时,report.to_dict()["script_summary"] 不应包含明文密钥。
  • 补充用例:默认策略下 BashTool 执行 ls -la 等普通命令的放行/拦截预期需明确(当前会拦截,确认是否符合设计)。
  • 补充用例:SafetyWrapper.run_safe 传入同步 callable 与 async callable 的行为契约。

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