refactor(core): Classify shell safety as read-only, write, or unknown - #7053
Conversation
E2E test report (partial)This PR changes shell safety facts without changing the follow-up one-off approval routing.
|
doudouOUC
left a comment
There was a problem hiding this comment.
Review — tri-state shell safety classifier
Reviewed end-to-end: built and ran all changed suites (494 AST / 202 sync / lazy runtime / scheduler / permission), all green. Probed ~90 adversarial mutating commands — zero write→read-only bypasses, and the PR #4386 substitution guard is fully preserved after the guard/comment refactor. Solid, fail-closed design; passing the original command to the sync checker so wrappers stay unknown, and the env-prefix hardening (FOO=bar ls → not read-only), are both good calls.
Two items introduced by the new sed/awk regexes are worth a look before merge (left inline). Minor notes:
- shellAstParser.ts:777 —
git -C <dir> …/git -c k=v …→unknown(any leading global option fails closed). Intended per the design doc, but-C/-care common enough to consider allowing explicitly. - shellAstParser.ts:583 —
let parserClass: typeof Parser;is typed non-null but is uninitialized and not reset in_resetParser/_setParserFailedForTesting; consider| undefinedfor symmetry. - shellAstParser.ts:641 — on a parse throw,
parserInstanceis reassigned beforeparser.delete(); ifnew/setLanguagethrows, the old parser leaks and the new error masks the original. - shellAstParser.ts:599 —
if (parserInstance) return;is effectively dead:initPromiseisn't cleared on success, so the earlierif (initPromise) return initPromise;always short-circuits first. - docs/design/shell-safety-classification.md — references
.qwen/e2e-tests/shell-safety-classification.md, which isn't in the repo/PR. - shellReadOnlyChecker.ts:373 — the env-assignment guard also matches awk/sed script bodies like
awk '{x=1}'(the{x=), forcing them sequential. Safe/conservative; noting only.
Introduce an internal tri-state shell safety fact layer while preserving the legacy boolean fallback and keeping Plan routing unchanged. Refs QwenLM#6949 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
035c52c to
d7bf2e3
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
|
Follow-up on the remaining notes in the review: Not taking:
Already addressed in
The force-push reminder is acknowledged; future updates to this active PR will avoid rebasing or force-pushing. Both actionable inline review threads have been replied to and resolved (2/2). |
doudouOUC
left a comment
There was a problem hiding this comment.
Re-review of d7bf2e3
Both earlier important items are verified fixed (see the two thread replies): the sed/awk ReDoS is gone (linear to 10k-char inputs) and common read-only substitutions classify correctly. Re-probed ~70 adversarial write forms — escaped/alternate delimiters, multi-command scripts, -e 'w file' writes, awk print > "f" / printf >> "f" / system() / getline — with zero write→read-only and zero write→concurrent-safe. All shell-safety suites plus the shell / monitor / speculation / scheduler consumer suites pass. The design-doc verification section no longer dangles.
Two non-blocking over-conservatism notes on the new shared scanner are left inline (both safe — never classify a write as read-only). Minor points:
- shell-safety-rules.ts (336 lines, several exported functions) has no dedicated unit test file — it's exercised only indirectly through the two consumer suites. A direct
shell-safety-rules.test.tstargetingclassifySedScriptSafety/classifyAwkScriptSafety/classifySedCommandSafetywould be worth adding. sed 'y/abc/xyz/'(transliterate — read-only) →unknown, sinceyisn't inSAFE_SED_COMMAND(shell-safety-rules.ts:14). Trivial to add.- The top-level "E2E test report" comment still references
.qwen/e2e-tests/shell-safety-classification.md, which isn't in the PR (the design doc reference was already removed — thanks).
|
Follow-up review triage (no code changes):
Both new inline notes are therefore recorded as intentional conservative behavior and resolved. |
|
Thanks for the PR! Template looks good ✓ — all required sections present with bilingual translation. Problem: real and well-documented. Issue #6949 needs Plan mode to distinguish proven-safe commands from commands whose behavior cannot be established statically. The current boolean API ( Direction: aligned with the project's shell-safety posture. The three-state model ( Size (corrected from prior triage): 1,285 production logic lines (931+ / 354−) across 7 core files (
Approach: the code itself is well-structured. The 中文说明感谢贡献! 模板完整 ✓ — 所有必需部分均存在,包含双语翻译。 问题:真实且有据可查。#6949 需要 Plan 模式区分"已证明安全"的命令和"无法静态确认行为"的命令。当前布尔 API 无法表达这种不确定性——三态事实层是路由变更落地前的正确基础。 方向:与项目的 Shell 安全姿态一致。三态模型配合 规模(纠正前次 triage 的错误):7 个核心文件共 1,285 行生产逻辑代码(931+ / 354−),加上 896 行测试和 47 行设计文档。作者拥有
方案:代码本身结构良好。 — Qwen Code · qwen3.7-max Reviewed at |
Code ReviewReviewed the full diff at Architecture: Resource management: parse trees are released in Shared rules ( Sync checker hardening ( Scheduler fix ( Compatibility: the boolean API correctly maps to Non-blocking observations:
TestingAll affected test suites pass locally at This is a non-UI refactor (shell safety classifier) — no user-visible TUI changes to capture via tmux. The meaningful verification is unit test coverage of the three-state contract: table-driven matrices for all three states, compound precedence, substitutions, parser failures, adversarial inputs, and scheduler partitioning. The adversarial timing tests confirm bounded behavior (10K-char sed/awk inputs complete within 1s). The prior review rounds by @doudouOUC and @wenshao independently verified 119 real-parser E2E commands with zero write→read-only bypasses, monotonicity (no command newly classified read-only), and poisoned-parser self-healing. Those findings are consistent with my diff review. 中文说明代码审查在 架构: 资源管理: 语法树在 共享规则: sed 扫描器正确处理地址可选命令、替换分隔符和值选项感知的标志解析。awk 扫描器的 同步检查器加固: 通过 调度器修复: 传递原始命令(非 兼容性: 布尔 API 正确映射为 非阻塞观察:
测试所有受影响测试套件在 此为非 UI 重构(Shell 安全分类器),无可捕获的 TUI 变更。验证重点是三态契约的单元测试覆盖:所有三态的表驱动矩阵、复合优先级、替换、解析器失败、对抗性输入和调度器分区。 此前 @doudouOUC 和 @wenshao 的审查轮次独立验证了 119 条真实解析器 E2E 命令,零 write→read-only 绕过、单调性成立、中毒解析器自愈。这些发现与我的 diff 审查一致。 — Qwen Code · qwen3.7-max Reviewed at |
|
Confidence: 3/5 — clean code review, but Stage 0 hard block applies and a maintainer has requested changes. The code is solid across every review dimension. The three-state classifier is the right abstraction for #6949's follow-up approval routing — a boolean genuinely cannot represent the uncertainty this PR captures. The The gawk What changed from the prior triage: the Stage 0 assessment has been corrected. The previous triage incorrectly stated the author's collaborator status exempted them from the hard block. The author has Current blockers:
⏸️ Deferring to @yiliang114 and @samuelhsin for the final call. The code review found no blocking issues at 中文说明置信度:3/5 — 代码审查通过,但 Stage 0 硬限制适用且维护者已请求修改。 代码在所有审查维度表现良好。三态分类器是 #6949 后续审批路由的正确抽象。 755c14a 中的 gawk 与前次 triage 的变化: Stage 0 评估已纠正。前次 triage 错误认为 collaborator 身份可豁免硬限制。作者有 当前阻塞项:
⏸️ 转交 @yiliang114 和 @samuelhsin 做最终决定。代码审查在 — Qwen Code · qwen3.7-max Reviewed at |
|
⏸️ Deferring to @samuelhsin and @yiliang114 — this is a cross-repository 中文⏸️ 转交 @samuelhsin 和 @yiliang114 — 这是一个跨仓库 |
yiliang114
left a comment
There was a problem hiding this comment.
Requesting changes for one correctness blocker and two scope/design concerns.
Blocking correctness issue
The AWK classifier currently treats an unsupported GNU awk indirect call as read-only:
awk "BEGIN { fn = \"system\"; @fn(\"touch /tmp/pwned\") }"I verified this through the full classifyShellCommandSafety() entry point. It returns read-only, although GNU awk can use this syntax to invoke the built-in system() function and execute an external command. Since existing boolean consumers auto-allow read-only commands, this can bypass permission checks and scoped-agent/speculation boundaries. I left the detailed reproduction and references inline.
This does not yet fix #6949 end to end
The new three-state API has no production policy caller beyond its compatibility path. Plan-mode routing, scoped approval behavior, ACP guidance, and ExitPlanMode user-interaction/provenance behavior remain unchanged. Even after fixing the classifier bug, merging this PR alone would not resolve the reported user-visible behavior.
Scope
This is a fork-authored refactor under packages/core/src/** with approximately 1,162 lines of production-code churn. That exceeds the repository 500-line hard-block threshold for external core refactors, so this should be maintainer-initiated or split into substantially smaller changes.
The three-state type itself is small. Most of the scope comes from implementing command-specific semantic scanners for git, find, sed, awk, sort, and other commands, plus parallel async and synchronous classification paths. This creates a large security surface before there is a production consumer for the new distinction.
Suggested split
- Route commands that cannot be proven read-only through scoped approval without leaving Plan mode.
- Handle ExitPlanMode user interaction and trusted provenance separately.
- If three-state classification is still required, initially recognize only indisputable write evidence and classify every unsupported syntax form as
unknown. - Move parser lifecycle, scheduler wrapper handling, and additional command-specific precision into separate focused changes.
This is also closer to the official Claude Code package behavior: it separates commands proven read-only from commands that pass through normal permission checks, while ExitPlanMode independently requires user interaction. It does not require an exhaustive shell-language read/write classifier for the first Plan-mode fix.
I ran the five affected unit-test files: all 1,329 existing tests passed. A focused GNU awk regression test fails because the command is currently classified as read-only.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
[codex] Review update for 755c14a:
The remaining top-level blocker is the repository policy for an external core refactor over 500 production-logic lines, together with the requested end-to-end scope for #6949. That requires a maintainer decision on whether to take over, exempt, or split this PR; I have not expanded or reshaped the PR without that decision. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Unresolved, please confirm: [Critical] shellAstParser.ts:940 — pkill -h/-V semantics disputed; cannot verify procps-ng behavior from code alone
Not reviewed: reverse audit — its prompt was built, but no agent was launched with it that opened its brief, so the reverse-audit pass did not run.
[Critical] Open blocker (still stands): sed -e flag index not added to scriptArguments in shell-safety-rules.ts:184 — author responded 'not taking this change' to preserve the compatibility invariant.
[Critical] Open blocker (still stands): SAFE_SED_OPTION pre-filter in shellReadOnlyChecker.ts:192 rejects sed -e before reaching the shared classifier — author responded 'not taking this change' for the same compatibility reason.
[Critical] Open blocker (cannot tell): processSafety applies blanket -h/-V help check to pkill — disputed whether procps-ng pkill treats -h as help or hierarchy match.
— qwen3.7-max via Qwen Code /review
|
[codex] Review follow-up for 755c14a:
Additional requested-change context:
No code changes were needed in this review round. Both remaining inline threads have been answered and are being resolved. |
Review of 755c14a — tri-state shell safety classifierIndependently verified at head 1.
|
Local build & real-behavior verification report (at head
|
| Gate | Result |
|---|---|
npm run lint / npm run typecheck / npm run build |
✅ / ✅ / ✅ |
PR test files (shellAstParser, shell-ast-parser-lazy, shellReadOnlyChecker, coreToolScheduler, permission-manager) |
✅ 1335/1335 |
Consumer suites (shell, monitor, speculationToolGate, memory-scoped-agent-config, shell-utils) |
✅ 537/537 |
Local-environment note (not a PR defect): shell-ast-parser-lazy.test.ts timed out for me on a cold cache in a tmpfs worktree — the first mocked test took 27s of transform time, and the esbuild-bundling test exceeded its inline 20s budget cold (5.3s warm). All 5 pass with raised timeouts, and the full CI suite is green at this head. If other machines hit this, the bundling test's inline 20_000 might deserve a small bump.
Real-parser E2E battery (no mocks)
I drove the PR classifier and the origin/main (3e003b018) baseline side-by-side in one process, real web-tree-sitter + bash WASM, across 119 commands covering the Reviewer Test Plan matrix plus adversarial probes:
- 113/113 contract cases match the documented three-state result — reads (incl. read-only pipelines, pure assignments, subshells,
git branch -a, plainsed/awk), write evidence (sort -o,uniq in out,git diff --output=, sedw/-i, awk static file writes,find -delete/-fprintf, redirections incl.>&file, nested$(touch …)promoting through the unknown floor, signals), and unknown (interpreters, wrappers,eval, expansions in sensitive args,--help/--dry-runforms, malformed syntax, case-mismatched names,rg -z/--pre,printf -v, signal-0 probes). - Boolean-API consistency holds for all 119:
isShellCommandReadOnlyAST(cmd) === (classify(cmd) === 'read-only'). - Monotonicity holds for both APIs (PR read-only ⊆ main read-only — no command newly classified read-only). The hardenings are enumerable and intentional: 27 on the AST API, 33 on the sync checker.
Poisoned-parser self-healing — found while testing, worth knowing at merge time
case x in x) rm target;; esac crashes the web-tree-sitter runtime (TypeError: resolved is not a function) on both main and this PR — a pre-existing upstream quirk (and evidently the motivation for the replace-on-throw logic and the unasserted case call in the lazy test). The behavioral difference is significant:
- main: after one such input, the parser singleton stays broken — every later parse throws, so
isShellCommandReadOnlyASTsilently degrades to the regex fallback for the remaining process lifetime (verified:git statusparse THROWS right after the crash). - PR: the poisoned instance is replaced — the very next parse succeeds (
git status→read-only), and the crashing input itself fails closed tounknownthrough the new API.
Consequence worth documenting: case write-branches classify unknown (fail-closed) rather than write, while if/while/until/select bodies correctly promote to write. Safe direction, but a line in docs/design/shell-safety-classification.md plus an upstream web-tree-sitter issue would capture it.
Scheduler partitioning, adversarial bounds, leaks
isToolCallConcurrencySafeA/B: proven reads still batch concurrently (git log,cat); wrappers (bash -c 'git log') — previously unwrapped and batched — and newly-conservative forms (sort,printf, assignment compounds, malformed lists) now stay sequential; writes sequential on both sides.- Adversarial inputs stay bounded, matching the linear-scanner claim: 150KB escaped-backslash sed, 300KB awk, 20k chained sed commands, 200-deep
$()nesting, 2000-stage pipeline — ≤ 45ms async classifier, ≤ 57ms sync checker. - Leak probe: 5000 classifications (including syntax errors) → RSS delta +0.6MB; unit tests separately pin exact
tree.delete()/parser-delete counts.
Non-blocking observations
sed 'y/abc/xyz/'(transliterate) is nowunknown(main: read-only).yis semantically read-only; it could joinSAFE_SED_COMMANDlater if this shows up as approval noise.- Negated commands:
! rm x→write,! grep -q x f→read-only. This matches fresh-mainbehavior (verified in a fresh process — my first battery run flagged it as new permissiveness, which turned out to be main's poisoned-parser degradation, not a PR delta). Noting it only becausenegated_commandsits on the read-eligible path. - Commands with trailing comments (
git status # note) classifyunknownon both main and PR — pre-existing conservatism, no regression.
The permission-manager.ts change is comment-only (verified in the diff); its behavior shift comes entirely through the hardened isShellCommandReadOnlyAST, which the battery covers.
中文版本
本地构建与真实行为验证报告(head 755c14a6b)
结论:LGTM。 干净本地构建下所有质量门禁全绿;通过真实 WASM parser 驱动的 119 条命令战役证实了文档化的三态契约,且两个布尔 API 相对 origin/main 均无新增放行。战役还实证了一个本 PR 实际交付但描述中未突出的健壮性修复:在 main 上,一条让 parser 崩溃的输入会使 AST 检查器在整个进程剩余生命周期内永久失效;本 PR 可自愈(详见下文)。
环境与门禁
隔离 worktree(head 755c14a6b,base 落后 origin/main 21 个提交、与新提交无文件重叠),干净 npm ci,macOS,Node v22.23.1。
| 门禁 | 结果 |
|---|---|
npm run lint / npm run typecheck / npm run build |
✅ / ✅ / ✅ |
PR 测试文件(shellAstParser、shell-ast-parser-lazy、shellReadOnlyChecker、coreToolScheduler、permission-manager) |
✅ 1335/1335 |
消费方套件(shell、monitor、speculationToolGate、memory-scoped-agent-config、shell-utils) |
✅ 537/537 |
本地环境备注(非 PR 缺陷):shell-ast-parser-lazy.test.ts 在 tmpfs worktree 冷缓存下超时——首个 mock 测试的转译耗时 27s,esbuild 打包测试冷态超过其内联 20s 预算(热态 5.3s)。放宽超时后 5 个测试全部通过,且该 head 的 CI 全量套件为绿。如其他机器也遇到,打包测试的内联 20_000 或可略微上调。
真实 parser E2E 战役(无 mock)
在同一进程中并排驱动 PR 分类器与 origin/main(3e003b018)基线,使用真实 web-tree-sitter + bash WASM,覆盖 Reviewer Test Plan 矩阵及对抗性探针共 119 条命令:
- 113/113 契约用例与文档化三态结果一致——只读(含只读管道、纯赋值、子 Shell、
git branch -a、普通sed/awk)、写证据(sort -o、uniq in out、git diff --output=、sedw/-i、awk 静态文件写、find -delete/-fprintf、含>&file的重定向、穿透 unknown 下限的嵌套$(touch …)、进程信号)、unknown(解释器、包装器、eval、敏感参数中的展开、--help/--dry-run形式、畸形语法、大小写不匹配、rg -z/--pre、printf -v、signal-0 探测)。 - 布尔 API 一致性在全部 119 条上成立:
isShellCommandReadOnlyAST(cmd) === (classify(cmd) === 'read-only')。 - 两个 API 的单调性均成立(PR 只读集 ⊆ main 只读集——没有任何命令被新判为只读)。收紧项可枚举且符合设计意图:AST API 27 项、同步检查器 33 项。
中毒 parser 自愈——测试过程中发现,合并前值得了解
case x in x) rm target;; esac 在 main 与本 PR 上都会让 web-tree-sitter 运行时崩溃(TypeError: resolved is not a function)——这是上游既有问题(显然也是 replace-on-throw 逻辑以及 lazy 测试中那个不带断言的 case 调用的动机)。行为差异非常关键:
- main:一条此类输入后 parser 单例永久损坏——所有后续 parse 都抛错,
isShellCommandReadOnlyAST在进程剩余生命周期内静默降级为 regex fallback(已验证:崩溃后git status的 parse 立即 THROWS)。 - PR:中毒实例被替换——下一次 parse 即成功(
git status→read-only),崩溃输入本身经新 API fail-closed 返回unknown。
值得写入文档的后果:case 的写分支被判为 unknown(fail-closed)而非 write,而 if/while/until/select 循环体可正确提升为 write。方向安全,但建议在 docs/design/shell-safety-classification.md 中补一句,并向上游 web-tree-sitter 提 issue。
调度器分组、对抗性边界与泄漏
isToolCallConcurrencySafeA/B:已证明只读的命令仍可并发批处理(git log、cat);包装器(bash -c 'git log'——此前会被解包后并发)及新收紧形式(sort、printf、赋值复合、畸形列表)现在保持串行;写命令两侧均串行。- 对抗性输入保持有界,符合线性扫描声明:150KB 转义反斜杠 sed、300KB awk、2 万条链式 sed 命令、200 层
$()嵌套、2000 级管道——异步分类器 ≤ 45ms,同步检查器 ≤ 57ms。 - 泄漏探测:5000 次分类(含语法错误)→ RSS 增量 +0.6MB;单元测试另精确锁定
tree.delete()/parser 释放次数。
非阻塞观察
sed 'y/abc/xyz/'(转写)现为unknown(main:只读)。y语义上只读;若实际造成审批噪音,后续可加入SAFE_SED_COMMAND。- 否定命令:
! rm x→write,! grep -q x f→read-only。与全新进程中的main行为一致(已在独立进程验证——我第一轮战役曾把它标记为新增放行,实为 main 的中毒 parser 降级所致,并非 PR 差异)。提及仅因negated_command位于可判只读路径上。 - 带尾随注释的命令(
git status # note)在 main 与 PR 上均为unknown——既有保守行为,无回归。
permission-manager.ts 的改动仅为注释删除(已在 diff 中核实);其行为变化完全来自收紧后的 isShellCommandReadOnlyAST,战役已覆盖。
|
@qwen-code /triage |
|
Thanks for the thorough real-parser verification. I reproduced and triaged the observations at
No write-as-read-only bypass was found. All required checks remain green; the cancelled Qwen Autofix route run was review-event automation with no executed steps, not a source/check failure, so it was not retried. |
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |
|
Triage re-run at Stage 0 hard block unchanged: this is a fork-authored
Threshold: 500 production lines for non-maintainer core refactors → hard block. Current review state:
Note on the gawk The Stage 0 size gate is the remaining blocker. The suggested path forward from @yiliang114 is to split into smaller focused PRs — that approach would clear the hard block. 中文说明Triage 重新运行于 Stage 0 硬阻止未变: 这是一个来自 fork 的 当前 review 状态:
关于 gawk Stage 0 规模门是剩余的阻止项。 @yiliang114 建议的路径是拆分为更小的专注 PR — 这样可以清除硬阻止。 — Qwen Code · qwen3.7-max Reviewed at |
|
Triage re-run at Stage 0 hard block unchanged: this is a cross-repository Production line breakdown (excluding
Excluded: What would unblock: a maintainer picks up this refactor as the PR author, or the PR is reduced to under 500 production lines in core. 中文说明Triage 重新运行于 Stage 0 硬阻止不变: 这是一个跨仓库 解除阻止条件: 维护者以 PR 作者身份接手此重构,或将 PR 缩减至核心代码 500 行以下。 — Qwen Code · qwen3.7-max Reviewed at |
|
@qwen-code /triage |
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |
|
@qwen-code /triage |
|
Triage re-run at Key correction: the Stage 0 assessment from the prior triage has been fixed. The previous run incorrectly stated the author's collaborator status exempted them from the 500-line hard block. The author has No new review submitted — active Current state:
Needs maintainer decision on: (1) Stage 0 hard-block waiver, (2) scope resolution per @yiliang114's suggested split, (3) pkill semantics clarification. 中文说明在 关键纠正: 前次 triage 的 Stage 0 评估已修正。前次错误认为 collaborator 身份可豁免 500 行硬限制。作者有 未提交新 review — @yiliang114 和前次 bot review 的活跃 需要维护者决定:(1) Stage 0 硬限制豁免,(2) 按 @yiliang114 建议拆分解决范围问题,(3) pkill 语义澄清。 — Qwen Code · qwen3.7-max Reviewed at |
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |
|
@qwen-code /triage |
|
Triage re-run at Stage 0 hard block unchanged: fork-authored
Threshold: 500 production lines for non-maintainer core refactors → hard block. Current review state:
No new review submitted — existing To unblock: a maintainer explicitly waives the Stage 0 hard block and resolves @yiliang114's scope concern, or the PR is reduced below 500 production lines in core. 中文说明Triage 重新运行于 Stage 0 硬阻止不变: 来自 fork 的 当前 review 状态:
未提交新 review — 现有 解除阻止条件: 维护者明确豁免 Stage 0 硬限制并解决 @yiliang114 的范围顾虑,或将 PR 缩减至核心代码 500 行以下。 — Qwen Code · qwen3.7-max Reviewed at |
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |
|
[codex] Thanks — no code change is needed for the |
|
@qwen-code /triage |
|
Triage re-run at Stage 0 hard block still applies: this is a cross-repository (fork)
Excluded: Open question — maintainer exemption: the author claims this PR qualifies for the maintainer exemption (AGENTS.md: "maintainer-authored PRs are exempt"). The author is a repository COLLABORATOR, and @wenshao (also a COLLABORATOR) has approved the PR and appears to vouch for the work. Whether collaborator status = "known maintainer" for the Stage 0 exemption is ambiguous — previous triage runs treated it as not exempt. This needs a definitive call from a maintainer. Current review state:
What would unblock: a maintainer confirms the exemption applies (collaborator = maintainer), or the PR is split to under 500 production lines, or a maintainer takes over authorship. 中文说明Triage 重新运行于 Stage 0 硬阻止仍然适用: 这是一个跨仓库(fork)的 待解决问题 — 维护者豁免: 作者声称此 PR 符合维护者豁免条件(AGENTS.md:"maintainer-authored PRs are exempt")。作者是仓库 COLLABORATOR,@wenshao(同为 COLLABORATOR)已批准该 PR 并似乎为此工作背书。Collaborator 身份是否等同于 Stage 0 豁免中的"已知维护者"尚不明确——之前的 triage 运行将其视为不符合豁免条件。这需要维护者做出明确判断。 解除阻止条件: 维护者确认豁免适用(collaborator = maintainer),或将 PR 拆分至 500 行生产代码以下,或维护者接手作者身份。 — Qwen Code · qwen3.7-max Reviewed at |
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |
yiliang114
left a comment
There was a problem hiding this comment.
The tri-state design is solid — fail-closed contract is well-maintained, bounded scanners prevent adversarial blowup, and tree lifecycle is clean (exactly-once release in finally, poisoned-parser replacement without reloading the runtime). Test matrix is thorough.
Two non-blocking observations and one question:
1. sed compatibilityUnknown false-positive surface
classifySingleSedCommandSafety gates the read-only result on /(?:^|[^\\])[ewr]\s/. This fires on replacement text containing common letter+space sequences:
sed 's/a/new value/' file→ unknown ("ne**w **value")sed 's/a/blue sky/' file→ unknown ("blu**e **sky")sed 's/a/car value/' file→ unknown ("ca**r **value")
The conservatism is fine for the safety contract. But once the follow-up Plan routing lands, this class of natural-language substitutions will trigger approval prompts. Might be worth narrowing to command-position matching (only [ewr]\s at a sed command boundary, not inside s/// replacement text) in a follow-up to avoid approval fatigue.
2. Permission-manager test: wrapped-command behavior change
The old test verified a configured Monitor(FOO="bar baz" tail -f /var/log/app.log) allow rule matching the wrapped form FOO="bar baz" /bin/bash --noprofile -c 'tail -f /var/log/app.log &' → allow. The replacement drops the rule and checks the default → ask.
The underlying shift: the regex fallback no longer strips the wrapper, so wrapped commands that previously resolved to read-only via fallback now stay unknown. Correct security posture. But users with existing Monitor allow rules against the unwrapped form will see those rules stop matching. Worth a one-liner in the design doc's migration-boundary section so the follow-up routing PR doesn't get "my allow rule stopped working" reports.
3. Question: git branch --sort refname → unknown
withoutOptionValues(rest, /^--(?:format|sort)$/) strips the --sort value, then actions.length !== rest.length triggers unknown. The bare form git branch --sort refname (no branch name) is unambiguously a read-only listing. Is the conservatism here intentional for the --sort refname topic ambiguity, or could the zero-positional-args case be refined to read-only?
Maintainer Local Verification ReportEnvironment: macOS (darwin), Node.js v22.22.2, local source build on branch Unit Tests — 744 tests, all passing ✅
Static Analysis ✅
Functional Verification — 50 real-world commands, 50/50 correct ✅Ran read-only (16 commands): write (19 commands): unknown (15 commands): Key Observations
VerdictThe PR is safe to merge from a functional correctness standpoint. The classifier is intentionally bounded and conservative — unsupported constructs stay 中文验证报告维护者本地验证报告环境: macOS (darwin),Node.js v22.22.2,本地源码构建,分支 单元测试 — 744 个测试,全部通过 ✅
静态分析 ✅
功能验证 — 50 条真实命令,50/50 分类正确 ✅对 read-only(16 条): write(19 条): unknown(15 条): 关键观察
结论从功能正确性角度,本 PR 可以安全合并。分类器有意保持有限和保守——未支持的结构保持 |
|
@yiliang114 Thanks — I checked all three observations against
These are non-blocking precision/documentation points, and this PR is already beyond the repository’s roughly five review-round threshold, so I am keeping this round to Critical fixes and making no code change. |
Maintainer Local Verification ReportEnvironment: macOS (darwin), Node.js v22.22.2, local source build on branch 1. Build, Typecheck & Lint
2. Unit Tests — 1,335 passedRan all 5 test files touched by this PR: 3. Three-State Classification VerificationWrote a verification script exercising 50 commands across all classification categories against the async AST-based
¹ The 4 Compat API ( 4. Scheduler Batching Verification — 35/35 passedVerified the synchronous
5. Real CLI E2E — Plan ModeRan the bundled CLI (
ConclusionAll verification passed. The three-state classification ( 中文验证报告维护者本地验证报告环境: macOS (darwin),Node.js v22.22.2,本地源码构建,分支 1. 构建、类型检查与 Lint
2. 单元测试 — 1,335 个全部通过运行本 PR 涉及的全部 5 个测试文件: 3. 三态分类验证编写验证脚本,对异步 AST
¹ 4 个 兼容 API( 4. 调度器批处理验证 — 35/35 通过验证了
5. 真实 CLI E2E — Plan 模式使用打包后的 CLI(
结论全部验证通过。三态分类( |
|
@qwen-code /triage |
|
Triage re-run at Stage 0 hard block does NOT apply. The author Blocking correctness issue resolved. @yiliang114's yiliang114's follow-up is positive. Their July 18 review opens with "The tri-state design is solid" and lists only non-blocking observations (sed Code review clean. 1,330 tests pass across all affected suites (542 AST classifier + 230 sync checker + 280 permission-manager + 278 scheduler). Typecheck clean. Design doc is clear and the fail-closed contract is well-maintained. Fork-refactor approval guardrail applies. Current review state:
What's needed: a maintainer to dismiss the stale 中文说明在 Stage 0 硬阻止不适用。 作者 阻止性正确性问题已解决。 @yiliang114 的 yiliang114 的后续评价为正面。 其 7 月 18 日的 review 以"三态设计扎实"开头,仅列出非阻止性观察。其 代码审查干净。 所有受影响测试套件 1,330 个测试通过。TypeCheck 干净。 Fork-refactor 审批护栏适用。 确定性检查返回 需要: 维护者解除过时的 — Qwen Code · qwen3.7-max Reviewed at |
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |
yiliang114
left a comment
There was a problem hiding this comment.
LGTM ✅ — Clean three-state shell safety classifier. Fail-closed design, backward-compatible API, and thorough test coverage across all three states plus parser failure modes.
Already have 2 approved ,3ks






What this PR does
This PR introduces an internal three-state shell safety fact layer that classifies commands as
read-only,write, orunknown. Valid Bash syntax combines results withwrite > unknown > read-onlyprecedence; syntax errors, parser unavailability, unsupported constructs, wrappers, and dynamic execution remain unknown. Command and process substitutions impose an unknown floor while still promoting nested known writes, and supported control-flow ASTs scan possible branches without treating function definitions as execution.The classifier recognizes bounded mutation evidence for output redirections, direct filesystem writers, process signals, Git operations, find actions, sed and awk scripts, and explicit output modes in sort, tree, uniq, tee, and dd. Shared linear scanners keep sed and awk analysis bounded on adversarial input. Command matching is case-sensitive, parser failures fail closed for the new API, poisoned parser instances are replaced safely, and every classification tree is released exactly once.
The existing boolean API remains compatible: it returns true only for a proven read-only AST and keeps the legacy synchronous fallback when the parser cannot load or throws at runtime. The synchronous checker used by scheduling is hardened conservatively so wrappers, expansions, stateful assignments, malformed command lists, hidden output modes, and differently-cased command names stay sequential. This PR documents the contract and keeps the future Plan-mode approval routing change outside the fact layer.
Why it's needed
Issue #6949 needs Plan mode to distinguish commands that are proven safe from commands whose behavior cannot be established statically. The current boolean result loses that uncertainty and cannot support a later one-off approval path without either over-allowing unknown commands or treating every non-read-only command as a known write. This refactor adds the missing safety fact while leaving Plan routing, approval copy, ACP behavior, and Plan exit semantics unchanged.
Reviewer Test Plan
How to verify
Confirm that ordinary reads such as
git status --short, read-only pipelines, pure assignments, and subshell reads classify asread-only; direct writers, output redirections, Git mutations,find -delete, file-writing sed or awk forms,sort -o, and nested writers classify aswrite; and interpreters, wrappers, dynamic execution, read-only substitutions, control flow without a known write, malformed syntax, differently-cased commands, and parser failures classify asunknown.Confirm that nested known writes win over an unknown floor, syntax errors do not enter the regex fallback, runtime parser failures return unknown through the new API while the compatibility API retains its fallback, and repeated initialization or parser replacement does not leak parser or tree resources. Confirm that the scheduler batches proven reads but keeps wrappers, output-writing commands, expansions, malformed command lists, and stateful assignments sequential.
Automated verification completed on macOS: 1,866 tests passed sequentially across the classifier, lazy parser runtime, compatibility checker, Shell, Monitor, PermissionManager, speculation gate, memory-scoped agent configuration, scheduler, and shell utilities.
npm run lint,npm run build,npm run typecheck, Prettier checks, and diff whitespace checks passed. A full model-driven Plan-mode E2E run was unavailable because the configured API quota was exhausted; the checked-in design and local E2E plan preserve that follow-up verification boundary.Evidence (Before & After)
N/A — this is a non-UI refactor and does not change Plan-mode routing in this PR.
Tested on
Environment (optional)
macOS 26.4.1, Node.js v22.22.3, npm 10.9.8, local source build; the targeted unit tests are sandbox-independent.
Risk & Scope
unknown; the synchronous scheduler checker is stricter and may serialize some commands that are safe in practice. Environment-prefixed commands are intentionallyunknown; the existing exact Monitor/permission rule matcher does not normalize leading assignments, so an exact rule for such a command may still prompt.casestatements; this PR fails those commands closed tounknownand rebuilds the parser for subsequent commands, while the underlying parser limitation remains follow-up work. Windows and Linux were not tested locally and are left to CI.Linked Issues
Refs #6949
中文说明
本 PR 做了什么
本 PR 引入了一个内部三态 Shell 安全事实层,将命令分类为
read-only、write或unknown。对于有效 Bash 语法,结果按write > unknown > read-only的优先级聚合;语法错误、解析器不可用、未支持结构、包装器和动态执行均保持为 unknown。命令替换和进程替换设置 unknown 下限,同时仍会把嵌套的已知写操作提升为 write;能够成功生成 AST 的受支持控制流会扫描可能执行的分支,但不会把函数定义视为函数体已经执行。分类器为输出重定向、直接文件系统写命令、进程信号、Git 操作、find 动作、sed 和 awk 脚本,以及 sort、tree、uniq、tee、dd 的显式输出模式识别有限且明确的修改证据。共享的线性扫描器保证 sed 和 awk 分析面对对抗性输入时仍保持有界。命令匹配区分大小写,新 API 在解析器失败时 fail closed,中毒的解析器实例会被安全替换,并且每棵用于分类的语法树都恰好释放一次。
现有布尔 API 保持兼容:只有 AST 被证明为只读时才返回 true,解析器无法加载或运行时抛错时仍保留旧同步 fallback。调度器使用的同步检查器也进行了保守加固,使包装器、展开、带状态赋值、畸形命令列表、隐藏输出模式以及大小写不同的命令名保持串行执行。本 PR 记录了完整契约,并把后续 Plan 模式审批路由变更留在事实层之外。
为什么需要它
#6949 要求 Plan 模式区分“已经证明安全”的命令和“无法静态确认行为”的命令。当前布尔结果丢失了这种不确定性,后续若要增加一次性审批路径,就只能在过度放行 unknown 命令和把所有非只读命令都视为已知写操作之间做错误取舍。本重构补上缺失的安全事实,同时不修改 Plan 路由、审批文案、ACP 行为或 Plan 退出语义。
Reviewer Test Plan
如何验证
确认
git status --short、只读管道、纯赋值和子 Shell 读取等普通读取命令分类为read-only;直接写命令、输出重定向、Git 修改命令、find -delete、sed 或 awk 的文件写入形式、sort -o以及嵌套写命令分类为write;解释器、包装器、动态执行、只读替换、不含已知写操作的控制流、畸形语法、大小写不同的命令和解析器失败分类为unknown。确认嵌套已知写操作优先于 unknown 下限,语法错误不会进入 regex fallback,解析器运行时失败时新 API 返回 unknown 而兼容 API 保留 fallback,并且重复初始化或解析器替换不会泄漏解析器和语法树资源。确认调度器可以并发批处理已证明只读的命令,但包装器、输出写入命令、展开、畸形命令列表和带状态赋值保持串行。
已在 macOS 完成自动验证:分类器、lazy parser runtime、兼容检查器、Shell、Monitor、PermissionManager、speculation gate、memory-scoped agent 配置、scheduler 和 shell utilities 共 1,866 个测试顺序通过。
npm run lint、npm run build、npm run typecheck、Prettier 检查和 diff 空白检查均通过。由于配置的 API quota 已耗尽,未能完成完整的模型驱动 Plan 模式 E2E;已提交的设计文档和本地 E2E 计划保留了该后续验证边界。证据(修改前后)
N/A——这是一个非 UI 重构,本 PR 不修改 Plan 模式路由。
测试平台
环境(可选)
macOS 26.4.1,Node.js v22.22.3,npm 10.9.8,本地源码构建;相关单元测试不依赖 sandbox。
风险与范围
unknown;同步调度检查器更严格,因此部分实际安全的命令可能被串行执行。带环境变量前缀的命令会有意归为unknown;现有 Monitor/permission 精确规则匹配器不会归一化前导赋值,因此为这类命令配置精确规则后仍可能提示审批。case语句上抛错;本 PR 会将其 fail closed 为unknown并为后续命令重建 parser,底层 parser 限制留待后续处理。Windows 和 Linux 未在本地测试,留给 CI 验证。关联 Issue
Refs #6949