# Shell 工具

> Bash 工具的依赖注入与执行通道、跨平台 shell 选型、四条加固措施、前后台统一的任务模型，以及 tree-sitter-bash 在命令分析中的角色。

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/shell-tools
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

Shell 是威力最大也最危险的工具。现役实现在 `agent-core-v2/src/agent/tools/os/bash/`：`bash.ts` 定义 schema 与超时常量，`bash.md` 是描述模板，`bashTool.ts`（480 行）是实现，`process-task.ts` 把进程包装成任务。v0.29 的 `bash.ts` 在文件头注释里写满了设计说明；v2 引擎是无注释区，下面的结论都直接取自代码。

## 依赖注入与执行通道

```ts
  constructor(
    @IAgentRuntimeService private readonly runtime: IAgentRuntimeService,
    @ISessionContext private readonly ctx: ISessionContext,
    @ISessionWorkspaceContext private readonly workspaceCtx: ISessionWorkspaceContext,
    @IAgentTaskService private readonly tasks: IAgentTaskService,
    @IAgentToolPolicyService private readonly toolPolicy: IAgentToolPolicyService,
    @IConfigService private readonly config: IConfigService,
  ) {}
```

v0.29 构造时注入的是 `Kaos`、`cwd`、`Environment`、`BackgroundManager`，关键约束是「执行一律走 [Kaos](https://daiw.org/manual/kimi-code/kaos)」。v0.43 全部换成 DI 服务，执行通道也变了：Bash 调用 `this.runtime.acquire(['process'])` 拿到运行时租约，再用租约里的 `IHostProcessService.spawn()` 起进程。`agent-core-v2` 并不导入 `@moonshot-ai/kaos`，它有自己的 `os/` 抽象层——`os/interface/` 定义接口，`os/backends/node-local/` 是本机实现，真正 spawn 进程的是其中的 `hostProcessService.ts`。

原则没变：工具只认抽象接口，不直接碰 `child_process`，执行环境仍然可以替换。

## 跨平台 shell

```ts
    const shellCwd = getShellPathBridge(env).toShellPath(effectiveCwd);
    const shellCommand = `cd ${shellQuote(shellCwd)} && ${command}`;
    const noninteractiveEnv: Record<string, string> = {
      NO_COLOR: '1',
      TERM: 'dumb',
      GIT_TERMINAL_PROMPT: process.env['GIT_TERMINAL_PROMPT'] ?? '0',
      SHELL: env.shellPath,
    };

    return processService.spawn(env.shellPath, ['-c', shellCommand], { env: noninteractiveEnv });
```

- 命令交给 `env.shellPath` 以 `-c` 执行，先 `cd` 到目标目录；每次调用都是新 shell，`cd` 和变量不跨调用保留。
- 环境变量压成非交互风格：关颜色、`TERM=dumb`、禁止 git 弹出凭据提示。
- Windows 上用 **Git Bash**：`environmentProbe.ts` 先看 `KIMI_SHELL_PATH`，再从 PATH 上的 `git.exe` 推断，最后查常见安装位置，都找不到就报 `shell.git_bash_not_found`。这也是[安装时要求先装 Git for Windows](https://daiw.org/manual/kimi-code/what-is-kimi-code) 的原因。Windows 上还会把 `> NUL` 改写成 `> /dev/null`，工作目录经路径桥转成 Git Bash 认识的形式。

统一到 POSIX shell 而非适配 cmd / PowerShell 是务实选择：模型生成的命令绝大多数是 POSIX 风格，让环境适配模型比让模型适配环境更省事。

## 四条加固措施

### 1. 双边超时

```ts
export const DEFAULT_TIMEOUT_S = 60;
export const MAX_TIMEOUT_S = 5 * 60;
export const DEFAULT_BACKGROUND_TIMEOUT_S = 10 * 60;
export const MAX_BACKGROUND_TIMEOUT_S = 24 * 60 * 60;
```

四档常量没变：前台默认 1 分钟、上限 5 分钟；后台默认 10 分钟、上限 24 小时（还可以 `disable_timeout`）。计时交给任务服务，工具参数给的超时与外部 abort 任一触发，任务都会收场。

**新变化：前台超时默认「转后台」而不是杀掉。** `task` 配置段（旧名 `background`）的 `bashAutoBackgroundOnTimeout` 默认开启，只要后台工具可用，超时的前台命令就挪到后台继续跑，模型拿到任务 ID，完成后自动通知；关掉它才回到「Command killed by timeout」。

### 2. 立即关闭 stdin

进程一起来就调用 `closeProcessStdin(proc)`，也就是 `proc.stdin.end()`。交互式命令（`cat`、`read`、`python -c 'input()'`）立刻收到 EOF 退出，而不是挂到超时。

### 3. 两阶段 kill

归任务服务所有：`AgentTaskService.terminateWithGrace()` 先中止任务，`ProcessTask` 收到信号发 `SIGTERM`；宽限期（`killGracePeriodMs`，默认 5 秒）内没退出，再调 `forceStop()` 发 `SIGKILL`。本机后端在 POSIX 上对整个进程组发信号（`process.kill(-pid, …)`）；Windows 没有温和阶段，直接用 `taskkill /T /F` 连进程树一起结束。

### 4. 输出捕获分流

`ProcessTask` 监听 stdout / stderr，每块输出都进任务的输出记录；前台运行时再回调一次：推 `ToolUpdate`（`kind: 'stdout'` / `'stderr'`）给 UI 流式显示，同时写入 `ToolOutputAccumulator` 作为工具结果。前台输出超过 5 万字符时，结果附上任务日志路径（后台工具可用时还会提示用 `TaskOutput` 查全文）；单个进程任务输出超过 16 MiB 会被直接终止。

## 前台与后台统一

前台执行本身就是一个**前台任务**：

```ts
      taskId = this.tasks.registerTask(
        new ProcessTask(proc, command, description, onProcessOutput, () => lease.dispose(), toolCallId),
        {
          detached: startsInBackground,
          timeoutMs,
          detachTimeoutMs: this.detachTimeoutMs(),
          autoBackgroundOnTimeout: this.allowBackground() && this.autoBackgroundOnTimeout(),
          signal: startsInBackground ? undefined : signal,
        },
      );
```

`run_in_background=true` 时任务一开始就是 `detached`，工具立即返回任务 ID；否则工具在 `waitForForegroundRelease()` 上等三种结局：命令结束、超时转后台、用户手动转后台。所有进程都在同一个[任务服务](https://daiw.org/manual/kimi-code/background)下管理，不会有游离进程。`TaskList`、`TaskOutput`、`TaskStop` 任一被禁用时，Bash 的描述会改写成「不允许后台」，带 `run_in_background` 的调用也会被拒绝。

## 规则匹配与调度

```ts
      approvalRule: literalRulePattern(this.name, args.command),
      matchesRule: (ruleArgs) => matchesGlobRuleSubject(ruleArgs, args.command),
```

Bash 用 **glob** 匹配整条命令（如 `Bash(git *)`），文件工具用**路径规则**（`matchesPathRuleSubject`），都服务于[权限系统](https://daiw.org/manual/kimi-code/permission)。「本会话允许」记下的是 `literalRulePattern()` 生成的字面量规则，只放行完全相同的命令。

`ToolAccesses` 方面，Bash 的 `resolveExecution()` 干脆不声明 `accesses`，执行器按 `ToolAccesses.all()` 处理——shell 的副作用无法静态描述，所以它与任何工具互斥、独占执行。这是正确且必要的保守。

## tree-sitter-bash：看懂命令，但不替你做决定

v0.43 新增的 `packages/tree-sitter-bash` 是纯 TypeScript 的 bash 解析器（无 wasm、无运行时依赖），命名节点类型与 tree-sitter-bash 一一对应。它**只解析，不做安全判断**；引擎通过 App 作用域的 `IBashParserService` 使用它，调用方目前有两个：

- 权限策略 `dangerous-command-ask`：识别关机重启、`mkfs`、`dd of=/dev/...`、`rm -rf` 等危险命令，能拆开 `sudo`、`env`、`bash -c`、`eval` 等包装（最多嵌套 4 层，再深视为无法分析）。
- AGENTS.md 提醒：从 `ls`、`tree`、`find` 等命令里提取目标目录。

两处都用 `{ timeoutMs: 500, maxNodes: 10_000 }` 的确定性预算，也都遵守包的约定：`aborted` 或 `hasError` 的树一律当作「无法分析」。危险命令检查因此**转为询问用户**，AGENTS.md 提醒则退回只看 `cwd` 参数。

## 用户中断

用户中断有专门的 `UserCancellationError`：被打断的前台命令，结果是「Interrupted by user」；执行器层面，用户取消得到的中止文案也与普通中止不同，明确告诉模型这是用户的主动操作、不要自动重试——对应[回合收敛](https://daiw.org/manual/kimi-code/turn-convergence)里「用户主动取消要与程序性中止区分」的要求。

下一篇：[后台任务系统](https://daiw.org/manual/kimi-code/background)。
