Skip to content

fix(cosh-ng): [shell] intercept zsh slash-bearing natural-language prompts - #2210

Open
SunnyQjm wants to merge 1 commit into
mainfrom
fix/cosh-1943-zsh-slash-nl-intercept
Open

fix(cosh-ng): [shell] intercept zsh slash-bearing natural-language prompts#2210
SunnyQjm wants to merge 1 commit into
mainfrom
fix/cosh-1943-zsh-slash-nl-intercept

Conversation

@SunnyQjm

@SunnyQjm SunnyQjm commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

zsh sibling of #1919 (bash fixed in #1942): a natural-language prompt whose
first IFS token contains a slash and does not resolve to an existing path is
executed by zsh as a path — zsh never invokes command_not_found_handler for
slash-bearing command words, so the NL classifier hooked there is unreachable
and the user sees a raw zsh: no such file or directory error instead of the
agent taking over. zsh's DEBUG trap has no veto-execution semantics (probe
evidence in #1943), so the bash mechanism cannot be ported; the only
pre-execution seam zsh offers is the accept-line ZLE widget.

Changes

  • marker/zsh.rs: new _cosh_should_intercept_missing_path — the same
    five-gate verdict as the bash missing-path fix (slash-bearing first word,
    AI enabled, path provably ENOENT, missing-path classification says
    natural_language), reusing the shared input_intent.sh classifier verbatim
    (single source of truth; classifier and bash side have zero diff).

  • marker/zsh.rs: _cosh_accept_line ZLE widget mounted over the active
    accept-line (alias-saving chain keeps user widgets working) evaluates the
    gates at line submission. On a natural_language verdict it re-adds the line
    to history (print -sr; skipped for secret-bearing lines), re-echoes the
    prompt+text (ZLE erases the edit line on buffer clear; sensitive lines
    re-echo the redaction placeholder), emits the same intercept marker shape
    as the bash missing-path route (natural_language,
    top_level_missing=false, sensitive as-is), clears the buffer and
    accepts the empty line. Every gate failure and internal error falls open to
    the original accept-line: the worst case is the interception not firing,
    never a broken native line. CONTEXT != start submissions (PS2/heredoc
    continuations, vared) always pass through.

  • The zsh marker script body lives in zsh_marker.sh
    (include_str!, byte-identical move) keeping zsh.rs under the
    700-line layout gate; slash registry tests re-anchored to the script
    file.

Tests

Seven zsh-side integration tests mirroring the bash anchors
(tests/shell_host/marker.rs):

  • shell_host_zsh_missing_path_natural_language_intercepts — positive
    intercept, no native error, re-echo visible.
  • shell_host_zsh_sensitive_missing_path_natural_language_intercepts
    sensitive flag, journal whole-field redaction, redaction placeholder
    re-echo, raw key absent from events/journal.
  • shell_host_zsh_missing_path_counterproofs_stay_native — existing
    executable, English typo path, dangling symlink, permission-opaque parent
    all keep native behavior; slash-free CNF route unaffected.
  • shell_host_zsh_missing_path_intercepts_with_user_accept_line_widget
    interception works with a user accept-line widget present and the user
    widget stays in the chain for pass-through lines.
  • shell_host_zsh_missing_path_heredoc_continuation_stays_native
    CONTEXT=cont lines pass through untouched.
  • shell_host_zsh_missing_path_history_respects_user_policy
    HIST_IGNORE_SPACE keeps a space-prefixed intercepted prompt out of
    history while the interception itself still fires (review P1).
  • shell_host_zsh_missing_path_intercepts_with_builtin_alias_accept_line
    zle -A builtin-alias customizations survive the mount and native lines
    keep executing through the preserved alias (review P2).

Verification

Focused (alinux3 arm64 container, zsh 5.5.1):

  • cargo test -p cosh-shell --test shell_host — new cases green; 37 zsh
    filter tests green. Pre-existing environment failures unrelated to this
    diff (heavy::raw_relay_host_shows_isolated_sudo_prompt_and_keeps_shell_usable,
    termios::cosh_owned_timeout_recovery_restores_pty_without_visible_command,
    tools::readonly_compound_tests::* reap-deadline pair) fail identically on
    the clean base commit (stash-control attribution).
  • cargo test -p cosh-shell --lib / --bin cosh-shell — green except the
    same pre-existing readonly_compound pair (also fails on clean base).
  • cargo clippy --workspace --all-targets -- -D warnings (container) green;
    cargo fmt --check green; check-layout.sh and check-test-inventory.sh
    green.

Real-machine acceptance (real PTY 120x40, real cosh-core adapter, real LLM):

  • Container zsh 5.5.1: FAIL→PASS on both defect scenarios (f1 full-width
    single-token absolute path, f2 relative path), slash-free control scenario
    unchanged; native error gone, agent takes over.
  • macOS zsh 5.9 spot check (f1): intercept + takeover + re-echo correct
    under a themed user prompt.

Not run: workspace-wide test targets beyond cosh-shell; macOS cosh-core
build (pre-existing Linux-only rustix::openat2 usage, unrelated).

Evidence

Fork-hosted, commit-SHA-pinned
(SunnyQjm/anolisa@c9a65b3e312458743766d4f530ed28b5d4ff0e8c, branch
pr-2210-assets). All screenshots are full-size (120x40) final frames
rendered from the real-PTY casts.

FAIL→PASS on the exact issue scenario (f1, container zsh 5.5.1):

Before (main b0a57a26) After (this PR)
head f1 fixed f1

Relative-path variant (f2):

Before After
head f2 fixed f2

Slash-free control (c1, no regression):

Before After
head c1 fixed c1

macOS zsh 5.9 spot check (f1, themed user prompt):

macos f1

Casts:
head-f1.cast ·
fixed-f1.cast

Closes #1943

@github-actions github-actions Bot added the component:cosh-ng src/cosh-ng label Aug 4, 2026
@SunnyQjm
SunnyQjm marked this pull request as ready for review August 4, 2026 10:01

@qoderai qoderai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

本次审查范围内未发现需要修改的问题。


🤖 Generated by QoderView workflow run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d1af0b142

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cosh-ng/crates/cosh-shell/src/shell_host/marker/zsh.rs Outdated
Comment thread src/cosh-ng/crates/cosh-shell/src/shell_host/marker/zsh.rs Outdated
@SunnyQjm

SunnyQjm commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

PR number: #2210
head_sha: 5d1af0b
reviewed_at: 2026-08-04T10:12:58Z

评审结论

Findings

未发现 blocking package/module/public API 组织问题。

结构核对

  • Owner:production 变更仅落在 shell_host/marker/zsh.rs,zsh 侧 slash-bearing 自然语言拦截属 shell_host owner 的 shell 级输入意图分类职责(复用 owner 内 input_intent.sh);测试落在 tests/shell_host/marker.rs,与 bash 对侧既有锚点同位置。单 owner patch,无跨 owner 混杂,无 root src/*.rs 新增。
  • Public API:未触碰 lib.rs,无新增 pub mod/pub use/pub。测试仅消费已登记的 internal-migration-surface 项(run_scripted_zshShellHostConfigScriptedInputbuild_command_blocksShellEventKind),未为测试新增 production 可见性。
  • 依赖方向:新增内容全部为内嵌 zsh 脚本,复用 owner 内既有 _cosh_classify_missing_cosh_path_provably_missing_cosh_emit_intercept_marker;未引入 shell_host -> agent/approval/ui/runtime 依赖。production Rust 代码零变更,无 cosh_shell:: self-crate path 新增。
  • 新 crate / 新依赖 / lockfile:均无。

大文件 / 阈值

  • marker/zsh.rs 净增 +71 行(两个 hunk:+14、+57),按 diff 推算改后约 690+ 行(估算,diff 无法确定精确值)。该文件未登记在 large-file-inventory;输入快照中 CI Test cosh-ng fast checksTest cosh-ng 均已 SUCCESS,按"行数判定以 CI gate 实测为准"口径未越 700 行阈值,不构成 finding。但余量接近零,后续增长将触发 owner note/拆分登记;bash 对侧(台账 934 行)已有 .sh asset extraction 计划,zsh 侧持续增长时建议沿用同路径治理。

Open Questions / 剩余风险

  • zsh.rs:178 附近 _cosh_should_intercept_missing_path 与 bash 五门 verdict 逐字镜像;分类器本体共用 input_intent.sh 保持单一事实源,但 gate 编排逻辑双侧并行,后续语义变化需双侧同步。per-shell 机制差异(DEBUG trap vs ZLE accept-line)决定 glue 无法直接共用,登记为已知结构风险而非阻断。
  • tests/shell_host/marker.rs 改后约 3200+ 行;测试文件行数治理以 shell-test-organization 为准,本输入未含其阈值口径,不作判定。
  • 其他评审者已在 PR 上提出行为级意见(print -sr 绕过用户 history exclusion 策略、非 user:* 形式的 accept-line 定制在 fallback 链中丢失),超出本结构评审范围,仅提示作者跟进,不作为组织问题 finding。

Validation

  • 本评审不执行工具,未跑任何验证;结构结论基于输入 diff 静态核对。
  • 输入快照时 CI gate(Test cosh-ng fast checksTest cosh-ngBuild cosh-ng release)均已 SUCCESS;行数/阈值类判定以 CI gate 实测为准。
  • PR 自述 check-layout.shcheck-test-inventory.sh、clippy、fmt 及 focused shell_host 测试通过,无法独立核实。

…ompts

Problem: zsh executes a slash-bearing command word as a path without
invoking command_not_found_handler, so the natural-language classifier
hooked there is unreachable. A prompt like
"你读一下,并安装这个skill:/usr/share/.../SKILL.md" (full-width
punctuation keeps the whole line one IFS token) surfaces as a raw
"zsh: no such file or directory" error instead of being taken over by
the agent (#1943). The bash sibling was fixed in #1942 via the extdebug
DEBUG-trap veto, but zsh's DEBUG trap has no veto-execution semantics,
so that mechanism cannot be ported.

Fix (zsh-only, per SDD cosh-1943-zsh-slash-nl-intercept):
- marker/zsh.rs: new _cosh_should_intercept_missing_path helper with
  the same five-gate verdict as the bash fix (slash-bearing first word,
  AI enabled, path provably ENOENT, missing-path classification says
  natural_language), reusing the shared input_intent.sh classifier
  verbatim (single source of truth, zero classifier changes).
- marker/zsh.rs: a _cosh_accept_line ZLE widget mounted over the
  active accept-line (alias-saving chain keeps user widgets working)
  evaluates the gates at line submission — the only pre-execution seam
  zsh offers. On a natural_language verdict it re-adds the line to
  history (print -sr; skipped for secret-bearing lines), re-echoes the
  prompt+text (ZLE erases the edit line on buffer clear; sensitive
  lines re-echo the redaction placeholder), emits the same intercept
  marker shape as the bash missing-path route, clears the buffer and
  accepts the empty line. Every gate failure and internal error falls
  open to the original accept-line: worst case is the interception not
  firing, never a broken native line. CONTEXT!=start submissions
  (PS2/heredoc continuations, vared) always pass through.
- bash side and input_intent.sh are untouched (zero diff).

Tests: five zsh-side cases mirroring the bash anchors — positive
intercept, sensitive intercept with journal redaction, fail-closed
counterproofs (existing path, English typo path, dangling symlink,
permission-opaque parent), user accept-line widget compatibility, and
heredoc continuation pass-through.

Review round 1 (Codex P1/P2 + layout gate):
- P1: the manual history re-add now replays the active history policy
  (HIST_IGNORE_SPACE and the zshaddhistory hook chain) before print -sr,
  so explicit exclusions and user hooks keep vetoing persistence; any
  veto skips the add (worst case: non-recallable intercepted prompt).
- P2: the accept-line save uses an unconditional zle -A alias, so
  customizations aliased to another builtin (not only user:* widgets)
  are preserved through the dispatch chain.
- Layout: the zsh marker script body moved verbatim to zsh_marker.sh
  (include_str!), keeping zsh.rs under the 700-line gate; slash
  registry tests re-anchored to the script file. Emitted protocol is
  byte-identical.

Assisted-by: Qoder:1.22.0
Signed-off-by: SunnyQjm <mfeng@linux.alibaba.com>
@SunnyQjm
SunnyQjm force-pushed the fix/cosh-1943-zsh-slash-nl-intercept branch from 5d1af0b to 27551a8 Compare August 4, 2026 11:23
@SunnyQjm

SunnyQjm commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1 addressed (Codex P1 history policy, P2 builtin-alias accept-line) plus the layout gate split (zsh_marker.sh extraction, byte-identical script). History rewritten by amend:

SHA
Reviewed (round 1) 5d1af0b142ec666f73d058417a3e05609d5b2b43
Current head 27551a84

Full verification re-run on the new head: workspace clippy -D warnings, fmt, check-layout.sh, check-test-inventory.sh green; 37 zsh-filter shell_host tests green (incl. 2 new review-anchored tests); real-PTY f1 scenario re-verified end-to-end (agent takeover, no native error).

@SunnyQjm

SunnyQjm commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

PR number: #2210
head_sha: 27551a8
reviewed_at: 2026-08-04T14:27:13Z

评审结论

Findings

未发现 blocking package/module/public API 组织问题。

结构核对

  • Owner:production 变更落在 shell_host/marker/zsh.rs(622 → 10 行,diff 精确值)与新增 shell_host/marker/zsh_marker.sh。zsh 侧 slash-bearing 自然语言拦截(accept-line ZLE widget、五门 verdict)属 shell_host owner 的 shell 级输入意图分类职责,分类器本体复用 owner 内 input_intent.sh,保持单一事实源。slash/registry.rs:485:520 仅测试模块内两处 include_str! 锚点从 zsh.rs 改指 zsh_marker.sh,是脚本提取的必要配套(bash 对侧原有同款 include),不构成跨 owner 混杂。新增测试落在 tests/shell_host/marker.rs,与 bash 锚点同位置。无 root src/*.rs 新增,无 utils/common/misc
  • Public API:未触碰 lib.rs,无新增 pub mod/pub use/pubzsh_marker_script() 维持 pub(in crate::shell_host) 可见性。测试仅消费已登记 internal-migration-surface 项(run_scripted_zshShellHostConfigScriptedInputbuild_command_blocksShellEventKind),未为测试新增 production 可见性。
  • 依赖方向:新增逻辑全部为 owner 内嵌 zsh 脚本,复用既有 _cosh_classify_missing_cosh_path_provably_missing_cosh_emit_intercept_marker;未引入 shell_host -> agent/approval/ui/runtime 依赖,无 cosh_shell:: self-crate path。
  • 新 crate / 新依赖 / lockfile / 跨 crate:均无。

大文件 / 阈值

  • zsh.rs 提取方式与 large-file-inventory 中 bash 对侧(marker/bash.rs 台账 934 行)已登记的 .sh asset extraction 治理路径一致(参照 input_intent.sh 模式),zsh 侧先一步落地。若功能留在 zsh.rs 内将形成 622+≈90 ≈ 710+ 行的新越线 .rs,提取后规避了"新建越线文件必须拆分而非登记"的触发。
  • zsh_marker.sh 为 704 行(diff 精确值)。行数阈值治理口径(check-layout.sh、large-file-inventory)针对 *.rs production 文件,.sh 静态 asset 不在其列,不构成 finding;但该 asset 出生即超 700 规模且后续增长无闸门,建议参照 bash 台账条目补一条 owner note 明确 .sh asset 治理口径(非阻断建议)。
  • 字节同一性:旧 raw string 以换行开头,新 .sh 首行为空行,include_str! 内容与旧内嵌字符串一致;byte-identity 由既有 golden 测试覆盖(本评审未跑)。
  • tests/shell_host/marker.rs +440 行(改后约 3351 行,估算);测试文件行数治理以 shell-test-organization 为准,本输入未含其阈值口径,不作判定。

Open Questions / 剩余风险

  • _cosh_should_intercept_missing_path 与 bash 五门 verdict 逐字镜像(zsh_marker.sh 内注释亦声明 "Body mirrors marker/bash.rs");分类器本体共用 input_intent.sh,但 gate 编排逻辑双侧并行,后续语义变化需双侧同步。per-shell 机制差异(DEBUG trap vs ZLE accept-line)决定 glue 无法直接共用,登记为已知结构风险而非阻断。
  • 当前 head 上存在一条 CHANGES_REQUESTED 评审,含两条行为级 P1(拦截成功后仍经用户 widget dispatch 可能执行非预期原生命令;直接重放 zshaddhistory hook 绕过 zsh 原生 fc -p 上下文恢复)。两者均指向新 .sh asset 内的行为逻辑,属行为层而非组织问题,超出本结构评审范围,但合并前必须由作者闭环。
  • 上一轮两条行为级意见(history policy、zle -A builtin alias)已在本 head 修复并附锚点测试,属行为层闭环。

Validation

  • 本评审不执行工具,未跑任何验证;结构结论基于输入 diff 静态核对。
  • 输入快照时本 head 的 CI(Test cosh-ng fast checksTest cosh-ngBuild cosh-ng release、PR Checks)均已 SUCCESS;行数/阈值类判定以 CI gate 实测为准,上文 .rs 行数为 diff 精确推算。
  • PR 自述 clippy、fmt、check-layout.shcheck-test-inventory.sh 及 focused shell_host 测试通过,无法独立核实。

@kongche-jbw kongche-jbw 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 blocking ZLE/history integration regressions reproduced against 27551a84 with the cosh-shell PTY harness. The PR's seven focused zsh tests pass, but they do not cover these user-customization paths.

zle -I 2>/dev/null || true
print -r -- "${(%%)PS1}${echo_line}" 2>/dev/null || true
_cosh_emit_intercept_marker "$line" "natural_language" false "$sensitive"
_cosh_dispatch_accept_line

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.

[P1] Bypass the saved widget after a successful intercept

At this point BUFFER has been cleared and the intercept marker has already been emitted, but _cosh_dispatch_accept_line invokes an arbitrary user accept-line widget. Such a widget may legitimately synthesize a command for an empty buffer. I reproduced this with a widget that sets an empty buffer to echo review-unexpected-native: the event stream became UserInputIntercepted followed by CommandStarted("echo review-unexpected-native"). This violates the interception guarantee and can execute unintended native commands. Submit the cleared line through .accept-line directly on the successful-intercept path; keep the saved user widget only for pass-through paths.

local hook
for hook in "${zshaddhistory_functions[@]}"; do
(( ${+functions[$hook]} )) || continue
"$hook" "$line"$'\n' || return 1

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.

[P1] Do not invoke zshaddhistory hooks outside native hook processing

zsh automatically restores history contexts pushed with fc -p after native zshaddhistory processing. Calling each hook function directly here bypasses that restoration. With a valid per-directory-history hook that runs fc -p local-history, one intercepted prompt permanently changed subsequent $HISTFILE from global-history to local-history in the PTY session. Later commands can therefore be persisted to the wrong file. Preserve zsh's native hook-context semantics instead of directly replaying arbitrary hooks; the replacement should also retain status 2 semantics (internal history only, no history-file write).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[cosh-ng] zsh: slash-bearing natural-language prompts bypass interception (bash fixed in #1942)

2 participants