Skip to content

feat(core): show what a workflow will do, and scope the grant that approves it - #9837

Merged
qqqys merged 2 commits into
QwenLM:mainfrom
qqqys:feat/workflow-consent-dialog
Aug 24, 2026
Merged

feat(core): show what a workflow will do, and scope the grant that approves it#9837
qqqys merged 2 commits into
QwenLM:mainfrom
qqqys:feat/workflow-consent-dialog

Conversation

@qqqys

@qqqys qqqys commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Gives the Workflow tool a real approval dialog. Instead of Confirm WorkflowTool over Run a workflow script (4127 chars), the user now sees what the script says it will do — the workflow's name and description, its declared phases with their details, the resolved args, and a bounded excerpt of the source — under the title Run a dynamic workflow?.

All of that is read from export const meta via extractAndStripMeta, which parses the literal rather than evaluating it. That property is what makes reading it safe on the approval path: nothing model-authored executes before the user has approved anything. It does throw on a malformed literal, so the call is wrapped and degrades to a reduced dialog — a script with a broken meta block has to stay approvable-or-rejectable rather than take the dialog down with it.

Everything displayed is passed through stripAnsiAndControl first, and the screen ships with the preview rather than as a follow-up, because until now nothing was displayed and so nothing could be spoofed — a preview without the screen is what would open the hole. Single-line fields are flattened, which is the desired behaviour for a meta.name spanning three lines. The script excerpt is sanitized line by line instead, because \n is a C0 control character and the naive call would collapse the script into one unreadable line.

The same object also scopes the grant. An inline script is fresh model-authored source on every call, so it can never be pre-approved: hideAlwaysAllow removes the option and an empty permissionRules stops injectPermissionRulesIfMissing from supplying the bare tool name, which buildPermissionRules documents as matching all invocations of the tool. A scriptPath names a file the user chose, so it stays pre-approvable, scoped to that path through a key:value param matcher.

Finally, the token-cost warning now arrives before the spend it warns about, rather than only on the success path after the run.

Why it's needed

The user is asked to authorize arbitrary model-authored JavaScript that can dispatch up to the per-run agent cap, provision git worktrees, and spend an uncapped token budget — and the entire disclosure was a character count. The asymmetry is visible within a single run: the subagent approvals that same workflow bubbles up each get a full rich dialog, while the run that spawns them gets a number.

The grant was the second half of the problem. WorkflowToolInvocation supplied no permissionRules, so injectPermissionRulesIfMissing filled in the bare tool name — and one click on "always allow" turned consent for the one script the user had read into standing consent for every inline script the model writes afterwards.

Reviewer Test Plan

How to verify

cd packages/core
npx vitest run src/tools/workflow/ src/config/config.workflows.test.ts src/config/config.workflow-registration.test.ts src/core/permission-helpers.test.ts

Observed: Test Files 4 passed (4), Tests 68 passed (68). The workflow tool file alone is 52 passing, 10 of them new.

The ten new tests sit under a single approval dialog describe block and each names the regression it exists to catch. The two worth reading first:

Malformed meta must degrade, never throw. This is the load-bearing case. Reading meta now happens on the approval path, and extractAndStripMeta throws on a broken literal — if the dialog threw, the user could not even say no. The test feeds export const meta = { name: someIdentifier } (an identifier, which the parser refuses) and asserts the dialog still renders and still shows the script.

The scriptPath grant is asserted behaviourally, not textually. A rule that reads plausibly but never matches would make "always allow" appear to work while silently doing nothing, which is a worse affordance than not offering the button. So the test parses the rule the tool actually emitted and runs it back through matchesRule: it must resolve an invocation carrying the same scriptPath, must not resolve a different path, and must not resolve an inline script.

To confirm the tests are anchored to the implementation rather than passing vacuously, revert only the production file and re-run: git stash push -- packages/core/src/tools/workflow/workflow.ts && cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts. Observed: all 10 new tests fail, the other 42 stay green.

Also run: npx tsc --noEmit -p tsconfig.json (exit 0), npx prettier --check, npx eslint on both files — all clean.

Evidence (Before & After)

Before, for any workflow at all:

Confirm WorkflowTool
Run a workflow script (4127 chars)

After, for a script whose meta declares two phases:

Run a dynamic workflow?

Workflow: audit-deps
Audit dependencies for CVEs

Phases (2):
  1. Scan — one agent per manifest
  2. Verify

Args: {"target":"packages/core"}

Script:
export const meta = {
  name: 'audit-deps',
  ...
… (2841 more characters)

These are the strings the new tests assert on rather than a screenshot — the change is in packages/core and the TUI renders the prompt field it is given, unmodified, via the existing renderPromptAsPlainText path.

Tested on

OS Status
🍏 macOS ⚠️ not tested
🪟 Windows ⚠️ not tested
🐧 Linux ✅ tested

Environment

Unit tests via npx vitest run from packages/core, plus tsc --noEmit, prettier and eslint.

Risk & Scope

  • Main risk or tradeoff: this puts meta parsing on the approval path, which is a path that must never throw. The mitigation is the wrap-and-degrade helper plus the malformed-meta test above; note also that parsing is what makes this safe at all, and it is only available because fix(core): parse the workflow meta literal instead of evaluating it #9325 replaced the previous vm.Script evaluation. The second tradeoff is deliberate: users who relied on "always allow" for inline scripts lose it. That is the point of the change, but it is a real workflow change for anyone who had granted it, and their existing broad rule is not migrated or revoked by this PR.
  • Not validated / out of scope: meta.title is not implementedWorkflowMeta has no title field and adding one means touching the literal parser's contract and the run registry, which is a separate concern from disclosure; the dialog uses meta.name. A scriptPath workflow's own meta is not read — the dialog names the resolved path but does not open the file, because readWorkflowFileSecurely is private to workflow-saved.ts and its realpath/symlink fence is load-bearing; adding file I/O and its failure modes to the dialog path was not worth widening this diff, so the full preview covers inline scripts, which is where arbitrary model-authored code actually arrives. There is no "refuse if too large to display" branchgetConfirmationDetails has no refusal channel, so the excerpt is bounded and labelled instead; refusing an oversized script pre-launch belongs with the pre-launch compile gate, not here. No changes to enablement, the keyword trigger, the runtime, or the script authoring contract.
  • Breaking changes / migration notes: no API or schema change. One behaviour change worth calling out for release notes: "always allow" is no longer offered for inline workflow scripts, and a previously-persisted bare Workflow rule keeps working until the user removes it — this PR stops new ones being created, it does not revoke old ones.

Linked Issues

None.

中文说明

这个 PR 做了什么

给 Workflow 工具一个真正的审批对话框。用户看到的不再是 Confirm WorkflowTool 加上 Run a workflow script (4127 chars),而是脚本自己声明将要做的事——workflow 的名称与描述、声明的各个阶段及其细节、解析后的 args,以及一段有长度上限的源码摘录,标题为 Run a dynamic workflow?

这些内容全部通过 extractAndStripMetaexport const meta 读取,该函数是解析字面量而非求值。正是这个性质使得在审批路径上读取它是安全的:在用户批准任何东西之前,不会有任何模型编写的代码被执行。它在字面量格式错误时会抛异常,因此调用被包裹起来并降级为简化版对话框——meta 块损坏的脚本必须仍然可批准或可拒绝,而不是把对话框一起拖垮。

所有展示出来的内容都先经过 stripAnsiAndControl,并且这道防护是与预览同时加入的,而不是事后补上:在此之前什么都不显示,所以什么都无法被伪造——只加预览不加防护恰恰会打开这个口子。单行字段会被压平,这对于跨三行的 meta.name 正是我们想要的行为。脚本摘录则改为逐行净化,因为 \n 属于 C0 控制字符,直接调用会把整个脚本压成一行不可读的文本。

同一个对象还收窄了授权范围。内联 script 每次调用都是全新的模型编写源码,因此永远不能被预先批准:hideAlwaysAllow 移除了该选项,而空的 permissionRules 阻止 injectPermissionRulesIfMissing 填入裸工具名——buildPermissionRules 明确记载该规则会匹配该工具的所有调用。scriptPath 指向的是用户自己选择的文件,因此仍可预先批准,但通过 key:value 参数匹配器限定到该路径。

最后,token 成本警告现在出现在它所警告的那笔开销之前,而不是仅在运行成功之后才出现。

为什么需要

用户被要求授权的是任意的、由模型编写的 JavaScript,它可以派发到每次运行的 agent 上限、创建 git worktree、并消耗不设上限的 token 预算——而全部披露信息只是一个字符数。这种不对称在同一次运行内就能看到:该 workflow 冒泡上来的子 agent 审批,每一个都能拿到完整的富对话框,而派发它们的那次运行只拿到一个数字。

授权是问题的另一半。WorkflowToolInvocation 没有提供任何 permissionRules,于是 injectPermissionRulesIfMissing 填入了裸工具名——点一次"always allow",就把用户对自己读过的那一个脚本的同意,变成了对模型此后编写的每一个内联脚本的长期同意。

审阅者测试计划

如何验证

cd packages/core
npx vitest run src/tools/workflow/ src/config/config.workflows.test.ts src/config/config.workflow-registration.test.ts src/core/permission-helpers.test.ts

实测结果:Test Files 4 passed (4)Tests 68 passed (68)。仅 workflow 工具文件本身为 52 个通过,其中 10 个是新增的。

十个新测试集中在一个 approval dialog describe 块里,每个都写明了它存在是为了捕捉哪一类回归。其中最值得先读的两个:

meta 格式错误时必须降级,绝不能抛异常。 这是承重的用例。读取 meta 现在发生在审批路径上,而 extractAndStripMeta 在字面量损坏时会抛异常——如果对话框抛了,用户连"拒绝"都点不了。该测试传入 export const meta = { name: someIdentifier }(标识符,解析器会拒绝),并断言对话框仍然渲染、仍然显示脚本。

scriptPath 授权是按行为断言的,不是按文本。 一条读起来合理却永远匹配不上的规则,会让"always allow"看似生效实则静默失效,这比不提供这个按钮更糟。因此测试把工具实际产出的规则解析出来,再送回 matchesRule:它必须匹配携带相同 scriptPath 的调用,必须不匹配另一个路径,也必须不匹配内联 script

要确认这些测试是真的锚定在实现上、而非空转通过,可只回滚生产文件后重跑:git stash push -- packages/core/src/tools/workflow/workflow.ts && cd packages/core && npx vitest run src/tools/workflow/workflow.test.ts。实测结果:10 个新测试全部失败,其余 42 个保持通过。

另外执行了:npx tsc --noEmit -p tsconfig.json(退出码 0)、npx prettier --checknpx eslint,两个文件均无问题。

证据(前后对比)

之前,对任何 workflow 都是:

Confirm WorkflowTool
Run a workflow script (4127 chars)

之后,对一个 meta 声明了两个阶段的脚本:

Run a dynamic workflow?

Workflow: audit-deps
Audit dependencies for CVEs

Phases (2):
  1. Scan — one agent per manifest
  2. Verify

Args: {"target":"packages/core"}

Script:
export const meta = {
  name: 'audit-deps',
  ...
… (2841 more characters)

这里给出的是新测试所断言的字符串而非截图——改动位于 packages/core,TUI 会通过既有的 renderPromptAsPlainText 路径原样渲染它收到的 prompt 字段。

测试环境

OS 状态
🍏 macOS ⚠️ 未测试
🪟 Windows ⚠️ 未测试
🐧 Linux ✅ 已测试

packages/core 下通过 npx vitest run 运行单元测试,另加 tsc --noEmit、prettier 与 eslint。

风险与范围

  • 主要风险或取舍:本改动把 meta 解析放到了审批路径上,而这条路径绝不能抛异常。缓解措施是上面那个包裹并降级的辅助函数加上 malformed-meta 测试;另外要说明的是,正是"解析"这个性质才让这件事安全可行,而它之所以可用,是因为 fix(core): parse the workflow meta literal instead of evaluating it #9325 已经把此前的 vm.Script 求值替换掉了。第二个取舍是刻意的:此前依赖对内联脚本使用"always allow"的用户会失去该选项。这正是本改动的目的,但对于已经授予过该权限的人来说是真实的流程变化,而本 PR 并不迁移或撤销他们已有的宽泛规则。
  • 未验证 / 不在范围内:未实现 meta.title——WorkflowMeta 没有 title 字段,新增它意味着改动字面量解析器的契约和运行注册表,这与"披露"是两件事;对话框使用 meta.name不读取 scriptPath workflow 自身的 meta——对话框会给出解析后的路径,但不会打开该文件,因为 readWorkflowFileSecurelyworkflow-saved.ts 的私有函数,且它的 realpath/符号链接围栏是承重的;把文件 I/O 及其失败模式引入对话框路径不值得为此扩大本次改动,因此完整预览覆盖的是内联脚本,而那正是任意模型编写代码真正到达的地方。没有"内容过大则直接拒绝"的分支——getConfirmationDetails 没有拒绝通道,因此改为对摘录设上限并明确标注;对超大脚本在启动前予以拒绝属于启动前编译门的职责,不属于这里。未改动启用方式、关键词触发器、运行时,以及 script 的编写契约。
  • 破坏性变更 / 迁移说明:无 API 或 schema 变更。有一处行为变化值得写进发布说明:内联 workflow 脚本不再提供"always allow",而此前已持久化的裸 Workflow 规则在用户手动移除前仍然有效——本 PR 阻止新规则的产生,但不撤销旧规则。

关联 Issue

无。

qqqys and others added 2 commits August 24, 2026 10:35
…proves it

A user approving a workflow was shown `Confirm WorkflowTool` over `Run a
workflow script (4127 chars)` — a character count standing in for arbitrary
model-authored JavaScript that can fan out to the per-run agent cap,
provision git worktrees and spend an uncapped token budget. The asymmetry
was visible inside one run: the subagent approvals that same workflow
bubbles up each got a full dialog.

Override getConfirmationDetails to show what the script says it will do,
read from `export const meta`: the workflow's name and description, its
declared phases, the resolved args, and a bounded excerpt of the source.
Meta is obtained through `extractAndStripMeta`, which parses rather than
evaluates, so nothing model-authored runs before the user has approved
anything. It throws on a malformed literal, so the call is wrapped: a
script with a broken meta block stays approvable-or-rejectable rather than
taking the dialog down with it.

Everything displayed goes through `stripAnsiAndControl` first. The screen
ships with the preview rather than after it, because until now nothing was
displayed and so nothing could be spoofed — a preview without it is what
would open the hole. Single-line fields are flattened, which is what we
want for a `meta.name` spanning three lines. The script excerpt is
sanitized per line instead: `\n` is a C0 control character, so the naive
call would collapse the script into one unreadable line.

Scope the grant on the same object. An inline `script` is fresh source
every time, so it can never be pre-approved: `hideAlwaysAllow` removes the
option, and an empty `permissionRules` stops `injectPermissionRulesIfMissing`
from supplying the bare tool name, which `buildPermissionRules` documents as
matching every invocation of the tool. A `scriptPath` names a file the user
chose, so it stays pre-approvable but scoped to that path via a `key:value`
param matcher. The rule is built with the same helpers the matcher uses, and
the test asserts it behaviourally -- a rule that reads plausibly but never
matches would make "always allow" silently do nothing.

Finally, move the token-cost warning ahead of the spend it warns about. It
previously appeared only on the success path, i.e. after the run, and never
at all when the run failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDuRWMhKbqEYZXbbLhn3en
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on e5047af and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— e5047af 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: real, and verifiable in code rather than reported. Without this change the base BaseToolInvocation dialog renders Confirm WorkflowTool over Run a workflow script (N chars) — exactly as the PR states. The grant half is equally demonstrable: WorkflowToolInvocation supplies no permissionRules, and injectPermissionRulesIfMissing only fills in the bare tool-name rule when the field is absent — one "always allow" click then persists a rule that matches every invocation of the tool.

Direction: aligned. Approval disclosure is core to qwen-code's safety model, and the upstream CHANGELOG shows sustained investment in exactly this area — including "Fixed permission prompts so commands padded with tabs or invisible Unicode can no longer hide part of the command from the approval dialog", the same threat family (attacker-shaped text displayed on the approval path) that this PR's sanitization addresses.

Size: touches core paths (packages/core/src/tools/workflow/). Production logic 231 lines (workflow.ts +230/-1), tests 194 lines, no generated/schema files. Below every threshold — no maintainer-awareness escalation needed.

Approach: scope is right. Disclosure and grant scoping are two halves of one consent decision, and shipping them together is correct — a rich preview without the grant fix would still leave the blanket "always allow" hole open. The PR reuses existing load-bearing primitives (extractAndStripMeta from #9325, stripAnsiAndControl, the rule-parser's key:value param matcher) instead of adding parallel machinery, and the diff stays inside one tool plus its tests — no drive-by changes.

Risk: no high-risk path matches. No elevated risk signals.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:真实存在,且可直接在代码中验证。改动之前,基类对话框渲染的就是 Confirm WorkflowToolRun a workflow script (N chars)——与 PR 描述一致。授权那半同样可验证:WorkflowToolInvocation 没有提供任何 permissionRules,而 injectPermissionRulesIfMissing 只在字段缺省时填入裸工具名规则——点一次"always allow"就会持久化一条匹配该工具所有调用的规则。

方向:对齐。审批披露是 qwen-code 安全模型的核心,上游 CHANGELOG 也显示了对这一领域的持续投入——包括"修复权限提示,使带制表符或不可见 Unicode 的命令无法再在审批对话框中隐藏部分命令",与本 PR 净化逻辑应对的是同一威胁家族(审批路径上的攻击者可控展示文本)。

规模:触及核心路径(packages/core/src/tools/workflow/)。生产逻辑 231 行(workflow.ts +230/-1),测试 194 行,无生成/schema 文件。低于所有阈值,无需维护者关注升级。

方案:范围合理。披露与授权收窄是同一个同意决定的两半,一起交付是正确的——只加预览不修授权,"always allow" 的口子仍然开着。PR 复用了既有的承重原语(#9325extractAndStripMetastripAnsiAndControl、rule-parser 的 key:value 参数匹配器),而不是新造平行机制;diff 限定在一个工具及其测试内,无顺手改动。

风险:未命中高风险路径。无升级风险信号。

进入代码审查。🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

My independent proposal for this problem — parse export const meta without evaluating it, sanitize everything displayed, degrade instead of throwing on malformed meta, supply explicit permissionRules so the scheduler can't inject a bare-name rule, and hide "always allow" for inline scripts — is essentially what this PR does; I did not find a simpler path that covers both halves. The load-bearing claims check out against the base code:

  • The empty rule array is doing real work. injectPermissionRulesIfMissing checks !confirmationDetails.permissionRules, so [] (truthy) blocks the bare-tool-name rule the base path would otherwise inject, and persistPermissionOutcome persists nothing for an empty list. hideAlwaysAllow is honored by every confirmation surface — TUI, non-interactive CLI, useGeminiStream, and the ACP permission utils.
  • The scriptPath grant actually matches at runtime. evaluatePermissionFlow builds the permission context from the invocation's real params, PermissionManager passes toolParams into matchesRule, and evaluateParamMatchers enforces the scriptPath key — so a persisted workflow(scriptPath:<path>) rule resolves only invocations carrying that path. The test asserts this behaviourally (parse the emitted rule, run it back through matchesRule) rather than textually — the right way, since a plausible-but-dead rule would be worse than no button.
  • Meta reading is safe on the approval path. extractAndStripMeta parses the literal (never evaluates — that's the fix(core): parse the workflow meta literal instead of evaluating it #9325 property) and throws on malformed input; readMetaForConfirmation wraps that throw into a degrade, so a broken meta block stays approvable-or-rejectable. stripAnsiAndControl removes ANSI plus C0/C1 including \n, which justifies both the per-line sanitization of the script excerpt and the flattening of single-line fields. The renderPromptAsPlainText flag is an established, TUI-honored path.
  • The banner move keeps "exactly once" semantics. resolveMaxTokensPerWorkflow() defaults to process.env, matching the execution path's budget, and the registry latch flips on read — so surfacing the warning in the dialog consumes the latch and the post-hoc copy suppresses itself, rather than double-firing.

No critical findings. One non-blocking nit: the persisted rule inherits glob semantics from the path — a scriptPath containing * would match more files than the literal path, and )/, inside a path reshapes rule parsing (every case I traced stays either transparent — the dialog shows the exact param — or stricter). Rare in practice since scriptPath is normally user-selected via the saved-workflow slash command, not model-authored. Worth knowing, not worth blocking.

Not verified here: the author's claim that reverting workflow.ts makes all 10 new tests fail (the stash-and-rerun recipe in the PR body) — that is the author's evidence, not independently re-run. Unattended runs don't execute PR code; the CI section below is the PR's own signal.

Test evidence (PR's own CI, via API)

Final CI results for e5047af (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

The unit suite (ubuntu) is still running on the reviewed commit; the macOS/Windows matrix and integration jobs show skipped at this point. The Qwen Triage Finalize job rewrites the table above in place once CI settles. Nothing red so far.

Sandboxed verification would settle what static review cannot: @qwen-code /tmux — that the new dialog actually renders in a real TUI (title, phase list, bounded script excerpt), since the unit tests pin the string the TUI is handed, not what the terminal shows. The author has write access, so the lane is directly available.

中文说明

代码审查

我对该问题的独立方案——解析(而非求值)export const meta、净化所有展示内容、meta 损坏时降级而非抛异常、显式提供 permissionRules 使调度器无法注入裸名规则、对内联脚本隐藏"always allow"——与本 PR 的做法基本一致;没有发现能同时覆盖两半的更简路径。承重声明均已在基础代码中核实:

  • 空规则数组是有效的。 injectPermissionRulesIfMissing 检查 !confirmationDetails.permissionRules,因此 [](真值)阻止了基类路径原本会注入的裸工具名规则,且 persistPermissionOutcome 对空列表不会持久化任何内容。hideAlwaysAllow 在所有确认界面(TUI、非交互 CLI、useGeminiStream、ACP 权限工具)均被遵守。
  • scriptPath 授权在运行时确实生效。 evaluatePermissionFlow 用调用的真实参数构建权限上下文,PermissionManagertoolParams 传入 matchesRuleevaluateParamMatchers 强制检查 scriptPath 键——因此持久化的 workflow(scriptPath:<path>) 规则只会匹配携带该路径的调用。测试按行为(解析工具实际产出的规则再送回 matchesRule)而非按文本断言——这是正确的做法,因为"看似合理却永不匹配"的规则比不提供按钮更糟。
  • 审批路径上读取 meta 是安全的。 extractAndStripMeta 解析字面量(从不求值——这正是 fix(core): parse the workflow meta literal instead of evaluating it #9325 的性质),对格式错误的输入抛异常;readMetaForConfirmation 把该异常包裹为降级,损坏的 meta 块保持可批准或可拒绝。stripAnsiAndControl 移除 ANSI 及包括 \n 在内的 C0/C1 控制字符,这同时论证了脚本摘录逐行净化与单行字段压平的做法。renderPromptAsPlainText 是既有的、TUI 已遵守的路径。
  • 横幅移动保持了"仅一次"语义。 resolveMaxTokensPerWorkflow() 默认读取 process.env,与执行路径的预算一致;注册表的闩锁在读取时翻转——在对话框中展示警告会消耗闩锁,事后副本随之自我抑制,不会重复触发。

无阻塞性发现。一个非阻塞的小问题:持久化规则会继承路径的 glob 语义——若 scriptPath*,规则将匹配比字面路径更多的文件;路径中含 )/, 会改变规则解析(我推演的所有情形要么透明——对话框展示的就是该参数原值——要么更严格)。实际上罕见,因为 scriptPath 通常由保存工作流的斜杠命令由用户选定,而非模型生成。值得知晓,不构成阻塞。

此处未验证:作者关于"回滚 workflow.ts 会使 10 个新测试全部失败"的声明(PR 正文中的 stash 重跑步骤)——那是作者的证据,未被独立复跑。无人值守运行不执行 PR 代码;下节为 PR 自身的 CI 信号。

测试证据(PR 自身 CI,经 API 获取)

单元测试套件(ubuntu)在被审提交上仍在运行;macOS/Windows 矩阵与集成测试此时显示为 skipped。CI 落定后,Qwen Triage Finalize 任务会原地重写上方表格。目前无红色检查。

沙盒验证可以解决静态审查看不到的部分:@qwen-code /tmux——验证新对话框在真实 TUI 中的实际渲染(标题、阶段列表、有上限的脚本摘录),因为单元测试锚定的是交给 TUI 的字符串,而非终端实际显示的内容。作者有写权限,该通道可直接使用。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, focused change with every load-bearing claim verified against the base code; the one open item is the unit suite still running on the reviewed commit (plus the non-blocking wildcard-in-path nit from Stage 2).

Stepping back: this fixes a consent asymmetry that is visible in the code itself — the user was asked to authorize arbitrary model-authored JavaScript with a character count as the only disclosure, and one "always allow" click extended that consent to every future script. The implementation matches my independent proposal, reuses the repo's existing primitives instead of adding parallel machinery, and the diff is the minimal set the goal needs — one tool file, one test file, nothing else. The judgment calls are sound and stated out loud: inline scripts lose "always allow" entirely (there is no stable identity to scope fresh model-authored source against), malformed meta degrades instead of taking the dialog down, and the cost warning moves ahead of the spend. If I were maintaining this in six months, the degrade-on-malformed test and the behavioural rule-match test are exactly the anchors I would want.

CI is still running on the reviewed commit (Qwen Code CI in progress), so approval is deferred until it lands green on e5047af5fcf8123244a816c8db1f5c74f6590977 — the finalize job performs it.

中文说明

置信度:4/5 —— 干净、聚焦的改动,所有承重声明均已在基础代码中核实;唯一未落定的是被审提交上仍在运行的单元测试套件(外加第 2 阶段提到的"路径含通配符"非阻塞小问题)。

退一步看:这个改动修复的是代码中直接可见的同意不对称——用户被要求授权任意的模型编写 JavaScript,而全部披露只是一个字符数;一次"always allow"点击就把这份同意扩展到此后的每一个脚本。实现与我的独立方案一致,复用了仓库既有原语而非新造平行机制,diff 也是达成目标所需的最小集合——一个工具文件、一个测试文件,别无其他。几个判断都合理且明说:内联脚本完全失去"always allow"(全新的模型编写源码没有可供收窄的稳定身份)、格式错误的 meta 降级而非拖垮对话框、成本警告移到开销发生之前。若六个月后由我维护,malformed-meta 降级测试与按行为断言的规则匹配测试正是我会想要的锚点。

被审提交上的 CI 仍在运行(Qwen Code CI 进行中),因此批准推迟到它在 e5047af5fcf8123244a816c8db1f5c74f6590977 上全绿之后——由 finalize 任务执行。

Qwen Code · qwen3.8-max

Reviewed at e5047af5fcf8123244a816c8db1f5c74f6590977 · 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 — CI landed green after the review. ✅

@qqqys
qqqys enabled auto-merge August 24, 2026 03:26
@qqqys
qqqys added this pull request to the merge queue Aug 24, 2026
Merged via the queue into QwenLM:main with commit 674afdf Aug 24, 2026
101 of 102 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

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.

3 participants