Skip to content

fix(triage): actually restrict the CI review agent's tools - #7647

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
wenshao:triage/agent-permission-hardening
Jul 24, 2026
Merged

fix(triage): actually restrict the CI review agent's tools#7647
wenshao merged 1 commit into
QwenLM:mainfrom
wenshao:triage/agent-permission-hardening

Conversation

@wenshao

@wenshao wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

The bug

The settings_json: input on the QwenLM/qwen-code-action step does not exist — the action reads settings:. The block was silently dropped (the run logs show Unexpected input(s) 'settings_json'), so the triage agent has been running with the full default toolset and no restrictions — in a job whose environment carries a write PAT.

Change

  • Rename to settings: and express it in the current schema (tools.core + permissions.deny). Verified to load through the real settings pipeline: 11 tools registered, 106 deny rules active.
  • Deny interpreters, build tools, shells, network binaries, path execution, and the git/gh write subcommands that execute configured commands or materialize PR code.
  • Route fork PRs (and comment/dispatch reruns, which may target fork PRs) to ephemeral hosted runners, so a steered agent cannot persist on the shared ECS pool. Issue triage and same-repo PRs keep the ECS pool.
  • Wipe stale git exec vectors (hooks / core.hooksPath / core.fsmonitor / aliases) each run before checkout.

What this is — and is not

A command denylist is not a security boundary under the action's --yolo, and this PR does not treat it as one. $(...) substitution is not reliably deny-matchable, and even with it blocked, exec can hide elsewhere. The denylist is defense-in-depth against the normal failure (the agent running npm/node because a prompt implied it) and low-effort injection.

The real controls are, in order:

  1. the skill's no-PR-code rule (fix(triage): make unattended PR review static — read CI via API, never run PR code #7646),
  2. ephemeral runners for fork PRs (this PR),
  3. the per-run git exec-vector cleanup (this PR).

Residual risk is documented inline: the agent still holds a write PAT, so a successful prompt-injection could still exfiltrate it. The structural fix is token isolation (agent runs read-only; a separate PR-code-free step publishes its drafts, as publish-tmux already does) — flagged as a follow-up, not wired here.

Verification

  • The exact settings JSON was fed through the real PermissionManager: 38/38 attack-shaped commands hard-denied, 34/34 legitimate triage commands allowed, write-path rules correct (git -c denied, git -C allowed).
  • Confirmed the installed CLI enforces it: a smoke run had echo execute and node -e … hard-denied.
  • Two full real-model triage runs against feat(channels): GitHub polling adapter with notification-as-wakeup architecture #7632 (write-blocked) hit zero false denials and executed no PR code.

Depends on #7646

#7646 switches test evidence to the CI API and the CHANGELOG fetch to gh api. Merge #7646 first so the npm/curl denials introduced here are harmless.

中文说明

Bug

QwenLM/qwen-code-action step 上的 settings_json: 输入根本不存在 —— action 读的是 settings:。这个块被静默丢弃了(运行日志里能看到 Unexpected input(s) 'settings_json'),所以 triage agent 一直在用全量默认工具集、零限制运行 —— 而该 job 的环境里带着有写权限的 PAT。

改动

  • 改名为 settings:,并用当前 schema 表达(tools.core + permissions.deny)。已验证能通过真实设置加载链路生效:注册 11 个工具,106 条 deny 规则激活。
  • Deny 解释器、构建工具、shell、网络二进制、路径执行,以及会执行配置命令或落地 PR 代码的 git/gh 写子命令。
  • 把 fork PR(以及可能指向 fork PR 的 comment/dispatch 重跑)路由到临时 hosted runner,使被操纵的 agent 无法在共享 ECS 池上持久化。issue triage 和同仓 PR 仍用 ECS 池。
  • 每次运行在 checkout 前清除陈旧的 git 执行向量(hooks / core.hooksPath / core.fsmonitor / alias)。

它是什么,不是什么

在 action 的 --yolo 下,命令 denylist 不是安全边界,本 PR 也没把它当边界。$(...) 替换无法可靠地被 deny 匹配,即便挡住,执行也能藏在别处。denylist 是针对常见失误(agent 因为 prompt 暗示就去跑 npm/node)和低成本注入的纵深防御

真正的控制,按强度排序:

  1. skill 的"不执行 PR 代码"规则(fix(triage): make unattended PR review static — read CI via API, never run PR code #7646);
  2. fork PR 用临时 runner(本 PR);
  3. 每次运行清 git 执行向量(本 PR)。

残余风险已在文件内注明:agent 仍持有写权限 PAT,成功的 prompt 注入仍可能把它外带。结构性修复是 token 隔离(agent 用只读 token,由单独的、不接触 PR 代码的步骤发布其草稿,正如 publish-tmux 已经在做的)—— 已标为后续项,本 PR 未接线。

验证

  • 把确切的 settings JSON 喂进真实 PermissionManager:38/38 攻击形态命令被硬拒,34/34 合法 triage 命令放行,写路径规则正确(git -c 拒、git -C 放)。
  • 确认装机 CLI 会强制执行:smoke 运行中 echo 执行、node -e … 被硬拒。
  • 两轮完整真实模型 triage(写被拦)对 feat(channels): GitHub polling adapter with notification-as-wakeup architecture #7632:误拒,零 PR 代码执行。

依赖 #7646

#7646 把测试证据切到 CI API、CHANGELOG 拉取切到 gh api请先合并 #7646,这样本 PR 引入的 npm/curl deny 才不会造成困扰。

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — headings are non-standard but all required content is covered.

Problem: observed bug with clear evidence. The run logs show Unexpected input(s) 'settings_json' — the action's input is settings, not settings_json (verified against qwen-code-action@6d08e91 action.yml). The settings block was silently dropped, so the triage agent has been running with the full default toolset and no restrictions in a job carrying a write PAT. This is real, not theoretical.

Direction: squarely within scope — fixing a misconfiguration that left the CI review agent unrestricted. The layered approach (deny rules as defense-in-depth, ephemeral runners for fork PRs, git exec vector cleanup) is the right shape for this problem. The PR is honest that the denylist is not a security boundary under --yolo, and names the real controls in order of strength.

Size: not applicable — single workflow file, no core paths.

Approach: scope feels right. Each change serves the stated goal — rename the input, update to the current schema, add the deny rules, route fork PRs to ephemeral runners, wipe stale git exec vectors. No drive-by changes. Dependency on #7646 is now resolved (merged 2026-07-24).

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ —— 标题非标准格式,但所有必需内容均已覆盖。

问题:已观测到的 bug,有明确证据。运行日志显示 Unexpected input(s) 'settings_json' —— action 的输入是 settings 而非 settings_json(已对照 qwen-code-action@6d08e91action.yml 验证)。设置块被静默丢弃,triage agent 一直在携带写权限 PAT 的 job 中以全量默认工具集、零限制运行。这是真实问题,不是理论性的。

方向:完全在范围内 —— 修复一个导致 CI 审查 agent 不受限制的错误配置。分层方案(deny 规则作为纵深防御、fork PR 用临时 runner、清理 git 执行向量)是正确的问题形态。PR 诚实地说明 denylist 在 --yolo 下不是安全边界,并按强度排序列出了真正的控制措施。

规模:不适用 —— 单个 workflow 文件,未触及核心路径。

方案:范围合理。每项改动都服务于既定目标 —— 重命名输入、更新到当前 schema、添加 deny 规则、将 fork PR 路由到临时 runner、清除陈旧的 git 执行向量。无夹带改动。对 #7646 的依赖已解决(2026-07-24 已合并)。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 5ca087624a643f6c44d53e471b133e7db6d182d3 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal (before reading the diff): given "settings_json doesn't exist, agent runs unrestricted", I'd (1) rename to settings:, (2) update to the current tools.core + permissions.deny schema, (3) add deny rules for interpreters/shells/network/git-write, (4) consider runner isolation for fork PRs.

Comparison with the diff: the PR does exactly this, plus adds git exec vector cleanup and thorough inline documentation. No simpler path missed.

Findings — no blockers.

The three changes are each correct:

  1. Runner routing — the runs-on expression correctly partitions events: issues and same-repo pull_request_target keep the ECS pool; fork PRs, issue_comment reruns (which may target fork PRs), and workflow_dispatch (no PR context) all fall through to ubuntu-latest. Traced all event scenarios — the logic holds. The github.event.pull_request && guard correctly short-circuits for events without PR context. Slightly more conservative than needed for issue-comment reruns targeting issues (those carry no foreign code but still go to ubuntu-latest), but that's a safe default and the PR description acknowledges the tradeoff.

  2. Git exec vector cleanup — placed in "Clean stale agent state" after the .git existence check and before checkout, so it operates on the stale workspace from a prior run. core.hooksPath, core.fsmonitor, core.pager, pager, core.editor, sequence.editor, core.sshCommand, diff.external, filter.*, credential.*, url.*, init.templateDir, aliases, include/includeIf sections, and non-sample hook files (including symlinks) are all neutralized with || true so the job never fails. The find here is a workflow-step shell command, not an agent tool call, so the run_shell_command(find) deny rule doesn't interfere.

  3. Settings — verified against qwen-code-action@6d08e91 action.yml: the input is settings (written to .qwen/settings.json), not settings_json. YAML parses clean, JSON parses clean (11 tools, 106 deny rules). The deny list covers interpreters, build tools, shells, privilege escalation, execution wrappers, network binaries, containers, path execution, git write/exec subcommands (git -c denied, git -C allowed — case-sensitive prefix match), shell injection patterns (backticks, <(), and gh write subcommands. Legitimate triage operations (gh api, gh pr view, gh pr review, git log, git diff, git show, text processing) are all unblocked. The tmux and curl denials are correct for the post-fix(triage): make unattended PR review static — read CI via API, never run PR code #7646 world where test evidence comes from the CI API and CHANGELOG fetch uses gh api.

The inline comments are extensive but earn their place — this is security-sensitive configuration, and the comments document the threat model, the honest limitations of a denylist under --yolo, and the residual risk. Future maintainers need this context.

CI Test Evidence

CI checks for commit 5ca08762:

Check Status Conclusion
review-config completed success
All other checks completed skipped (expected — workflow-only change, no source code touched)

No failures. The skipped checks are expected: this PR modifies only .github/workflows/qwen-triage.yml, which doesn't trigger the build/test/lint pipeline.

Not verified: runtime behavior of the deny rules in a live triage run — this requires the workflow to actually execute on a PR event. The PR author (maintainer/collaborator) reports 38/38 attack-shaped commands hard-denied and 34/34 legitimate triage commands allowed through the real PermissionManager, plus two full real-model triage runs with zero false denials. These are the author's claims, not independently verified here.

中文说明

代码审查

独立方案(读 diff 前):给定"settings_json 不存在,agent 不受限制"的问题,我会 (1) 改名为 settings:,(2) 更新到当前 tools.core + permissions.deny schema,(3) 添加解释器/shell/网络/git 写操作的 deny 规则,(4) 考虑 fork PR 的 runner 隔离。

与 diff 对比: PR 完全按此方案执行,另外增加了 git 执行向量清理和详尽的内联文档。没有遗漏更简路径。

发现 —— 无阻塞项。

三项改动均正确:

  1. Runner 路由 —— runs-on 表达式正确分区事件:issues 和同仓 pull_request_target 保留 ECS 池;fork PR、issue_comment 重跑(可能指向 fork PR)和 workflow_dispatch(无 PR 上下文)均回退到 ubuntu-latest。遍历所有事件场景,逻辑成立。github.event.pull_request && 守卫在无 PR 上下文时正确短路。对 issue 评论重跑(不携带外部代码)略保守(仍走 ubuntu-latest),但这是安全的默认选择,PR 描述中也说明了这一取舍。

  2. Git 执行向量清理 —— 放在"Clean stale agent state"步骤中,在 .git 存在性检查之后、checkout 之前,因此操作的是上次运行的陈旧工作区。core.hooksPathcore.fsmonitorcore.pagerpagercore.editorsequence.editorcore.sshCommanddiff.externalfilter.*credential.*url.*init.templateDir、alias、include/includeIf 段,以及非 sample 的 hook 文件(含符号链接)全部被清除,|| true 确保 job 不会失败。这里的 find 是 workflow 步骤的 shell 命令,不是 agent 工具调用,因此 run_shell_command(find) deny 规则不会干扰。

  3. Settings —— 已对照 qwen-code-action@6d08e91action.yml 验证:输入是 settings(写入 .qwen/settings.json),不是 settings_json。YAML 解析正常,JSON 解析正常(11 个工具,106 条 deny 规则)。deny 列表覆盖解释器、构建工具、shell、权限提升、执行包装器、网络二进制、容器、路径执行、git 写/执行子命令(git -c 拒绝、git -C 放行 —— 大小写敏感前缀匹配)、shell 注入模式(反引号、<()和 gh 写子命令。合法 triage 操作(gh apigh pr viewgh pr reviewgit loggit diffgit show、文本处理)均未被阻断。tmuxcurl 的拒绝在 fix(triage): make unattended PR review static — read CI via API, never run PR code #7646 之后的世界中是正确的(测试证据来自 CI API,CHANGELOG 拉取使用 gh api)。

内联注释很详尽但物有所值 —— 这是安全敏感配置,注释记录了威胁模型、denylist 在 --yolo 下的诚实局限性和残余风险。未来的维护者需要这些上下文。

CI 测试证据

提交 5ca08762 的 CI 检查:

检查 状态 结论
review-config 已完成 成功
其他所有检查 已完成 跳过(预期 —— 仅 workflow 变更,未触及源代码)

无失败。跳过的检查是预期的:本 PR 仅修改 .github/workflows/qwen-triage.yml,不触发构建/测试/lint 流水线。

未验证:deny 规则在实际 triage 运行中的运行时行为 —— 这需要 workflow 在 PR 事件上实际执行。PR 作者(维护者/协作者)报告 38/38 攻击形态命令被硬拒、34/34 合法 triage 命令通过真实 PermissionManager 放行,以及两轮完整真实模型 triage 运行零误拒。这些是作者的声明,此处未独立验证。

Qwen Code · qwen3.8-max-preview

Reviewed at 5ca087624a643f6c44d53e471b133e7db6d182d3 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean across every stage; this fixes a real, observed security gap and the implementation matches the problem exactly.

Stepping back: the bug is unambiguous — settings_json: was silently dropped, the agent ran unrestricted with a write PAT. The fix is the minimal set of changes that addresses it: rename the input, update the schema, add deny rules as defense-in-depth, isolate fork PRs on ephemeral runners, and clean stale git exec vectors. Nothing extra, nothing missing.

The PR is refreshingly honest about what the denylist does and doesn't do — it's a speed bump, not a boundary, and the real controls are the no-PR-code rule, runner isolation, and exec vector cleanup. The residual risk (write PAT exfiltration under successful injection) is documented with a clear path to the structural fix (token isolation). That's the right way to ship security hardening: name the layers, name the gaps, name the follow-up.

The dependency on #7646 is resolved (merged). The squashed commit is clean. CI shows no failures (workflow-only change, all source checks skipped as expected). Already approved by @yiliang114.

Ships it. ✅

中文说明

置信度:5/5 —— 每个阶段都干净;这修复了一个真实的、已观测到的安全缺口,实现与问题完全匹配。

退一步看:bug 是明确的 —— settings_json: 被静默丢弃,agent 在携带写权限 PAT 的情况下不受限制地运行。修复是解决它的最小改动集:重命名输入、更新 schema、添加 deny 规则作为纵深防御、将 fork PR 隔离到临时 runner、清理陈旧的 git 执行向量。没有多余的东西,也没有遗漏。

PR 对 denylist 能做和不能做什么坦诚得令人耳目一新 —— 它是减速带,不是边界,真正的控制是"不执行 PR 代码"规则、runner 隔离和执行向量清理。残余风险(成功注入下写权限 PAT 被外带)已记录,并给出了结构性修复(token 隔离)的清晰路径。这是交付安全加固的正确方式:命名层次、命名缺口、命名后续项。

#7646 的依赖已解决(已合并)。压缩后的提交干净。CI 无失败(仅 workflow 变更,所有源代码检查按预期跳过)。已获 @yiliang114 批准。

可以合并 ✅

Qwen Code · qwen3.8-max-preview

Reviewed at 5ca087624a643f6c44d53e471b133e7db6d182d3 · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.

— qwen3.7-max via Qwen Code /review

# PRs), go to ephemeral hosted runners so a steered agent cannot persist
# anything across runs. Forks of this repo fall back to ubuntu-latest as
# before.
runs-on: "${{ (github.repository == 'QwenLM/qwen-code' && vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true' && (github.event_name == 'issues' || (github.event.pull_request && github.event.pull_request.head.repo.full_name == github.repository))) && fromJSON('[\"self-hosted\", \"linux\", \"x64\", \"ecs-qwen\"]') || fromJSON('[\"ubuntu-latest\"]') }}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No regression test for the fork-PR-to-ephemeral-runner routing decision.

The runs-on expression is the only control preventing fork PR code from executing on persistent ECS runners (which carry write PATs and reuse workspace state across runs). There are no workflow-level assertions on head.repo.full_name, ecs-qwen, or the ubuntu-latest fallback. A future PR that simplifies or restructures this expression — removing the fork check, or adding issue_comment to the ECS path — would route fork PRs to persistent runners without any test catching it.

Consider adding assertions in the existing workflow test file verifying: (a) head.repo.full_name == github.repository is present (same-repo gate), (b) ecs-qwen is present (persistent pool label), and (c) the ubuntu-latest fallback exists.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — deferring, not dismissing. There's no workflow-YAML assertion harness in the repo today (the .github/scripts/*.test.mjs files test scripts, not this workflow). A harness asserting the head.repo.full_name / ecs-qwen / ubuntu-latest routing invariants is worth doing deliberately in its own PR rather than bolting a bespoke grep-test onto this change. Tracking as a follow-up.

— via Claude Code (Opus 4.8)

Comment on lines +351 to 352
settings: |-
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No regression test for the settings input name that replaces the silently-dropped settings_json.

The comment on lines 305–309 documents that the previous settings_json: input was silently ignored by the action, causing the agent to run with the full default toolset and no deny list. No test asserts that the Run Qwen Triage step uses the correct input name settings:. The exact bug this diff fixes — wrong input name silently dropped, full toolset exposed — has no regression guard.

Consider adding a test asserting that the step contains settings: |- and does not contain settings_json:.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed this is the highest-value guard (it's the exact bug this PR fixes). Deferring only because there's no workflow-YAML test harness yet — the .github/scripts/*.test.mjs suite covers scripts, not this YAML. A small harness asserting settings:-not-settings_json:, the deny entries, and the tools.core allowlist is the right home for it, as its own PR. Tracked as a follow-up.

— via Claude Code (Opus 4.8)

Comment thread .github/workflows/qwen-triage.yml Outdated
Comment on lines +254 to +256
git config --local --unset-all core.hooksPath 2>/dev/null || true
git config --local --unset-all core.fsmonitor 2>/dev/null || true
git config --local --remove-section alias 2>/dev/null || true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No regression test for the new git exec vector cleanup.

These lines defend against persistent exec vectors on ECS runners (hooks/config/aliases planted by a prior run). The existing workflow test file asserts on QWEN_HOME, rm -rf, and workspace cleanup, but has no assertions on hooksPath, fsmonitor, alias, HOOKS_DIR, or hook deletion. A future PR that removes any of these cleanup operations would leave persistent exec vectors intact on the ECS workspace without test detection.

Consider extending the cleanup-isolation test with assertions that the clean step contains core.hooksPath, core.fsmonitor, --remove-section alias, and HOOKS_DIR.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — deferring to the same workflow-test-harness follow-up. No YAML assertion harness exists today; a dedicated one asserting the cleanup knobs (core.hooksPath/fsmonitor/pager/editor, the include/includeIf unset, and the symlink-aware find) is worth adding deliberately rather than inline here.

— via Claude Code (Opus 4.8)

Comment thread .github/workflows/qwen-triage.yml Outdated
Comment thread .github/workflows/qwen-triage.yml Outdated
@wenshao
wenshao force-pushed the triage/agent-permission-hardening branch from d6f7c54 to f0cc2b5 Compare July 24, 2026 05:36
@wenshao
wenshao requested a review from Copilot July 24, 2026 05:36

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

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)为单个提交。

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review — addressed in the latest push.

Fixed (real hardening gaps — good catches):

  • git config cleanup was incomplete: now also clears core.pager / pager / core.editor / sequence.editor / core.sshCommand / diff.external — these fire on the plain git log / git diff / git show that triage runs — plus the include / includeIf sections that can re-introduce any of them from an arbitrary file.
  • find -type f missed symlinked hooks: now find "$HOOKS_DIR" \( -type f -o -type l \) ! -name '*.sample' -delete. A symlinked post-checkout -> ../payload survived the old -type f sweep and would still fire on the next actions/checkout.

Deferred — worth their own PR:

  • The three "add a regression test" suggestions (routing expression, settings: input name, cleanup knobs) are valid, but there is no workflow-YAML assertion harness today — the .github/scripts/*.test.mjs files test scripts, not this workflow. A small harness asserting settings:-not-settings_json:, the ecs-qwen / head.repo.full_name / ubuntu-latest routing invariants, and the cleanup knobs is worth doing deliberately rather than bolting a bespoke grep-test onto this PR. Tracking as a follow-up.

— via Claude Code (Opus 4.8)

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.

— qwen3.7-max via Qwen Code /review

Comment thread .github/workflows/qwen-triage.yml Outdated
Comment thread .github/workflows/qwen-triage.yml Outdated
Comment on lines +257 to +260
for k in core.hooksPath core.fsmonitor core.pager pager core.editor \
sequence.editor core.sshCommand diff.external; do
git config --local --unset-all "$k" 2>/dev/null || true
done

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No regression test verifies the git exec vector cleanup commands are present. The existing 'isolates agent state per run' test only checks QWEN_HOME, rm -rf, mkdir -p — none of the new git config --unset-all / --remove-section / find -delete commands are asserted. A future cleanup of the step body could drop core.hooksPath or the symlink-aware find, and no test would catch it.

Also: filter.<name>.smudge, diff.<name>.command, and merge.<name>.driver sections are not cleaned — they are git-native code-execution vectors (smudge filters fire on git checkout). Consider either enumerating them or stripping all sections except core/remote/branch.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same theme as the cleanup-test thread above — deferred to a dedicated workflow-test-harness PR (no YAML assertion harness exists yet). Tracked together.

— via Claude Code (Opus 4.8)

Comment on lines +361 to +365
settings: |-
{
"coreTools": [
"run_shell_command",
"write_file",
"read_file",
"grep_search",
"glob",
"agent",
"enter_worktree",
"exit_worktree"
],
"sandbox": false
"tools": {
"core": [
"run_shell_command",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No regression test verifies that the input key is settings (not the silently-dropped settings_json), that key deny entries are present, or that the tools.core allowlist is correct. The exact regression this PR fixes — settings_json being silently ignored — could recur if a future edit renames the key back, and no assertion would catch it.

The autofix workflow tests (scripts/tests/qwen-autofix-workflow.test.js) already follow this pattern — asserting toContain for specific deny entries and not.toContain for the wrong input name. A parallel test here would be consistent with existing practice.

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the settings-input-name test thread above — deferred to the same workflow-test-harness follow-up. Tracked together.

— via Claude Code (Opus 4.8)

@wenshao
wenshao force-pushed the triage/agent-permission-hardening branch from f0cc2b5 to 4d99603 Compare July 24, 2026 06:50
@wenshao
wenshao requested a review from Copilot July 24, 2026 06:50

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment thread .github/workflows/qwen-triage.yml Outdated
The `settings_json:` input on the Qwen Code action does not exist — the action
reads `settings:`. The block was silently dropped ("Unexpected input(s)
'settings_json'" in the run logs), so the triage agent ran with the full
default toolset and no restrictions in a job that carries a write PAT.

- Rename to `settings:` and express it in the current schema (tools.core +
  permissions.deny); verified to load through the real settings pipeline
  (11 tools registered, 106 deny rules active).
- Deny interpreters, build tools, shells, network binaries, path execution, and
  the git/gh write subcommands that execute configured commands or materialize
  PR code. This is defense-in-depth, NOT a boundary: under the action's --yolo a
  command denylist cannot be one (`$(...)` is not reliably deny-matchable, and
  exec can hide in other commands) — see the in-file note. The real controls are
  the skill's no-PR-code rule, ephemeral runners for fork PRs, and the git
  exec-vector cleanup added here. Residual token-exfil-under-injection risk is
  documented, with token isolation flagged as the structural follow-up.
- Route fork PRs (and comment/dispatch reruns, which may target fork PRs) to
  ephemeral hosted runners so a steered agent cannot persist on the shared ECS
  pool. Issue triage and same-repo PRs keep the ECS pool.
- Wipe stale git hooks/config/aliases each run before checkout.

Depends on the skill change that switches test evidence to the CI API and the
CHANGELOG fetch to `gh api`; merge that first so the npm/curl denials are
harmless.
@wenshao
wenshao force-pushed the triage/agent-permission-hardening branch from 4d99603 to 5ca0876 Compare July 24, 2026 07:42
@wenshao
wenshao requested a review from Copilot July 24, 2026 07:42

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

# runs, filter.*.{clean,smudge,process} (run on checkout via
# .gitattributes), credential.helper, url.*.insteadOf, init.templateDir,
# aliases, and include/includeIf which pull any of them back in from an
# arbitrary file. Enumerate the actual keys and --unset-all each:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The git config cleanup covers hookspath, fsmonitor, pager, editor, sshCommand, filter., credential., url.*, init.templateDir, alias, include, includeIf. The hooks dir sweep also covers symlinks (-type f -o -type l). Good defense-in-depth against planted exec vectors. Consider also unsetting core.alternateRefsCommand and core.pager (already covered) — but diff.guitool and merge.tool are not in the denylist and could fire on git diff/git merge invocations. Minor gap.

— qwen3.8-max-preview via Qwen Code /review

# command is a gh-API call, a read-only git command, or text
# processing — none touch the PR's tree in an executable way).
# 2. Fork PRs run on ephemeral hosted runners (see the triage job
# `runs-on`), bounding persistence/lateral movement.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The deny list is comprehensive (106 rules) but correctly documented as NOT a security boundary under --yolo. The residual risk (agent holds write PAT, could exfiltrate under successful injection) is well-documented. The structural fix (token isolation — agent runs read-only, separate step publishes with PAT) is the right end-state. Tracked as follow-up.

— qwen3.8-max-preview via Qwen Code /review

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Critical security fix — settings_json: was silently dropped, agent ran with full toolset + write PAT. Fix correctly renames to settings: with 11 core tools + 106 deny rules. Defense-in-depth approach is well-documented: deny list is NOT a security boundary under --yolo, real controls are (1) no PR code execution, (2) fork PRs on ephemeral runners, (3) stale git vectors wiped. Residual risk (PAT exfiltration under injection) correctly tracked as follow-up for token isolation. Two inline suggestions on minor gaps (diff.guitool/merge.tool, core.alternateRefsCommand).

— qwen3.8-max-preview via Qwen Code /review

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

"run_shell_command(git merge)",
"run_shell_command(git push)",
"run_shell_command(patch)",
"run_shell_command(git -c)",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The deny list blocks git -c (lowercase, sets config key=value) but not git -C (uppercase, changes working directory), git --git-dir, or git --work-tree. These global git flags prefix any subcommand before the denied pattern, bypassing all git <subcommand> deny rules.

Failure scenario: git -C . config --local core.hooksPath /tmp/evil — prefix matching checks startsWith('git config ') which fails because the command starts with git -C .. Similarly, git -C . rebase --exec 'malicious' bypasses run_shell_command(git rebase). Verified in matchesCommandPattern (rule-parser.ts:769-772).

Suggested change
"run_shell_command(git -c)",
"run_shell_command(git -c)",
"run_shell_command(git -C)",
"run_shell_command(git --git-dir)",
"run_shell_command(git --work-tree)",

— qwen3.7-max via Qwen Code /review

"run_shell_command(ksh)",
"run_shell_command(su)",
"run_shell_command(sudo)",
"run_shell_command(env)",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The deny list blocks run_shell_command(env) but not run_shell_command(printenv) — a direct synonym that dumps all environment variables. The step's env carries GH_TOKEN (write PAT) and OPENAI_API_KEY.

Failure scenario: A prompt injection steers the agent to call printenv. Prefix word-boundary matching ("printenv".startsWith("env ") → false) lets it through. Secrets are dumped in plaintext and can be exfiltrated via gh pr comment (the agent has legitimate comment-posting capability).

Suggested change
"run_shell_command(env)",
"run_shell_command(env)",
"run_shell_command(printenv)",

— qwen3.7-max via Qwen Code /review

# a keep-known-safe config allowlist is the exhaustive end-state, tracked
# with the workflow-test-harness follow-up since it needs a checkout test.
git config --local --name-only --list 2>/dev/null \
| grep -iE '^(core\.(hookspath|fsmonitor|pager|editor|sshcommand)|sequence\.editor|diff\.external|filter\.|credential\.|url\.|init\.templatedir|pager\.|alias\.|include\.|includeif\.)' \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The cleanup grep regex omits gpg\., a direct code-execution config knob: git runs the value of gpg.program as an external command when verifying signatures (git log --show-signature, git verify-commit).

Failure scenario: A prior run on the persistent ECS pool plants git config --local gpg.program /tmp/evil. The cleanup grep doesn't match gpg.program, so it survives. A subsequent triage run invokes git log --show-signature (not in the deny list) and git executes /tmp/evil --status-fd ... --verify ... with the agent's full environment.

Suggested change
| grep -iE '^(core\.(hookspath|fsmonitor|pager|editor|sshcommand)|sequence\.editor|diff\.external|filter\.|credential\.|url\.|init\.templatedir|pager\.|alias\.|include\.|includeif\.)' \
| grep -iE '^(core\.(hookspath|fsmonitor|pager|editor|sshcommand)|sequence\.editor|diff\.external|filter\.|credential\.|url\.|init\.templatedir|pager\.|alias\.|include\.|includeif\.|gpg\.)' \

— qwen3.7-max via Qwen Code /review

Comment on lines +473 to +474
"write_file(/.git/hooks/**)",
"write_file(/.git/config)",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] All file-path deny rules use write_file(...) as the tool name, but sed -i via run_shell_command produces virtual edit operations (shell-semantics.ts:1596), and toolMatchesRuleToolName('write_file', 'edit') returns false (rule-parser.ts:230-256). The edit rule direction matches write_file contexts, but not the reverse.

Failure scenario: The agent runs sed -i 's/.*/malicious/' ~/.bashrc. Shell semantics extracts a virtual edit operation for ~/.bashrc. The deny rule write_file(~/.bashrc) is checked via toolMatchesRuleToolName('write_file', 'edit') → false. The virtual-op pass returns default, and the command is allowed. This applies to every write_file(...) path in the deny list.

Suggested fix: Use edit(...) instead of write_file(...) for all file-path deny rules. An edit(...) rule matches both edit and write_file contexts via EDIT_TOOLS (rule-parser.ts:184).

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments above for the result.

Qwen Triage 已完成 —— 查看运行。结果见上方各阶段评论。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Jul 24, 2026
Merged via the queue into QwenLM:main with commit d550aea Jul 24, 2026
169 of 170 checks passed
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.

4 participants