Shell 工具

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

作者 David更新于 第 14 篇(共 33 篇)

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 引擎是无注释区,下面的结论都直接取自代码。

依赖注入与执行通道

  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」。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

    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 的原因。Windows 上还会把 > NUL 改写成 > /dev/null,工作目录经路径桥转成 Git Bash 认识的形式。

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

四条加固措施

1. 双边超时

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 会被直接终止。

前台与后台统一

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

      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() 上等三种结局:命令结束、超时转后台、用户手动转后台。所有进程都在同一个任务服务下管理,不会有游离进程。TaskList、TaskOutput、TaskStop 任一被禁用时,Bash 的描述会改写成「不允许后台」,带 run_in_background 的调用也会被拒绝。

规则匹配与调度

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

Bash 用 glob 匹配整条命令(如 Bash(git *)),文件工具用路径规则(matchesPathRuleSubject),都服务于权限系统。「本会话允许」记下的是 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」;执行器层面,用户取消得到的中止文案也与普通中止不同,明确告诉模型这是用户的主动操作、不要自动重试——对应回合收敛里「用户主动取消要与程序性中止区分」的要求。

下一篇:后台任务系统。

本页目录