Merge remote-tracking branch 'origin/worktree/context-source-cards' into worktree/context-forms-remaining

# Conflicts:
#	apps/web/tests/snapshots/queue-actions/layout.expected.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/core-data-structures/goal.i18n.yaml
#	docs/core-data-structures/goal.md
#	docs/core-data-structures/goal.zh.md
#	docs/event-producer-consumer.md
#	examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl
#	examples/acp-agent/tests/goal-snapshots/goal-wrapup/session.expected.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/acp-agent/tests/snapshots/bash-spill/session.jsonl
#	examples/acp-agent/tests/snapshots/bash-tool-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl
#	examples/acp-agent/tests/snapshots/cancel/session.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl
#	examples/acp-agent/tests/snapshots/error-finish/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-edit/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-read/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl
#	examples/acp-agent/tests/snapshots/fs-write/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-invalid-matcher/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-invalid-matcher/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl
#	examples/acp-agent/tests/snapshots/lsp-definition/session.jsonl
#	examples/acp-agent/tests/snapshots/missing-sandbox-runner/session.jsonl
#	examples/acp-agent/tests/snapshots/multi-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/packed-chunks/session.jsonl
#	examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl
#	examples/acp-agent/tests/snapshots/partial-landlock-child-failure/session.jsonl
#	examples/acp-agent/tests/snapshots/pty-tools/session.jsonl
#	examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl
#	examples/acp-agent/tests/snapshots/session-query-spill/session.jsonl
#	examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl
#	examples/acp-agent/tests/snapshots/session-title-after-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl
#	examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl
#	examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl
#	examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-published-run-failure/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-report/session.jsonl
#	examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl
#	examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/todo-write/session.jsonl
#	examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/web-fetch/session.jsonl
#	examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl
#	examples/acp-agent/tests/snapshots/workflow-run/session.jsonl
#	examples/acp-agent/tests/snapshots/workspace-context/session.jsonl
#	examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl
#	examples/headless-agent/tests/snapshots/goal-tools/stream-json.expected.jsonl
#	examples/headless-agent/tests/snapshots/pty-tools/session.jsonl
#	examples/headless-agent/tests/snapshots/pty-tools/stream-json.expected.jsonl
#	examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl
#	examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl
#	examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl
#	examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl
#	packages/bash/tool-bash/tests/integration.spec.ts
#	packages/context/time-context/src/index.ts
#	packages/context/tmux-context/src/index.ts
#	packages/core/agent-loop/src/agent.ts
#	packages/core/system-prompt/src/index.ts
#	packages/goal/goal/src/domain.ts
#	packages/goal/goal/src/index.ts
#	packages/goal/goal/src/render.ts
#	packages/plan/plan-mode/src/index.ts
This commit is contained in:
creatixchu
2026-08-06 11:49:03 +08:00
1357 changed files with 27674 additions and 18803 deletions

View File

@@ -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/bash/README.md
README.md: ef82e9f4684ecf551ac7701d812088dd6b2ef6d0
README.zh.md: 84ff244ec3d1ff5d385a3eb334e4cf9f1fe31e03
README.md: 601782caad24d3555a206365a3f1954d81af1cf0
README.zh.md: 8b96c4f80ba8776bfd8bdde178178cea946ff36c

View File

@@ -2,16 +2,16 @@
English | [中文](README.zh.md)
The capability family spans the canonical executor seam, its implementations, the shared shell environment, and the model-facing tools. All **product** packages.
The capability family spans the canonical executor seam, its implementations, the shared shell environment, and the model-facing tools. All are **product** packages.
| Package | Role | ctx key |
|---|---|---|
| `bash/` | Abstract bash executor seam (interface + vocabulary; sandbox result facts carry the [`sandbox/`](../sandbox/README.md) seam's mode/enforcement vocabulary, and the managed-env/output vocabulary is re-exported from the [`subprocess/`](../subprocess/README.md) seam) | `ctx.bash` |
| `bash-local/` | Local `BashExecutor` implementation over the [`subprocess/`](../subprocess/README.md) service (command defaulting, deadlines, terminal env, background-read merge) | (registers `ctx.bash`) |
| `bash-sandbox/` | Sandbox-consuming `BashExecutor` (wraps every command argv via `ctx.sandbox`, stamps denial/enforcement facts; extends `bash-local`'s mechanics) | (registers `ctx.bash`) |
| `pwsh-local/` | Local PowerShell `BashExecutor` implementation over the [`subprocess/`](../subprocess/README.md) service (executable resolution, UTF-8-pinned spawn, Windows termination semantics) | (registers `ctx.bash`) |
| `bash-env/` | Tool-independent managed `DSH_*` shell environment registry shared by the shell tools (built-in facts + effect-scoped contributors) | (registers `ctx.bashEnv`) |
| `tool-bash/` | Model-facing `bash` schema; background processes register with the generic [`tasks/`](../tasks/README.md) runtime | (registers on `ctx.tools`) |
| `tool-pwsh/` | Model-facing PowerShell-dialect `pwsh` schema (behavior mirrors `tool-bash` minus the sandbox surface); background processes register with the generic [`tasks/`](../tasks/README.md) runtime | (registers on `ctx.tools`) |
| [`bash/`](bash/README.md) | Defines the executor contract shared by implementations and consumers. | `ctx.bash` |
| [`bash-local/`](bash-local/README.md) | Executes commands through the local [`subprocess`](../subprocess/README.md) service. | (registers `ctx.bash`) |
| [`bash-sandbox/`](bash-sandbox/README.md) | Applies the configured [`sandbox`](../sandbox/README.md) backend before local execution. | (registers `ctx.bash`) |
| [`pwsh-local/`](pwsh-local/README.md) | Executes PowerShell commands with Windows-specific process behavior. | (registers `ctx.bash`) |
| [`bash-env/`](bash-env/README.md) | Provides the managed `DSH_*` environment shared by shell tools. | `ctx.bashEnv` |
| [`tool-bash/`](tool-bash/README.md) | Exposes Bash execution and background-task integration to the model. | (registers on `ctx.tools`) |
| [`tool-pwsh/`](tool-pwsh/README.md) | Exposes PowerShell execution to the model. | (registers on `ctx.tools`) |
The interface lives at `bash/bash/`. `bash-sandbox` replacing `bash-local` without touching the interface or the tool is the split doing exactly what it exists for — a leaf `cordis.yml` picks one executor entry, plus a `ctx.sandbox` provider entry for the confined one (see [the acp-agent example's default composition](../../examples/acp-agent/)).
A leaf `cordis.yml` selects one executor implementation and the model-facing tools it needs. A sandboxed composition also selects a `ctx.sandbox` provider; the [ACP example](../../examples/acp-agent/) shows one complete wiring.

View File

@@ -1,17 +1,17 @@
# bash/bash 能力家族
# bash/bash 能力家族
[English](README.md) | 中文
能力家族横跨规范执行器 seam、其实现、共享 shell 环境面向模型的工具。这些全是**产品**包。
能力家族涵盖规范执行器 seam、其实现、共享 shell 环境面向模型的工具。这些全是**产品**包。
| 包 | 职责 | ctx key |
|---|---|---|
| `bash/` | 抽象 bash 执行器 seam接口 + 词汇;沙箱结果事实携带 [`sandbox/`](../sandbox/README.md) seam 的模式/强制执行词汇,受管环境/输出词汇则从 [`subprocess/`](../subprocess/README.md) seam 重导出) | `ctx.bash` |
| `bash-local/` | 构建在 [`subprocess/`](../subprocess/README.md) 服务之上的本地 `BashExecutor` 实现命令默认值补全、deadline、终端环境、后台读取合并 | (注册 `ctx.bash` |
| `bash-sandbox/` | 消费沙箱的 `BashExecutor`(通过 `ctx.sandbox` 包装每个命令 argv标记拒绝强制执行事实扩展 `bash-local` 的机制) | (注册 `ctx.bash` |
| `pwsh-local/` | 构建在 [`subprocess/`](../subprocess/README.md) 服务之上的本地 PowerShell `BashExecutor` 实现可执行文件解析、UTF-8 固定 spawn、Windows 终止语义) | (注册 `ctx.bash` |
| `bash-env/` | 工具无关的受管 `DSH_*` shell 环境注册表,由 shell 工具共享(内置事实 + 受 effect 作用域约束的 contributor | (注册 `ctx.bashEnv` |
| `tool-bash/` | 面向模型的 `bash` schema后台进程注册到通用 [`tasks/`](../tasks/README.md) 运行时 | (注册到 `ctx.tools` |
| `tool-pwsh/` | 面向模型的 PowerShell 方言 `pwsh` schema行为镜像 `tool-bash`,减去 sandbox 面);后台进程注册到通用 [`tasks/`](../tasks/README.md) 运行时 | (注册到 `ctx.tools` |
| [`bash/`](bash/README.md) | 定义实现与消费方共享的执行器契约。 | `ctx.bash` |
| [`bash-local/`](bash-local/README.md) | 通过本地 [`subprocess`](../subprocess/README.md) 服务执行命令。 | (注册 `ctx.bash` |
| [`bash-sandbox/`](bash-sandbox/README.md) | 在本地执行前应用已配置的 [`sandbox`](../sandbox/README.md) 后端。 | (注册 `ctx.bash` |
| [`pwsh-local/`](pwsh-local/README.md) | 以 Windows 专用进程行为执行 PowerShell 命令。 | (注册 `ctx.bash` |
| [`bash-env/`](bash-env/README.md) | 提供 shell 工具共享的托管 `DSH_*` 环境。 | `ctx.bashEnv` |
| [`tool-bash/`](tool-bash/README.md) | 向模型公开 Bash 执行和后台任务集成。 | (注册到 `ctx.tools` |
| [`tool-pwsh/`](tool-pwsh/README.md) | 向模型公开 PowerShell 执行。 | (注册到 `ctx.tools` |
接口位于 `bash/bash/`。以 `bash-sandbox` 替换 `bash-local`,同时不改动接口或工具,正是这种拆分存在的意义:叶级 `cordis.yml` 选择一个执行器插件条目;受限实现还需再选择一个 `ctx.sandbox` 提供方插件条目(见 [acp-agent 示例的默认组合](../../examples/acp-agent/)
叶节点 `cordis.yml` 选择一个执行器实现和所需的面向模型工具。沙箱化组合还会选择一个 `ctx.sandbox` 提供方[ACPAgent Client Protocol示例](../../examples/acp-agent/)展示一套完整接线

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/bash/bash-env/README.md
README.md: 7b939326d4effd14fc83ef0ad4e133f019f1011f
README.zh.md: aeb33629def3fcc10294bbca19b37d43dcc73a0c
README.zh.md: b6f3aca41771f1ca990a4b708cd53119b8e8ad78

View File

@@ -17,7 +17,7 @@
## Managed environment
每次前台与后台模型 shell 调用都会收到一份新收集的受信任 `DSH_*` 环境。`DSH_HOME` 是由 [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) 解析的 Harness 主目录绝对路径(`dshHome` 配置,然后环境变量 `$DSH_HOME`,然后 `~/.dsh``DSH_SHELL=1` 标识受管理的子进程。带 agent 的调用额外收到 `DSH_SESSION_ID=agent.session.header.id`;当活动的持久化 seam 定位到 JSONL 工件时,它们还会收到 `DSH_SESSION_JSONL=<绝对目标路径>`。JSONL 路径只是位置提示:首次 flush 之前它可能不存在,也不一定包含当前缓冲中的轮次,并且它不是授权凭据。
每次前台与后台模型 shell 调用都会收到一份新收集的受信任 `DSH_*` 环境。`DSH_HOME` 是由 [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) 解析的 Harness 主目录绝对路径(`dshHome` 配置,然后环境变量 `$DSH_HOME`,然后 `~/.dsh``DSH_SHELL=1` 标识受管理的子进程。带 agent(智能体)的调用额外收到 `DSH_SESSION_ID=agent.session.header.id`;当活动的持久化 seam 定位到 JSONL 工件时,它们还会收到 `DSH_SESSION_JSONL=<绝对目标路径>`。JSONL 路径只是位置提示:首次 flush 之前它可能不存在,也不一定包含当前缓冲中的轮次,并且它不是授权凭据。
`ctx.bashEnv` 负责收集。其他插件可以注册一个受 effect 作用域约束的 contributor带有稳定名称、已声明的键/描述以及 `resolve(execution: ToolExecution)`;重复所有权与未声明的运行时键会响亮失败,而 `list()` 只枚举声明、不执行 provider。Harness 内置键保留 `DSH_HOME``DSH_SHELL``DSH_SESSION_ID`;本插件的持久化翻译器通过读取与后端无关的 `sessionPersistence.locate()` seam 拥有 `DSH_SESSION_JSONL`

View File

@@ -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/bash/bash-local/README.md
README.md: 694b7a7686ea6c38da5a354ff6b6e6d2c4520706
README.zh.md: 8d6e3b8d2fdd92faeb7b1e6e373a938ae2064cd4
README.md: bb87ad6fe021e3144cef4adced3d798bf3d94d67
README.zh.md: d2f8c9091072bbf3d75909f6826432601001ab88

View File

@@ -20,15 +20,13 @@ The package root exports the default and named `LocalBashExecutor` plugin plus i
graceMs: 3000 # kill escalation and post-exit pipe-drain grace
```
## Behavior (and where it came from)
## Behavior
Design surveyed against the bash tools of Claude Code, OpenCode, Codex, and pi; the notable choices:
- **Spawn per call, no shell state** — every call is a fresh non-login `bash -c` (deterministic; no rc files). All four surveyed tools spawn per call. `XXX(stateful-shell)` in `src/index.ts` records the two proven stateful designs (Claude Code's cwd-only persistence; Codex's PTY exec sessions) for when real workflows demand them.
- **Configured budgets over managed groups** — `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config, and every spawn hands the service explicit byte caps, spill cap, and `graceMs` (default 3s — OpenCode's escalation). Process-group kills, the post-exit pipe-drain grace, tail-keep truncation, and bounded spill files are [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) mechanics. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background runs still use `maxOutputBytes`.
- **Spawn per call, no shell state** — every call is a fresh non-login `bash -c` with no rc files.
- **Configured budgets over managed groups** — `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config, and every spawn hands the service explicit byte caps, spill cap, and `graceMs`. Process-group kills, post-exit pipe draining, tail retention, and bounded spill files are [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) mechanics. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background runs still use `maxOutputBytes`.
- **Timeout and cancel classification** — `run()` fuses its config-clamped timeout with the caller's signal through one deadline; only the executor's own timeout reports `timedOut`, an upstream cancel reports `aborted`, and a self-signaled command reports neither ([timeout-library Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)).
- **Model-friendly terminal env** — `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` (Codex's hardcoded set) so pagers and ANSI color don't garble results, merged as ordinary env under the service's credential scrub and `DSH_*` channel rules; an explicit caller entry still wins. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
- **Background processes** — `start()` returns a live `BashProcess` handle immediately, no timeout applies (Claude Code detaches timeouts when backgrounding), and the handle's `readOutput()` merges the service's offset-based stdout/stderr reads into one marked-section delta with a consuming cursor. A still-running process belongs to the subprocess service, so it survives executor reloads and dies (killed and joined) with the service's disposal. Everything task-shaped (ids, ownership, polling, notices) lives in the generic [`ctx.tasks` runtime](../../tasks/tasks/README.md), which the tool layer registers the handle with — this executor never sees a session or a registry.
- **Model-friendly terminal env** — `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` prevents pagers and ANSI color from garbling results. These values merge as ordinary env under the service's credential scrub and `DSH_*` channel rules; an explicit caller entry still wins. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
- **Background processes** — `start()` returns a live `BashProcess` handle immediately with no timeout, and `readOutput()` merges offset-based stdout/stderr reads into one consuming delta, placing stderr under a `[stderr]` marker when present. A running process belongs to the subprocess service, survives executor reloads, and is killed and joined on service disposal. Task ids, ownership, polling, and notices belong to the generic [`ctx.tasks` runtime](../../tasks/tasks/README.md), which the tool layer registers the handle with.
## Model Experience

View File

@@ -20,15 +20,13 @@
graceMs: 3000 # kill escalation and post-exit pipe-drain grace
```
## 行为(以及设计来源)
## 行为
设计时调研了 Claude Code、OpenCode、Codex 和 pi 的 bash 工具,主要取舍如下:
- **每次调用都 spawn不保留 shell 状态**:每次调用都启动新的非登录 `bash -c`(行为确定,不读取 rc 文件)。调研的四种工具均会每次调用单独 spawn。`XXX(stateful-shell)` 位于 `src/index.ts`记录了两种已验证的有状态设计Claude Code 仅持久化 cwdCodex 使用 PTY exec 会话),供真实工作流需要时采用。
- **在受管进程组之上应用配置预算**`resolve()` 从配置补全 `workdir``timeoutMs``stdoutMaxBytes`,每次 spawn 都向服务传入显式的字节上限、spill 上限与 `graceMs`(默认 3 秒,沿用 OpenCode 的升级策略)。进程组终止、退出后的管道排空宽限期、尾部保留截断与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `BashExecRequest.stdoutMaxBytes` 可为某个受信任调用方提高单次 stdout 捕获预算stderr 和后台运行仍使用 `maxOutputBytes`
- **每次调用都 spawn不保留 shell 状态**:每次调用都启动新的非登录 `bash -c`,且不读取 rc 文件。
- **在受管进程组之上应用配置预算**`resolve()` 从配置补全 `workdir``timeoutMs``stdoutMaxBytes`,每次 spawn 都向服务传入显式的字节上限、spill 上限与 `graceMs`。进程组终止、退出后管道排空、尾部保留与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `BashExecRequest.stdoutMaxBytes` 可为某个受信任调用方提高单次 stdout 捕获预算stderr 和后台运行仍使用 `maxOutputBytes`
- **超时与取消分类**`run()` 通过同一个 deadline 把经配置钳位的超时与调用方的信号融合;只有执行器自身的超时报告 `timedOut`,上游取消报告 `aborted`,自身因信号终止的命令两者皆不报告(见[超时库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md))。
- **适合模型的终端环境**设置 `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat`Codex 硬编码的集合),防止分页器与 ANSI 颜色破坏结果这些条目作为普通 env 合并,遵循服务的凭据清除与 `DSH_*` 通道规则;调用方的显式条目依旧优先。详见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) 与 [受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
- **后台进程**`start()` 会立即返回活动的 `BashProcess` 句柄不应用超时Claude Code 在转为后台时会解除超时);句柄的 `readOutput()`服务基于偏移量的 stdout/stderr 读取合并为一条带分节标记的增量,并以消费游标记录读取进度。仍在运行的进程则由 subprocess 服务负责,因此它能在执行器重载后存活,并服务 dispose 被终止且等待退出。所有具有任务形态的事项(id、所有权、轮询通知)都属于通用 [`ctx.tasks` 运行时](../../tasks/tasks/README.md),工具层会在其中注册该句柄;本执行器不会接触会话或注册表
- **适合模型的终端环境**`NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` 防止分页器与 ANSI 颜色破坏结果这些作为普通 env 合并,遵循服务的凭据清除与 `DSH_*` 通道规则;调用方的显式条目依旧优先。详见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) 与 [受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
- **后台进程**`start()` 会立即返回活动的 `BashProcess` 句柄不应用超时`readOutput()` 把基于偏移量的 stdout/stderr 读取合并为一条消费式增量,并在存在 stderr 时将其置于 `[stderr]` 标记下。运行的进程属于 subprocess 服务,可在执行器重载后存活,并服务 dispose 被终止且等待退出。task id、所有权、轮询通知属于通用 [`ctx.tasks` 运行时](../../tasks/tasks/README.md),工具层会在其中注册该句柄。
## 模型体验

View File

@@ -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/bash/bash-sandbox/README.md
README.md: 2f69ea66251f00c74779f1decc69abc6003a4398
README.zh.md: fc4afb554442dfaf806292f30ddf9c495c427831
README.md: 74c1e28f76db35603bb9f72e0522e16ece5589f7
README.zh.md: 2593049ce09c7bc1ae0a996321bb27cbac449977

View File

@@ -36,8 +36,6 @@ Deny-only at the seam: a denial is a reported fact, and this executor never nego
name: '@deepseek-ai/dsh-bash-sandbox'
```
The keyless consumer-integration proofs are `tests/bwrap.e2e.ts`, `tests/landlock.e2e.ts`, and `tests/seatbelt.e2e.ts` (the real provider + real runner driven through `ctx.bash`, world-verified, each self-skipping where its runner is absent). The agent-spine e2e additionally drives two concurrent sessions in one Cordis context and proves each real bash tool call can write only its own project. See [the acp-agent example's default composition](../../../examples/acp-agent/) for the runnable demo.
## Model Experience
### Bash tool schema, indirectly

View File

@@ -36,8 +36,6 @@
name: '@deepseek-ai/dsh-bash-sandbox'
```
无密钥消费方集成证明是 `tests/bwrap.e2e.ts``tests/landlock.e2e.ts``tests/seatbelt.e2e.ts`(通过 `ctx.bash` 驱动真实提供方 + 真实 runner从外部验证实际文件效果并在相应 runner 缺失时各自自行跳过。agent-spine e2e 还会在一个 Cordis 上下文中驱动两个并发会话,并证明每个真实 bash 工具调用只能写入自身项目。可运行 demo 见 [acp-agent 示例的默认组合](../../../examples/acp-agent/)。
## 模型体验
### 间接的 Bash 工具 schema

View File

@@ -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/bash/bash/README.md
README.md: d7bf746969f52000fe298b65b995b7c631d8001c
README.zh.md: 4eef38e436cb0a4bcb3505aeb228d18c4a8c170b
README.md: 88f519a21a0889d6b7649502c51077940c23709f
README.zh.md: 294044692133da8baa57583146352e84c1ff9946

View File

@@ -25,7 +25,7 @@ The split mirrors the LLM seam (`LlmService`/`LlmAdapter`) and the agent-tool su
| `BashProcess.readOutput()` | **Incremental** output read — consecutive reads never re-deliver. Reads that lost data to buffer bounds flag `lossy` and point at full-stream spill files. |
| `BashProcess.kill()` | Kill the process group. Returns `false` when it already finished. |
Implementations subclass `BashExecutor` and implement the abstract methods. Disposal must kill every running process and await its exit — see the HMR-safety tests.
Implementations subclass `BashExecutor` and implement the abstract methods. Disposal must kill every running process and await its exit.
## Vocabulary
@@ -35,6 +35,8 @@ The per-session sandbox-mode override vocabulary (the `'sandbox/mode'` event, th
`stdin` and ordinary `env` are set by in-process plugins (the hooks bridges, native plugins) to feed a hook command its JSON payload and `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` values. `dshEnv` is a separate trusted overlay restricted by type to managed keys; the exported `DSH_ENV_PREFIX` is the single source for that namespace, its `DshEnvironmentKey` template type, executor scrubbing, registry validation, derived built-in names, and model guidance. Model bash uses the current snapshot collected by `ctx.bashEnv`. Implementations remove inherited managed keys, then merge `dshEnv` after ordinary `env`, so an omitted current fact cannot fall back to stale ambient state and an `env` entry cannot displace a managed value. The model-facing tool exposes none of these as parameters. All three remain optional on the resolved spec; absent means no input/overlay. See [the bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [the session environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
The exported `parseExitStatus` (with `ParsedExitStatus`) is the shared rendering contract half of the shell tools: the inverse of the `[exit code: N]` / `[killed by signal: X]` markers `dsh-tool-bash`'s `renderResult` and `dsh-tool-pwsh`'s `renderPwshResult` append. Both tools' `presentResult` use it to split the rendered text into the terminal card's output body and its exit-status pill; it lives on the seam so the two tools never drift on the marker contract.
## Model Experience
Indirectly, through `dsh-tool-bash`, which turns executor output and sandbox facts into guidance and retained tool-result tokens.

View File

@@ -25,7 +25,7 @@
| `BashProcess.readOutput()` | **增量** 读取输出:连续读取绝不会重复交付。因缓冲区边界丢失数据的读取会标记 `lossy`,并指向完整流 spill 文件。 |
| `BashProcess.kill()` | 终止进程组。如果进程已结束,返回 `false`。 |
实现会继承 `BashExecutor` 并实现抽象方法。dispose资源释放必须终止每个运行中的进程并等待其退出,详见 HMR热模块替换安全测试
实现会继承 `BashExecutor` 并实现抽象方法。dispose资源释放必须终止每个运行中的进程并等待其退出。
## 词汇
@@ -35,6 +35,8 @@
`stdin` 与普通 `env` 由同进程插件hooks 桥接、原生插件)设置,用于向 hook 命令提供其 JSON payload 和 `CLAUDE_PROJECT_DIR``CLAUDE_PLUGIN_ROOT` 值。`dshEnv` 是受类型限制、仅允许受管 key 的独立受信任 overlay导出的 `DSH_ENV_PREFIX` 是该 namespace、其 `DshEnvironmentKey` 模板类型、执行器清理、注册表验证、派生内置名称与模型指引的统一来源。模型 bash 使用 `ctx.bashEnv` 收集的当前快照。实现会移除继承的受管 key再在普通 `env` 之后合并 `dshEnv`,因此省略的当前事实不会回退到陈旧环境状态,`env` 条目也无法顶掉受管值。面向模型的工具不将这三者中的任何一个公开为参数。这三者在已解析 spec 上仍然可选缺失表示没有输入overlay。详见 [bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) 与 [会话环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
导出的 `parseExitStatus`(连同 `ParsedExitStatus`)是 shell 工具共享渲染契约的另一半:`dsh-tool-bash``renderResult``dsh-tool-pwsh``renderPwshResult` 追加的 `[exit code: N]``[killed by signal: X]` marker 的逆解析。两个工具的 `presentResult` 都用它把渲染文本拆成 terminal 卡的输出正文与其退出状态 pill它放在 seam 上,两个工具便永远不会在 marker 契约上漂移。
## 模型体验
通过 `dsh-tool-bash` 间接影响;该工具会将执行器输出与沙箱事实转为指引和保留的工具结果 token。

View File

@@ -22,6 +22,8 @@ export type {
DshEnvironment,
DshEnvironmentKey,
} from './types.ts'
export { parseExitStatus } from './render.ts'
export type { ParsedExitStatus } from './render.ts'
declare module 'cordis' {
interface Context {

View File

@@ -0,0 +1,42 @@
/**
* Shared rendering helpers for the shell tools (`dsh-tool-bash`,
* `dsh-tool-pwsh`): the exit-status marker contract the tools' renderers
* emit and the presentation layer parses back.
* @module @deepseek-ai/dsh-bash/render
*/
/**
* The exit status recovered from a rendered result, with the output body that
* status was split off from.
*/
export type ParsedExitStatus =
& { body: string }
& ({ exitCode: number } | { signal: string })
/**
* Split a rendered shell-tool result string into its output body and the
* structured exit status — the inverse of the `[exit code: N]` /
* `[killed by signal: X]` markers the shell tools' renderers append. A killed
* marker yields `signal`; otherwise a non-zero marker yields `exitCode`;
* absent both means a clean exit 0.
*
* The consumed marker is removed from `body` because a terminal presentation
* shows the exit status as its own pill: leaving the marker in the output
* would render the exit twice. Other markers (timeout, sandbox denial) carry
* facts no pill shows, so they stay in the body.
*
* Replay only retains the rendered content text, not the original
* `BashRunResult`, so terminal presentation must recover the exit pill here.
* Requiring a leading newline and the end of the string keeps ordinary output
* that merely ends with marker-like text from matching unless the final line
* is indistinguishable from a real marker.
* @param text - rendered model-facing shell-tool result.
* @returns the marker-free body plus the recovered terminal exit code or signal.
*/
export function parseExitStatus(text: string): ParsedExitStatus {
const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text)
if (signal?.[1] !== undefined) return { body: text.slice(0, signal.index), signal: signal[1] }
const exit = /\n\[exit code: (\d+)\]$/.exec(text)
if (exit?.[1] !== undefined) return { body: text.slice(0, exit.index), exitCode: Number(exit[1]) }
return { body: text, exitCode: 0 }
}

View File

@@ -0,0 +1,36 @@
/**
* Shared exit-status parse contract: the inverse of the `[exit code: N]` /
* `[killed by signal: X]` markers `dsh-tool-bash` and `dsh-tool-pwsh` append.
* Both tools' presenter suites round-trip their own renderers through this
* parse; this spec pins the parse's own edges (marker-like output, body
* slicing) once, at the seam that owns it.
*/
import { describe, expect, it } from 'vitest'
import { parseExitStatus } from '../src/render.ts'
describe('parseExitStatus', () => {
it('recovers a clean exit 0 with the body verbatim when no marker is present', () => {
expect(parseExitStatus('hi\n\n')).toEqual({ body: 'hi\n\n', exitCode: 0 })
expect(parseExitStatus('')).toEqual({ body: '', exitCode: 0 })
})
it('recovers a non-zero exit and strips only its marker from the body', () => {
expect(parseExitStatus('oops\n[exit code: 3]')).toEqual({ body: 'oops', exitCode: 3 })
// The marker needs the leading newline and the end of the string, so a
// clean result whose output merely ENDS in marker-like text is not read
// as a failure and the text stays in the body.
expect(parseExitStatus('[exit code: 5]')).toEqual({ body: '[exit code: 5]', exitCode: 0 })
})
it('recovers a signal kill ahead of any non-zero exit marker', () => {
expect(parseExitStatus('gone\n[killed by signal: SIGKILL]')).toEqual({ body: 'gone', signal: 'SIGKILL' })
// A fake signal marker with no leading newline is output, not a kill.
expect(parseExitStatus('[killed by signal: SIGKILL]')).toEqual({ body: '[killed by signal: SIGKILL]', exitCode: 0 })
})
it('keeps markers no pill shows (timeout) in the body', () => {
expect(parseExitStatus('slow\n[timed out after 100ms]\n[exit code: 143]'))
.toEqual({ body: 'slow\n[timed out after 100ms]', exitCode: 143 })
})
})

View File

@@ -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/bash/pwsh-local/README.md
README.md: 9deba9c1b63ccfdb9e1805b9896db33f144839bf
README.zh.md: e45c820e1d5e31aebd9ed365c6130850f1db2a62
README.md: 2914c46ab91dd9555dab04551e52321f6eac05bf
README.zh.md: ce9696b276a2e60acf116d7124cd5cd256d7ebde

View File

@@ -46,7 +46,7 @@ No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **Unconfined by itself** — this executor always runs commands with the harness process's authority; deployments needing confinement compose a sandboxing bash executor or policy instead.
- **No persistent shell or PTY** — every call starts a fresh `pwsh -Command`; interactive terminal sessions remain deferred until the roadmap's pwsh TUI/GUI rendering work lands.
- **No persistent shell or PTY** — every call starts a fresh `pwsh -Command`.
- **The command string is PowerShell text** — the `-Command` domain has no shell-quoting layer, but a model-facing command is parsed by PowerShell itself, so PowerShell syntax errors are command failures, not launch failures.
- **A background spawn-failure note is single-delivery** — the subprocess service buffers no output for a process that never ran, so the executor injects `spawn failed: …` into exactly one `readOutput()` delta; a reader that discards that delta cannot recover it.
- **Windows termination reports no signal** — a force-killed process settles as exit 1 with `signal: null`, so signal-based status classification (POSIX `killed`) does not apply on Windows; `kill()`-initiated stops still stamp `killed` directly.

View File

@@ -46,7 +46,7 @@
## 已知局限与延期工作
- **自身不设沙箱**——本执行器始终以 harness 进程的权限运行命令;需要约束的部署应组合沙箱化 bash 执行器或策略。
- **无持久 shell 或 PTY**——每次调用都是全新的 `pwsh -Command`;交互式终端会话在路线图的 pwsh TUI/GUI 渲染工作落地之前保持延期
- **无持久 shell 或 PTY**——每次调用都是全新的 `pwsh -Command`
- **命令字符串是 PowerShell 文本**——`-Command` 域没有 shell 引号层,但面向模型的命令由 PowerShell 自己解析,因此 PowerShell 语法错误是命令失败,而非启动失败。
- **后台 spawn 失败提示只投递一次**——subprocess 服务不会为从未运行的进程缓冲输出,因此执行器只把 `spawn failed: …` 注入一次 `readOutput()` 增量;丢弃该增量的读取方无法恢复它。
- **Windows 终止不报告信号**——被强制终止的进程以退出码 1、`signal: null` 结束因此基于信号的状态分类POSIX `killed`)在 Windows 上不适用;`kill()` 发起的停止仍会直接盖上 `killed`

View File

@@ -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/bash/tool-bash/README.md
README.md: 35a5647365dab6daa903c14cba8b83702b50305d
README.zh.md: eb7901f3445117988d77e6d39b73681480b7a754
README.md: 47101e1198d13518c3d82877df8726c1fbf26b82
README.zh.md: d60ac4b3826838e875f7d43bc314f62e1453c1d9

View File

@@ -6,7 +6,7 @@ The model-facing `bash` tool registered over the `ctx.bash` executor seam. Foreg
Requires a loaded executor implementation (e.g. `@deepseek-ai/dsh-bash-local`) and the [`@deepseek-ai/dsh-bash-env`](../bash-env/README.md) registry; the plugin stays pending until every injected service exists (`inject: ['tools', 'bash', 'systemPrompt', 'bashEnv']`). The tool contract is bash-dialect — mount a bash-parsing executor.
The package root exposes only the Cordis plugin contract (`name`, `inject`, `Config`, `apply`); result rendering and background-process adaptation remain implementation details covered by same-package tests.
The package root exposes only the Cordis plugin contract (`name`, `inject`, `Config`, `apply`); result rendering and background-process adaptation remain package-internal.
The plugin also contributes the `tool:bash` prompt section (order 105): check the `[exit code: N]` marker on every result and investigate failures before moving on.

View File

@@ -6,7 +6,7 @@
需要加载执行器实现(例如 `@deepseek-ai/dsh-bash-local`)与 [`@deepseek-ai/dsh-bash-env`](../bash-env/README.md) 注册表;在每个注入服务就绪之前,插件会保持等待状态(`inject: ['tools', 'bash', 'systemPrompt', 'bashEnv']`)。工具契约是 bash 方言——请挂载能解析 bash 的执行器。
package根只公开 Cordis 插件契约(`name``inject``Config``apply`);结果渲染和后台进程适配仍是实现细节,由同包测试覆盖
package根只公开 Cordis 插件契约(`name``inject``Config``apply`);结果渲染和后台进程适配仍保留在包内部
插件还会提供 `tool:bash` 提示词段落(顺序 105检查每个结果中的 `[exit code: N]` 标记,发现失败时先调查原因再继续。
@@ -42,7 +42,7 @@
## 工具仅使用具名参数构建请求
`BashExecRequest` seam 携带可选的 `stdoutMaxBytes``stdin`、普通 `env` 和托管 `dshEnv`,供可信进程内插件及此工具的环境注册表使用。模型侧工具不公开 `stdoutMaxBytes``stdin``env`:它使用具名的命令/工作目录/超时/信号/沙箱字段,加上从注册表收集的 `dshEnv` 来构建请求。额外模型键会被忽略无法替换托管值。Shell 语法可以提供等价的命令级行为,而本地执行器会清除环境中的凭据和陈旧 `DSH_*` 值。参见 [stdin/env Agent Noteagent 决策记录)](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
`BashExecRequest` seam 携带可选的 `stdoutMaxBytes``stdin`、普通 `env` 和托管 `dshEnv`,供可信进程内插件及此工具的环境注册表使用。模型侧工具不公开 `stdoutMaxBytes``stdin``env`:它使用具名的命令/工作目录/超时/信号/沙箱字段,加上从注册表收集的 `dshEnv` 来构建请求。额外模型键会被忽略无法替换托管值。Shell 语法可以提供等价的命令级行为,而本地执行器会清除环境中的凭据和陈旧 `DSH_*` 值。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
## 权限与升权

View File

@@ -95,36 +95,9 @@ export function renderProcessRead(
}
/**
* The exit status recovered from a rendered result, with the output body that
* status was split off from.
* The exit-status parse is the shared marker-contract half of the shell-tool
* rendering story, owned by `@deepseek-ai/dsh-bash` so `dsh-tool-pwsh` reuses
* it (its renderer emits the same markers). Re-exported here to keep
* `../src/render.ts` a single import root for bash-tool consumers.
*/
export type ParsedExitStatus =
& { body: string }
& ({ exitCode: number } | { signal: string })
/**
* Split a rendered {@link renderResult} string into its output body and the
* structured exit status — the inverse of the status markers it appends. A
* killed marker yields `signal`; otherwise a non-zero marker yields `exitCode`;
* absent both means a clean exit 0.
*
* The consumed marker is removed from `body` because a terminal presentation
* shows the exit status as its own pill: leaving the marker in the output would
* render the exit twice. Other markers (timeout, sandbox denial) carry facts no
* pill shows, so they stay in the body.
*
* Replay only retains the rendered content text, not the original
* `BashRunResult`, so terminal presentation must recover the exit pill here.
* Requiring a leading newline and the end of the string keeps ordinary output
* that merely ends with marker-like text from matching unless the final line
* is indistinguishable from a real marker.
* @param text - rendered model-facing bash result.
* @returns the marker-free body plus the recovered terminal exit code or signal.
*/
export function parseExitStatus(text: string): ParsedExitStatus {
const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text)
if (signal?.[1] !== undefined) return { body: text.slice(0, signal.index), signal: signal[1] }
const exit = /\n\[exit code: (\d+)\]$/.exec(text)
if (exit?.[1] !== undefined) return { body: text.slice(0, exit.index), exitCode: Number(exit[1]) }
return { body: text, exitCode: 0 }
}
export { parseExitStatus, type ParsedExitStatus } from '@deepseek-ai/dsh-bash'

View File

@@ -174,7 +174,7 @@ describe('bash tool through the agent loop', () => {
expect(resultText(toolResult)).toContain('[exit code: 9]')
})
it('background: start ack → completion notice as user/message → task_output collects it', async () => {
it('background: start ack → pending completion notice → task_output collects it', async () => {
// The task id is deterministic (a fresh LocalTaskService counts per kind from 1),
// so the script can name `bash-1` without threading a generated id.
const adapter = new MockAdapter([
@@ -194,25 +194,27 @@ describe('bash tool through the agent loop', () => {
expect(resultText(firstResult)).toBe('started background task bash-1')
// The task settles on its own; the tool-tasks notice listener injects a
// durable plugin-sourced user/message into the owning agent's session
// (settlement may race turn end, so poll for it).
// pending next-step message without waking the idle agent.
const isNotice = (e: SessionEvent): e is SessionEvent<'user/message'> =>
e.type === 'user/message' && e.data.source.kind === 'plugin'
await pollUntil(() => events(agent).some(isNotice))
const notice = events(agent).find(isNotice)!
expect(notice.data.content.some(
await pollUntil(() => agent.inbox.nextStep.some(message => message.source.kind === 'plugin'))
const pendingNotice = agent.inbox.nextStep.find(message => message.source.kind === 'plugin')!
expect(pendingNotice.content.some(
block => block.type === 'text' && block.text.includes('background task bash-1 (bash: echo bg-ok) finished'),
)).toBe(true)
expect(notice.data.source).toEqual({
expect(pendingNotice.source).toEqual({
kind: 'plugin',
plugin: 'tool-tasks',
form: 'notice',
summary: 'bash echo bg-ok [status: completed, exit code: 0]',
})
// The next turn collects the output through the generic task tool.
// The next turn first admits that notice as user/message, then collects
// the output through the generic task tool.
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'collect it' }], source: { kind: 'user' } }))
await waitForIdle(ctx, agent)
const notice = events(agent).find(isNotice)!
expect(notice.data).toEqual(pendingNotice)
const readResult = findEvent(events(agent), 'tool/result', 'last')
expect(readResult.data.message.content[0].isError).toBe(false)
expect(resultText(readResult)).toContain('bg-ok')

View File

@@ -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/bash/tool-pwsh/README.md
README.md: dfe26a63684d61dcdd6f969c2c2261dac79325c7
README.zh.md: 2344f8477e5b15f2c4d366dd82b46358eacbc1b7
README.md: 78eb161f77b9524bc577b273abe59db6b931727c
README.zh.md: 17696fe6d908838aaaca12e8179f2ad9cb780210

View File

@@ -36,7 +36,7 @@ When `run_in_background` is true, this plugin preflights `ctx.tasks.start()` bef
## UI presentation
The tool owns its `presentCall`/`presentResult` render intent. A foreground call is a `terminal` card carrying command, description, and optional cwd; a `run_in_background` call is a `generic` card with the raw command, mirroring the bash tool's background presentation. A completed result is a `generic` card with the rendered output in a `console` fence. The bash tool's terminal card with its parsed exit-status pill has no pwsh counterpart yet — a PowerShell-aware presentation is roadmap work. These presenters are pure and replay-safe.
The tool owns its `presentCall`/`presentResult` render intent. A foreground call is a `terminal` card carrying command, description, and optional cwd; a `run_in_background` call is a `generic` card with the raw command, mirroring the bash tool's background presentation. A completed foreground result is a `terminal` card too: the exit marker becomes the card's exit-status pill (`exitCode`/`signal`), and the marker-free body is the card's output — exactly the bash tool's terminal-card story, via the shared exit-status parse from `@deepseek-ai/dsh-bash`. Background acks and execution errors stay `generic` cards with the rendered output in a `console` fence. These presenters are pure and replay-safe.
## Model Experience
@@ -121,5 +121,4 @@ Append-only; newly visible content follows the reusable request prefix and does
- **No sandbox escalation** — `sandbox_permissions`/`justification` are absent; escalation waits for a Windows-confining executor (the bash tool's sandbox surface is not mirrored).
- **No persistent shell or PTY** — every call starts a fresh `pwsh -Command`; the PTY backends are Linux/macOS-only today, and a Windows ConPTY persistent shell is roadmap work.
- **PowerShell-dialect contract** — the model must write PowerShell (native paths, `$env:` variables), not bash; there is no dialect translation.
- **Generic UI presentation** — results use the generic card; a PowerShell-aware terminal card with exit-status pill is roadmap work.
- **Session-cwd identity is not canonicalized** — the workdir base is the session header cwd as-is, unlike the bash tool's sandbox-root-canonicalized identity; only the sandbox-less case applies here.

View File

@@ -19,8 +19,8 @@
| `command` | string (required) | 通过 `pwsh -Command` 运行。调用之间不保留状态——用 `workdir`,不要用 `cd`。 |
| `description` | string (required) | 命令的一行主动语态摘要5-10 词),仅用于 UI/日志展示——不影响执行。 |
| `timeoutMs` | number | 超时覆盖值(毫秒)。执行器应用其配置的默认值与上限。 |
| `workdir` | string | 本次调用的工作目录。默认取调用 agent 的会话 cwd`session.header.cwd`),使每个会话在自己的工作区运行;相对 `workdir` 基于同一身份解析。 |
| `run_in_background` | boolean | 立即返回任务 id不适用超时。 |
| `workdir` | string | 本次调用的工作目录。默认取调用 agent(智能体)的会话 cwd`session.header.cwd`),使每个会话在自己的工作区运行;相对 `workdir` 基于同一身份解析。 |
| `run_in_background` | boolean | 立即返回 task id不适用超时。 |
`command``workdir``timeoutMs` 在执行前经 `ctx.bash.resolve()` 按执行器配置默认值解析。workdir 默认值在工具层于 `resolve()` 之前从调用 agent 的 `session.header.cwd` 取得——每次会话的 cwd 必须来自 `exec.agent`,因为 N 个会话共享一个执行器;仅当没有会话 cwd 时执行器才回退到自己的配置 / `process.cwd()`
@@ -36,7 +36,7 @@
## UI presentation
工具拥有自己的 `presentCall`/`presentResult` 呈现意图。前台调用是携带命令、描述与可选 cwd 的 `terminal` 卡;`run_in_background` 调用是携带原始命令的 `generic` 卡,镜像 bash 工具的后台呈现。完成的结果是以 `console` 围栏包裹渲染输出的 `generic` 卡。bash 工具那种带解析退出状态 pill 的 terminal 卡在 pwsh 侧暂无对应——PowerShell 感知的呈现属于路线图工作。这些 presenter 是纯函数且可重放。
工具拥有自己的 `presentCall`/`presentResult` 呈现意图。前台调用是携带命令、描述与可选 cwd 的 `terminal` 卡;`run_in_background` 调用是携带原始命令的 `generic` 卡,镜像 bash 工具的后台呈现。完成的前台结果同样是 `terminal` 卡:退出 marker 变成卡片的退出状态 pill`exitCode`/`signal`),去 marker 的正文成为卡片输出——与 bash 工具的 terminal 卡故事完全一致,经由 `@deepseek-ai/dsh-bash` 的共享退出状态解析。后台 ack 与执行错误保持 `generic` 卡,以 `console` 围栏包裹渲染输出。这些 presenter 是纯函数且可重放。
## Model Experience
@@ -121,5 +121,4 @@ ack 是固定短行;任务输出按读取有界。
- **无 sandbox 升级** — 没有 `sandbox_permissions`/`justification`;升级等待 Windows-confining 执行器bash 工具的 sandbox 面不被镜像)。
- **无持久 shell 或 PTY** — 每次调用都启动全新的 `pwsh -Command`PTY 后端目前仅限 Linux/macOSWindows ConPTY 持久 shell 属于路线图工作。
- **PowerShell 方言契约** — 模型必须写 PowerShell原生路径、`$env:` 变量),而不是 bash没有方言翻译。
- **通用 UI 呈现** — 结果使用 generic 卡;带退出状态 pill 的 PowerShell 感知 terminal 卡属于路线图工作。
- **会话 cwd 身份不做规范化** — workdir 基座直接取会话头 cwd 原值,不同于 bash 工具经 sandbox-root 规范化的身份;此处只涉及无 sandbox 场景。

View File

@@ -8,8 +8,9 @@
* foreground and `run_in_background` execution (background handles register
* with the generic `ctx.tasks` runtime), the managed `DSH_*` environment
* through the shared `bash-env` registry, and the bash marker/truncation
* rendering story. UI presentation stays on the existing generic/terminal
* cards; a pwsh-specific rendering twin is roadmap work.
* rendering story. UI presentation mirrors the bash tool's too: a completed
* foreground call is a terminal card with the parsed exit-status pill, using
* the shared exit-status parse from `@deepseek-ai/dsh-bash`.
*
* @module @deepseek-ai/dsh-tool-pwsh
*/
@@ -25,6 +26,7 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-tasks'
import type {} from '@deepseek-ai/dsh-bash-env'
import type { BashRunResult } from '@deepseek-ai/dsh-bash'
import { parseExitStatus } from '@deepseek-ai/dsh-bash'
import { processOutcome } from './background.ts'
import { renderPwshProcessRead, renderPwshResult } from './render.ts'
@@ -297,10 +299,20 @@ export function apply(ctx: Context, config: Config = {}): void {
}
},
/* jscpd:ignore-end */
presentResult: (_args: unknown, result: ToolResult): ToolResultView | undefined => {
/* jscpd:ignore-start -- the completed-result presentation mirrors presentBashResult's by design (Agent Note). */
presentResult: (args: unknown, result: ToolResult): ToolResultView | undefined => {
const block = result.content.length === 1 ? result.content[0] : undefined
if (block === undefined || block.type !== 'text') return undefined
return { card: 'generic', content: [{ type: 'text', text: `\`\`\`console\n${block.text.replace(/\n+$/, '')}\n\`\`\`` }] }
const raw = block.text
const isBackground = typeof args === 'object' && args !== null && (args as { run_in_background?: unknown }).run_in_background === true
// Background acknowledgements and errors have no terminal exit status.
if (isBackground || result.isError) {
return { card: 'generic', content: [{ type: 'text', text: `\`\`\`console\n${raw.replace(/\n+$/, '')}\n\`\`\`` }] }
}
// The exit marker becomes the card's exit pill, so it leaves the output body.
const { body, ...exit } = parseExitStatus(raw)
return { card: 'terminal', output: body, ...exit }
},
/* jscpd:ignore-end */
}))
}

View File

@@ -28,7 +28,7 @@ import * as ToolPwsh from '@deepseek-ai/dsh-tool-pwsh'
import * as BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
import type { BashProcessRead } from '@deepseek-ai/dsh-bash'
import { processOutcome } from '../src/background.ts'
import { renderPwshProcessRead } from '../src/render.ts'
import { renderPwshProcessRead, renderPwshResult } from '../src/render.ts'
const testToolSignal = new AbortController().signal
@@ -516,16 +516,16 @@ describe('background execution through the task runtime', () => {
})
describe('UI presentation', () => {
it('a real execute renders the console view through the tool definition presenter', async () => {
it('a real execute presents a completed foreground run as a terminal card with the parsed exit pill', async () => {
const { ctx, bash } = await setup()
bash.handler = () => runResult('hi\n')
const args = { command: 'Write-Output hi', description: 'say hi' }
const result = await call(ctx, 'pwsh', args)
const view = ctx.tools.get('pwsh')?.presentResult?.(args, result)
expect(view).toEqual({
card: 'generic',
content: [{ type: 'text', text: '```console\nhi\n```' }],
})
// A terminal result keeps the RAW bytes (newlines intact) a terminal
// renderer needs; a clean run renders no exit marker, so the body is the
// raw output with a clean exit-0 pill, mirroring the bash tool.
expect(view).toEqual({ card: 'terminal', output: 'hi\n', exitCode: 0 })
})
it('the pending call view is a terminal card carrying command, description, and optional cwd', async () => {
@@ -553,6 +553,83 @@ describe('UI presentation', () => {
})
})
it('presentResult: a non-zero exit and a signal kill parse into exitCode / signal', async () => {
const { ctx } = await setup()
const present = ctx.tools.get('pwsh')
const args = { command: 'x', description: 'x' }
expect(present?.presentResult?.(args, { content: [{ type: 'text', text: 'oops\n[exit code: 3]' }], isError: false }))
.toEqual({ card: 'terminal', output: 'oops', exitCode: 3 })
expect(present?.presentResult?.(args, { content: [{ type: 'text', text: 'gone\n[killed by signal: SIGKILL]' }], isError: false }))
.toEqual({ card: 'terminal', output: 'gone', signal: 'SIGKILL' })
})
it('presentResult: markers a pill CANNOT show (timeout) stay in the terminal output', async () => {
const { ctx } = await setup()
const args = { command: 'x', description: 'x' }
expect(ctx.tools.get('pwsh')?.presentResult?.(
args,
{ content: [{ type: 'text', text: 'slow\n[timed out after 100ms]\n[exit code: 143]' }], isError: false },
)).toEqual({ card: 'terminal', output: 'slow\n[timed out after 100ms]', exitCode: 143 })
})
it('presentResult exit parse is the inverse of renderPwshResult markers (round-trip)', async () => {
const { ctx } = await setup()
const present = ctx.tools.get('pwsh')!
const base = {
aborted: false,
timeoutMs: 1000,
stdout: { text: 'out', truncated: false },
stderr: { text: '', truncated: false },
}
const cases = [
{ result: { ...base, exitCode: 0, signal: null, timedOut: false }, expect: { exitCode: 0 } },
{ result: { ...base, exitCode: 7, signal: null, timedOut: false }, expect: { exitCode: 7 } },
{ result: { ...base, exitCode: null, signal: 'SIGTERM' as const, timedOut: false }, expect: { signal: 'SIGTERM' } },
// A trapped-timeout run that exits 0 has no signal/exit marker → reads as exit 0 (it did exit 0).
{ result: { ...base, exitCode: 0, signal: null, timedOut: true }, expect: { exitCode: 0 } },
]
for (const c of cases) {
const rendered = renderPwshResult(c.result)
const out = present.presentResult!({ command: 'x', description: 'x' }, { content: [{ type: 'text', text: rendered }], isError: false })
// Drop card + output; the remaining fields are the parsed exit.
const { card: _c, output, ...exit } = out as { card: string; output?: string; exitCode?: number; signal?: string }
expect(exit).toEqual(c.expect)
// Whatever the parse consumed is gone from the body, so a card with an
// exit pill never shows the same status twice.
expect(output).not.toMatch(/\[exit code: \d+\]|\[killed by signal: /)
}
})
it('presentResult: a clean exit-0 whose output ENDS in marker-like text is NOT read as a failure', async () => {
const { ctx } = await setup()
const args = { command: 'Write-Output "[exit code: 5]"', description: 'print' }
// A successful command may print marker-like text. A clean result appends no marker or
// newline; parsing requires the leading newline emitted for real markers, so this stays exit 0.
const out = ctx.tools.get('pwsh')!.presentResult!(args, { content: [{ type: 'text', text: '[exit code: 5]' }], isError: false })
expect(out).toEqual({ card: 'terminal', output: '[exit code: 5]', exitCode: 0 })
// Same for a fake signal marker with no leading newline.
const sig = ctx.tools.get('pwsh')!.presentResult!(args, { content: [{ type: 'text', text: '[killed by signal: SIGKILL]' }], isError: false })
expect(sig).toEqual({ card: 'terminal', output: '[killed by signal: SIGKILL]', exitCode: 0 })
})
it('presentResult: a run_in_background ack is a generic card and carries no exit pill', async () => {
const { ctx } = await setup()
const result = ctx.tools.get('pwsh')!.presentResult!(
{ command: 'Start-Sleep -Seconds 60', description: 'long wait', run_in_background: true },
{ content: [{ type: 'text', text: 'started background task pwsh-1' }], isError: false },
)
expect(result).toEqual({ card: 'generic', content: [{ type: 'text', text: '```console\nstarted background task pwsh-1\n```' }] })
})
it('presentResult: an isError result is a generic card (no real process exit to report)', async () => {
const { ctx } = await setup()
const out = ctx.tools.get('pwsh')!.presentResult!(
{ command: 'x', description: 'x' },
{ content: [{ type: 'text', text: 'tool call aborted' }], isError: true },
)
expect(out).toEqual({ card: 'generic', content: [{ type: 'text', text: '```console\ntool call aborted\n```' }] })
})
it('presentResult falls back to undefined for multi-block or non-text content', async () => {
const { ctx } = await setup()
const definition = ctx.tools.get('pwsh')