mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
test(pty): cover restricted proc syscall access
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md
|
||||
2026-07-16-persistent-pty-sessions.md: 8cbec14dae0517a82925d871843a1998f4734709
|
||||
2026-07-16-persistent-pty-sessions.zh.md: 91d07c2dcd1f58ffb53a20955740c7205ace5d92
|
||||
2026-07-16-persistent-pty-sessions.md: 08ccf64586034b5a0524889f33da89213ac7275d
|
||||
2026-07-16-persistent-pty-sessions.zh.md: 5225d781fc8eba1fb4a6c7f4d3b5d4d87547b71d
|
||||
|
||||
@@ -74,7 +74,7 @@ With `run_in_background: true`, `dsh-tool-terminal` registers the in-flight send
|
||||
|
||||
The local backend first recognizes a private OSC prompt marker emitted by its controlled bash startup, then requires the printable tail after the latest marker to exactly equal the controlled `PS1` before declaring prompt readiness and runs three bounded fallback tiers. Carrying that tail across data callbacks covers delivery where the marker and prompt arrive separately; requiring the exact tail rejects a delayed earlier prompt once echoed input or output follows it, so it cannot settle the current send. The marker is removed before output reaches the model and avoids a fixed silence delay for ordinary shell commands on both platforms. Unpublished startup does not accept zero-output silence as readiness; timeout rejects the spawn. If caller cancellation wins during startup, the backend closes the private session and propagates the exact `AbortSignal.reason`; a foreground PGID that is not observable yet cannot replace cancellation with a lookup error. All timings are validated config fields: `pollIntervalMs`, `exactProbeAfterMs`, `idleSilenceMs`, `handoffGraceMs`, and `timeoutMs`.
|
||||
|
||||
On Linux, the inspector reads the shell's terminal foreground PGID from `/proc/<shellPid>/stat`, enumerates every process and thread in that process group, and probes their current syscalls. A positive Tier 1 result requires an observed stdin wait: direct `read(0)`, a permitted read of a `select`/`pselect6` or `poll`/`ppoll` argument containing fd 0, or an epoll interest list containing fd 0. The waiting thread's `/proc/<pid>/task/<tid>/fd/0` must identify the shell's controlling terminal device, so a thread-local fd table cannot substitute its leader's terminal descriptor and a pipeline reader blocked on its pipe remains a running command. Direct PTY descriptors use their device number; `/dev/tty` uses the owning process's `tty_nr` because `stat` reports the alias device rather than the selected PTY. A wait already present before terminal input is not post-write readiness: the same PGID must be observed outside that wait before re-entering it, while a changed foreground PGID is new evidence. Unreadable process memory and unrecognized syscalls are misses, never positive guesses. Architecture tables contain only syscall numbers defined by the corresponding Linux UAPI; the inspector admits the runtime architecture, then checks every supported kernel ABI because `/proc` can report a different ABI under user-mode emulation. Unsupported runtime architectures skip Tier 1.
|
||||
On Linux, the inspector reads the shell's terminal foreground PGID from `/proc/<shellPid>/stat`, enumerates every process and thread in that process group, and probes their current syscalls. A positive Tier 1 result requires an observed stdin wait: direct `read(0)`, a permitted read of a `select`/`pselect6` or `poll`/`ppoll` argument containing fd 0, or an epoll interest list containing fd 0. The waiting thread's `/proc/<pid>/task/<tid>/fd/0` must identify the shell's controlling terminal device, so a thread-local fd table cannot substitute its leader's terminal descriptor and a pipeline reader blocked on its pipe remains a running command. Direct PTY descriptors use their device number; `/dev/tty` uses the owning process's `tty_nr` because `stat` reports the alias device rather than the selected PTY. A wait already present before terminal input is not post-write readiness: the same PGID must be observed outside that wait before re-entering it, while a changed foreground PGID is new evidence. Unreadable process memory and unrecognized syscalls are misses, never positive guesses. A host policy that denies `/proc/<pid>/task/<tid>/syscall`, including hardened ptrace policy, likewise skips Tier 1 and preserves the bounded Tier 2 idle inference; process sleep state never substitutes for inaccessible syscall evidence. Architecture tables contain only syscall numbers defined by the corresponding Linux UAPI; the inspector admits the runtime architecture, then checks every supported kernel ABI because `/proc` can report a different ABI under user-mode emulation. Unsupported runtime architectures skip Tier 1.
|
||||
|
||||
On macOS there is no exact syscall tier. Output silence returns `inferred_idle` for any foreground process group, including Python and `gdb`; `ps`-derived terminal PGID is used for signaling, not as proof that only the shell can be idle. Pure process-inspector logic is injectable and unit-tested on Linux, while a macOS CI job exercises the real PTY and process-table path.
|
||||
|
||||
@@ -158,7 +158,7 @@ The package ships concise tool guidance explaining persistent state, owner isola
|
||||
|
||||
- Per-file coverage pins owner fencing, concurrent reservations, cancellation during pre-write inspection, unpublished-spawn cancellation and awaited teardown, sandbox-mode change rejection, retriable lifecycle cleanup, readiness tiers, rejection of pre-write stdin waits and delayed earlier prompts, the configured handoff grace holding the idle fallback past one poll and its rejection below `pollIntervalMs`, sanitizer carry state, complete UTF-8 bounds, task integration, schemas, and exact render intents.
|
||||
- Subprocess process fixtures cover non-leader and non-main-thread stdin waits, thread-local fd tables, the `/dev/tty` alias, supported kernel ABIs under user-mode emulation, rejection of fd 0 backed by a pipe, zombie quiescence, unreadable process state, unsupported architectures, and other false-positive rejection; macOS inspector logic is injected into the same unit suite.
|
||||
- Real `node-pty` and PTY-consumer tests jointly exercise shell state, controlling-terminal input through `/dev/tty`, shared sandbox policy, environment scrubbing, raw-mode foreground `SIGINT`, a TERM-ignoring descendant, and immediate post-disposal quiescence on supported hosts.
|
||||
- Real `node-pty` and PTY-consumer tests jointly exercise shell state, controlling-terminal input through `/dev/tty`, the exact attribution when process syscalls are readable, its bounded idle fallback when host policy denies them, shared sandbox policy, environment scrubbing, raw-mode foreground `SIGINT`, a TERM-ignoring descendant, and immediate post-disposal quiescence on supported hosts.
|
||||
- A Loader-driven `cordis.yml` test mounts the real three-package composition and verifies that delayed pipeline output returns with the completed command instead of being classified as terminal-input readiness. The SDK minimal snapshot pins that output through the persistent Bash tool; ACP and headless snapshots pin the six terminal schemas, bounded results, and errors through opt-in overlays; TUI snapshots pin terminal and generic card presentation.
|
||||
- Package contracts, the architecture map, subsystem pages, generated catalogs, and the website API describe the same shipped surface.
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ UI 渲染约定精确且不携带位置信息。`terminal_send` 只为前台发
|
||||
|
||||
本地后端先识别受控 bash 启动时发出的私有 OSC prompt marker,并且只有在最近一个 marker 后的可打印尾部与受控 `PS1` 完全相等时才声明 prompt 就绪;除此之外,它还运行 3 个有界 fallback 层级。在 data callback 之间保留该尾部,可以适配 marker 与 prompt 被分开交付的情况;如果回显的输入或输出跟在延迟到达的先前 prompt 之后,要求尾部完全相等会拒绝该 prompt,使其无法完成当前 send。marker 在输出到达模型前被移除,使两个平台上的普通 shell 命令都无需固定等待静默阈值。尚未发布的 startup 不会把零输出静默视为就绪;timeout 会拒绝 spawn。若调用方取消在 startup 期间胜出,后端会关闭私有会话并原样抛出 `AbortSignal.reason`;尚不可观察的前台 PGID 不会再用查找错误覆盖取消原因。所有时间参数都是经校验的配置字段:`pollIntervalMs`、`exactProbeAfterMs`、`idleSilenceMs`、`handoffGraceMs` 和 `timeoutMs`。
|
||||
|
||||
在 Linux 上,检查器从 `/proc/<shellPid>/stat` 读取 shell 的终端前台 PGID,枚举该进程组中的每个进程与线程,并检查它们当前的 syscall。Tier 1 只有观察到 stdin 等待才返回正结果:直接 `read(0)`、获准读取且含 fd 0 的 `select`/`pselect6` 或 `poll`/`ppoll` 参数,或者含 fd 0 的 epoll interest list。等待线程的 `/proc/<pid>/task/<tid>/fd/0` 还必须标识 shell 的控制终端设备,因此线程本地 fd 表无法用 leader 的终端描述符冒充自身 fd,阻塞于管道的流水线读取端仍属于正在运行的命令。直接 PTY 描述符使用其设备号;`/dev/tty` 则使用所属进程的 `tty_nr`,因为 `stat` 报告的是别名设备而非选定的 PTY。终端输入前就已存在的等待并不代表写入后就绪:必须先观察到同一 PGID 脱离该等待,之后再次进入等待才能使该次 send 完成;前台 PGID 发生变化则构成新的证据。无法读取的进程内存和未识别的 syscall 都是 miss,绝不作为正向猜测。架构表只包含对应 Linux UAPI 定义的 syscall number;检查器先准入运行时架构,再检查每个受支持的内核 ABI,因为在用户态模拟下,`/proc` 可能报告不同的 ABI。不受支持的运行时架构会跳过 Tier 1。
|
||||
在 Linux 上,检查器从 `/proc/<shellPid>/stat` 读取 shell 的终端前台 PGID,枚举该进程组中的每个进程与线程,并检查它们当前的 syscall。Tier 1 只有观察到 stdin 等待才返回正结果:直接 `read(0)`、获准读取且含 fd 0 的 `select`/`pselect6` 或 `poll`/`ppoll` 参数,或者含 fd 0 的 epoll interest list。等待线程的 `/proc/<pid>/task/<tid>/fd/0` 还必须标识 shell 的控制终端设备,因此线程本地 fd 表无法用 leader 的终端描述符冒充自身 fd,阻塞于管道的流水线读取端仍属于正在运行的命令。直接 PTY 描述符使用其设备号;`/dev/tty` 则使用所属进程的 `tty_nr`,因为 `stat` 报告的是别名设备而非选定的 PTY。终端输入前就已存在的等待并不代表写入后就绪:必须先观察到同一 PGID 脱离该等待,之后再次进入等待才能使该次 send 完成;前台 PGID 发生变化则构成新的证据。无法读取的进程内存和未识别的 syscall 都是 miss,绝不作为正向猜测。宿主策略(包括加固的 ptrace 策略)若拒绝读取 `/proc/<pid>/task/<tid>/syscall`,同样会跳过 Tier 1 并保留有界的 Tier 2 idle 推断;进程休眠状态绝不会代替不可访问的 syscall 证据。架构表只包含对应 Linux UAPI 定义的 syscall number;检查器先准入运行时架构,再检查每个受支持的内核 ABI,因为在用户态模拟下,`/proc` 可能报告不同的 ABI。不受支持的运行时架构会跳过 Tier 1。
|
||||
|
||||
macOS 没有精确 syscall 层。任何前台进程组输出静默都会返回 `inferred_idle`,包括 Python 和 `gdb`;从 `ps` 推导的终端 PGID 只用于发送信号,不作为「只有 shell 才能 idle」的证明。纯进程检查逻辑可注入,并在 Linux 上经过单元测试,同时由 macOS CI job 驱动真实 PTY 和进程表路径。
|
||||
|
||||
@@ -158,7 +158,7 @@ plugins:
|
||||
|
||||
- 逐文件覆盖测试锁定了 owner 隔离、并发预留、写入前检查期间的取消、未发布 spawn 的取消与等待式 teardown、沙箱模式变更拒绝、可重试的生命周期清理、就绪层级、对写入前 stdin 等待与延迟到达的先前 prompt 的拒绝、配置化交接宽限把 idle fallback 顶过一次轮询以及低于 `pollIntervalMs` 时的拒绝、sanitizer carry state、完整 UTF-8 结果上限、task 集成、schema 和精确 render intent。
|
||||
- 子进程 fixture(测试前置数据)覆盖非 leader 与非主线程的 stdin 等待、线程本地 fd 表、`/dev/tty` 别名、用户态模拟下受支持的内核 ABI、拒绝把指向管道的 fd 0 当作终端输入、僵尸进程完全停稳、不可读进程状态、不支持的架构和其他误报拒绝;同一单元测试套件通过注入覆盖 macOS 检查器逻辑。
|
||||
- 真实 `node-pty` 与 PTY 消费方测试共同在受支持宿主上覆盖 shell 状态、通过 `/dev/tty` 读取控制终端输入、共享沙箱策略、环境清洗、raw mode 前台 `SIGINT`、忽略 `SIGTERM` 的后代进程,以及 dispose 返回后立即完全停稳。
|
||||
- 真实 `node-pty` 与 PTY 消费方测试共同在受支持宿主上覆盖 shell 状态、通过 `/dev/tty` 读取控制终端输入、进程 syscall 可读时的精确归因、宿主策略拒绝读取时的有界 idle fallback、共享沙箱策略、环境清洗、raw mode 前台 `SIGINT`、忽略 `SIGTERM` 的后代进程,以及 dispose 返回后立即完全停稳。
|
||||
- Loader 驱动的 `cordis.yml` 测试挂载真实三包组合,并验证延迟到达的流水线输出随已完成命令返回,而不会被归类为终端输入就绪。SDK minimal 快照通过持久 Bash 工具固定该输出;ACP 与 headless 快照通过 opt-in overlay 固定 6 个终端 schema、有界结果和错误;TUI 快照固定 terminal 与 generic 卡片展示。
|
||||
- 包约定、架构图、子系统页面、生成目录和 website API 描述同一个已发布接口。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subprocess/subprocess-local/README.md
|
||||
README.md: b3e74b75b69dc4c726749b4798f24f3058749ea9
|
||||
README.zh.md: bdbbc723fd89574425b3391bc6c4d9639b0cfdac
|
||||
README.md: 9dc9efff220f8d1e60766438be39c3a5a0708cfa
|
||||
README.zh.md: 0090942019a693025f653a0f52fc4f4243fb679a
|
||||
|
||||
@@ -11,7 +11,7 @@ Local Service Provider for the [`@deepseek-ai/dsh-subprocess`](../subprocess/REA
|
||||
- **Credential scrub + explicit merge** — `process.env` minus credential-shaped vars (`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`) and all ambient `DSH_*` names; the spec's explicit `env` merges after that scrub with no namespace validation, so a deliberately supplied credential or current `DSH_*` fact wins while stale nested-harness identity cannot leak in ambiently. Supplied stdin is written and closed; otherwise fd 0 is `/dev/null`. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
|
||||
- **Offset-based reads** — collect-mode readers return deltas in whole-stream byte coordinates; the service never holds a cursor, so consumer-owned cursors (the bash background read path) and full-stream re-reads coexist, before and after settlement.
|
||||
- **Executable lookup** — `resolveExecutable` checks absolute files or searches the scrubbed effective PATH with platform-aware executable extensions; relative paths containing separators are rejected at the seam, and relative PATH entries resolve from the host process cwd.
|
||||
- **Terminal-process ownership** — `spawnTerminal` allocates `node-pty`, bridges UTF-8 terminal text, inspects and signals the current foreground process group, and exposes one awaited termination operation that sweeps descendants before and after terminating the top-level shell. Linux reports an exact input wait only when the waiting thread's own fd 0 identifies the shell's controlling terminal, including the `/dev/tty` alias, so a pipeline reader blocked on `pipe:[…]` cannot publish terminal readiness. The syscall probe admits supported runtime architectures and matches every supported kernel ABI so user-mode emulation cannot hide the wait. Each foreground inspection retains exact identities from the rooted tree; Linux also enumerates the POSIX session after its leader exits. A previously observed macOS descendant and any same-session Linux member therefore remain fenced after reparenting, while pid/start identity prevents cleanup from following PID reuse. On Windows the koffi-backed inspector enumerates the process table through Toolhelp32, combines GetProcessTimes start identities with zero-time process-handle waits for liveness, reports the shell pid as the pseudo foreground group (Windows has no POSIX groups), and teardown verifies the shell's termination because externally taskkilled shells may never fire node-pty's exit notification. The higher PTY backend owns prompt readiness, buffers, and model-facing operations.
|
||||
- **Terminal-process ownership** — `spawnTerminal` allocates `node-pty`, bridges UTF-8 terminal text, inspects and signals the current foreground process group, and exposes one awaited termination operation that sweeps descendants before and after terminating the top-level shell. Linux reports an exact input wait only when the waiting thread's own fd 0 identifies the shell's controlling terminal, including the `/dev/tty` alias, so a pipeline reader blocked on `pipe:[…]` cannot publish terminal readiness. The syscall probe admits supported runtime architectures and matches every supported kernel ABI so user-mode emulation cannot hide the wait. When Linux denies `/proc/<pid>/task/<tid>/syscall`, the inspector reports no exact wait and leaves the higher PTY backend to its configured idle inference; process sleep state never substitutes for syscall evidence. Each foreground inspection retains exact identities from the rooted tree; Linux also enumerates the POSIX session after its leader exits. A previously observed macOS descendant and any same-session Linux member therefore remain fenced after reparenting, while pid/start identity prevents cleanup from following PID reuse. On Windows the koffi-backed inspector enumerates the process table through Toolhelp32, combines GetProcessTimes start identities with zero-time process-handle waits for liveness, reports the shell pid as the pseudo foreground group (Windows has no POSIX groups), and teardown verifies the shell's termination because externally taskkilled shells may never fire node-pty's exit notification. The higher PTY backend owns prompt readiness, buffers, and model-facing operations.
|
||||
- **Terminate-and-join disposal** — the service retains live handles so its own disposal can escalate every running tree and await its exit; quiescent and spawn-failed handles leave the live set after whole-tree or terminal-session cleanup finishes.
|
||||
- **Synchronous host-exit finalization** — while the service effect is active, a Node `exit` listener force-terminates every ordinary tree and observable terminal session still in the same live sets. The local-only operations send POSIX SIGKILL to the managed group, run Windows `taskkill /T /F`, and synchronously signal captured/current terminal identities around the PTY root kill; they create no promise or timer, preserve the host's exit code and diagnostic, contain each target's failure, and do not claim quiescence. Normal disposal keeps the awaited graceful path above. See the [host-exit cleanup decision](../../../.agents/notes/implemented/bug-fix/2026-08-11-synchronous-subprocess-exit-cleanup.md).
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- **凭据清除 + 显式合并**:以 `process.env` 为基础,移除形似凭据的变量(`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`)和所有环境中已有的 `DSH_*` 名称;spec 的显式 `env` 在该清除之后合并且不做命名空间校验,因此有意提供的凭据或当前 `DSH_*` 事实会胜出,而陈旧的嵌套 harness 身份无法从环境中隐式漏入。提供的 stdin 会被写入后关闭;否则 fd 0 指向 `/dev/null`。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.zh.md)与[受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.zh.md)。
|
||||
- **基于偏移量的读取**:收集模式的读取器按完整流的字节坐标返回增量;服务自身从不持有游标,因此消费方自有的游标(bash 的后台读取路径)与完整流重读可以共存,结算前后皆然。
|
||||
- **可执行文件查找**:`resolveExecutable` 检查绝对文件,或根据平台可执行文件扩展名在清理后的有效 PATH 中搜索;含分隔符的相对路径在该 seam 处被拒绝,相对 PATH 条目从宿主进程 cwd 解析。
|
||||
- **终端进程所有权**:`spawnTerminal` 分配 `node-pty`,桥接 UTF-8 终端文本,检查当前前台进程组并向其发送信号,还会公开一项须等待的终止操作,在终止顶层 shell 前后清理后代进程。Linux 只有在等待线程自身的 fd 0 标识 shell 的控制终端(包括 `/dev/tty` 别名)时才报告精确输入等待,因此阻塞于 `pipe:[…]` 的流水线读取端无法发布终端就绪状态。syscall 探针会准入受支持的运行时架构并匹配每个受支持的内核 ABI,使用户态模拟无法隐藏该等待。每次前台检查都会保留根进程树中的精确身份;Linux 还会在 POSIX 会话 leader 退出后枚举该会话。因此,之前观察到的 macOS 后代以及同会话 Linux 成员在重新设定父进程后仍受围栏保护,pid/start 身份则防止清理跟随 PID 复用。在 Windows 上,基于 koffi 的检查器通过 Toolhelp32 枚举进程表,把 GetProcessTimes 启动身份与进程句柄零时等待结合起来判断存活状态,并把 shell pid 作为伪前台进程组(Windows 没有 POSIX 进程组)。拆卸会验证 shell 已终止,因为被外部 taskkill 的 shell 可能永远不会触发 node-pty 的退出通知。上层 PTY 后端负责提示符就绪、缓冲区与面向模型的操作。
|
||||
- **终端进程所有权**:`spawnTerminal` 分配 `node-pty`,桥接 UTF-8 终端文本,检查当前前台进程组并向其发送信号,还会公开一项须等待的终止操作,在终止顶层 shell 前后清理后代进程。Linux 只有在等待线程自身的 fd 0 标识 shell 的控制终端(包括 `/dev/tty` 别名)时才报告精确输入等待,因此阻塞于 `pipe:[…]` 的流水线读取端无法发布终端就绪状态。syscall 探针会准入受支持的运行时架构并匹配每个受支持的内核 ABI,使用户态模拟无法隐藏该等待。当 Linux 拒绝读取 `/proc/<pid>/task/<tid>/syscall` 时,检查器不会报告精确等待,而是由上层 PTY 后端按配置执行 idle 推断;进程休眠状态绝不会代替 syscall 证据。每次前台检查都会保留根进程树中的精确身份;Linux 还会在 POSIX 会话 leader 退出后枚举该会话。因此,之前观察到的 macOS 后代以及同会话 Linux 成员在重新设定父进程后仍受围栏保护,pid/start 身份则防止清理跟随 PID 复用。在 Windows 上,基于 koffi 的检查器通过 Toolhelp32 枚举进程表,把 GetProcessTimes 启动身份与进程句柄零时等待结合起来判断存活状态,并把 shell pid 作为伪前台进程组(Windows 没有 POSIX 进程组)。拆卸会验证 shell 已终止,因为被外部 taskkill 的 shell 可能永远不会触发 node-pty 的退出通知。上层 PTY 后端负责提示符就绪、缓冲区与面向模型的操作。
|
||||
- **先终止再等待退出的 dispose(资源释放)**:服务保留存活句柄,使自身的 dispose 能对每个仍在运行的进程树执行升级并等待其退出;完全停稳与 spawn 失败的句柄会在整棵进程树或 terminal session 清理完成后离开存活集合。
|
||||
- **同步宿主退出最终清理**:服务 effect 仍有效时,Node `exit` listener 会强制终止同一组存活集合中仍存在的每棵普通进程树和可观察 terminal session。这些仅供本地实现使用的操作会向受管 POSIX 进程组发送 SIGKILL、在 Windows 运行 `taskkill /T /F`,并在终止 PTY root 前后同步向已捕获及当前可观察的 terminal 身份发送信号;它们不会创建 Promise 或 timer,不改变宿主退出码与诊断,会分别包含每个目标的失败,也不会声称已经完全停稳。正常 dispose 仍使用上面的须等待温和路径。参见[宿主退出清理决策](../../../.agents/notes/implemented/bug-fix/2026-08-11-synchronous-subprocess-exit-cleanup.zh.md)。
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, mkdtempSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync } from 'node:fs'
|
||||
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -118,53 +118,15 @@ function processIsRunning(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function linuxBashProcDiagnostics(): string {
|
||||
const read = (path: string): string => {
|
||||
try {
|
||||
return readFileSync(path, 'utf8').trim()
|
||||
} catch (error) {
|
||||
return error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
}
|
||||
function canReadLinuxProcessSyscall(pid: number): boolean {
|
||||
try {
|
||||
readFileSync(`/proc/${pid}/task/${pid}/syscall`, 'utf8')
|
||||
return true
|
||||
} catch (error) {
|
||||
const code = (error as NodeJS.ErrnoException).code
|
||||
if (code === 'EACCES' || code === 'EPERM') return false
|
||||
throw error
|
||||
}
|
||||
const ps = spawnSync('/bin/ps', ['-eo', 'pid=,ppid=,pgid=,sid=,stat=,args='], { encoding: 'utf8' })
|
||||
const bash = ps.stdout.split('\n').filter(line => /(?:^|\s)(?:\/bin\/)?bash(?:\s|$)/.test(line))
|
||||
const processes = bash.map((line) => {
|
||||
const pid = /^\s*(\d+)/.exec(line)?.[1]
|
||||
if (pid === undefined) return { line }
|
||||
let tids: string[]
|
||||
try {
|
||||
tids = readdirSync(`/proc/${pid}/task`)
|
||||
} catch (error) {
|
||||
return { line, tasks: error instanceof Error ? `${error.name}: ${error.message}` : String(error) }
|
||||
}
|
||||
return {
|
||||
line,
|
||||
stat: read(`/proc/${pid}/stat`),
|
||||
tasks: tids.map((tid) => {
|
||||
const fd = `/proc/${pid}/task/${tid}/fd/0`
|
||||
let fd0: string
|
||||
let rdev: string
|
||||
try {
|
||||
fd0 = readlinkSync(fd)
|
||||
} catch (error) {
|
||||
fd0 = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
}
|
||||
try {
|
||||
const device = statSync(fd)
|
||||
rdev = `${device.rdev} character=${device.isCharacterDevice()}`
|
||||
} catch (error) {
|
||||
rdev = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
|
||||
}
|
||||
return { tid, syscall: read(`/proc/${pid}/task/${tid}/syscall`), fd0, rdev }
|
||||
}),
|
||||
}
|
||||
})
|
||||
return JSON.stringify({
|
||||
arch: process.arch,
|
||||
machine: spawnSync('/bin/uname', ['-m'], { encoding: 'utf8' }).stdout.trim(),
|
||||
ptraceScope: read('/proc/sys/kernel/yama/ptrace_scope'),
|
||||
processes,
|
||||
}, undefined, 2)
|
||||
}
|
||||
|
||||
// The real-shell suite drives a POSIX bash over the actual node-pty terminal;
|
||||
@@ -213,23 +175,27 @@ describe.skipIf(process.platform === 'win32')('terminal-bash real shell', () =>
|
||||
}, 20_000)
|
||||
|
||||
it.skipIf(process.platform !== 'linux')('recognizes a foreground read opened through /dev/tty', async () => {
|
||||
const { ctx, agent } = await harness('danger-full-access', {
|
||||
const { ctx, root, agent } = await harness('danger-full-access', {
|
||||
idleSilenceMs: 5_000,
|
||||
timeoutMs: 8_000,
|
||||
})
|
||||
const created = await ctx.terminals.spawn(agent, { type: 'shell' })
|
||||
const readerPidFile = join(root, 'tty-reader.pid')
|
||||
|
||||
const waiting = ctx.terminals.startSend(agent, created.sessionId, {
|
||||
text: 'bash -c \'exec </dev/tty; printf "WAITING\\n"; read -r answer; printf "ANSWER=%s\\n" "$answer"\'',
|
||||
text: `bash -c 'exec </dev/tty; printf "%s" "$BASHPID" > "$1"; printf "WAITING\\n"; read -r answer; printf "ANSWER=%s\\n" "$answer"' dsh "${readerPidFile}"`,
|
||||
submit: true,
|
||||
})
|
||||
await waitForOutput(waiting, 'WAITING')
|
||||
expect((await waiting.done).waitReason, linuxBashProcDiagnostics()).toBe('stdin_read')
|
||||
const result = await waiting.done
|
||||
const readerPid = Number(readFileSync(readerPidFile, 'utf8'))
|
||||
expect(readerPid).toBeGreaterThan(0)
|
||||
expect(result.waitReason).toBe(canReadLinuxProcessSyscall(readerPid) ? 'stdin_read' : 'inferred_idle')
|
||||
|
||||
const answer = ctx.terminals.startSend(agent, created.sessionId, { text: 'accepted', submit: true })
|
||||
const result = await answer.done
|
||||
expect(result.waitReason).toBe('stdin_read')
|
||||
expect(result.viewport).toContain('ANSWER=accepted')
|
||||
const answered = await answer.done
|
||||
expect(answered.waitReason).toBe('stdin_read')
|
||||
expect(answered.viewport).toContain('ANSWER=accepted')
|
||||
await ctx.terminals.kill(agent, created.sessionId)
|
||||
}, 20_000)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user