Skip to content

feat: 新增 Skills+沙箱+DB 自动代码审查智能体示例 (#92)#206

Open
EricChan2028 wants to merge 1 commit into
trpc-group:mainfrom
EricChan2028:feat/92-code-review-agent
Open

feat: 新增 Skills+沙箱+DB 自动代码审查智能体示例 (#92)#206
EricChan2028 wants to merge 1 commit into
trpc-group:mainfrom
EricChan2028:feat/92-code-review-agent

Conversation

@EricChan2028

@EricChan2028 EricChan2028 commented Jul 19, 2026

Copy link
Copy Markdown

PR: 基于 Skills + 沙箱 + 数据库的自动代码评审 Agent (#92)

PR: Automatic Code-Review Agent with Skills + Sandbox + Database (#92)

  • Issue: 基于 Skills + 沙箱 + 数据库存储构建自动代码评审 Agent #92(2026 犀牛鸟开源人才培养活动)
  • Branch: feat/92-code-review-agentmain
  • Commit: 1b8907da3f4c7af51cd53967985fd17d0ea2df5c(单提交,64 files, +6065 lines)
  • Patch: docs/patches/issue-92/0001-feat-Skills-DB-92.patch
  • Deliverable root / 交付目录: examples/skills_code_review_agent/

一、摘要 / Summary

中文:新增示例 examples/skills_code_review_agent/——一个可离线验证的自动代码评审
Agent 原型。它读取 git diff / PR patch / 本地变更目录,经 code-review Skill
加载规则与脚本,通过 Filter 治理链(真实 BaseFilter + run_filters)放行后,
沙箱(container 生产默认 / Cube-E2B / local 开发回退)中执行检查脚本,将结构化
findings、拦截记录、沙箱日志与监控指标全部写入 SQLAlchemy 五表 schema,最终输出
中英双语 review_report.json + review_report.md。全链路支持 --dry-run(fake
model + local 沙箱),无任何 API Key、无 Docker 也能完整跑通并通过 70 项测试。

English: Adds examples/skills_code_review_agent/ — an offline-verifiable
automatic code-review agent prototype. It ingests a git diff / PR patch / local
change set, loads rules and scripts through a code-review Skill, passes a
Filter governance chain (real BaseFilter + run_filters), executes checks
inside a sandbox (container as production default / Cube-E2B / local as dev
fallback), persists structured findings, filter events, sandbox logs and
telemetry metrics into a five-table SQLAlchemy schema, and renders bilingual
review_report.json + review_report.md. The whole pipeline runs under
--dry-run (fake model + local sandbox) with zero API keys and zero Docker,
covered by 70 tests.

二、动机 / Motivation

中文:Issue #92 的难点不是“让 LLM 评论代码”,而是把 SDK 的 Skills、
CodeExecutor 沙箱、SQL Storage、Filter、Telemetry 六大能力串成一个可验证系统
风险识别可复现、执行有安全边界、每一步决策可审计可回放。本 PR 给出一个端到端参考
实现,同时演示了这些 SDK 模块的组合姿势(对齐 examples/claude_agent_with_skills
examples/code_executorsexamples/filter_with_agent 的既有习惯用法)。

English: The hard part of issue #92 is not "LLM comments on code" but wiring
the SDK's Skills, CodeExecutor sandboxes, SQL storage, Filters and Telemetry
into one verifiable system: reproducible risk detection, hard safety
boundaries around execution, and a fully auditable/replayable decision trail.
This PR provides an end-to-end reference implementation, mirroring the idioms of
the existing claude_agent_with_skills, code_executors and
filter_with_agent examples.

三、设计 / Design

--diff-file | --repo-path | --files | --fixture
      │ inputs.py → RawChangeSet(unified diff + 可选全文件内容)
      ▼
ReviewPipeline.run()                          [span code_review.total]
  1 建任务 cr_review_task(status=running)
  2 宿主解析 diff → 摘要(无内容, 安全入库)     [span code_review.parse]
  3 Filter 治理门 (run_filters)
      allow → 4        deny/needs_human_review → 记录拦截, 走宿主回退
  4 沙箱执行 run_checks.py                    [span code_review.sandbox]
      stage skill → 注入 diff.json → 白名单环境运行(超时/输出上限)
      失败/超时 → cr_sandbox_run 记录 + 宿主回退, 任务继续
  5 后处理: 去重 → 降噪分桶 → 二次脱敏        [span code_review.postprocess]
  6 LLM 摘要 (fake|real|off)                  [span code_review.llm]
  7 落库 findings/filter_events/report + 渲染 json/md 报告

关键决策 / Key decisions

  1. 单一实现、双端复用 / Single source of truth: diff 解析器、6 类规则引擎、
    密钥正则表全部在 skills/code-review/scripts/lib/(纯 Python 标准库)。沙箱内
    直接执行这份代码;宿主端通过 importlib 加载同一份文件做回退检测与脱敏——检测
    逻辑与脱敏规则永不漂移。Sandbox executes these files directly; the host loads
    the very same files via importlib for fallback detection and redaction — no
    drift possible.
  2. 沙箱分层 / Sandbox tiers: create_sandbox_runtime("container"|"cube"|"local")
    container(Docker,create_container_workspace_runtime)为生产默认
    cube 走 Cube/E2B 云沙箱(惰性导入);local 仅为开发 fallback,且用
    EnvWhitelistLocalProgramRunner 强制环境变量白名单(金丝雀测试证明
    TRPC_AGENT_API_KEY 永不进入子进程)。Container is the documented production
    default; local is dev-only and env-whitelisted.
  3. 失败即数据 / Failure-as-data: 超时、非零退出、输出截断、运行时 OSError 全部
    落为 cr_sandbox_run 行(status/exit_code/timed_out/error_type/脱敏 stderr 摘录),
    随后自动回退到宿主内规则引擎——评审任务永不崩溃(AC4)。
  4. 治理先行 / Governance-first: SandboxGovernanceFilter 走 SDK 真实
    BaseFilter/run_filters 链,做 5 项前置检查(高风险脚本内容、非白名单命令、
    禁止路径、网络访问、运行次数/时间预算)。deny/needs_human_review 通过
    rsp.rsp=PolicyDecision, is_continue=False 短路,终端 handler 不被调用;拦截
    原因写入报告与 cr_filter_event 表(AC7 / R8)。
  5. 三层脱敏 / Three-layer redaction: 沙箱内产出证据时先脱敏 → 宿主对每条
    finding 二次脱敏 → 报告/入库前全文档扫描。48 条密钥语料 100% 检出(含 AWS、
    GitHub classic + fine-grained PAT、GitLab、Slack、OpenAI、JWT、PEM、Azure
    conn-string、:= 赋值等),报告与 sqlite 文件字节级扫描无明文(AC5)。
  6. 去重与降噪 / Dedup & noise control: (file, line, category) 唯一,保最高
    severity/confidence 代表项并把并列规则合入 extra.also_matched;置信度 < 0.7
    的启发式结果进入 needs_human_review 独立桶(DB 有独立 bucket 列),绝不混入高
    置信 findings(R6)。
  7. 存储可移植 / Portable storage: ReviewStore ABC + SDK 可移植列类型;
    SQLite 默认,换 MySQL/PostgreSQL 只改 SQLAlchemy URL;init-db 幂等
    (create_all + 前向列迁移)。
  8. 模型可插拔 / Pluggable model: --model-mode fake|real|off。fake 与 real
    驱动完全相同LlmAgent + Runner 路径;real 用 OpenAIModel
    TRPC_AGENT_* 环境变量。规则检测本身不依赖 LLM,模型只做摘要增强。

数据库 Schema / Database schema(5 tables)

表 Table 内容 Content 关键字段 Key fields
cr_review_task 任务与状态机 task + state machine id, status, input_type/ref, diff_summary(JSON), config(JSON), error_*
cr_sandbox_run 每次沙箱尝试(含拦截/失败)every sandbox attempt task_id, status(ok/failed/timeout/blocked/error), exit_code, timed_out, filter_action, stdout/stderr excerpt(redacted+truncated), error_type
cr_filter_event 每个治理决策 every governance decision task_id, stage, target, action, rule, reasons(JSON)
cr_finding 结构化 finding task_id, severity, category, file, line, title, evidence(redacted), recommendation, confidence, source, rule_id, bucket, dedup_key
cr_report 最终报告 + 监控摘要 final report + metrics task_id(unique), summary, severity_stats, filter_summary, sandbox_summary, metrics, report(full JSON)

四、验收标准对照表 / Acceptance-Criteria Mapping

# 验收标准 Criterion 满足位置 Where satisfied 测试 Test
1 8 条公开 diff 样本全部可运行并生成报告 / all 8 public diff samples run and produce reports fixtures/ 8 条(clean、security、async-leak、db-lifecycle、missing-tests、duplicate、sandbox-failure、secret-redaction);CLI review --fixture X --dry-run 每次产出 review_report.json + .md;沙箱失败样本经 --inject-sandbox-failure 演示失败路径(completed_with_errors,报告照常渲染) tests/test_fixtures_e2e.py(8 fixtures 参数化全跑,逐条断言预期 findings)
2 隐藏样本高危检出 ≥80%、误报 ≤15% / hidden-set ≥80% detection, ≤15% FP 隐藏集不可得,以带标注公开语料为代理(README 验收表中明示):19/19 种子正样本全检出(召回 100%),10 条干净样本高置信 FP=0(0%);启发式规则置信度封顶 ≤0.65 → 进 needs_human_review,不污染高置信 findings tests/test_rules.py(labeled corpus)
3 DB 完整记录 task/sandbox run/finding/report,按 task id 可查 / complete DB records, queryable by task id 5 表 schema(codereview/store/models.py)+ ReviewStore.get_task_bundle(task_id);CLI show --task-id 返回全链路 bundle;init-db 幂等 tests/test_store.pytests/test_cli_and_report.py
4 沙箱超时/输出上限;失败不崩溃 / sandbox timeout & output cap; failure never crashes review codereview/sandbox.py:wall-clock 超时击杀、stdout/stderr 字节上限 + 截断标志、环境白名单;失败落 cr_sandbox_run 行并宿主回退,任务收敛为 completed_with_errors tests/test_sandbox_safety.py(超时、截断、金丝雀、强制失败、broken-runtime OSError、workspace 清理)
5 脱敏检出 ≥95%,报告与 DB 无明文 / redaction ≥95%, no plaintext in report or DB 三层脱敏(沙箱内 → 单 finding → 全文档);48 条密钥语料 100% 检出、良性列表 0 FP;e2e 对 review_report.json/.md 与原始 sqlite 文件做字节级明文扫描 tests/test_redaction.pytests/test_fixtures_e2e.py(secret_redaction fixture)
6 dry-run ≤ 2 分钟 / dry-run completes ≤ 2 min --dry-run = fake model + local 沙箱;实测管线 0.3–1.3 s/fixture(含解释器与 SDK 导入的冷启动 ~40 s,env -i 空环境验证零 Key 依赖) tests/test_cli_and_report.py::test_dry_run_speed_and_no_api_key(删除全部 Key 环境变量后断言 <120 s)
7 高风险脚本先经 Filter;deny/needs_human_review 不进沙箱 / filter decides first; deny & needs_human_review never execute codereview/governance.pySandboxGovernanceFilter,5 项检查);handler 哨兵证明被拒内容从未执行;拦截原因入报告 + cr_filter_event tests/test_governance_filter.py(含 pipeline 级 e2e:拦截同时出现在报告与 DB,沙箱运行数为 0,宿主回退仍出 findings)
8 报告含 7 个规定部分 / report contains all 7 mandated sections codereview/report.py:Findings 摘要 / 严重级别统计 / 人工复核项 / Filter 拦截摘要 / 监控指标 / 沙箱执行摘要 / 修复建议(编号、可执行、按严重级别排序),中英双语 tests/test_cli_and_report.py::test_report_sections_complete

具体要求 R1–R9 / Detailed requirements

R 要求 Requirement 实现 Implementation
R1 CR Skill(SKILL.md + 规则文档 ≥4 类 + 脚本) skills/code-review/:SKILL.md + 6 条规则文档(安全/异步/资源泄漏/测试缺失/密钥泄漏/DB 生命周期)+ scripts/parse_diff.pyrun_checks.py、纯标准库 lib/
R2 Container / Cube-E2B 沙箱;local 仅 dev fallback codereview/sandbox.py::create_sandbox_runtime;`--sandbox container
R3 unified diff / 文件列表 / git 工作区解析 codereview/inputs.py + diff_parser.py;支持 rename、binary、CRLF、\ No newline、删除文件等边界
R4 findings ≥9 字段 severity, category, file, line, title, evidence, recommendation, confidence, source(+ rule_id, bucket, dedup_key);e2e 逐条断言
R5 最小 schema + 可换 SQL 后端 5 表 + ReviewStore ABC + init_db.py(幂等)
R6 去重 + 低置信降噪 (file,line,category) 唯一;<0.7 → needs_human_review
R7 超时/输出上限/环境白名单/脱敏/失败记录 sandbox.py + redaction.py 三层脱敏 + cr_sandbox_run 失败行
R8 Filter 前置拦截 + 原因入报告和 DB governance.py 5 项检查;报告 **deny** 表 + cr_filter_event.reasons
R9 监控审计指标 cr_report.metrics:total/sandbox 耗时、工具调用数、拦截数、finding 数、severity 分布、error_types 分布;每阶段 tracer span

五、离线运行方法 / How to Run Offline

无需 API Key、无需 Docker、无需网络。/ No API key, no Docker, no network.

cd examples/skills_code_review_agent
# venv: ~/.venvs/trpc92/bin/python  (uv Python 3.12, CPU torch)

# 1. 离线体检(fake model + local 沙箱)/ offline smoke
python run_agent.py review --fixture security_issue --dry-run

# 2. 评审真实输入 / review real inputs
python run_agent.py review --diff-file my.patch
python run_agent.py review --repo-path /path/to/repo
python run_agent.py review --files a.py b.py

# 3. 数据库查询 / DB queries
python run_agent.py show --task-id <ID>   # 全链路 bundle:task+runs+events+findings+report
python run_agent.py list
python run_agent.py init-db               # 幂等初始化/迁移

# 4. 生产形态(本主机不可执行,代码完备)/ production shape (code-complete)
python run_agent.py review --diff-file my.patch --sandbox container --model-mode real

# 5. 测试与 lint / tests & lint(仓库根目录 / from repo root)
python -m pytest examples/skills_code_review_agent/tests   # 71 passed
python -m flake8 examples/skills_code_review_agent          # clean

六、示例输出路径 / Sample-Output Paths

  • 提交内样例 / committed samples(与新鲜运行字节形状一致,经校验):
    • examples/skills_code_review_agent/sample_output/review_report.json
    • examples/skills_code_review_agent/sample_output/review_report.md
  • 运行时产物 / runtime artifacts: 每次 review 在输出目录生成
    review_report.json + review_report.md,并全部落入 SQLite
    cr_report.report 存完整 JSON,可按 task id 回放)。
  • 测试 fixtures: examples/skills_code_review_agent/fixtures/*.diff(8 条)。

七、已知边界 / Known Boundaries(均已在 README/DESIGN 中声明)

  1. Container(Docker) 与 Cube/E2B 运行时代码完备、--sandbox 可选,但本开发主机无
    Docker daemon / E2B key,无法实机执行;按题意 local 仅为 dev fallback,文档已
    将 container 标注为生产默认。/ Docker & E2B paths are code-complete but not
    executable on the dev host; container is the documented production default.
  2. --model-mode real(OpenAIModel via TRPC_AGENT_*)代码完备但未用真实 Key
    演练;fake 模式驱动完全相同的 LlmAgent+Runner 路径。
  3. AC2 的隐藏集指标不可公开测量,以带标注公开语料为代理(公开集召回 100%、高置信
    FP 0%),README 验收表中明示。

八、测试结果 / Test Results

examples/skills_code_review_agent/tests : 70 passed in 21.95s
tests/filter tests/storage (SDK sanity) : 268 passed in 8.85s
flake8 examples/skills_code_review_agent : clean

独立验证记录 / independent verification record:
docs/verify/issue-92-verification.md(含验证期加固:密钥正则补强至独立 25 条
语料 100% 检出、pipeline 级治理 e2e、CLI 输入错误优雅退出)。


🤖 Generated with Claude Code

评审后更新 / Post-review update(2026-07-19)

独立对抗式评审证实 2 个 major(均为开箱体验)+ 5 个 minor,全部修复:

  • [major] 全新 checkout 首条命令崩溃:默认 SQLite 落在 out/ 但目录未创建 → _resolve_db_url 返回默认 URL 前先 os.makedirs(覆盖 init-db)。
  • [major] 子命令默认 DB 路径不一致(reviewout/review.db,show/list/init-db./review.db,文档流程报 task not found)→ 统一为 out/review.db;+1 条 CLI 回归测试(空目录里 review→show→list 全默认参数走通,修复前确认失败)。
  • [minor] 内部 _persisted 标志泄漏进公开报告 JSON → 嵌入报告前剥离下划线内部键(样例报告同步清理)。
  • [minor] 治理门只审 entry 参数 → 现在把真实完整 argv(含 diff/out/--files)交给门审。
  • [minor] .env 并不会被自动加载 → README/.env.example 改为 set -a; source .env; set +a/export 指引(不引入 dotenv 依赖)。
  • [minor] 本地沙箱硬编码 python3(Windows 无此名)→ 本地运行时改用 sys.executable(容器运行时不变)。
  • [minor/验收] issue 要求本地环境只能是 dev fallback 不得为默认 → SandboxConfig 默认 container,CLI --sandbox 默认 auto(检测 docker,缺失时显式警告回退 local);本机(无 docker)实测回退路径日志正确。

测试:71/71 全绿(+1),flake8 clean。

(EN) Post-review update 2026-07-19: an independent adversarial review (6 parallel judges, execution-backed verification) confirmed 2 majors (fresh-checkout crash on default DB dir; inconsistent default DB paths across subcommands) and 5 minors — incl. making container the sandbox default with auto-detected, clearly-logged local fallback per the issue's acceptance criteria; all items fixed as listed above, full suite green (pytest 71/71, flake8 clean).

@github-actions

Copy link
Copy Markdown

CLA Assistant Lite bot:
Thank you for your submission, we really appreciate it. Like many open-source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution. You can sign the CLA by just posting a Pull Request Comment same as the below format.


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


turrillcerza seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You can retrigger this bot by commenting recheck in this Pull Request

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@099b571). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff             @@
##             main        #206   +/-   ##
==========================================
  Coverage        ?   87.90237%           
==========================================
  Files           ?         479           
  Lines           ?       44984           
  Branches        ?           0           
==========================================
  Hits            ?       39542           
  Misses          ?        5442           
  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

该行确实包含了 task_id。很好。SandboxRunRow(**run) 包含了所有列(started_at 使用默认值,id 由 store 设置)。✓

现在进行最终定稿。让我再次重新考虑 outcome.result.timed_out 的防护:bool(outcome.result.timed_out) if outcome and outcome.result else False。Python 的优先级:outcome and outcome.result 先求值;然后是 if ... else False;接着是 bool(...timed_out)。但是 bool(outcome.result.timed_out) —— outcome.resultif 条件内被检查了真值,但 bool() 表达式再次访问了 outcome.result.timed_out。由于 outcome.result 在条件中被检查为真,它是存在的。✓ 没问题。

让我重新检查一下 _make_run_row 错误情况下的 run_index:outcome 存在(错误结果),metrics.sandbox_run_count 已经增加了(第 1388 行),所以 run_index = max(0, count - 1)。✓ 合理。

现在让我写审查意见。我对这些发现很有信心。让我再考虑一下 inputs.from_repo_path.git 的检查 —— os.path.isdir(os.path.join(repo_path, ".git"))。对于 worktrees/submodules,.git 可能是一个文件。轻微的兼容性差距 —— 警告级别,低价值。跳过。

让我再重新考虑一下:fixture 的 db_connection_lifecycle 测试断言了 DBL003 (BEGIN without commit/rollback)。规则 _RE_COMMIT_OR_ROLLBACK.search(scope_text) 检查整个 scope。fixture 包含 cur.execute('BEGIN')cur.execute('UPDATE...') 但没有 commit。所以 DBL003 触发。✓ 但该规则具有普遍的误报风险(commit 可能在 diff 之外)—— 已记录为启发式规则。不是 bug。

最终审查意见如下。

发现的问题

🚨 Critical

  • examples/skills_code_review_agent/codereview/pipeline.py:1328-1332:异常明文可能携带密钥写入数据库
    • except Exception as ex 分支用 redactor.redact_str(str(ex))[:2000] 写入 error_message,方向正确;但异常字符串(如 OpenAI/HTTP 库把请求 URL、响应体拼进 message)常包含未被 secret_patterns 覆盖的凭证片段,且 [:2000] 截断不保证截在脱敏边界。建议对 error_message 同样走 redact_obj 递归脱敏,并优先记录 error_type 而非整段 str(ex);至少对 HTTP/网络类异常只保留类型名。

⚠️ Warning

  • examples/skills_code_review_agent/codereview/sandbox.py:1956-1957run_checks 签名参数名与调用不一致

    • 该方法形参名为 changeset_json,但 pipeline.py:1373 以位置参数调用 executor.run_checks(task_id, parsed, changeset.file_contents)parsed 被当作 changeset_json 传入并随后 json.dumps({"changeset": changeset_json}) 序列化——这里 parsed 是已解析的 dict,序列化没问题;但参数名 changeset_json 误导(实际是 dict 而非 JSON 字符串),后续维护者按名称传 JSON 串会静默产出错误结构。建议形参改名 changeset 并加类型标注,或在内部断言 isinstance(changeset_json, dict)
  • examples/skills_code_review_agent/codereview/store/sql_store.py:2550-2557list_tasksSqlKey(key=()) 查询,但 query 内部仍按 task_id 过滤的同一实现走通;真正风险在 get_report 复用 _query_by_taskorder_col="created_at"ReportRow.task_idunique——多次 save_report 不会更新而是插入新行(add_and_commit 总是 INSERT),导致同一 task 产生多条 report,get_reportrows[0] 拿到的是最旧而非最新报告。

    • save_reportsql_store.py:2513-2517)每次都 setdefault("id", uuid4()) 生成新主键直接 add,对同一 task_id 不会 upsert;若 pipeline 因重试/异常对同一 task 二次落库,cr_report 会积累重复行且查询返回最旧一条。建议 save_report 先按 task_id 查存在行并 update,或对 task_id 做唯一约束 + upsert。
  • examples/skills_code_review_agent/codereview/pipeline.py:1380-1386:被 Filter 拦截的分支漏记 filter_events 到 DB 的时序

    • 拦截分支调用 self._add_filter_events(task_id, filter_events),但 on_decisiongated_sandbox_run 内部已把事件 append 进 filter_events;若同一 filter_events 列表在后续成功分支(pipeline.py:1399)被再次 _add_filter_events,靠 _persisted 标记去重。该机制正确,但 _add_filter_events 用字典 | 合并(pipeline.py:1448)要求 Python≥3.9 且 event 不含非字符串键——当前安全。属健壮性提示:建议显式构造新 dict 而非 |,避免 event 字段类型变化时出错。
  • examples/skills_code_review_agent/tests/test_cli_and_report.py:5067-5070:CLI 测试依赖脆弱的 stdout 子串

    • 测试断言 "task id" in review_output 等,但 _cmd_review 实际输出 f"task id : ..."run_agent.py:3019),子串匹配能过;但 --dry-run_cmd_review 里覆盖 args.model_mode/args.sandboxrun_agent.py:2987-2989)发生在构建 config 之前,顺序正确。真正脆弱点:测试未校验 show 子命令输出的 JSON 结构,仅靠 list 输出含 task id——若输出格式微调测试即误判。建议断言改用 JSON 解析 show 输出。

💡 Suggestion

  • examples/skills_code_review_agent/codereview/governance.py:740-753_check_paths~normalized.startswith("~") 判定,但 os.path.normpath 不会展开 ~,逻辑成立;不过 forbidden in normalized.split(os.sep) 会把任意包含 .. 段的合法相对路径(如 pkg/.. 规范化后消失)误判——当前 pipeline 传入的 args 固定,无实际触发,仅作可维护性提示:路径策略宜用 Path.resolve() 后做白名单根校验,比黑名单更稳。

总结

整体实现完整、测试覆盖较充分,核心沙箱/Filter/脱敏链路设计合理。存在一个 Critical(异常信息可能泄露密钥到 DB)和数个 Warning(report 重复落库导致查询取旧值、参数命名误导、CLI 测试脆弱);建议至少修复 Critical 与 save_report 重复落库问题后合入。

测试建议

  • 补充“同一 task_id 二次运行 / 重试”场景,断言 cr_report 对该 task 仅一行且 get_report 返回最新报告。
  • 补充“异常 message 含类密钥片段(如 Authorization: Bearer xxx)”时 error_message 落库后不含明文的回归测试。

新增 examples/skills_code_review_agent:基于 trpc_agent_sdk 的自动代码审查
Agent,组合 Skill(code-review 技能包:SKILL.md + 6 条规则文档 + 沙箱内检查
脚本)、code_executors 沙箱(container 生产默认 / cube(E2B) / local 开发回退,
带超时、输出字节上限、环境变量白名单)、BaseFilter 治理链(命令白名单、
路径/网络/预算 5 项检查,deny 与 needs_human_review 一律不执行并落库)、
SQLAlchemy 存储(cr_review_task / cr_sandbox_run / cr_filter_event /
cr_finding / cr_report 五表,按 task_id 可查全链路)与 telemetry 指标。

- 输入:--diff-file / --repo-path / --files / --fixture,支持
  rename/binary/CRLF/no-newline 等边界
- 发现项:9 必填字段 + rule_id,(file,line,category) 去重,
  <0.7 置信度进入人工复核桶
- 脱敏:三层(沙箱内 → 单条 finding → 全文档),48 样本 100% 检出、
  报告与 DB 无明文
- 报告:中英双语 Markdown/JSON,含 7 个规定章节
- 全程离线可跑:--model-mode fake / --dry-run,无需 API Key 与 Docker
- 测试:examples/skills_code_review_agent/tests 70 项全绿,flake8 通过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@EricChan2028
EricChan2028 force-pushed the feat/92-code-review-agent branch from 1985563 to 1b8907d Compare July 19, 2026 01:43
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

这非常全面。脱敏测试非常稳健。

现在让我重新考虑一下最后一个真正的问题:redact_obj 不会脱敏字典键。测试中植入的密钥是值,而不是键,因此测试无法捕捉到。但这确实是一个纵深防御方面的缺口。不过,字典键作为机密的情况很少见;这是一个低严重性的建议,而不是警告。

让我重新考虑一下 findings.py:150 中是否存在真正的 Bug:dist[finding.severity] = dist.get(finding.severity, 0) + 1。对于标准严重性,dist 已经有了这些键,所以 dist.get 返回当前计数,+1。这是有效的。对于未知严重性,dist.get 返回 0,然后赋值为 1 —— 从而创建了一个新键。函数按预期工作。这不是 Bug。

现在,一个真正的潜在问题severity_distribution 文档字符串说“所有严重性始终存在”,但 metrics.severity_distribution 在第 287 行被设置为 severity_distribution(findings)。这是没问题的。

让我重新考虑一下值得报告的最实质性问题。实际上,仔细看 pipeline.py 第 212 行:metrics.sandbox_duration_ms += outcome.duration_ms or sandbox_ms —— 这里存在重复计算的风险:outcome.duration_ms 已经被设置(第 211 行),但是当结果为 OK 且 result.duration 为 0.0(例如非常快)时,0.0 or sandbox_ms 会使用 sandbox_ms。没问题,这只是兜底逻辑。

好的。让我完成审查。基于我的彻底阅读,代码质量很高,测试也很全面。让我找出真正值得报告的问题。

真正的问题 1 (警告)redaction.py:45-54 中的 redact_obj 没有脱敏字典键。如果凭证作为字典键出现(例如 {"sk-...": ...}),它就会在报告/数据库中泄漏。这是一个真实的、尽管概率很小的纵深防御缺口,而该模块的核心目的就是脱敏。值得作为警告提出。

真正的问题 2 (警告)governance.py:142-155 中的 _check_paths:检查 forbidden in normalized.split(os.sep) 意味着任何禁止的路径字符串(例如 .ssh,但也包括 ..)如果在任何地方作为路径组件出现,就会阻断。但更重要的是,~ 的检查 normalized.startswith("~") 永远不会匹配,因为 os.path.normpath 不会展开 ~,而且用户提供的路径 ~ 确实会以 ~ 开头。实际上这确实有效。让我重新考虑一下。实际上真正的问题是:禁止路径 .. 在检查 forbidden in normalized.split(os.sep) 时也会匹配包含 .. 的合法相对路径,但沙箱并不使用这些路径。不是问题。

让我重新考虑一下 _check_paths 是否真的在误报。实际上有一个真正的问题:禁止列表包含 ..,但测试 test_forbidden_path_denied 期望 ../outside.py 被拒绝。normpath("../outside.py") = ../outside.pysplit('/') = ['..', 'outside.py'],包含 .. → 被拒绝。很好。这是按预期工作的。

所以治理机制是没问题的。让我重新考虑一下是否还有其他真正的问题。

真正的问题 3 (建议)findings.py:148-150severity_distribution 使用了 dict.get,而 dict 已经预填充了,显得冗余。微小问题。跳过。

让我重新考虑一下 from_repo_path 的竞态/安全性:它读取未跟踪的文件并对其进行快照。如果一个未跟踪的文件路径包含特殊字符,这是没问题的。没什么问题。

让我最终确定下来。我将报告脱敏字典键的问题作为警告,也许还有 _collect_findings 的截断中断问题。让我重新考虑一下 _collect_findings

for code_file in files:
    if code_file.truncated:
        break
    return json.loads(code_file.content)

这会在第一个未截断的文件上返回。如果文件被截断了,它会中断并进入主机读取流程。由于 collect 只请求了一个文件,所以这是没问题的。不是 Bug。

让我检查一下 report.py:53 中的 created_at 是否与数据库中的不同 —— 它使用了 datetime.now(timezone.utc),而不是 utcnow()。虽然模型默认值使用了 utcnow()。两者都是 UTC 时间。只是产生的时间戳略有不同。没问题。

现在让我重新考虑一下 redact_obj 和字典键的问题是否真的值得报告。设计文档声称“宿主对报告与全部入库字段递归二次脱敏”。字典键没有被脱敏,这与所陈述的保证相矛盾。这是一个合理的警告。

让我也重新考虑一下 run_agent.py 中的 _resolve_changeset 是否接受 --repo-path 指向攻击者控制的仓库 —— 但这是用户自己的 CLI 调用。不存在信任边界。

让我最终确定报告。我也重新考虑一下 llm_summarybuild_summary_model 在 "real" 模式下使用 OpenAIModel(api_key=os.getenv(..., "")) —— 如果环境变量未设置,api_key=""。这会在运行时失败并被捕获(第 311 行)。没问题。

现在让我写下审查意见。让我重新考虑严重性。脱敏字典键的问题:作为键的凭证会泄漏。现实影响是真实存在的,但概率很低。警告等级是合适的。

让我重新考虑一下是否还有我遗漏的更关键的问题。让我重新阅读 pipeline._run_checks_governed 以了解主机回退的双重计数:当沙箱 OK 时,findings 来自沙箱。当沙箱失败/阻塞时,主机回退运行 run_all_rules。但沙箱已经运行了 run_checks.py,它调用了 run_all_rules。在失败时,主机再次调用 run_all_rules —— 同样的逻辑,没问题。工具调用计数会增加。没问题。

但这里有一个真正的问题:当沙箱被阻塞时(第 198-208 行),metrics.sandbox_run_count 没有增加(第 210 行仅在被阻塞的分支之后才到达)。被阻塞的行使用了 _make_run_row(..., STATUS_BLOCKED, ...),其 run_index = metrics.sandbox_run_count = 0。测试断言 sandbox_run_count == 0。没问题。

让我完成审查。让我重新考虑一下 inputs.pyfrom_repo_path 第 97 行 _run_git(repo_path, "diff", "HEAD")超时问题 —— 没有每个命令的 check=True……实际上 _run_gitcheck=True,所以非零退出会引发异常。git diff HEAD 在干净的树上返回 0。没问题。

最终答案。让我重新考虑一下建议的数量 —— 保持在 ≤2 条。我将报告 1 个警告(脱敏键)和 1 个建议(最实质的一条)。

让我重新考虑一下最实质性的建议。findings.py:148-150 太小了。更好的是:redact_obj 同时处理键 —— 这也是同一个修复。所以我可以将脱敏键合并为一条,也许既作为警告又作为修复。就报告为警告吧。

实际上,让我重新考虑一下是否还有其他真正有价值的问题。让我重新考虑一下 secret_patterns 中的 generic_assignment 正则表达式,它可能会匹配脱敏后的占位符本身:***REDACTED***。占位符是 ***REDACTED***generic_assignment 正则表达式会匹配 REDACTED=... 吗?它需要像 password|secret|... 这样的关键字。REDACTED 不是关键字。所以占位符是安全的。很好。

但这里有一个真正的微妙问题:_SAFE_VALUE_PREFIXES 包含 "***"。在脱敏后,api_key = ***REDACTED*** 这样的值 —— 如果再次扫描,generic_assignment 匹配 api_key = ***REDACTED***,值是 ***REDACTED***,它以 *** 开头 → 安全 → 跳过。很好,幂等。

好的,最终定稿。让我再重新考虑一下 redact_obj 中的字典键是否确实可达 —— report["findings"][i] 是一个带有字符串键的字典(severity, file 等),而不是凭证。report["filter_summary"]["events"][i]["reasons"] 可能包含带有路径的拒绝原因,而不是凭证。所以作为键的凭证确实不太可能发生。我会降低到建议等级,或者直接跳过。

鉴于指令要求“如果没有发现明确问题,请直接说明‘未发现明显阻塞问题’”,但这里有一个真正(尽管很小)的问题。让我报告这个脱敏键的缺口作为一个警告,因为设计明确承诺对“全部入库字段”进行递归脱敏,而字典键打破了这一保证——这是一个可验证的、与设计声明相矛盾的缺口。

让我最后再重新考虑一下 severity_distribution 对未知严重性的处理。这不算 Bug。

我也再重新考虑一下 run_checks.py 第 69 行的 os.makedirs(... exist_ok=True) 在 Windows 上是否适用 —— 这是示例代码。没问题。

最终定稿。其实还有一个:让我重新检查一下 governance._check_paths 第 148 行:normalized.startswith(forbidden.rstrip("/") + "/")。对于 forbidden = "..""..".rstrip("/") + "/" = "../"。所以任何以 ../ 开头的路径都会被拒绝。很好。但 forbidden = "." 不在列表中。没问题。

现在,~ 的处理逻辑:normalized.startswith("~")。如果用户传递 --files ~/x.pyfrom_file_list 不会展开 ~,所以 input_ref = ~/x.pyabs_path = os.path.join(base, "~/x.py") —— 这会创建一个字面意义上的 ~ 目录!这是一个真正的问题:from_file_listfrom_repo_path 没有调用 os.path.expanduser,因此 ~/x.py 被按字面意思处理,而不是主目录。让我检查一下 _read_text(os.path.join(base, "~/x.py")) —— os.path.join(cwd, "~/x.py") = cwd/~/x.py,这不是一个真实的文件 → FileNotFoundError: cannot read。因此用户使用 ~/ 路径的 --files 会因为“无法读取”而失败。这是一个轻微的易用性/正确性缺口,但不是安全问题。可能是建议,但我应该控制建议的数量。

让我最终确定并输出。我将报告:

  • 警告:redact_obj 跳过字典键(与声明相矛盾的真正纵深防御缺口)。
  • 建议:inputs.py 中未调用 expanduser 导致 ~/ 路径出错(轻微)。

让我重新考虑一下 —— 其实让我重新考虑一下 redact_obj 的严重性。数据库中的 config 列存储了 ReviewConfig.to_dict(),其中包含 env_whitelist(排序后的列表)—— 没有机密。input_ref 在传递给 create_task 之前已经脱敏。所以字典键几乎永远不会是机密。我将其保留为警告,因为所陈述的保证(“全部入库字段递归二次脱敏”)被打破了,而且这很容易修复。

正在编写输出。## 发现的问题

⚠️ Warning

  • examples/skills_code_review_agent/codereview/redaction.py:45-54redact_obj 递归脱敏 dict 的值但未脱敏 dict 的 key

    • 该模块的设计目标(见模块 docstring 与 DESIGN.zh_CN.md)是“宿主对报告与全部入库字段递归二次脱敏”,但实现只对 value 调用 redact_obj,key 原样保留。若被持久化的结构里出现以密钥为 key 的情况(如 {token_value: ...}、LLM summary 反序列化结构、未来扩展字段),明文密钥会绕过脱敏进入报告/数据库。建议改为 {self.redact_obj(k) if isinstance(k, str) else k: self.redact_obj(v) ...},与“递归脱敏全部字段”的承诺一致。
  • examples/skills_code_review_agent/codereview/inputs.py:123-141from_file_list / from_repo_path 未对 ~ 展开

    • 用户传入 --files ~/secret.py--repo-path ~/repo 时,os.path.join(base, "~/...") 会把 ~ 当成字面目录名,_read_text 读不到文件直接抛 FileNotFoundError: cannot read .../~/secret.py,体验上像 bug 而非明确的路径错误。建议在读路径前调用 os.path.expanduser,与 governance._check_paths~ 的拒绝逻辑保持一致。

💡 Suggestion

  • examples/skills_code_review_agent/codereview/findings.py:146-151severity_distribution 对 dict 已预置全部 severity,循环中 dist.get(finding.severity, 0)get 回退多余且语义含糊。遇到未知 severity 时会 silently 新增一个计数键,与 docstring“all severities always present”不符。可直接 dist[finding.severity] = dist[finding.severity] + 1(未知 severity 走 KeyError 暴露问题),或显式过滤未知值。

总结

整体实现质量高,沙箱失败/拦截/超时均有容错与回退,脱敏与去重有较完整的端到端测试覆盖。未发现必须修复的阻塞问题;唯一的实质风险是 redact_obj 未脱敏 dict key,与设计声明的“递归脱敏全部字段”存在偏差,建议修复。

测试建议

  • 补充一条脱敏测试:构造 {"sk-FAKEfakeFAKEfakeFAKEfakeFAKE1234": "x"} 这样的 key 含密钥结构,经过 SecretRedactor.redact_obj 后断言 key 中不含明文,覆盖现有 test_secret_redaction_fixture_no_plaintext_anywhere 未触及的 key 路径。

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.

2 participants