oh-my-pi Bash 工具实战指南:持久 Shell 执行、命令拦截与输出截断机制
2026/9/12 4:30:51 网站建设 项目流程

oh-my-pi Bash 工具实战指南:持久 Shell 执行、命令拦截与输出截断机制

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

本文是 oh-my-pi(Coding agent with the IDE wired in)中bash 工具toolName: "bash")的完整技术指南。文章以模型侧提示词文档 bash.md 为骨架,结合配套文档 docs/tools/bash.md、运行时说明 docs/bash-tool-runtime.md 与源码实现,系统讲解该工具的使用边界、全部输入参数、输出结果形态、双通道命令策略(权限策略与专用工具路由)、五种执行模式以及输出截断与 artifact 溢出机制。读完本文,你将掌握如何正确、安全、高效地驱动 oh-my-pi 的 bash 工具,并理解其底层执行引擎的工作原理。

工具定位与使用边界

bash 工具的核心定位是在持久 Shell 会话中运行命令。它的设计哲学是"让 Agent 用最小的代价快速计算一个事实",而不是取代所有系统操作工具。

模型侧提示词文档 bash.md 明确划定了使用边界:

  • 应当使用:单个二进制程序或一个短管道,用于"计算一个事实"的场景,例如wc -lsort | uniq -cdiff
  • 不应使用:内联脚本(inline scripts)、heredoc、$(…)命令替换、复杂的控制流与引号嵌套、以及非平凡管道。这些场景在会话启用eval工具时路由到eval;未启用时,应使用专用工具或仓库中已检入的脚本(checked-in script)。

这一边界同时是能力边界而非性能偏好:拦截器(interceptor)只对"可以用路径类专用工具替代的简单命令"做路由,而bash本身承载的是那些无法被readgrepglobeditwrite覆盖的真实 Shell 计算能力,例如进程管理、文件操作与管道数据处理。

输入参数详解

工具入口为BashTool.execute(),实现在 packages/coding-agent/src/tools/bash.ts。完整参数如下:

字段类型必填说明
commandstring要执行的 Shell 命令文本。当cwd未提供时,开头的cd <path> && ...会被重写为cwd字段并从命令中剥离。
envRecord<string, string>附加环境变量。键名必须匹配^[A-Za-z_][A-Za-z0-9_]*$,否则抛出错误。值会经过内部 URL 展开,以环境值而非 Shell 文本的形式传入。
timeoutnumber超时秒数,默认3000表示禁用截止时间。正值先受tools.maxTimeout全局上限约束,再被钳制到 Bash 范围1..3600秒。
cwdstring工作目录,相对session.cwd通过resolveToCwd解析,必须存在且为目录。
ptyboolean请求 PTY 模式,默认false。仅在pty: truePI_NO_PTY !== "1"且工具上下文具备 UI 时生效。
asyncboolean后台执行请求。仅当会话启用async.enabled时出现。立即返回 job id 而不等待;不改变有效截止时间(包括timeout: 0的禁用状态)。

超时钳制规则

超时钳制逻辑见 packages/coding-agent/src/tools/tool-timeouts.ts:

export const TOOL_TIMEOUTS = { bash: { default: 300, min: 1, max: 3600 }, // ... } as const satisfies Record<string, ToolTimeoutConfig>; export function clampTimeout(tool: ToolTimeoutConfig, rawTimeout?: number, maxTimeout?: number): number { const config = TOOL_TIMEOUTS[tool]; const timeout = rawTimeout ?? config.default; const capped = maxTimeout !== undefined && maxTimeout > 0 ? Math.min(timeout, maxTimeout) : timeout; return Math.max(config.min, Math.min(config.max, capped)); }

关键语义:全局上限tools.maxTimeout也会钳制默认回退路径(即 Agent 省略timeout时),而不仅仅是显式传入的值;maxTimeout <= 0表示不设全局上限。当发生钳制时,#buildCompletedResult()/#buildBackgroundStartResult()会在结果中追加一行说明。

内部 URL 展开

expandInternalUrls()会重写command、每个env值以及疑似协议的cwd值中的内部 URL(如skill://agent://local://)。三种位置的替换方式不同,这是源码中的一个关键细节:

  • command中的替换经过Shell 转义(shell-escaped);
  • envcwd中的替换使用noEscape: true,因为它们将成为环境值/文件系统路径,不会插值进 Shell 文本,因而采用原始值。

local://路径还会通过expandInternalUrls(..., { ensureLocalParentDirs: true })执行前创建父目录

输出与结果形态

工具返回一个text内容块加可选的details。stdout 与 stderr 在模型看到之前合并;确定的非零退出码会以Command exited with code <n>追加到错误结果文本末尾。

前台成功

  • content[0].text:命令输出;无输出时为(no output)
  • details.timeoutSeconds:经过全局/工具级钳制后的有效正超时;timeout: 0时则为details.timeoutDisabled: true
  • details.requestedTimeoutSeconds:当请求的正超时与有效超时不一致时出现。
  • details.wallTimeMs:本地/客户端终端运行完成的墙钟毫秒数。
  • details.terminalId:经客户端终端桥(client terminal bridge)执行时出现。
  • details.exitCode:命令以非零码完成时出现。
  • details.timedOut: true:本地/PTY 超时结果上出现。
  • details.meta.truncation:输出在内存中被截断时出现;完整输出溢出到 artifact 时附带artifactId
  • 非零退出与本地/PTY 超时返回标记为isError的工具结果。

后台启动(async: true或自动后台化)

  • content[0].text:可选的前缀尾部与提示,末尾为Backgrounded as job <id>; result will be delivered automatically.
  • details.async{ state: "running", jobId, type: "bash" }

后台的进度与完成通过onUpdate/ 异步 job 管理器送达:运行中更新携带尾部文本与details.async.state: "running";完成/失败更新携带最终文本与details.async.state: "completed" | "failed"非零退出或超时被记录为失败的后台 job

失败

取消、缺失退出状态、校验失败、被拦截命令以及客户端终端桥超时会抛出ToolError/ToolAbortError

使用指令与最佳实践(模型侧)

提示词文档 bash.md 给出了一组必须遵守的操作规范,它们是驱动该工具的"正确姿势":

  1. cwd而非cd:设置cwd字段代替cd;需要多行或引号繁重的值时应使用env: { NAME: "…" }传参,而不是拼进命令字符串。
  2. pty: true仅用于终端交互:典型场景是sudossh这类需要真实 TTY 交互的命令。
  3. 顺序依赖的命令用&&放进一次调用;相互独立的调用可以并发执行(非 PTY 调用默认concurrency: "shared",同一条 assistant 消息里的多个 bash 调用并行运行)。
  4. 内部 URI 自动解析为路径skill://agent://等内部协议在命令、env、cwd 中自动展开。
  5. async: true延迟有限命令的结果,但不会延长timeout——后台化不改变有效截止时间。

关键禁令(critical)

提示词中的<critical>块定义了不可妥协的规则:

  • 绝不使用 Shell 的grep/rg,应使用内置grep工具(其尊重.gitignore并返回结构化结果)。
  • read列目录、用glob找路径,绝不使用ls/find
  • 避免headtail和重定向:输出会被捕获、截断并链接为artifact://<id>,Shell 侧的手工截断反而会丢失完整输出。
  • 服务、watcher、调试器与 REPL 必须使用hubop:"start":让进程保持可观测、可管理,而不是用nohup或 Shell 后台语法放飞。

辅助内置工具集

hasShellBuiltins为真时,持久 Shell 会话注册了一组进程内辅助工具(由 pi-shell 提供):mkdir, wc, sort, comm, diff, uniq, base64, cmp, md5sum, sha{1,224,256,384,512}sum, b2sum, basename, dirname, readlink, realpath, touch, stat, date, mktemp, seq, yes, printenv, truncate, tac, nproc, uname, whoami, hostname, which, ps, pgrep, pkill, pidwait, top, cut, tee, tr, paste, sed, xargs, jq, rm, mv, ln, ts, sponge, ifne, isutf8, combine(非 Windows 另有errno)。这意味着大部分管道运算无需依赖系统外部二进制,且行为高度一致。

双通道命令策略:权限策略与专用工具路由

文档 docs/tools/bash.md 指出,有两套相互独立的设置可以阻止 Bash 子进程启动,它们目的不同、在工具调用生命周期的不同阶段运行

设置目的规则语法命中结果
bash.patterns命令级执行策略*通配符的纯文本放行 / 请求人工批准 / 拒绝
bashInterceptor.patterns优先使用专用工具JavaScript 正则(可选 flags)+ 工具名 + 消息返回 Bash 工具错误,告知模型调用指定专用工具

选择原则一句话:bash.patterns回答"命令能不能执行",用bashInterceptor.patterns回答"这个操作该由哪个工具执行"。

bash.patterns:权限策略

规则有序,首个匹配者生效。每条规则含matchglob 与approval值(allow/prompt/deny):

bash: patterns: - match: "git *" approval: allow - match: "curl *" approval: prompt - match: "rm -rf *" approval: deny

行为要点:

  • denyBashTool.execute()运行前就终止调用,包括yolo模式。
  • prompt展示批准请求,仅被接受的请求继续执行。
  • allow可以为简单命令降低审批层级,但不能批准复合命令——match: "git *"不会放行git status && rm -rf build
  • denyprompt会检查完整命令以及每个 Shell 命令段,因此cd /tmp && rm -rf build也会被rm -rf *规则捕获。

bashInterceptor.patterns:专用工具路由

拦截器是默认关闭bashInterceptor.enabled默认false)的选入式路由层,专为"技术上合法、但用现有专用工具表达更佳"的命令设计:

bashInterceptor: enabled: true patterns: - pattern: '^\s*(cat|head|tail)\s+' tool: read message: "Use the read tool instead; it handles binary files and provides better context." - pattern: '^\s*(grep|rg)\s+' tool: grep message: "Use the grep tool instead; it respects .gitignore and returns structured results."

拦截器规则仅在对应tool在当前会话可用时生效。若read被禁用,指向readcat规则就不会拦截 Bash 调用——这是"尽力而为的能力偏好",而非安全边界。

内置默认规则定义于DEFAULT_BASH_INTERCEPTOR_RULES(packages/coding-agent/src/config/settings-schema.ts),覆盖五类常见误用:

  • 文件读取类cat|head|tail|less|moreread
  • 搜索类grep|rg|ripgrep|ag|ackgrep
  • 查找类find|fd|locate(带-name/-iname/-type/-glob等标志)→glob
  • 原地编辑sed -iperl -iawk -i inplaceedit
  • 重定向写入echo|printf|cat <<>write;以及nohup、后台语法、dev/start/watch类服务进程 →hub

拦截器的匹配策略

拦截器始终先检查完整原始命令,再检查由未被引号包裹/转义的&&||;||&&或换行分隔的扁平命令片段,最后检查去掉开头NAME=value赋值后的片段。例如:

git add file && git commit -m "message" GIT_AUTHOR_NAME=Dev git commit -m "message"

锚定规则^\s*git\s+commit\b因此能同时匹配两个例子中的git commit。一个重要的例外:通过未加引号的||&消费另一命令 stdout 的阶段不算拦截候选(如printf 'x\n' | grep x中的grep x),因为路径类专用工具无法提供管道 stdin。heredoc、参数展开、命令替换、反引号、分组与畸形引号只保留完整命令检查——拦截器刻意不做成一个完整的 Shell 解析器。

两套策略的交互

批准策略在执行前解析:deny永不抵达拦截器;prompt只有在用户接受批准后才进入拦截器。若被接受的调用随后命中拦截规则,Bash 调用仍然不会运行,模型会收到路由错误并应调用专用工具。因此应避免在两个地方配置同一操作,例如cat *prompt规则加上catread拦截器会先请求批准 Bash、再拒绝 Bash 并让模型改用read——两步行为通常不是期望结果。

执行管线:从命令规范化到结果返回

BashTool.execute()的执行管线在 docs/tools/bash.md 中被分解为 16 步,核心环节如下:

  1. 读取command、校验env(键名不合法抛ToolError("Invalid bash env name: <key>")),默认timeout300
  2. cwd缺省时,将开头的cd <path> && ...重写进结构化cwd字段并从命令中剥离前缀。
  3. async: trueasync.enabled关闭时,在任何执行前抛出ToolError
  4. 拦截器开启时,checkBashInterception()对原始命令与cd剥离后的命令分别检查(规则顺序:完整输入 → 扁平片段 → 去掉NAME=value前缀的片段),命中即在 URL 展开前抛出。
  5. expandInternalUrls()重写命令、env 值与 cwd 中的内部 URL(命令替换做 Shell 转义,env/cwd 用原始值)。
  6. resolveToCwd()相对session.cwd解析cwdfs.stat()校验存在且为目录。
  7. timeout: 0禁用截止时间;否则clampTimeout("bash", ...)应用全局上限与1..3600范围。
  8. 执行路径分叉:显式async→ 托管后台 job;非 PTY + 自动后台化 → 托管 job 并等待min(thresholdMs, timeoutMs - 1000);客户端终端桥 → 远程终端;否则前台执行。
  9. 前台非 PTY 无客户端终端时调用executeBash()(packages/coding-agent/src/exec/bash-executor.ts),该路径自行执行 direnv/devenv 预检。
  10. 前台 PTY 与客户端终端路径在分发前执行同样的 direnv 预检。bash.direnv: "auto"(默认)时允许的.envrc可能合并环境变更;"off"则禁用。bash.direnvLoadTimeoutMs默认30_000,正超时也会约束预检。
  11. 本地路径在有session.allocateOutputArtifact时先分配输出 artifact,大输出可溢出到磁盘。
  12. executeBash()加载 Shell 设置、可选 Shell 快照与 minimizer 设置,通过持久原生Shell会话或一次性executeShell()运行。
  13. runInteractiveBashPty()创建PtySession,叠加 xterm 控制台 UI,转发按键输入,通过OutputSink捕获输出。
  14. 客户端终端桥调用session.getClientBridge().createTerminal(...),发出terminalId更新,轮询输出直至退出/超时/中止,信号退出映射为137
  15. 完成时#buildCompletedResult()格式化(no output)、附加截断元数据与墙钟/超时/退出说明。
  16. 本地/PTY 超时成为带details.timedOutisError结果;客户端终端超时与取消路径在附带捕获输出时抛出。

五种执行模式

  1. 前台非 PTY 本地:无客户端终端桥时的默认路径,走executeBash(),通过streamTailUpdates()TailBuffer(DEFAULT_MAX_BYTES)流式输出尾部更新。
  2. 前台非 PTY 客户端终端session.getClientBridge()?.capabilities.terminal为真、存在createTerminalpty为 false 时使用;以details.terminalId轮询当前终端输出,实施相同超时与中止行为,随后释放终端句柄。
  3. 前台 PTY:需pty: true、UI 上下文与PI_NO_PTY !== "1";使用runInteractiveBashPty()PtySession叠加层,支持交互输入,在叠加层按Esc可终止会话。PTY 路径不做非交互硬化:继承用户环境,设置真实TERM=xterm-256color,让编辑器、分页器与 TUI 表现为普通终端。
  4. 显式后台 jobasync: trueasync.enabled开启;立即注册 job 并返回{ state: "running", jobId }timeout: 0表示无工具强加截止时间。
  5. 自动后台化非 PTY jobbash.autoBackground.enabled、无 PTY/客户端终端桥且 job 管理器未达运行上限;超出等待窗口后转后台,达容量时回退为前台直跑。
  6. 被拦截命令:不创建子进程,返回指向readgrepglobeditwriteToolError

自动后台化的阈值

自动后台化默认阈值60_000msDEFAULT_AUTO_BACKGROUND_THRESHOLD_MS,定义于 packages/coding-agent/src/tools/bash.ts),有截止时间时进一步封顶为timeoutMs - 1000timeout: 0的禁用截止时间使阈值不被封顶。

非交互执行引擎:Shell 会话复用、jq 兼容与 direnv

会话复用模型

executeBash()在进程级全局 map 中缓存原生Shell实例,键为:Shell 路径、配置的命令前缀、快照路径、序列化的 Shell 环境、可选的 Agent 会话键、minimizer 配置。工具调用传入sessionKey: this.session.getSessionId?.(),bang 命令传入sessionId,实现按会话隔离复用。

并发调用绝不共享同一个Shell:原生会话一次只运行一个命令,Shell.abort()会杀掉其上所有进行中的运行。executeBash()shellSessionsInUse跟踪进行中键;键忙时,重叠调用跳过缓存,改用一次性executeShell()(与隔离会话相同)。只有持有者释放占用标志或删除缓存会话。

内置 jq 兼容性

除非设置PI_DISABLE_UUTILS_BUILTINS,非 PTY 原生 Shell 注册的jq是内置的 jaq 后端,而非系统jq。两者行为存在可观测差异:jaq 在链式访问穿越 null/缺失中间节点时报错——.a.b作用于{}退出码 5,而 jq 返回null。运行时文档建议用[.a.b?][0]保护访问:

{"c": [.a.b?][0]}

?抑制 jaq 的遍历错误,[…][0]把被抑制的空输出映射为null同时保留合法的false/null。应避免朴素的.a.b? // null//把合法的false(及null)当作缺失,会静默改写布尔数据;且{"c": .a.b? // null}在 jq 中是语法错误(值需加括号:{"c": (.a.b? // null)})。

非交互环境硬化

buildNonInteractiveEnv()(packages/coding-agent/src/exec/non-interactive-env.ts)在调用方与 direnv 覆盖之下叠加非交互硬化默认值:

  • 分页器禁用(PAGER=catGIT_PAGER=cat等,LESS=FRX);
  • 编辑器提示禁用(GIT_EDITOR=trueEDITOR=trueVISUAL=true);
  • 终端/凭据提示收敛(TERM=dumbGIT_TERMINAL_PROMPT=0SSH_ASKPASS=/usr/bin/falseNO_COLOR=1CI=true,除非PI_BASH_NO_CI/CLAUDE_BASH_NO_CI已设置);
  • npm/pnpm/yarn/pip/cargo/terraform/gh 等包管理器的非交互自动化标志;
  • Windows 上补充 UTF-8 locale/codepage 默认值。

direnv 提供的变量合并到显式调用方env之下;被 direnv 安全移除的变量以unset -v ...前缀形式预置。

输出处理:流式、截断与 artifact 溢出

PTY 与非 PTY 路径统一使用OutputSink(packages/coding-agent/src/session/streaming-output.ts):

  • 尾部滚动窗口spillThreshold/DEFAULT_MAX_BYTES目前为50KB;溢出时裁剪到尾部(UTF-8 边界安全)并标记truncated
  • 头部窗口与中间省略headBytes > 0tools.artifactHeadBytes,默认 20KB)时保留头部窗口、省略中间,在dump()中于头尾之间拼接省略标记。
  • 行宽上限maxColumns > 0tools.outputMaxColumns,默认 768 字节)时超宽行在写入时以省略号截断,行内剩余内容丢弃。
  • 原始流镜像:输出溢出、行宽截断触发或文件已激活时,完整原始流镜像到 artifact 文件。

模型看到的输出是截断后的尾窗口(及可选的头部省略视图),完整内容通过artifact://<id>提供,模型可回读。dump()返回outputtruncatedtotalLines/totalBytesoutputLines/outputBytes、省略字节/行数(中间省略时)、columnDroppedBytes/columnTruncatedLines(行宽触发时)与artifactId

此外,非 PTY 执行还会把 minimizer 设置传入原生Shell会话:当 minimizer 重写冗长输出时,执行器用最小化文本替换可见输出,原始捕获存为独立bash-originalartifact,并可能追加[raw output: artifact://<id>]footer。运行时文档明确提醒:这里的截断基于字节阈值(50KB 尾窗 + 可选头窗),并非硬性行数上限

限制、错误与会话注意事项

限制与上限速查

  • 默认超时300sTOOL_TIMEOUTS.bash.default)。
  • timeout: 0禁用命令截止时间;正超时钳制范围1..3600s
  • 内存输出尾部上限 50KB;流式回调节流 50ms;TUI 折叠预览 10 视觉行(渲染器上限,非工具输出上限)。
  • 非 PTY 执行器带截止时间时,宿主侧定时器取max(1_000, timeoutMs),并向原生运行传递同一正超时;超时的持久 Shell 会话会被隔离(quarantine)。

错误分类

  • 输入校验:非法 env 键 →ToolError("Invalid bash env name: <key>");禁用时请求 async →ToolError("Async bash execution is disabled...");缺失 job 管理器 →ToolError("Background job manager unavailable for this session.")cwd缺失/非目录 →ToolError("Working directory does not exist: ...")/ToolError("Working directory is not a directory: ...")
  • 拦截器:命中 →ToolError("Blocked: <rule.message>")附带原始命令;非法正则被compileRules()静默跳过。
  • 内部 URL 展开:不支持的 scheme、未知 skill、路径穿越、缺失路由支持或路由解析失败均从 bash-skill-urls.ts 抛出ToolError
  • 执行:非零退出 →isError结果(details.exitCode+Command exited with code <n>);缺失退出码 →ToolError("Command failed: missing exit status");超时 → 本地/PTY 返回details.timedOut: true,客户端终端桥杀终端后抛ToolError;用户中止 →ToolAbortError

并发与会话细节

  • BashTool设置strict = trueconcurrency逐调用解析:pty: true"exclusive"(独占终端 UI),其余为"shared",因此同一条 assistant 消息中的多个非 PTY bash 调用并行运行。并行调用重叠同一 Shell 会话键时,首个持有持久Shell,其余使用隔离的一次性 Shell。
  • 拦截器仅在匹配规则的tool存在于ctx.toolNames时拦截;缺失工具会使对应规则失效。
  • bash.direnv默认"auto"并遵守 direnv 的 allow 列表——未允许的.envrc不会被执行;设为"off"可跳过预检。
  • PTY 在非 UI 上下文及PI_NO_PTY=1时被忽略(由canUseInteractiveBashPty()判定),回退为非 PTY 并追加pty requested but unavailable in this environment; ran without a terminal提示。

两种执行表面:工具调用与用户 bang 命令

运行时文档强调,coding-agent 中存在两个不同的 bash 执行表面

  1. 工具调用表面toolName: "bash"):模型调用 bash 工具时使用,入口BashTool.execute(),参数含command、可选envtimeoutcwdptyasyncasync.enabled开启时)。
  2. 用户 bang 命令表面(交互输入中的!cmd,或 RPCbash命令):会话级辅助路径,入口AgentSession.executeBash(),渲染走BashExecutionComponent(packages/coding-agent/src/modes/components/bash-execution.ts),交互 UI 组件折叠预览保留最近 20 个逻辑行、单行 4000 字符钳制。

两者最终都经由executeBash()执行非 PTY 逻辑,但只有工具调用路径运行规范化/拦截、可选托管后台 job 与工具渲染器逻辑。将bash.enabled: false写入设置可从工具注册表中移除模型侧的 bash 工具,但这不会禁用用户 bang 命令与 RPCbash请求。

进一步阅读

  • 工具入口与执行管线:packages/coding-agent/src/tools/bash.ts
  • 非 PTY 执行器、会话复用与取消:packages/coding-agent/src/exec/bash-executor.ts
  • 拦截器规则匹配:packages/coding-agent/src/tools/bash-interceptor.ts
  • 内部 URL 展开:packages/coding-agent/src/tools/bash-skill-urls.ts
  • 输出流、截断与 artifact 溢出:packages/coding-agent/src/session/streaming-output.ts
  • 默认拦截器规则与设置 schema:packages/coding-agent/src/config/settings-schema.ts
  • 完整工具规范:docs/tools/bash.md
  • 运行时内部机制(会话复用键、快照、前缀处理、原生超时行为):docs/bash-tool-runtime.md

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询