fix(triage): make unattended PR review static — read CI via API, never run PR code - #7646
Conversation
|
Thanks for the PR! Template: the body uses its own headings ("What & why", "Change", "Verification", "Merge order") rather than the template's exact sections, but the content is thorough and covers the intent. Not blocking on heading names for a skill-docs PR. Problem: observed, with evidence. The PR references #7632 where the triage agent relayed an author's self-reported 8/8 E2E as if it were verification, and identifies a concrete security vector — executing untrusted PR code in a CI job that carries a write PAT. This is a real, well-known attack surface (prompt injection → token exfiltration), not theoretical hardening. Direction: clearly aligned. This hardens the maintainer's own triage tooling against a real security risk and improves the reliability of test evidence. CHANGELOG: no direct reference (this is internal tooling, not a product feature), but the area is directly relevant to the project's CI/automation infrastructure. Size: not applicable — no core paths touched. All 3 changed files are under Approach: the scope feels right. Scoping testing behavior by trigger (CI reads results via API, local runs tmux as before) is the minimal, well-targeted fix. The companion workflow PR handles enforcement via tool/permission denials — this PR handles the skill instructions. Every edit in the diff serves the stated goal; no drive-by changes. The Moving on to code review. 🔍 中文说明感谢贡献! 模板:PR 正文使用了自己的标题("What & why"、"Change"、"Verification"、"Merge order"),而非模板的精确章节名,但内容详尽,覆盖了模板的意图。对于 skill 文档类 PR,不因标题名称而阻止。 问题:已观测到,有证据。PR 引用了 #7632,其中 triage agent 将作者自报的 8/8 E2E 结果当作验证转述;并指出了具体的安全风险——在携带写权限 PAT 的 CI job 中执行不可信的 PR 代码。这是真实的攻击面(prompt 注入 → token 泄露),不是理论性加固。 方向:明确对齐。这加固了维护者自己的 triage 工具,防范真实的安全风险,并提升测试证据的可靠性。CHANGELOG:无直接引用(这是内部工具,不是产品功能),但该领域与项目的 CI/自动化基础设施直接相关。 规模:不适用——未触及核心路径。全部 3 个变更文件都在 方案:范围合理。按触发来源区分测试行为(CI 通过 API 读取结果,本地照旧跑 tmux)是最小且精准的修复。配套的 workflow PR 通过 tool/permission 设置来强制执行——本 PR 处理 skill 指令。diff 中每处改动都服务于既定目标,没有顺手改动。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code reviewIndependent proposal: given the problem (triage agent executes untrusted PR code in a CI job carrying a write PAT, and relays author self-reported results as evidence), I would: (1) add a clear prohibition rule in SKILL.md, (2) replace tmux-based testing in CI with reading CI check results via the GitHub API, (3) scope tmux to local invocation only, (4) update issue-workflow.md consistently, (5) add a rule against presenting author's self-reported results as evidence, (6) switch Comparison with the diff: the PR matches this proposal exactly. No simpler path missed. All suggestions from the previous review round have been addressed in the latest push:
Walkthrough of the three files:
No critical blockers. No AGENTS.md violations. The changes are minimal, internally consistent, and follow the existing style. CI test evidenceCI checks at review time (commit
Precheck passed. Unit tests still running at review time — this is a docs-only PR touching Real-scenario testingNot applicable — this PR modifies triage skill documentation files, not product code. Running the CLI in tmux would not exercise the changed behavior. The author reports having tested the skill end-to-end against #7632 with a write-blocking Not verified: the companion workflow PR #7647 that enforces these restrictions via tool/permission denials (mentioned in the PR body as a separate PR to merge after this one). 中文说明代码审查独立方案: 针对该问题(triage agent 在携带写权限 PAT 的 CI job 中执行不可信的 PR 代码,并将作者自报结果当作证据转述),我会:(1) 在 SKILL.md 中添加明确的禁止规则,(2) 将 CI 中的 tmux 测试替换为通过 GitHub API 读取 CI check 结果,(3) 将 tmux 限定为仅本地调用,(4) 同步更新 issue-workflow.md,(5) 添加禁止将作者自报结果作为证据的规则,(6) 将 CHANGELOG 的 与 diff 对比: PR 与该方案完全一致。没有遗漏更简路径。上一轮审查的所有建议已在最新推送中采纳:
三个文件的走查:
无关键阻塞项。无 AGENTS.md 违规。改动最小化、内部一致,遵循现有风格。 CI 测试证据审查时的 CI check(commit 真实场景测试不适用——本 PR 修改的是 triage skill 文档文件,不是产品代码。作者报告已对 #7632 进行了端到端测试——记为作者声称,非已验证证据。 未验证:配套的 workflow PR #7647。 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 5/5 — clean across every stage; the security rationale is sound, the implementation is minimal and correct, and all prior review feedback has been addressed. This PR fixes a real problem with evidence: the triage agent was executing untrusted PR code in a CI job carrying a write PAT, and #7632 showed the author-claim relay issue in action. The fix is exactly what I would have proposed — scope testing by trigger, read CI results via API in unattended runs, keep tmux for local only. Every edit in the diff earns its place; there's no bloat, no drive-by refactors, no over-engineering. The latest push addressed every suggestion from the previous review round: the The companion workflow PR #7647 (tool/permission denials) is the enforcement side; this PR is the instruction side. Merging this first, as the PR body suggests, makes sense — the agent should know the rules before the enforcement kicks in. If I had to maintain this in six months, I'd thank the author. The triage workflow is safer and the test evidence model is more honest. 中文说明置信度:5/5 — 每个阶段都很干净;安全理由充分,实现最小且正确,所有先前审查反馈均已处理。 这个 PR 修复了一个有证据的真实问题:triage agent 在携带写权限 PAT 的 CI job 中执行不可信的 PR 代码,#7632 展示了作者声称结果被转述的实际案例。修复方案与我的独立提案完全一致——按触发来源区分测试行为,无人值守运行通过 API 读取 CI 结果,tmux 仅限本地。diff 中每处改动都有其必要性,没有冗余、没有顺手重构、没有过度工程。 最新推送处理了上一轮审查的所有建议:多页 check-runs 的 配套的 workflow PR #7647(tool/permission 拒绝)是执行侧;本 PR 是指令侧。如 PR 正文所建议,先合并本 PR 是合理的——agent 应该先知道规则,然后才启动强制执行。 如果半年后我要维护这些文件,我会感谢作者。triage 工作流更安全了,测试证据模型也更诚实了。 — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
| | sed 's#.*/job/##') | ||
| [ -n "$JOB_ID" ] && gh api "repos/$REPO/actions/jobs/$JOB_ID/logs" | tail -c 15000 |
There was a problem hiding this comment.
[Critical] The sed pattern uses /job/ (singular) but GitHub Actions details_url uses /jobs/ (plural) — e.g. …/actions/runs/12345/jobs/67890. The expression s#.*/job/## does not match, so JOB_ID becomes the full URL string. The [ -n "$JOB_ID" ] guard passes (non-empty), and gh api "repos/$REPO/actions/jobs/<full-URL>/logs" 404s. The Stage 2 comment is posted without the failing job's log excerpt — the very evidence this section is designed to surface. — Failure scenario: any PR with a failing CI check → log excerpt silently missing from the triage comment.
| | sed 's#.*/job/##') | |
| [ -n "$JOB_ID" ] && gh api "repos/$REPO/actions/jobs/$JOB_ID/logs" | tail -c 15000 | |
| | sed 's#.*/jobs/##') | |
| [ -n "$JOB_ID" ] && gh api "repos/$REPO/actions/jobs/$JOB_ID/logs" | tail -c 15000 |
Also update the comment on line 353 from "after /job/" to "after /jobs/".
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Verified against the live API — the check-run details_url for an Actions job uses singular /job/, not /jobs/:
$ gh api ".../check-runs?per_page=100" --paginate --jq '.check_runs[].details_url' | grep -oE '/jobs?/' | sort | uniq -c
418 /job/
(418/418 real check-runs on this repo — e.g. .../actions/runs/30064620665/job/89393060859.) The gh api .../actions/jobs/<id>/logs endpoint is plural, but the details_url path segment is singular, so sed 's#.*/job/##' extracts the id correctly. Confirmed end-to-end in a real triage run (extracted 89384467683, then gh api .../jobs/89384467683/logs returned the log). Switching to /jobs/ would make the strip a no-op and 404 the fetch — the exact failure this comment describes, but inverted.
Keeping /job/. I did adopt the caching half of this thread (fetch check-runs once, read locally) in the latest push.
— via Claude Code (Opus 4.8)
4b08bf1 to
f57a460
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
|
Thanks for the review — addressed in the latest push (force-updated the branch). Fixed:
Not changed — false positive:
— via Claude Code (Opus 4.8) |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, Agent 4: Performance & efficiency, Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies. Not reviewed: verification — a verifier ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and the posted findings cannot be counted as verified against it.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
…r run PR code The triage skill instructed the review agent to run the PR's tests (npm/tmux) in the CI worktree. That executes untrusted PR code in a job whose environment carries a write PAT, and the "tests pass" evidence it produced came from self-run commands rather than the PR's own CI. Scope the behavior by trigger: - Unattended CI runs (GITHUB_EVENT_NAME set): never build or run PR-derived code. Stage 2 test evidence comes from the PR's own CI checks via the API (check-runs + the failing job's log excerpt). Real-scenario TUI coverage is left to the isolated `@qwen-code /tmux` job. - Local invocation only (no GITHUB_EVENT_NAME): drive the app in tmux as before. Also: never present the author's self-reported results under a testing heading — attribute them as a claim if referenced at all; and fetch the CHANGELOG via `gh api` instead of `curl`. The companion workflow change that enforces these restrictions with tool/permission settings should merge after this, so its npm/curl denials never surprise the agent.
f57a460 to
ae90226
Compare
|
Follow-up — the re-review caught three real issues in the caching change from the last push; all adopted:
Resolving the addressed threads. — via Claude Code (Opus 4.8) |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
|
||
| ```bash | ||
| # Fetch check-runs ONCE for the reviewed commit, then read locally. --paginate | ||
| # runs --jq per page, so it emits one array per page; `jq -s 'add'` flattens |
There was a problem hiding this comment.
[Suggestion] jq -s 'add' flattens paginated arrays correctly when each page outputs a valid array. If an API page returns a non-array (rare API error), add will fail. Consider adding // empty fallback or wrapping in error handling — but given --paginate + --jq executes jq per page independently, this is an extremely unlikely edge case. Non-blocking.
— qwen3.8-max-preview via Qwen Code /review
yiliang114
left a comment
There was a problem hiding this comment.
LGTM. Critical security hardening — prevents executing untrusted PR code in CI. Trust boundary well-defined (GitHub metadata trusted, log body untrusted). API-based CI evidence fetching is correct. One inline nit on jq pagination edge case.
— qwen3.8-max-preview via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
| # runs --jq per page, so it emits one array per page; `jq -s 'add'` flattens | ||
| # them into a single merged array (this repo has hit 500+ checks on a commit): | ||
| gh api "repos/$REPO/commits/$HEAD_SHA/check-runs?per_page=100" --paginate \ | ||
| --jq '.check_runs' | jq -s 'add' > /tmp/triage-checks.json |
There was a problem hiding this comment.
[Suggestion] jq -s 'add' produces null when gh api produces no stdout (e.g. gh api exits non-zero due to bad SHA, network error, or expired token). The downstream jq -r '.[] | ...' then fails with Cannot iterate over null (null). — Failure scenario: gh api error → empty stdout → jq -s 'add' yields null → every subsequent jq query on the file errors.
| --jq '.check_runs' | jq -s 'add' > /tmp/triage-checks.json | |
| --jq '.check_runs' | jq -s 'add // []' > /tmp/triage-checks.json |
— qwen3.7-max via Qwen Code /review
| # verified); third-party checks (Codecov, SonarCloud, …) point at their own | ||
| # domain, so filter to /job/ URLs before stripping the id — otherwise the first | ||
| # non-Actions failure yields a bogus job path and the real one goes unread: | ||
| JOB_ID=$(jq -r '[.[] | select(.conclusion == "failure") | select(.details_url | test("/job/"))][0].details_url // empty' \ |
There was a problem hiding this comment.
[Suggestion] select(.details_url | test("/job/")) errors when any failed check_run has details_url: null — jq's test() cannot match against null input (null cannot be matched, as it is not a string), aborting the entire filter so no JOB_ID is extracted. — Failure scenario: a failed third-party check with null details_url → jq exits 5 → JOB_ID empty → log fetch skipped → Stage 2 comment lacks the failing job's diagnostic excerpt.
| JOB_ID=$(jq -r '[.[] | select(.conclusion == "failure") | select(.details_url | test("/job/"))][0].details_url // empty' \ | |
| JOB_ID=$(jq -r '[.[] | select(.conclusion == "failure") | select((.details_url // "") | test("/job/"))][0].details_url // empty' \ |
— qwen3.7-max via Qwen Code /review
| # non-Actions failure yields a bogus job path and the real one goes unread: | ||
| JOB_ID=$(jq -r '[.[] | select(.conclusion == "failure") | select(.details_url | test("/job/"))][0].details_url // empty' \ | ||
| /tmp/triage-checks.json | sed 's#.*/job/##') | ||
| [ -n "$JOB_ID" ] && gh api "repos/$REPO/actions/jobs/$JOB_ID/logs" | tail -c 15000 |
There was a problem hiding this comment.
[Suggestion] [ -n "$JOB_ID" ] && … returns exit code 1 when $JOB_ID is empty (the common case when all checks pass or only third-party checks fail). An agent's shell execution layer may interpret this as a command failure and retry or abort. — Concrete cost: on the happy path (no failed Actions jobs), the agent sees a non-zero exit from a no-op.
| [ -n "$JOB_ID" ] && gh api "repos/$REPO/actions/jobs/$JOB_ID/logs" | tail -c 15000 | |
| if [ -n "$JOB_ID" ]; then gh api "repos/$REPO/actions/jobs/$JOB_ID/logs" | tail -c 15000; fi |
— qwen3.7-max via Qwen Code /review
What & why
During Stage 2 the triage skill instructed the review agent to run the PR's tests (
npm, tmux) in the CI worktree. Two problems in an unattended run:Change
Scope testing behavior by trigger:
GITHUB_EVENT_NAMEset): never build or run PR-derived code. Stage 2 test evidence now comes from the PR's own CI checks via the API —check-runsplus the failing job's log excerpt. Real-scenario TUI coverage is left to the isolated@qwen-code /tmuxjob.GITHUB_EVENT_NAME): drive the app in tmux exactly as before.Also:
gh apiinstead ofcurl.Verification
Ran the real triage skill end-to-end against #7632 with the
qwen3.8-max-previewmodel (the CI review model) through a write-blockingghshim, so nothing posted to the PR. Across two model runs the agent:npm/node/vitest),check-runs+ the failing job's logs exactly as intended,Merge order
The companion workflow PR #7647 enforces these restrictions with tool/permission settings that deny
npm/node/curl. Merge this PR first so those denials never surprise the agent.中文说明
改动内容与原因
triage skill 在 Stage 2 让 review agent 在 CI worktree 里跑 PR 的测试(
npm、tmux)。无人值守运行下有两个问题:改动
按触发来源区分测试行为:
GITHUB_EVENT_NAME):绝不构建或运行 PR 代码。Stage 2 的测试证据改为通过 API 读 PR 自身的 CI check ——check-runs加上失败 job 的日志片段。真实场景 TUI 验证交给隔离的@qwen-code /tmuxjob。GITHUB_EVENT_NAME):照旧用 tmux 驱动应用。另外:
gh api拉取,不用curl。验证
用
qwen3.8-max-preview(CI 实际使用的 review 模型)对 #7632 端到端跑了真实 triage skill,并通过一个写拦截的ghshim 保证不向 PR 发任何内容。两轮模型运行中,agent:npm/node/vitest);check-runs+ 失败 job 日志获取 CI 证据;合并顺序
配套的 workflow PR #7647 用 tool/permission 设置来强制这些限制(会 deny
npm/node/curl)。请先合并本 PR,这样那些 deny 才不会让 agent 意外受阻。