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 -l、sort | uniq -c、diff。 - 不应使用:内联脚本(inline scripts)、heredoc、
$(…)命令替换、复杂的控制流与引号嵌套、以及非平凡管道。这些场景在会话启用eval工具时路由到eval;未启用时,应使用专用工具或仓库中已检入的脚本(checked-in script)。
这一边界同时是能力边界而非性能偏好:拦截器(interceptor)只对"可以用路径类专用工具替代的简单命令"做路由,而bash本身承载的是那些无法被read、grep、glob、edit、write覆盖的真实 Shell 计算能力,例如进程管理、文件操作与管道数据处理。
输入参数详解
工具入口为BashTool.execute(),实现在 packages/coding-agent/src/tools/bash.ts。完整参数如下:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
command | string | 是 | 要执行的 Shell 命令文本。当cwd未提供时,开头的cd <path> && ...会被重写为cwd字段并从命令中剥离。 |
env | Record<string, string> | 否 | 附加环境变量。键名必须匹配^[A-Za-z_][A-Za-z0-9_]*$,否则抛出错误。值会经过内部 URL 展开,以环境值而非 Shell 文本的形式传入。 |
timeout | number | 否 | 超时秒数,默认300。0表示禁用截止时间。正值先受tools.maxTimeout全局上限约束,再被钳制到 Bash 范围1..3600秒。 |
cwd | string | 否 | 工作目录,相对session.cwd通过resolveToCwd解析,必须存在且为目录。 |
pty | boolean | 否 | 请求 PTY 模式,默认false。仅在pty: true、PI_NO_PTY !== "1"且工具上下文具备 UI 时生效。 |
async | boolean | 否 | 后台执行请求。仅当会话启用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);env与cwd中的替换使用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 给出了一组必须遵守的操作规范,它们是驱动该工具的"正确姿势":
- 用
cwd而非cd:设置cwd字段代替cd;需要多行或引号繁重的值时应使用env: { NAME: "…" }传参,而不是拼进命令字符串。 pty: true仅用于终端交互:典型场景是sudo、ssh这类需要真实 TTY 交互的命令。- 顺序依赖的命令用
&&放进一次调用;相互独立的调用可以并发执行(非 PTY 调用默认concurrency: "shared",同一条 assistant 消息里的多个 bash 调用并行运行)。 - 内部 URI 自动解析为路径:
skill://、agent://等内部协议在命令、env、cwd 中自动展开。 async: true延迟有限命令的结果,但不会延长timeout——后台化不改变有效截止时间。
关键禁令(critical)
提示词中的<critical>块定义了不可妥协的规则:
- 绝不使用 Shell 的
grep/rg,应使用内置grep工具(其尊重.gitignore并返回结构化结果)。 - 用
read列目录、用glob找路径,绝不使用ls/find。 - 避免
head、tail和重定向:输出会被捕获、截断并链接为artifact://<id>,Shell 侧的手工截断反而会丢失完整输出。 - 服务、watcher、调试器与 REPL 必须使用
hub(op:"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行为要点:
deny在BashTool.execute()运行前就终止调用,包括yolo模式。prompt展示批准请求,仅被接受的请求继续执行。allow可以为简单命令降低审批层级,但不能批准复合命令——match: "git *"不会放行git status && rm -rf build。deny与prompt会检查完整命令以及每个 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被禁用,指向read的cat规则就不会拦截 Bash 调用——这是"尽力而为的能力偏好",而非安全边界。
内置默认规则定义于DEFAULT_BASH_INTERCEPTOR_RULES(packages/coding-agent/src/config/settings-schema.ts),覆盖五类常见误用:
- 文件读取类
cat|head|tail|less|more→read; - 搜索类
grep|rg|ripgrep|ag|ack→grep; - 查找类
find|fd|locate(带-name/-iname/-type/-glob等标志)→glob; - 原地编辑
sed -i、perl -i、awk -i inplace→edit; - 重定向写入
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规则加上cat→read拦截器会先请求批准 Bash、再拒绝 Bash 并让模型改用read——两步行为通常不是期望结果。
执行管线:从命令规范化到结果返回
BashTool.execute()的执行管线在 docs/tools/bash.md 中被分解为 16 步,核心环节如下:
- 读取
command、校验env(键名不合法抛ToolError("Invalid bash env name: <key>")),默认timeout为300。 cwd缺省时,将开头的cd <path> && ...重写进结构化cwd字段并从命令中剥离前缀。async: true而async.enabled关闭时,在任何执行前抛出ToolError。- 拦截器开启时,
checkBashInterception()对原始命令与cd剥离后的命令分别检查(规则顺序:完整输入 → 扁平片段 → 去掉NAME=value前缀的片段),命中即在 URL 展开前抛出。 expandInternalUrls()重写命令、env 值与 cwd 中的内部 URL(命令替换做 Shell 转义,env/cwd 用原始值)。resolveToCwd()相对session.cwd解析cwd,fs.stat()校验存在且为目录。timeout: 0禁用截止时间;否则clampTimeout("bash", ...)应用全局上限与1..3600范围。- 执行路径分叉:显式
async→ 托管后台 job;非 PTY + 自动后台化 → 托管 job 并等待min(thresholdMs, timeoutMs - 1000);客户端终端桥 → 远程终端;否则前台执行。 - 前台非 PTY 无客户端终端时调用
executeBash()(packages/coding-agent/src/exec/bash-executor.ts),该路径自行执行 direnv/devenv 预检。 - 前台 PTY 与客户端终端路径在分发前执行同样的 direnv 预检。
bash.direnv: "auto"(默认)时允许的.envrc可能合并环境变更;"off"则禁用。bash.direnvLoadTimeoutMs默认30_000,正超时也会约束预检。 - 本地路径在有
session.allocateOutputArtifact时先分配输出 artifact,大输出可溢出到磁盘。 executeBash()加载 Shell 设置、可选 Shell 快照与 minimizer 设置,通过持久原生Shell会话或一次性executeShell()运行。runInteractiveBashPty()创建PtySession,叠加 xterm 控制台 UI,转发按键输入,通过OutputSink捕获输出。- 客户端终端桥调用
session.getClientBridge().createTerminal(...),发出terminalId更新,轮询输出直至退出/超时/中止,信号退出映射为137。 - 完成时
#buildCompletedResult()格式化(no output)、附加截断元数据与墙钟/超时/退出说明。 - 本地/PTY 超时成为带
details.timedOut的isError结果;客户端终端超时与取消路径在附带捕获输出时抛出。
五种执行模式
- 前台非 PTY 本地:无客户端终端桥时的默认路径,走
executeBash(),通过streamTailUpdates()与TailBuffer(DEFAULT_MAX_BYTES)流式输出尾部更新。 - 前台非 PTY 客户端终端:
session.getClientBridge()?.capabilities.terminal为真、存在createTerminal且pty为 false 时使用;以details.terminalId轮询当前终端输出,实施相同超时与中止行为,随后释放终端句柄。 - 前台 PTY:需
pty: true、UI 上下文与PI_NO_PTY !== "1";使用runInteractiveBashPty()与PtySession叠加层,支持交互输入,在叠加层按Esc可终止会话。PTY 路径不做非交互硬化:继承用户环境,设置真实TERM=xterm-256color,让编辑器、分页器与 TUI 表现为普通终端。 - 显式后台 job:
async: true且async.enabled开启;立即注册 job 并返回{ state: "running", jobId },timeout: 0表示无工具强加截止时间。 - 自动后台化非 PTY job:
bash.autoBackground.enabled、无 PTY/客户端终端桥且 job 管理器未达运行上限;超出等待窗口后转后台,达容量时回退为前台直跑。 - 被拦截命令:不创建子进程,返回指向
read、grep、glob、edit或write的ToolError。
自动后台化的阈值
自动后台化默认阈值60_000ms(DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS,定义于 packages/coding-agent/src/tools/bash.ts),有截止时间时进一步封顶为timeoutMs - 1000;timeout: 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=cat、GIT_PAGER=cat等,LESS=FRX); - 编辑器提示禁用(
GIT_EDITOR=true、EDITOR=true、VISUAL=true); - 终端/凭据提示收敛(
TERM=dumb、GIT_TERMINAL_PROMPT=0、SSH_ASKPASS=/usr/bin/false、NO_COLOR=1、CI=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 > 0(tools.artifactHeadBytes,默认 20KB)时保留头部窗口、省略中间,在dump()中于头尾之间拼接省略标记。 - 行宽上限:
maxColumns > 0(tools.outputMaxColumns,默认 768 字节)时超宽行在写入时以省略号截断,行内剩余内容丢弃。 - 原始流镜像:输出溢出、行宽截断触发或文件已激活时,完整原始流镜像到 artifact 文件。
模型看到的输出是截断后的尾窗口(及可选的头部省略视图),完整内容通过artifact://<id>提供,模型可回读。dump()返回output、truncated、totalLines/totalBytes、outputLines/outputBytes、省略字节/行数(中间省略时)、columnDroppedBytes/columnTruncatedLines(行宽触发时)与artifactId。
此外,非 PTY 执行还会把 minimizer 设置传入原生Shell会话:当 minimizer 重写冗长输出时,执行器用最小化文本替换可见输出,原始捕获存为独立bash-originalartifact,并可能追加[raw output: artifact://<id>]footer。运行时文档明确提醒:这里的截断基于字节阈值(50KB 尾窗 + 可选头窗),并非硬性行数上限。
限制、错误与会话注意事项
限制与上限速查
- 默认超时
300s(TOOL_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 = true;concurrency逐调用解析: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 执行表面:
- 工具调用表面(
toolName: "bash"):模型调用 bash 工具时使用,入口BashTool.execute(),参数含command、可选env、timeout、cwd、pty及async(async.enabled开启时)。 - 用户 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),仅供参考