feat(cli): enable dynamic workflows from a settings key - #9098
Conversation
`ConfigParameters.workflowsEnabled` is declared, defaulted, and read by `Config.isWorkflowsEnabled()` — but `loadCliConfig` never writes it, so no setting has ever reached it. The only way to turn dynamic workflows on is the undocumented `QWEN_CODE_ENABLE_WORKFLOWS=1`, which has to be exported in every shell that launches qwen. AGENTS.md names this shape directly: an optional field that is declared and read but never set by any caller is a dead switch. Add `tools.workflowsEnabled` to the settings schema and populate the field from it. Precedence is unchanged and still resolved in core: `QWEN_CODE_DISABLE_WORKFLOWS` beats everything, then `QWEN_CODE_ENABLE_WORKFLOWS`, then the setting. Because `settings.merged` already folds the System scope, an operator gets a fleet-wide force-off with no extra code. `requiresRestart` is load-bearing rather than decorative: the Workflow tool is registered once while the tool registry is built, `/workflows` is gated when commands load, and keyword steering resolves at startup — so a mid-session toggle would leave the dialog claiming the feature is on while the tool is absent from the registry. The setting description also disambiguates it from the unrelated `experimental.sessionWorkflow` plan-and-review view, which shares the word "workflow" and would otherwise be easy to confuse in the settings dialog. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks for the PR — re-running triage on the current head, which has grown well beyond the originally reviewed wiring commit. Template looks good ✓ Problem: observed, not theoretical — verified in the tree. Direction: aligned, and the expanded scope holds together. The first commit wires the setting; everything after answers one question that wiring creates — who may grant the opt-in. A workflow run can dispatch many subagents against the user's token budget, so enabling it is a capability grant, and the PR scopes it to user/system using the repo's own precedent ( Size: core paths touched ( Approach: coherent, nothing drive-by. Each added surface closes the same door: the merge strip + startup warning (settings files), the dialog filter (TUI), Risk: no elevated risk signals (no high-risk paths matched). Moving on to code review. 🔍 中文说明感谢贡献——本次是对当前 head 的重新 triage,PR 的范围已远超最初审查的接线 commit。 模板完整 ✓ 问题:已观测到,而非理论性问题——已在代码树中核实。 方向:对齐,且扩展后的范围自洽。第一个 commit 完成接线;之后的所有内容都在回答接线所引出的一个问题——谁有权授予这项开关。一次 workflow 运行可以派发大量子智能体、消耗用户的 token 预算,因此启用它是一次能力授予;本 PR 沿用仓库自身的先例将其限定在 user/system 作用域( 规模:触及核心路径( 方案:自洽,无夹带改动。每个新增表面都在关闭同一扇门:merge 剥离 + 启动警告(settings 文件)、对话框过滤(TUI)、 风险:无升级风险信号(未命中任何高风险路径)。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewBaseline first, written before reading the diff: wire
No critical issues. The six items the Files changed (17)
Test evidenceUnattended CI run — no PR code was built or executed here; the table below is the PR's own CI read through the API at the reviewed commit. Everything that ran is green:
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 The Linux unit suite runs all six changed test files; Serve A/B and the real daemon E2E are meaningful signal for the new serve-route guard. Sandboxed verification would settle the remaining surface claim: 中文说明代码审查先写基线方案(读 diff 之前):在
无阻塞问题。 测试证据无人值守 CI 运行——此处未构建或执行任何 PR 代码;下表是通过 API 读取的该 PR 在 reviewed commit 上的自身 CI 结果,所有运行的检查均为绿色。Linux 单元测试套件会运行全部六个被修改的测试文件;Serve A/B 与真实 daemon E2E 对新增的 serve 路由守卫是有效信号。 沙箱验证可以落定剩下的表层断言: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean, well-scoped fix for a verified dead switch, with the grant-ownership question answered the right way; the leftovers are cosmetic follow-ups. Stepping back: the problem is real and code-evident — before this change no writer for The remaining nits — the schema description missing the workspace-scope caveat, the dialog call site lacking its own test, and a user-scope 中文说明置信度:4/5 —— 对一个已核实的死开关做了干净、恰当范围的修复,能力授予的归属问题也以正确的方式解决;剩余项均为外观层面的跟进事项。 退一步看整体:问题真实存在且可在代码中直接证实——本次改动之前,整个 CLI 包里不存在任何 剩余的小问题——schema 描述缺少 workspace 作用域提示、对话框调用点缺独立测试、user 作用域 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not explored to full depth (tool budget reached): "PR #9098 adds a tools.workflowsEnabled settings key…": execute scripts/generate-settings-schema.ts and diff its output against the committed settings.schema.json — the review worktree has no node_modules or bu…; "You are review agent reverse-audit — Reverse audit agent…": none — all planned checks completed within budget..
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Test Plan (not a blocker): 377 passed — this review observed 19468, 494 passed.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未探索到全部深度(达到工具调用预算):"PR #9098 adds a tools.workflowsEnabled settings key…":execute scripts/generate-settings-schema.ts and diff its output against the committed settings.schema.json — the review worktree has no node_modules or bu…;"You are review agent reverse-audit — Reverse audit agent…":none — all planned checks completed within budget.。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
Test Plan(非阻断):377 passed — this review observed 19468, 494 passed。
— qwen3.8-max via Qwen Code /review (v0.21.11)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): 377 passed — this review observed 494 passed.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):377 passed — this review observed 494 passed。
— qwen3.8-max via Qwen Code /review (v0.21.11)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "PR #9098 adds a tools.workflowsEnabled settings key and…": none — all checks I started completed within budget.; "PR #9098 adds a tools.workflowsEnabled settings key and…": none — all checks in my dimension completed within budget..
Test Plan (not a blocker): 377 passed — this review observed 19472, 494 passed.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"PR #9098 adds a tools.workflowsEnabled settings key and…":none — all checks I started completed within budget.;"PR #9098 adds a tools.workflowsEnabled settings key and…":none — all checks in my dimension completed within budget.。
Test Plan(非阻断):377 passed — this review observed 19472, 494 passed。
— qwen3.8-max via Qwen Code /review (v0.21.11)
已被后续 commit 取代,当前 head 需重新 review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "PR #9098 (QwenLM/qwen-code) adds a tools.workflowsEnabled…": could not execute the PR's unit tests in this worktree — npx vitest run src/config/config.test.ts -t "workflowsEnabled" from packages/cli fails during transfo…; "PR #9098 (QwenLM/qwen-code) adds a tools.workflowsEnabled…": did not execute npm run typecheck or the new test files in the worktree (review-only checkout; relied on CI + the fact that every type used is either pre-exis….
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"PR #9098 (QwenLM/qwen-code) adds a tools.workflowsEnabled…":could not execute the PR's unit tests in this worktree — npx vitest run src/config/config.test.ts -t "workflowsEnabled" from packages/cli fails during transfo…;"PR #9098 (QwenLM/qwen-code) adds a tools.workflowsEnabled…":did not execute npm run typecheck or the new test files in the worktree (review-only checkout; relied on CI + the fact that every type used is either pre-exis…。
— qwen3.8-max via Qwen Code /review (v0.21.11)
已被后续 commit ade8371 取代,当前 head 需重新 review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "PR #9098 (QwenLM/qwen-code) adds a tools.workflowsEnabled…": none — all checks I started were completed within budget.; "PR #9098 (QwenLM/qwen-code) adds a tools.workflowsEnabled…": none — all planned checks completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget..
Test Plan (not a blocker): 377 passed — this review observed 494 passed.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"PR #9098 (QwenLM/qwen-code) adds a tools.workflowsEnabled…":none — all checks I started were completed within budget.;"PR #9098 (QwenLM/qwen-code) adds a tools.workflowsEnabled…":none — all planned checks completed within budget.;"You are review agent reverse-audit — Reverse audit agent…":none — all checks above completed within budget.。
Test Plan(非阻断):377 passed — this review observed 494 passed。
— qwen3.8-max via Qwen Code /review (v0.21.11)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "You are review agent reverse-audit — Reverse audit agent…": direct source read of packages/cli/src/serve/fast-path-settings.ts — its coverage of the new keys was inferred from the environment.ts:33-38 comment and fa….
Test Plan (not a blocker): 555 passed — this review observed 19478, 494 passed.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"You are review agent reverse-audit — Reverse audit agent…":direct source read of packages/cli/src/serve/fast-path-settings.ts — its coverage of the new keys was inferred from the environment.ts:33-38 comment and fa…。
Test Plan(非阻断):555 passed — this review observed 19478, 494 passed。
— qwen3.8-max via Qwen Code /review (v0.21.11)
|
@qwen-code /review |
|
Qwen Code review request accepted. Review is queued in workflow run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R4-2 showInDialog + Workspace-scope save trap — already reported (comment 3779912243)
- R4-3 hand-maintained workspace-restricted list in three parallel places — already reported (comment 3779912255)
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — stopped before round 5 by the review time budget.
Test Plan (not a blocker): 555 passed — this review observed 19478, 494 passed.
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:反向审计——评审时间预算不足,未能开始第 5 轮。
Test Plan(非阻断):555 passed — this review observed 19478, 494 passed。
— qwen3.8-max via Qwen Code /review (v0.21.13)
R4-3: the restricted set was hand-maintained in three parallel places — a per-key warning block, the condition in `stripWorkspaceRestrictedSettings`, and that function's destructure. Adding one restricted setting needed three synchronized edits, and either omission is silent: forgetting the warning discards a workspace value with no diagnostic, forgetting the strip honors a value the warning says is ignored. `WORKSPACE_RESTRICTED_SETTINGS` is now the single source, and the warning loop and the strip both derive from it. It lives in `settingsUtils.ts` rather than `settings.ts` because `settings.ts` already value-imports that module — defining it there and importing it back would close a runtime import cycle. R4-2: `tools.workflowsEnabled` is the first setting that is both `showInDialog: true` and stripped from Workspace scope, so the dialog offered a toggle that silently never took effect — it renders from the raw scope file, so it kept showing the value it wrote while the feature stayed at its merged value, leaving a dead entry in the repo's .qwen/settings.json. `getDialogSettingKeys` gained `excludeWorkspaceRestricted`, which the dialog passes when the selected scope is Workspace. The scope comparison stays in the component so settingsUtils keeps its type-only dependency on settings.ts. Unlike `showInDialog: false` (what the two pre-existing restricted settings use), the setting stays visible and editable under the scopes that honor it. Verified: settings 169/169, settingsUtils 85/85, BuiltinCommandLoader 13/13; packages/cli typecheck clean. Mutation-checked both ways — forcing the filter off fails 1 test, dropping a key from the list fails 3. SettingsDialog.test.tsx's 23 failures are pre-existing and environmental: identical counts on upstream/main and on this branch before the change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): 555 passed — this review observed 19482, 494 passed.
Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:
packages/cli/src/config/shared-env-keys.ts:44 — [probe] no membership pin or .env-file boundary test for the two new PROJECT_ENV_HARDCODED_EXCLUSIONS entries (D8-1)packages/cli/src/ui/components/SettingsDialog.tsx:226 — [probe] the dialog's workspace-scope filter has no test; both named mutants ship green (D8-2)packages/cli/src/ui/components/SettingsDialog.tsx:226 — [probe] cross-scope pending overlay can flush a queued restricted-key change into the workspace file via applyRestart (D8-3)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):555 passed — this review observed 19482, 494 passed。
收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
R8-1. The workspace restriction stopped at the TUI dialog. `stripWorkspace RestrictedSettings` drops these keys before every merge, so a workspace-scope write through the settings API persists a committable dead entry into the repo's `.qwen/settings.json` and answers 200 + `requiresRestart: true` while the feature never turns on — GET then reports `workspace: true` beside `effective: false`, and the warnings channel carries only `corrupted`, so the client never learns the write was inert. Exactly the trap the SettingsDialog comment in this same PR says it eliminates, one layer over. `tools.workflowsEnabled` is the first workspace-restricted key with `showInDialog: true`, which is what puts it in `getDialogSettingKeys()` and therefore in `getAllowedKeys()` — the two pre-existing restricted keys are `showInDialog: false` and never reached the API. Both POST handlers now call one shared `rejectWorkspaceRestrictedWrite`, answering 400 `workspace_restricted_setting`. One helper rather than two copies, for the reason the previous commit collapsed the warning/strip pair. User scope is untouched — that scope honors the key, and a guard that reached it would kill this PR's whole enablement path. Verified: workspace-settings 22/22, settings 169/169, settingsUtils 85/85. Mutation-checked three ways — dropping either call site fails a test, and widening the guard past workspace scope fails the user-scope test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@qwen-code /review |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 4 scenario(s). — Qwen Code · serve A/B |
|
Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with |
|
Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round:
packages/cli/src/ui/components/SettingsDialog.tsx:227 — [probe] the dialog's workspace-scope filter for the restricted key has no test; the inversion mutation ships green (D9-1)packages/cli/src/config/shared-env-keys.ts:44 — [probe] the env exclusion also blocks a user-scope settings.json env opt-in; docs don't qualify the source (D9-2)packages/cli/src/config/settingsSchema.ts:2717 — [review] the new key's description omits the workspace-scope caveat both sibling restricted settings carry (D9-3)packages/cli/src/ui/components/SettingsDialog.tsx:227 — [probe] deferred restart-save flush can write the restricted key into the workspace file and discards the user-scope reset (D9-4)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
|
@qwen-code /review Requesting a fresh round so the verdict reflects the current head. The standing |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 1a": executing the new unit tests (settings.test.ts, config.test.ts, settingsUtils.test.ts, workspace-settings.test.ts, settingsSchema.test.ts) — the worktree has no….
Test Plan (not a blocker): 555 passed — this review observed 494 passed.
Deferred under the convergence posture (round 10, not a blocker) — recorded, not requested in this round:
packages/cli/src/utils/settingsUtils.ts:381 — [review] restricted-settings list never type-checks its keys against the schemapackages/cli/src/ui/components/SettingsDialog.tsx:226 — [review] applyRestart save path lacks the workspace-restricted key filterpackages/cli/src/ui/components/SettingsDialog.tsx:227 — [review] dialog workspace-scope filter condition has no test (inversion mutant ships green)packages/cli/src/config/shared-env-keys.ts:44 — [review] env exclusion also blocks a user-scope settings.json env opt-in, silentlypackages/cli/src/serve/routes/workspace-settings.ts:123 — [review] served GET reports an inert workspace value with no warning; served clients show the feature as enabledpackages/cli/src/config/settingsSchema.ts:2717 — [review] schema descriptions omit the workspace-scope caveat both sibling restricted keys carry
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 1a":executing the new unit tests (settings.test.ts, config.test.ts, settingsUtils.test.ts, workspace-settings.test.ts, settingsSchema.test.ts) — the worktree has no…。
Test Plan(非阻断):555 passed — this review observed 494 passed。
收敛姿态下延后(第 10 轮,非阻断)——已记录,本轮不要求修改:共 6 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.14)
Critical findings were fixed on the current head and their review threads are resolved; the current-head bot review reports no blocking findings. Dismissing this stale review before a fresh triage.
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 867 passed · 2 failed · 869 total Flakiness gate: ✅ 6 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:867 通过 · 2 失败 · 869 总计 抖动门:✅ 6 changed test file(s) x 5 identical rounds, no divergence Verification reportPR 9098 Deep Verification —
|
| cell | scenario | base (HEAD^1) |
head (PR) |
|---|---|---|---|
| C1 | no settings, no env | off | off |
| C2 | user settings.json: workflowsEnabled: true |
off (value merged, field dead) | on, /workflows present |
| C3 | user true + DISABLE_WORKFLOWS=1 |
off | off (kill switch wins) |
| C4 | user false + ENABLE_WORKFLOWS=1 |
on | on |
| C5 | workspace true (trusted) | off, merged=true, no warning | off, merged value stripped, warning names the file |
| C6 | user false + workspace true | off, merged=true | off, merged=false (strip beats workspace override), warning |
| C7 | System scope true | off (dead) | on (operator force-on works) |
| C8 | SystemDefaults true | off (dead) | on |
| C9 | both env vars set | off | off (disable wins) |
| C10 | launch env ENABLE=1 only |
on | on (user env path untouched) |
| E1 | project .env sets ENABLE=1 |
on — a repo could enable it | off — excluded |
| E2 | workspace settings.json env: block |
on | off — excluded |
| E3 | user ~/.env sets ENABLE=1 |
on | on (user-owned path kept) |
The C2 row is the load-bearing flip: on base the setting value demonstrably reaches the merged settings (merged=true) yet isWorkflowsEnabled() is false and /workflows is absent from the real command loader; on head the same file enables the feature. E1/E2 show the env-exclusion claim is not decorative — on base, an untrusted repository's .env or settings.env genuinely turned the feature on.
The startup warning observed on head (C5/C6), verbatim:
Warning: tools.workflowsEnabled in workspace settings (<path>/.qwen/settings.json) is ignored. This setting is only honored from User, System, or SystemDefaults scope settings.
Daemon settings API (wire oracle)
Harness 2 (harness/h2-run.mjs → h2-child.mjs) builds a real express app with the real registerWorkspaceSettingsRoutes from each dist, wired to the real persistence chain (loadSettings(...).setValue, mirroring persistSettingFn in run-qwen-serve.ts), on a loopback port; 9 cells × 3 arms, 42/42 assertions pass — witness 02-daemon-api-wire-ab.png.
| cell | base | head | head, guard mutated off |
|---|---|---|---|
POST workspace tools.workflowsEnabled=true |
400 disallowed_key (key unknown to base schema) |
400 workspace_restricted_setting + "set it at user scope instead" |
200 requiresRestart:true |
| workspace settings file after | no entry | no entry | dead entry persisted |
GET /workspace/settings |
key not listed | descriptor present, workspace: —, effective: false (default) | workspace: true beside effective: false + broadcast |
| POST user scope same key | 400 disallowed_key |
200 + persisted to user file (accept end intact) | 200 + persisted |
POST workspace ui.hideTips (control) |
200 persisted | 200 persisted | 200 persisted (guard doesn't over-block) |
POST workspace security.allowPrivateNetworkHooks (sibling) |
400 disallowed_key |
400 disallowed_key |
400 disallowed_key |
Two attribution notes. (1) The R8-1 "200 + inert write" trap is not reproducible against the merge base — base rejects the key wholesale because its schema lacks the entry; the trap exists in the PR's intermediate state (schema added before the guard). The guard-mutant arm (single-point mutation inside rejectWorkspaceRestrictedWrite) reproduces it exactly, including the workspace: true / effective: false GET shape and the success broadcast, which is what proves the guard load-bearing. (2) The guard sits after the allowedKeys check; the two pre-existing restricted keys are showInDialog: false and never reach it — they are rejected by the older disallowed_key path on both arms, unchanged.
Mutation matrix (vacuity + pinning)
All on HEAD source; positive controls are the four killed mutants plus green unmutated baselines. Witness 03-mutation-matrix.png, logs in logs/m*.log.
| mutation | suite | result |
|---|---|---|
M1 revert the one-line config.ts fix |
config.test.ts -t "loadCliConfig workflowsEnabled" |
killed — exactly 1 red: should be enabled when workflowsEnabled is set to true in settings, expected false to be true (behavioral, not import/compile). Matches the author's claim precisely. |
M2 drop the key from WORKSPACE_RESTRICTED_SETTINGS |
settings + settingsUtils + workspace-settings suites | killed — 6 new reds: strip ×2, warning ×1, single-source dotted keys ×1, both route guards ×2 |
M3 SettingsDialog wiring → excludeWorkspaceRestricted: false |
SettingsDialog.test.tsx |
survived 61/61 → Finding F2 |
M3b filter implementation in settingsUtils.getDialogSettingKeys neutered |
settingsUtils.test.ts |
killed — exactly 1 red (to not include 'tools.workflowsEnabled') — mechanism pinned, wiring not |
| M4 remove only the qualified-route guard call site | workspace-settings.test.ts |
killed — exactly 1 red on the qualified-route test, primary-route test stays green |
M5 remove both workflow env vars from PROJECT_ENV_HARDCODED_EXCLUSIONS |
shared-env-keys.test.ts + environment.test.ts |
survived 76/76 → Finding F1 |
Findings
F1 (Suggestion) — the two env exclusions are not pinned by any test
QWEN_CODE_ENABLE_WORKFLOWS / QWEN_CODE_DISABLE_WORKFLOWS were added to PROJECT_ENV_HARDCODED_EXCLUSIONS, but no test asserts their presence (M5 survives: removing both leaves shared-env-keys.test.ts + environment.test.ts at 76/76 green), even though every other exclusion tier in that file has explicit cases. The behavior is correct — harness cells E1/E2 prove head blocks project .env/settings.env while E3 keeps the user path — so this is a regression-latency gap, not a defect. A future edit dropping the two entries would ship silently and reopen the exact repository-enable path this PR closes.
Measured fix (applied in a scratch copy, then removed): add to packages/cli/src/config/shared-env-keys.test.ts, in the file's existing style,
it('excludes the dynamic-workflow feature flags', () => {
for (const key of [
'QWEN_CODE_ENABLE_WORKFLOWS',
'QWEN_CODE_DISABLE_WORKFLOWS',
]) {
expect(PROJECT_ENV_HARDCODED_EXCLUSIONS).toContain(key);
expect(isHardcodedProjectEnvExclusion(key)).toBe(true);
}
});Results: green on head (1/1); against the M5 mutant it fails expected [ 'QWEN_HOME', …(56) ] to include 'QWEN_CODE_ENABLE_WORKFLOWS' (logs/f1-proposed-pin-vs-m5.log). No production code changes needed.
F2 (Suggestion) — the dialog's Workspace-scope exclusion wiring is not pinned
SettingsDialog.tsx passes excludeWorkspaceRestricted: selectedScope === SettingScope.Workspace; M3 (forcing the option to false) leaves SettingsDialog.test.tsx at 61/61 — no rendered-output test asserts the key disappears under Workspace scope. The filter mechanism itself is pinned (M3b), so the exposure is limited to the one-line wiring regressing (e.g. a constant or an inverted comparison) without notice. Suggested fixture (shape only, not measured in this round): render the dialog, select the Workspace scope, assert tools.workflowsEnabled is absent from the rendered items while still present under User scope — the file's existing should render with different scope selected (Workspace) test is the natural home.
Not covered
- Per-commit attribution: the checkout is depth 2 — only the merge commit,
HEAD^1, andHEAD^2exist locally while the metadata lists 8 PR commits (git rev-list HEAD^1..HEAD^2returns 1 at the shallow boundary). The aggregateHEAD^1..HEADdiff is what was verified. - Trial merge into current main: the snapshot's
baseRefOid(fe6d2ac…) differs from the merge-ref base tip (3b3818d…) — the merge commit was built against3b3818d, so main moved past the PR's recorded base (a shallow clone cannot prove which is newer, nor reach amainref to re-run the suite against a fresher tip). The A/B uses the merge-ref base tip, the strongest base available here. - Full
qwen serveprocess boot: harness 2 drives the real route module with the real persistence chain but not the complete daemon (auth token, workspace registry, bridge). The guard sits before all of that, and M4 pins the second route's call site. - TUI-rendered settings dialog in a real terminal and the keyword-steering surface (needs live model calls); the command-loader surface is covered by real
BuiltinCommandLoaderruns in harness 1, and the schema'srequiresRestartrationale (three startup-built surfaces) is pinned bysettingsSchema.test.ts. - The author's "23 pre-existing SettingsDialog failures" did not reproduce here: the suite is 61/61 green in this environment on head (and the test file is unmodified by the PR). That is better than claimed, not a regression; the discrepancy is environmental.
- 12
settings.test.tsfailures in this sandbox are environmental, A/A-proven: the same 12 names fail on the base build (logs/gate-settings-base.log); they concern home-dir/.env resolution, and the PR-attributed delta is +8 tests, all passing (04-gates-and-attribution.png). packages/coreprecedence tests (config.workflows.test.ts) were not re-run — core is untouched by the diff and the precedence behavior is covered by harness cells C3/C4/C9/C10 against the realConfig.
Methodology
Environment: node:22-bookworm container (Node v22.23.2), CI-built head dist at the merge commit; base arm = git worktree add tmp/pr9098-base HEAD^1 with packages/cli rebuilt there (tsc --build after generating git-commit.ts), worktree removed after the A/B cells were captured. Base-tree node_modules were symlinked to the root install; readlink -f node_modules/@qwen-code/qwen-code-core resolves into the head tree — accepted as a clean control because the changed-path census (git diff HEAD^1..HEAD --name-only) confines the entire diff to docs/, packages/cli/src/, and packages/vscode-ide-companion/schemas/, so no internal dependency's code differs between the arms (package.json/package-lock.json untouched). Harnesses import compiled dist modules by absolute path and spawn fresh node processes with hermetic HOME/QWEN_HOME/QWEN_CODE_SYSTEM_SETTINGS_PATH fixtures; oracles are isWorkflowsEnabled(), merged-value inspection, getSettingsWarnings(), real command-loader output, HTTP status/body on loopback, settings-file contents on disk, and recorded broadcasts. Mutations were applied to source, run through the package's own vitest, and restored (git status clean after each). Raw per-arm logs live in logs/; harness scripts in harness/; the guard-mutant dist in mutant-dist/. Evidence captures were produced with scripts/verify-capture.mjs.
Flakiness gate log
rounds=5 files=6 skipped=0
file packages/cli/src/config/config.test.ts: (cd packages/cli) npx --no-install vitest run ./src/config/config.test.ts
file packages/cli/src/config/settings.test.ts: (cd packages/cli) npx --no-install vitest run ./src/config/settings.test.ts
file packages/cli/src/config/settingsSchema.test.ts: (cd packages/cli) npx --no-install vitest run ./src/config/settingsSchema.test.ts
file packages/cli/src/serve/routes/workspace-settings.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/routes/workspace-settings.test.ts
file packages/cli/src/services/BuiltinCommandLoader.test.ts: (cd packages/cli) npx --no-install vitest run ./src/services/BuiltinCommandLoader.test.ts
file packages/cli/src/utils/settingsUtils.test.ts: (cd packages/cli) npx --no-install vitest run ./src/utils/settingsUtils.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/config/config.test.ts: PPPPP
packages/cli/src/config/settings.test.ts: PPPPP
packages/cli/src/config/settingsSchema.test.ts: PPPPP
packages/cli/src/serve/routes/workspace-settings.test.ts: PPPPP
packages/cli/src/services/BuiltinCommandLoader.test.ts: PPPPP
packages/cli/src/utils/settingsUtils.test.ts: PPPPP
verdict: pass
summary: 6 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/config/config.test.ts: P (exit 0)
round 1 · packages/cli/src/config/settings.test.ts: P (exit 0)
round 1 · packages/cli/src/config/settingsSchema.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/routes/workspace-settings.test.ts: P (exit 0)
round 1 · packages/cli/src/services/BuiltinCommandLoader.test.ts: P (exit 0)
round 1 · packages/cli/src/utils/settingsUtils.test.ts: P (exit 0)
round 2 · packages/cli/src/config/config.test.ts: P (exit 0)
round 2 · packages/cli/src/config/settings.test.ts: P (exit 0)
round 2 · packages/cli/src/config/settingsSchema.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/routes/workspace-settings.test.ts: P (exit 0)
round 2 · packages/cli/src/services/BuiltinCommandLoader.test.ts: P (exit 0)
round 2 · packages/cli/src/utils/settingsUtils.test.ts: P (exit 0)
round 3 · packages/cli/src/config/config.test.ts: P (exit 0)
round 3 · packages/cli/src/config/settings.test.ts: P (exit 0)
round 3 · packages/cli/src/config/settingsSchema.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/routes/workspace-settings.test.ts: P (exit 0)
round 3 · packages/cli/src/services/BuiltinCommandLoader.test.ts: P (exit 0)
round 3 · packages/cli/src/utils/settingsUtils.test.ts: P (exit 0)
round 4 · packages/cli/src/config/config.test.ts: P (exit 0)
round 4 · packages/cli/src/config/settings.test.ts: P (exit 0)
round 4 · packages/cli/src/config/settingsSchema.test.ts: P (exit 0)
round 4 · packages/cli/src/serve/routes/workspace-settings.test.ts: P (exit 0)
round 4 · packages/cli/src/services/BuiltinCommandLoader.test.ts: P (exit 0)
round 4 · packages/cli/src/utils/settingsUtils.test.ts: P (exit 0)
round 5 · packages/cli/src/config/config.test.ts: P (exit 0)
round 5 · packages/cli/src/config/settings.test.ts: P (exit 0)
round 5 · packages/cli/src/config/settingsSchema.test.ts: P (exit 0)
round 5 · packages/cli/src/serve/routes/workspace-settings.test.ts: P (exit 0)
round 5 · packages/cli/src/services/BuiltinCommandLoader.test.ts: P (exit 0)
round 5 · packages/cli/src/utils/settingsUtils.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Local real-environment verification — head
|
| # | Scenario | base 7141976 |
head 0e01e7a |
|---|---|---|---|
| 1 | user settings.json tools.workflowsEnabled: true |
/workflows absent — value accepted but dead |
/workflows in typeahead |
| 2 | /settings dialog, search "workflow" (User scope) |
no Dynamic Workflows row |
row present, true*, beside the two unrelated workflow settings |
| 3 | dialog switched to Workspace scope | — | exactly that row disappears; the two unrelated ones stay |
| 4 | workspace .qwen/settings.json true |
silently ignored, no warning | startup warning names the file, feature stays off |
| 5 | project .env QWEN_CODE_ENABLE_WORKFLOWS=1 |
/workflows appears — a repo could self-grant the feature |
excluded, stays off |
| 6 | /tools with user-scope true |
no Workflow tool | Workflow tool registered |
| 7 | user true + QWEN_CODE_DISABLE_WORKFLOWS=1 |
— | off (kill switch wins) |
| 8 | user-owned paths on head: ~/.env ENABLE=1; launch-env ENABLE=1 |
— | both still enable (no regression on the kept paths) |
Row 1 is the dead switch coming alive; row 5 is the real security hole this PR closes — on base an untrusted repository's .env turns the feature on, on head it cannot.
Row 1 — same user settings file, head vs base:
Rows 2–3 — dialog, User scope then Workspace scope (head). The three same-word settings the description disambiguates are visible side by side; under Workspace scope only Dynamic Workflows is filtered out:
Row 4 — workspace-scope value warned about and ignored (head):
Row 5 — project .env, base vs head (the closed hole):
Row 6 — Workflow tool actually in the registry (head, /tools):
Real qwen serve daemon
Started each arm's bundled CLI as a real daemon (serve --token …) and hit the settings API:
- head: workspace-scope
POST /workspace/settings→ 400workspace_restricted_settingwith the actionable "set it at user scope instead" message, and no dead entry is written into the repo's.qwen/settings.json. User-scope write → 200requiresRestart: trueand the value genuinely lands in~/.qwen/settings.jsonon disk.GETthen reportsvalues: { effective: true, user: true }. - base: the key is invisible to the API — both scopes answer 400
disallowed_keyand the key is absent fromGET. So the new 400 is purely additive; no previously-working client call changes shape. - The second call site (workspace-qualified route) needs an active multi-workspace runtime to drive over HTTP; it is pinned by this PR's route tests (
workspace-settings.test.ts, R8-1 second-call-site case), which pass here.
Generated schema check
Re-running npx tsx scripts/generate-settings-schema.ts in the head worktree reproduces packages/vscode-ide-companion/schemas/settings.schema.json byte-for-byte — the vscode hunk is generated output, as the PR states.
Not covered / notes
- Keyword steering wasn't driven (needs a real model turn); it reads the same startup-built
Configas the two surfaces verified above. - On the earlier sandbox findings: the settings.env-side exclusion of the two env keys is unit-pinned (
settings.test.tsasserts both stay undefined); the project-.envpath is covered live by row 5 here. The dialog's workspace-scope filter still has no render test, but rows 2–3 exercise the real wiring end to end. Both remain reasonable unit-coverage follow-ups, not blockers. - Linux only (matches the green ubuntu CI lane; macOS/Windows lanes were routed off for this PR).
中文说明
本地真实环境验证 — head 0e01e7a
结论:该 PR 的所有用户可见主张在真实构建中全部成立,无阻塞性发现。 本地构建了两个完整臂 —— base = merge-base 7141976,head = 0e01e7a,各自编译全部 workspace 兄弟包并产出 esbuild bundle —— 分别驱动真实 TUI(node-pty + xterm.js 渲染,截图见上)与真实 qwen serve 守护进程。这正好补上 CI 沙箱验证明确列为未覆盖的两块:TUI 实机渲染与 serve 进程级 E2E。
方法护栏:head bundle 含 PR 独有字符串 workspace_restricted_setting / Dynamic Workflows,base bundle 两者皆无(证明 A/B 真的换了代码);所有 TUI 场景连着一个记录请求的 fake OpenAI 端点,全程 0 次模型请求(纯按键路径);每个场景使用全新隔离的 $HOME 与工作区。
单元测试 + 回归护栏
- 六个被改测试文件 666/666 通过(在
packages/cli下用 vitest 实跑)。 - RED/GREEN:仅把
packages/cli/src/config/config.ts的一行接线回退到 base,恰好should be enabled when workflowsEnabled is set to true in settings一条变红、同块其余三条保持绿 —— 与 PR 测试计划所述完全一致;恢复后全绿。
真实 TUI 矩阵(对应上文截图)
- 用户
settings.json置true:base 无/workflows(值被合并但字段死);head typeahead 出现/workflows。
2–3./settings对话框搜 "workflow":head 在 User scope 显示Dynamic Workflows true*,与两个同名不相干设置同屏(正是描述里担心的歧义,肉眼可辨);切到 Workspace scope 后仅该行消失。base 无此行。 - 工作区
.qwen/settings.json置true:head 启动警告点名文件、功能保持关;base 静默吞掉、无警告。 - 项目
.env写QWEN_CODE_ENABLE_WORKFLOWS=1:base 上/workflows直接出现 —— 仓库可自授能力;head 上被排除,保持关。 这就是本 PR 堵上的真实口子。 /tools:head 注册了 Workflow 工具,base 没有。- 用户
true+QWEN_CODE_DISABLE_WORKFLOWS=1:head 保持关(kill switch 优先)。 - 保留路径无回归(head):用户自己的
~/.env与启动环境变量置ENABLE=1均仍能开启。
真实 serve 守护进程
- head:workspace scope 写入 → 400
workspace_restricted_setting,报错信息给出"改用 user scope"指引,且不会往仓库.qwen/settings.json写死条目;user scope 写入 → 200requiresRestart: true,值真实落盘到~/.qwen/settings.json;GET报values: { effective: true, user: true }。 - base:该 key 对 API 完全不可见 —— 两个 scope 都是 400
disallowed_key,GET里没有该条目。因此新增的 400 纯属增量,不改变任何既有客户端调用的行为。 - 第二个调用点(workspace-qualified 路由)需要激活的多 workspace runtime 才能走 HTTP 驱动;由本 PR 的路由测试(R8-1 第二调用点用例)钉住,本地实跑通过。
生成物校验
在 head worktree 重跑 npx tsx scripts/generate-settings-schema.ts,packages/vscode-ide-companion/schemas/settings.schema.json 逐字节一致 —— vscode 那块 hunk 确为生成产物。
未覆盖 / 备注
- 关键词引导路径未驱动(需要真实模型回合);它读取的是与上面两个已验证面相同的启动期
Config。 - 关于此前沙箱报告的两条建议:两个环境变量在 settings.env 层的排除已有单测钉住(
settings.test.ts断言两者保持 undefined);项目.env路径由上面第 5 行实测覆盖。对话框 Workspace scope 过滤仍无渲染测试,但第 2–3 行已端到端驱动了真实接线。两条仍是合理的补测建议,不构成阻塞。 - 仅 Linux(与本 PR 绿灯的 ubuntu CI 道一致;macOS/Windows 道被路由跳过)。
🤖 Generated with Claude Code — Claude Fable 5
One conflict, in the workspace-scope strip this PR had added to. `main` (QwenLM#9098, QwenLM#9737) generalised that strip into a single data list: WORKSPACE_RESTRICTED_SETTINGS in settingsUtils.ts now drives the strip, the "your workspace value was ignored" warning, and the settings dialog's scope filter, so the three surfaces cannot drift apart. This PR had hand-rolled the same thing for permissions.planMode — a second branch inside stripWorkspaceSecurityBypasses and a third copy of the warning text. Both are deleted in favour of main's version, and the setting is registered as one entry in the list instead. The behaviour is unchanged and the four scope tests added here still pass against the generic mechanism; the dialog filter comes along for free, though planMode sets showInDialog: false.
|
Released in v0.22.2. |












What this PR does
Adds a
tools.workflowsEnabledsetting that actually turns dynamic workflows on, wires it into the config field that has been waiting for it, and scopes the opt-in so that only the user — never a repository — can grant it. Until now the only way to enable the feature was an undocumented environment variable.Why it's needed
ConfigParameters.workflowsEnabledis declared, defaulted in theConfigconstructor, and read byConfig.isWorkflowsEnabled()— but nothing inpackages/cliever writes it.loadCliConfigpopulates dozens of neighbouring fields from settings and simply skips this one, soparams.workflowsEnabledis alwaysundefinedand the field always falls back tofalse. AGENTS.md names this exact shape in its review guidance: an optional field that is declared and read but never set by any caller is a dead switch.The practical effect is that dynamic workflows can only be reached by exporting
QWEN_CODE_ENABLE_WORKFLOWS=1, which is undocumented, has to be repeated in every shell that launches qwen, and cannot be discovered from the settings dialog. A user reading the settings surface has no way to learn the feature exists.This adds the setting and populates the field from it. Precedence is unchanged and still resolved entirely in core:
QWEN_CODE_DISABLE_WORKFLOWSbeats everything, thenQWEN_CODE_ENABLE_WORKFLOWS, then the setting.Who may grant the opt-in
A workflow run can dispatch many subagents against the user's token budget, so enabling it is a capability grant, not a preference. It must come from the user or their operator, never from whatever repository happens to be open. Three changes enforce that:
tools.workflowsEnabledis stripped from Workspace scope during the merge, joiningsecurity.allowPrivateNetworkHooksandsecurity.allowedInsecureVoiceBaseUrlsin the existingstripWorkspaceRestrictedSettingsmechanism (renamed fromstripWorkspaceSecurityBypasses, since the concern is no longer only network security). User, System and SystemDefaults scopes are unaffected, so an operator can still force the feature on or off fleet-wide.QWEN_CODE_ENABLE_WORKFLOWSandQWEN_CODE_DISABLE_WORKFLOWSjoinPROJECT_ENV_HARDCODED_EXCLUSIONS, so a project.envorsettings.envcannot enable the feature or override a user's opt-out through the environment either. Closing the settings path while leaving the env path open would not have been worth much.Why it requires a restart
requiresRestart: trueis load-bearing rather than decorative. The Workflow tool is registered once while the tool registry is built,/workflowsis gated when the builtin commands load, and keyword steering reads the same startup-builtConfigon each submission. A live toggle would leave the settings dialog reporting the feature as on while the tool is absent from the registry and the command is missing from typeahead — worse than requiring a restart.The description also disambiguates this key from the unrelated
experimental.sessionWorkflow("Session Workflow Plan & Review") and points atui.disableWorkflowKeywordTrigger. Three settings now share the word "workflow" and they control entirely different things, so the dialog needs to say which is which.Reviewer Test Plan
How to verify
cd packages/cli && npx vitest run src/config/config.test.ts src/config/settingsSchema.test.ts src/config/settings.test.ts src/services/BuiltinCommandLoader.test.ts— 555 passed.Coverage added:
settingsSchema.test.tspins the key's type,falsedefault,requiresRestart,showInDialogand category.config.test.tsadds aloadCliConfig workflowsEnabledblock: default-off, the setting turning it on, theQWEN_CODE_DISABLE_WORKFLOWSkill switch overriding atruesetting, andQWEN_CODE_ENABLE_WORKFLOWSoverriding afalseone.settings.test.tscovers the scope rules: a workspace-scope value is stripped and warned about, while User/System/SystemDefaults values survive the merge.BuiltinCommandLoader.test.tscovers/workflowsappearing only when the feature resolves on.The second
config.test.tscase is the regression guard for the bug being fixed: reverting only the one-line change inpackages/cli/src/config/config.tsand re-running that block fails exactlyshould be enabled when workflowsEnabled is set to true in settings, and the other three still pass — which is precisely why the dead switch went unnoticed. The core-side precedence tests inconfig.workflows.test.tsalready passed against the dead field, since they injectworkflowsEnabledthroughConfigParametersdirectly.End to end: add
{ "tools": { "workflowsEnabled": true } }to your usersettings.json, restart, and confirm/workflowsis available and the Workflow tool is registered. Put the same key in a workspace.qwen/settings.jsoninstead and confirm it is ignored and warned about. WithQWEN_CODE_DISABLE_WORKFLOWS=1set, neither enables it.packages/vscode-ide-companion/schemas/settings.schema.jsonis regenerated by the build from the schema definition; that hunk is generated output, not a hand edit.Evidence (Before & After)
Before: no workflow entry exists in the settings dialog, and
tools.workflowsEnabledinsettings.jsonhas no effect at any scope — the only way in isQWEN_CODE_ENABLE_WORKFLOWS=1.After: "Dynamic Workflows" appears under Tools in the settings dialog, defaulting to off, and setting it from user scope enables the feature on the next launch. From workspace scope it is ignored, with a warning naming the file.
Tested on
Environment (optional)
Unit tests, Node 22.23.0 on Linux.
Risk & Scope
truesetting. All four combinations are pinned by test. The default staysfalse, so no existing installation changes behaviour.TipContextcarries noConfig, so a tip could not tell whether the user had already enabled the feature and would keep advising them to.stripWorkspaceSecurityBypassestostripWorkspaceRestrictedSettingsis internal tosettings.ts.Linked Issues
None.
中文说明
这个 PR 做了什么
新增一个真正能打开 dynamic workflows 的
tools.workflowsEnabled设置项,把它接到那个一直在等待它的 config 字段上,并且限定这项开关只能由用户授予、而不能由仓库授予。在此之前,启用该功能的唯一途径是一个没有文档的环境变量。为什么需要
ConfigParameters.workflowsEnabled已经声明、在Config构造函数里有默认值、并且被Config.isWorkflowsEnabled()读取——但packages/cli里没有任何地方写入过它。loadCliConfig从 settings 填充了几十个相邻字段,唯独跳过了这一个,所以params.workflowsEnabled永远是undefined,该字段永远回落到false。AGENTS.md 的评审指引里恰好点名了这种形态:一个被声明、被读取、却从未被任何调用方设置的可选字段,就是一个死开关。实际后果是,dynamic workflows 只能通过
export QWEN_CODE_ENABLE_WORKFLOWS=1触达——这个变量没有文档,需要在每个启动 qwen 的 shell 里重复设置,而且无法从设置对话框里发现。一个只看设置界面的用户根本无从得知这个功能的存在。本 PR 新增该设置项并用它填充字段。优先级保持不变,且仍然完全在 core 内解析:
QWEN_CODE_DISABLE_WORKFLOWS优先于一切,其次是QWEN_CODE_ENABLE_WORKFLOWS,最后才是设置项。谁有权授予这项开关
一次 workflow 运行可以派发大量 subagent、消耗用户的 token 预算,因此"启用"是一次能力授予,而不是一项偏好设置。它必须来自用户或其运维方,绝不能来自恰好被打开的那个仓库。三处改动落实了这一点:
tools.workflowsEnabled在合并阶段会被从 Workspace 作用域剥离,与security.allowPrivateNetworkHooks、security.allowedInsecureVoiceBaseUrls一起纳入既有的stripWorkspaceRestrictedSettings机制(由stripWorkspaceSecurityBypasses更名而来,因为关注点已不只是网络安全)。User、System、SystemDefaults 作用域不受影响,运维方仍可全机群强制开启或关闭。QWEN_CODE_ENABLE_WORKFLOWS与QWEN_CODE_DISABLE_WORKFLOWS加入PROJECT_ENV_HARDCODED_EXCLUSIONS,因此项目的.env或settings.env也无法通过环境变量启用该功能或覆盖用户的关闭决定。只堵住 settings 路径而放着 env 路径不管,意义有限。为什么需要重启
requiresRestart: true是有实际作用的,而非装饰。Workflow 工具在构建工具注册表时只注册一次,/workflows在内置命令加载时就已确定是否可用,关键词引导每次提交都读取同一个启动时构建的Config。一个可即时生效的开关会导致设置对话框显示功能已开启,而工具其实并不在注册表里、命令也不在候选补全中——这比要求重启更糟。描述文案还把这个键与不相关的
experimental.sessionWorkflow("Session Workflow Plan & Review")区分开,并指向ui.disableWorkflowKeywordTrigger。现在有三个设置项都带 "workflow" 这个词,控制的却是完全不同的东西,所以对话框里必须说清楚谁是谁。审阅者验证方案
如何验证
cd packages/cli && npx vitest run src/config/config.test.ts src/config/settingsSchema.test.ts src/config/settings.test.ts src/services/BuiltinCommandLoader.test.ts——555 项通过。新增覆盖:
settingsSchema.test.ts固定该键的类型、false默认值、requiresRestart、showInDialog与分类。config.test.ts新增loadCliConfig workflowsEnabled块:默认关闭、设置项开启、QWEN_CODE_DISABLE_WORKFLOWS覆盖true设置、QWEN_CODE_ENABLE_WORKFLOWS覆盖false设置。settings.test.ts覆盖作用域规则:workspace 作用域的值被剥离并给出警告,而 User/System/SystemDefaults 的值能通过合并保留。BuiltinCommandLoader.test.ts覆盖/workflows仅在功能解析为开启时出现。config.test.ts中的第二个用例是本次修复的回归护栏:只回退packages/cli/src/config/config.ts里那一行改动并重跑该测试块,恰好只有should be enabled when workflowsEnabled is set to true in settings失败,另外三个仍然通过——这也正是这个死开关一直没被发现的原因。config.workflows.test.ts里 core 侧的优先级测试在字段还是死的时候就已经全部通过,因为它们是直接通过ConfigParameters注入workflowsEnabled的。端到端验证:在用户级
settings.json中加入{ "tools": { "workflowsEnabled": true } },重启,确认/workflows可用且 Workflow 工具已注册。改为放进 workspace 的.qwen/settings.json,确认它被忽略并给出警告。设置QWEN_CODE_DISABLE_WORKFLOWS=1后,两者都无法启用。packages/vscode-ide-companion/schemas/settings.schema.json是构建过程根据 schema 定义重新生成的,那一段属于生成产物,不是手工编辑。证据(前后对比)
修复前:设置对话框中不存在任何 workflow 条目,
settings.json里的tools.workflowsEnabled在任何作用域下都不起作用——唯一入口是QWEN_CODE_ENABLE_WORKFLOWS=1。修复后:"Dynamic Workflows" 出现在设置对话框的 Tools 分类下,默认关闭;从用户作用域设置后在下次启动生效。从 workspace 作用域设置则被忽略,并给出指明文件的警告。
测试环境
环境(可选)
单元测试,Linux 上的 Node 22.23.0。
风险与范围
true的设置。四种组合均已被测试固定。默认值仍为false,因此任何现有安装的行为都不会改变。TipContext里没有Config,提示无法判断用户是否已经开启该功能,只会不停地劝已经开启的人去开启。stripWorkspaceSecurityBypasses更名为stripWorkspaceRestrictedSettings属于settings.ts内部改动。关联 Issue
无。