Skip to content

examples: add skill-driven code review agent#233

Open
Enjoykkk wants to merge 3 commits into
trpc-group:mainfrom
Enjoykkk:feat/code-review-agent
Open

examples: add skill-driven code review agent#233
Enjoykkk wants to merge 3 commits into
trpc-group:mainfrom
Enjoykkk:feat/code-review-agent

Conversation

@Enjoykkk

Copy link
Copy Markdown

Summary

新增 examples/skills_code_review_agent 自动代码评审 Agent 示例。

该示例支持输入 unified diff、Git 工作区变更或文件列表;通过加载代码评审 Skill、在隔离 Workspace 中执行受控检查,生成结构化 findings、JSON/Markdown 报告和可按 task id 回读的审计记录。

Related Issue

Fixes #92

PR Type

type/feature

Changes

  • 新增基于 LlmAgentSkillToolSetRunner 的代码评审流程。
  • 新增 unified diff、仓库变更和文件列表输入解析,以及 hunk 与候选行提取。
  • 新增敏感信息脱敏、确定性评审规则、finding 校验、去重和低置信度人工复核。
  • 新增 Docker、local 开发 fallback 和 Cube Runtime 入口。
  • 新增命令白名单、网络/依赖安装拦截、禁止路径、超时预算和 stdin 治理。
  • 新增 SQLite 审计存储,保存任务、findings、Filter 决策、Skill 运行、模型调用、监控指标和最终报告。
  • 新增按 task id 划分的 review_report.jsonreview_report.md
  • 新增 --dry-run--fake-model 与真实 OpenAI 兼容模型三种运行模式。
  • 新增公开 fixture,覆盖无问题 diff、敏感信息、异步资源、数据库事务、测试缺失、去重、沙箱失败和脱敏。

Skill Attribution

skills/code-review/awesome-skills/code-review-skill 引入,并保留上游许可证。

Validation

python -m pytest tests/examples/test_code_review_agent.py -q
python -m compileall -q examples/skills_code_review_agent
git diff --check

已手动验证 Docker Runtime 下的 --diff-file--repo-path 输入路径。

Add a code review example that loads the code-review Skill, stages review
inputs in supported workspace runtimes, applies execution governance, and
persists structured reports and audit metadata.

The example supports dry-run, FakeModel, and OpenAI-compatible model paths,
with Docker/Cube runtime configuration, fixtures, and Chinese documentation.

RELEASE NOTES: Added a skill-driven code review Agent example with sandboxed
review execution and structured reports.
@Enjoykkk

Copy link
Copy Markdown
Author

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

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff             @@
##             main        #233   +/-   ##
==========================================
  Coverage        ?   87.84906%           
==========================================
  Files           ?         482           
  Lines           ?       45157           
  Branches        ?           0           
==========================================
  Hits            ?       39670           
  Misses          ?        5487           
  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

发现的问题

⚠️ Warning

  • examples/skills_code_review_agent/agent/tools.py:177-183:diff 引用路径可穿越仓库根目录,导致宿主机任意文件被 stage 进沙箱

    • parse_review_input--repo-path 模式下用 (root / line.file).resolve() 解析 diff 中 +++ b/<path> 指向的文件,line.file 来自不可信 diff,可写成 ../../etc/secrets.pyresolve() 后落到 root 之外,只要该文件存在就会被加入 sources 并经 _stage 拷进沙箱(仅用 source.name 作为 dst)。当前 analyze_pr 不读取这些源文件,但 README 明确 ruff/pytest 即将作为 action 暴露,届时会变成可读的宿主文件泄露。修复:仅当 candidate.is_relative_to(root) 为真且未含 .. 跳出时才纳入 sources,否则跳过。
    ...
    candidate = (root / line.file).resolve()
    if candidate.is_file():
        sources.add(candidate)
    ...
  • examples/skills_code_review_agent/agent/tools.py:38-39:宿主机绝对路径通过 workspace_inputs 写入模型输入与审计,违反 README/instruction 声称的“模型不获取宿主机绝对路径”

    • _stage 生成 host://{absolute_path} 并经 model_dump() 进入 result["workspace_inputs"]run_review.py 又把整个 review_input 作为 payload 文本发给 Runner;before_model_audit 还把该 request 原样存入 code_review_model_runs 审计(redact 只脱敏密钥不脱敏路径),最终落盘到 reviews.sqlite 与报告。对不可信 PR 场景,等于把宿主机目录结构泄露给模型和审计读者。修复:发送给模型/审计前剥离 src(仅保留 dst),执行器需要的 host 映射从 ctx.metadata 读取(_review_action_tool 已是这样取的)。
  • examples/skills_code_review_agent/.env:9:将 .env 作为受版本控制文件提交,易引发真实密钥误提交

    • 仓库根 .gitignore 未忽略 .env,该文件被追踪;用户按 README “编辑仓库提供的 .env 模板”填入真实 OPENAI_API_KEY 后,git status/git add 极易把密钥带入提交。修复:改名为 .env.example 并在 .gitignore 中忽略 .env,运行时仍 load_dotenv(".env")

💡 Suggestion

  • examples/skills_code_review_agent/agent/policy.py:17ReviewPolicyEngine.decidecommand.split() 做白名单与 -c 检测,token 化过于朴素(如 python -c"..." 会让 command[:2] != ["python","-c"] 从而绕过动态代码判定)。当前因模型只能触发固定 analyze_pr action 而不可利用,但作为防御纵深建议用 shlex.split 并对 shell 元字符单独拦截,避免后续放开 action 时被绕过。

  • examples/skills_code_review_agent/agent/storage.py:31-32with self._connect() as db: 只管理事务、不关闭连接,连接释放依赖 GC;长会话多次调用 save_native/get_task 会堆积句柄。建议在 _connect 返回后显式 try/finally db.close(),或改用上下文管理器封装。

总结

整体为示例性质实现,核心编排与脱敏/审计链路设计合理,当前不存在可直接利用的 Critical 漏洞。存在 3 处建议修复的 Warning:diff 路径穿越导致宿主文件被 stage、宿主机绝对路径泄露给模型与审计、.env 作为受追踪文件带来的密钥误提交风险。

测试建议

  • 补充 parse_review_input 的路径穿越用例:--repo-path 下传入含 +++ b/../../<外部文件> 的 diff,断言该文件未出现在 workspace_inputs
  • 补充断言:发给模型的 payload 文本与持久化的 model_runs 审计中不含 host:// 绝对路径。

@Enjoykkk Enjoykkk closed this Jul 27, 2026
@Enjoykkk Enjoykkk reopened this Jul 27, 2026
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

发现的问题

🚨 Critical

  • examples/skills_code_review_agent/agent/tools.py:275-276save_review_report 未校验模型传入的 task_idoutput_dir,存在路径穿越/任意文件写入风险
    • 该函数作为 FunctionTool 暴露给模型(agent/tools.py:454),task_idoutput_dir 均由模型/不可信 diff 上下文提供。task_root = root / task_id 后直接 task_root.mkdir(parents=True, exist_ok=True) 并写 review_report.json/.md,若 task_id../../output_dir 指向任意路径,可在宿主机任意目录创建目录并写入文件,SQLite 主键也会被污染。建议对 task_id 做白名单字符校验(如 ^[A-Za-z0-9._-]+$ 且禁止 ..),并强制 task_root.resolve().is_relative_to(root.resolve())
    ...
    root, task_root = Path(output_dir or Path(__file__).parents[1] / "review-output"), None
    task_root = root / task_id   # task_id 来自模型,未校验
    task_root.mkdir(parents=True, exist_ok=True)
    ...

⚠️ Warning

  • examples/skills_code_review_agent/.env:1-20:真实环境文件 .env 被纳入版本库跟踪

    • 该文件为 load_dotenv 实际读取的配置文件,当前虽为占位符,但被 git ls-files 确认已跟踪。后续开发者填入真实 OPENAI_API_KEY/E2B_API_KEY 后极易被误提交泄露。建议改为提交 .env.example,将 .env 加入 .gitignore(当前 .gitignore 未忽略它)。
  • examples/skills_code_review_agent/agent/storage.py:87,99get_task 读取的 sandbox_runs 表从未被写入,审计字段恒为空

    • save_native 只写 skill_runs,不写 sandbox_runs,因此 get_task 返回的 sandbox_runs 永远是 [],与 metrics.sandbox_run_count(取自 skill_runs 长度)语义不一致,会误导下游审计/查询。建议删除该表与查询,或与 skill_runs 统一。
  • examples/skills_code_review_agent/agent/tools.py:179parse_review_inputrepo_path 直读仓库时,若未传 staging_dir 则不会 stage 生成的 diff

    • 仅当 root and staging_dir 同时为真才写出 input.diff;当该函数被未来调用方以 repo_path 但无 staging_dir 调用时,沙箱内不会有 work/inputs/input.diff,导致 analyze_pr--diff-file ../../work/inputs/input.diff)必然失败。当前 CLI 路径总会传 staging_dir,但函数为公共入口,建议在 root 存在时强制要求 staging_dir 或自动回退到 files/diff 模式并明确报错。
  • examples/skills_code_review_agent/agent/policy.py:30-c 动态代码检测可被参数拼接绕过

    • 检测条件为 token == "-c" or token.startswith("-c"),对 python 直接执行脚本文件无拦截,但若模型传入 python+通过环境变量或 PYTHONSTARTUP/PYTHONPATH 间接执行代码则不在拦截范围;同时 startswith("-c") 会误拦 -cache 类不存在的参数。建议改为精确匹配 -c/-c 后跟代码(如 token == "-c" 或正则 ^-c$|^-c.+)并补充对 python 执行非工作目录脚本的审计。当前 stdin 已被 filter 拦截,风险可控但建议收紧。

💡 Suggestion

  • examples/skills_code_review_agent/agent/filter.py:55-58ruff 仅 diff 场景的拦截判断用 not any(path != ...) 表达“全部为 diff”,可读性差且与 dst 语义耦合,建议封装为显式变量(如 has_staged_source = any(...))以降低后续维护误改风险。

总结

整体安全边界(脱敏、host 路径隔离、命令白名单、stdin 拦截、路径穿越 staging 校验)设计较完整,但 save_review_report 对模型传入的 task_id/output_dir 缺少校验,存在任意文件写入的 Critical 风险,必须修复;.env 入库与 sandbox_runs 死表为需关注的 Warning。

测试建议

  • 增加对抗性测试:模型传入 task_id="../../evil"output_dir 指向仓库外路径时,断言 save_review_report 拒绝写入并报错。
  • 增加测试覆盖 parse_review_input(repo_path=..., staging_dir="") 的行为契约(报错或安全回退),避免未来调用方误用导致沙箱缺 diff。

continue
seen.add(key)
(human if item["confidence"] < 0.7 else accepted).append(item)
root, task_root = Path(output_dir or Path(__file__).parents[1] / "review-output"), None

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.

save_review_report 未校验 task_id/output_dir 致路径穿越

该函数作为 FunctionTool 暴露给模型,task_id 与 output_dir 由不可信上下文提供,未做校验即拼接 task_root 并 mkdir/写文件,存在任意文件写入风险。建议对 task_id 做白名单字符校验(如 ^[A-Za-z0-9._-]+$ 且禁止 ..),并强制 task_root.resolve().is_relative_to(root.resolve())。

@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

已确认:当 _before 设置 rsp.is_continue = False(被拦截)时,第 215-216 行会在执行 handle() 之前返回,因此 _after 不会被调用。因此被拦截的运行仅会记录在 _before 的追加逻辑中。不会有重复。很好——那里没有 bug。

现在我已经很清楚了。让我再核实一下 _safe_redact_audit_value 中是否存在主机路径泄露。_HOST_PATH = re.compile(r"host://[^\s\"']+")。它会替换 host://... 直到遇到空格或引号。_safe 会清理字符串。很好。

现在让我最后核实一个真正的问题:before_model_audit 第 106-114 行解析每个部分的文本作为 JSON。如果部分文本不是 JSON,则继续。然后 state["code_review_changed_lines"] = payload.get("changed_lines", [])。但此前已在 parse_review_input 中通过 tool_context 设置过。此处它从模型消息载荷中获取——该载荷来自 run_review.py 第 84 行构建的 payload。因此 changed_lines 来自不可信的模型输入载荷,但在第 245 行的 parse_review_input 中,如果存在 state,则保留它。顺序:parse_review_input(无 tool_context,由 CLI 调用)不会设置 state。之后 before_model_audit 从载荷 JSON 中设置 code_review_changed_lines。然后 save_review_report 使用 state.get("code_review_changed_lines", evidence.get(...))。载荷中的 changed_lines 是经过脱敏的。没问题。

但存在一个隐患:before_model_audit 是从模型接收到的请求载荷中提取 changed_lines 的。模型可能会篡改载荷文本吗?载荷是由 CLI 作为 Part.from_text(json.dumps(payload)) 发送的。模型在后续轮次可能会回显它。但 changed_lines 在这里用于验证发现(findings)的 file/line 是否在变更集中。如果模型篡改了 changed_lines 以包含其编造的 file/line,验证就会通过。这是一个真实的安全隐患——模型可以通过在回显的载荷中扩展 changed_lines 来绕过 file/line 验证。

等等——让我核实一下。在 save_review_report 中,第 277 行:changed = {(file, line) for item in evidence.get("changed_lines", [])}。第 290 行:(item["file"], item["line"]) not in changedevidence 在第 270-276 行合并了 state.get("code_review_changed_lines", ...)。而 code_review_changed_lines 是由 before_model_audit 在每一轮模型请求中根据模型输入载荷设置的。因此,模型可以在后续轮次中包含一个载荷,其 changed_lines 包含其编造的发现位置,从而绕过“发现必须在变更行内”的检查。这是一个真实的验证绕过漏洞。让我确认一下 before_model_audit 是否在每一轮都运行并覆盖。第 113 行:它设置了(没有使用 setdefaultstate["code_review_changed_lines"]。是的,每次模型调用都会覆盖。因此,模型在后续轮次中回显的载荷会覆盖受信任的 changed_lines

实际上,等等——受信任的 changed_lines 最初是在没有 tool_context 的情况下从 parse_review_input 计算出来的(CLI 路径)。因此,它们从未通过 tool_context 放入 state。它们仅通过 before_model_audit 解析模型消息进入 state。第一次模型调用的载荷就是 CLI 载荷(受信任)。但在工具调用之后的后续轮次中,模型会生成一个新请求,其 contents 包含历史记录——包括原始载荷部分?这取决于 SDK 是否保留历史记录。会话确实保留了历史记录。因此,原始载荷部分可能仍然存在。但 before_model_audit 遍历 request.contents 并在第一个带有 changed_lines 的载荷处中断。如果历史记录中保留了原始载荷,那么每一轮它都会找到受信任的那一个。但如果 SDK 重组了消息,模型可能会注入内容。这有争议。

鉴于指令要求仅报告可验证的内容,让我务实地评估一下:来自 code_review_workspace_inputs 的受信任 changed_lines 从未通过此路径设置(只有 code_review_workspace_inputs 是从 agent._code_review_workspace_inputs 获取的)。所以 changed_lines 总是来自模型消息载荷。这确实比注释所暗示的要弱。但为了确认其可利用性,我需要知道 SDK 是否允许模型注入任意部分文本。Part.from_text 的载荷是助手/用户内容。模型无法轻易伪造原始的 JSON 载荷文本部分。这充其量是一个弱点,而不是一个确凿的漏洞。我会将其标记为 Warning。

现在让我也重新考虑一下真正的 bug:after_model_audit 第 131 行弹出 code_review_model_pending。但 before_model_audit 即使在 code_review_model_pending 已经存在时也会设置它(第 123 行,没有使用 setdefault)。如果多次调用 before 而没有中间的 after(例如流式/重试),待处理状态就会丢失。次要问题。

让我敲定最终发现。让我再核实一下关于 .env 被提交以及 CODE_REVIEW_DOCKER_BUILD_CONTEXT 默认值的问题。_env_value_or_default("CODE_REVIEW_DOCKER_BUILD_CONTEXT", str(example_root))——构建上下文默认为 example_root。Dockerfile 在该目录下。镜像默认为 trpc-code-review:latest。没问题。

让我核实一下真正的问题:_env_positive_int 没有 最大值 上限。CODE_REVIEW_MODEL_RUN_MAX_COUNT 可能被设置为巨大的值,但它是计数,限制为列表大小。CODE_REVIEW_TOOL_OUTPUT_MAX_KIB——无上限,但仅影响审计大小。可接受。

现在让我核实最具体的发现:before_model_audit 从模型可见的载荷中派生 changed_lines(用于验证),这削弱了 save_review_report 中 file/line 的验证。让我确认是否存在其他受信任的来源。在 parse_review_input 中,if tool_context is not None 设置了 state code_review_changed_lines。但 CLI 调用 parse_review_input 时没有 tool_context(run_review.py 第 79 行)。所以 state 从未在那里被设置。已确认。

让我写一下审查结论。再检查一件事——_compact_skill_runs 没有限制运行次数(只有 model_runs 通过 model_count 限制)。skill_runs 可能会很大。轻微问题。还有 save_review_report 总是调用 task_root.mkdir(parents=True, exist_ok=True)——没问题。

让我核实 _resolve_report_task_root 是否在非 tool_context(CLI/dry-run)中正确验证了 task_id。测试 test_save_review_report_rejects_unsafe_task_ids 通过。很好。

让我重新考虑 redactor 是否会漏掉明显的模式。sk-[A-Za-z0-9_-]{8,}——但真正的 OpenAI key 是 sk-proj-...sk-...。匹配 sk- 后跟 8 个以上字符。没问题。密码模式 ((?:password|token|api[_-]?key)\s*=\s*[\"'])[^\"']+([\"'])——捕获引号。替换为 \1[REDACTED]\2。很好。但遗漏了不带引号的赋值,如 password = x(裸值)。以及环境变量样式的 OPENAI_API_KEY=sk-...。规则扫描第 25 行捕获了裸 token = "。可接受。

现在,关于实际安全性最具体的发现:让我重新检查 ruff 过滤器第 64 行。if not any(path != "work/inputs/input.diff" for path in staged)staged 是输入中的 dst 值。这仅检查是否暂存了非 diff 文件。但 ruff 命令目标路径没有被验证是否在暂存集合中。模型可以运行 ruff check /etc/passwd 吗?在沙箱内部,/etc/passwd 可能存在,但被禁止路径策略(.git//root/~/.ssh)限制。/etc/passwd 未被禁止。但 ruff 读取它只会输出 lint 警告——在沙箱中没有真正的泄露。而且输出是通过 _safe 脱敏的。严重性低。不报告。

让我敲定。让我重新考虑是否将 changed_lines 绕过报告为 Warning。说明中要求报告可验证的问题。风险是真实的:验证依赖于模型提供的载荷。但 save_review_report 本身通过 tool_context state 获取 changed_lines,而该 state 是由 before_model_audit 从模型请求中填充的。至少可以说,信任模型输入来进行验证是有疑问的。我会将其报告为 Warning,并附上适当的说明。

让我再核实一下:是否没有测试用例断言 changed_lines 验证拒绝了在变更集之外的模型编造发现。test_save_review_report_moves_low_confidence_to_human_review 使用 file=a.py line=1,且 changed_lines 包含 a.py:1 → 通过验证。没有针对变更集之外的测试。所以绕过测试未被覆盖。确认值得报告。

再检查一个:test_save_review_report_returns_safe_summary_to_model_tool_context 设置了 code_review_changed_lines: [],所以所有发现都被丢弃(不在 changed 中)。该测试有 0 个发现,所以无法观察。确认存在漏洞。

让我再重新考虑一下 parse_unified_diff 第 65 行针对单 hunk diff 中 index/diff --git 行的真正 bug。实际上在一个正确的 diff 中,diff --gitindex 行位于 hunk 外部(在任何 @@ 之前)。所以第 65 行会推进 new_line,但随后 @@ 会重置它。在 hunk 内部,只有 +-\ 行出现。所以 index 行不会出现在 hunk 内部。无 bug。不报告。

让我写最终审查结论。让我重新考虑严重性等级。changed_lines 绕过是 Warning(验证弱点,并非直接的关键漏洞,因为模型在沙箱中,且主机路径未泄露)。.env 的提交是指南中的占位符——甚至不是 Suggestion,因为它是有意的。_env_positive_int 无上限——次要问题,跳过。

让我重新考虑另一个真正的问题:save_review_report 第 284 行挑选了 ("severity", "category", "file", "line", "title", "evidence", "recommendation", "confidence", "source") 键,但模型的发现可能不包含 source → KeyError → continue。FakeModel 发现包含 source。真实模型必须包含它,否则会被丢弃。这是一个鲁棒性问题——没有 source 的模型发现会被静默丢弃。轻微的 Warning/Suggestion。结合测试覆盖缺失的顾虑。

实际上,让我重新检查:第 284 行 item = {key: raw[key] for key in (...)}。如果 source 缺失 → KeyError → 第 286 行捕获 continue。所以没有精确 source 字段的模型发现会被丢弃。指令 prompt 中没有明确告诉模型 source 是一个必需字段。这是一个真实的鲁棒性缺口——模型发现被静默丢弃。但 prompt 说的是“schema-shaped findings”。轻微的 Warning。

让我敲定报告。根据我对 diff 的审查,以下是我的发现。

发现的问题

⚠️ Warning

  • examples/skills_code_review_agent/agent/filter.py:106-114:报告校验用的 changed_lines 取自模型请求消息,削弱了 file/line 校验

    • before_model_audit 通过解析模型 request.contents 中 part 文本里的 changed_lines 写入 context state,而 save_review_report(tools.py:277,290) 又用该 state 校验“模型 finding 的 file/line 必须落在变更行集合内”。CLI 调用 parse_review_input 时未传 tool_contextrun_review.py:79),因此可信变更行从未被独立注入,校验来源完全依赖模型可见的 payload。模型在后续轮次回放/构造 payload 即可让伪造位置的 finding 通过校验。建议像 code_review_workspace_inputs 一样,把可信 changed_linesagent._code_review_changed_lines 注入 state,并在 before_model_audit 中仅在该 key 缺失时才回退解析(用 setdefault 而非覆盖)。
  • examples/skills_code_review_agent/agent/tools.py:284-287:模型 finding 缺少任一必需字段会被静默丢弃

    • {key: raw[key] for key in (...)} 要求 source 等全部存在,但 INSTRUCTION 并未明确声明 source 为必需字段;真实模型若漏掉 source 会触发 KeyErrorcontinue 吞掉,finding 直接丢失且无任何提示。建议对缺失字段给默认值或在校验失败时记录到 needs_human_review/审计,避免静默丢失。

💡 Suggestion

  • examples/skills_code_review_agent/agent/filter.py:62-65:ruff 沙箱校验只检查“是否暂存了非 diff 文件”,未校验命令目标路径是否属于已暂存集合
    • 模型可对未暂存的任意沙箱路径执行 ruff check(如 /etc/passwd)。沙箱内危害有限且输出已脱敏,但若后续放开更多命令,建议把命令中的目标路径与 inputsdst 做包含校验,收敛执行面。

总结

整体安全边界(命令白名单、路径穿越校验、host 路径脱敏、task_id 校验、可信 task 配置防伪造)设计较完整,未发现 Critical 阻塞问题。主要风险在于报告校验链路对模型输入的信任偏弱(changed_lines 来源、finding 字段静默丢弃),建议修复以收紧“模型不得伪造位置/绕过校验”的既定目标。

测试建议

  • 补充用例:构造一个 changed_lines 仅含 a.py:1、模型 finding 指向 a.py:99(或 b.py:1)的场景,断言该 finding 被拒绝,覆盖 file/line 校验在模型伪造位置时的拦截行为。
  • 补充用例:模型 finding 缺少 source 字段时不应被静默丢弃,断言其进入 needs_human_review 或审计记录。

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.

基于 Skills + 沙箱 + 数据库存储构建自动代码评审 Agent

2 participants