Bash 工具讲解

Bash 工具

1. 定位

Bash 是 Agent 执行平台 shell 命令的唯一入口:装依赖、跑测试、查 git 状态、调 CLI 工具,都通过这个工具完成。它的 capability 是 "Execute platform shell commands through the execution adapter"(bash.ts:447),是内置工具中副作用范围最大、风险等级最高 的一个(sideEffectScope: "system",riskLevel: "high",needsApproval: true)------其他工具只在各自领域内动作,Bash 能触达进程、文件系统、网络与系统状态的任意组合。

它不只是"能跑命令",还承担三件系统级职责:

  1. 权限模型的主战场:只读命令自动免批准、命令级 allow/deny 规则、稳定前缀规则建议,都建立在 Bash 的命令解析之上(见第 8 节)。
  2. 长任务与回合解耦 :run_in_background 让命令脱离当前 turn 继续运行,完成后通过通知 turn 唤醒模型(见第 6 节)。
  3. readFileState 的双向锚点:写类命令会把模型已读过的文件改"脏"(stale hint),读类命令会把文件回填为"已读"(backfill)------这是 Edit/Write "读后才能改" 约束在 shell 侧的延伸(见第 9 节)。

2. 基本信息

属性 值 说明
工具名 "Bash" 命令是 sed -i 原地编辑时 UI 显示为 "Update"(bash-metadata.ts)
只读 readOnly: false 默认按有副作用处理;只读命令经运行时判定后自动降级(第 8.1 节)
批准 needsApproval: true 高风险;只读判定通过则免批准
并发 concurrentSafe: false 不与其他工具并行调用
默认超时 120s 可被调用方 timeout 覆盖,上限 600s(第 4 节)
取消 支持 cleanup: "bestEffort";杀整个进程树(第 7.4 节)
结果预算 artifact 策略 inline/模型上限 30,000 字节,超限落盘为 artifact(第 7 节)
追踪 recordInput/Output: "summary" 只记摘要,不记全量输出

3. 输入参数

参数 类型 必填 说明
command string 是 要执行的命令
timeout number 否 超时毫秒数,上限 600,000;0/未传回落到默认值
description string 否 命令用途的主动语态简述,展示给用户
run_in_background boolean 否 true 时后台运行,跨 turn 存活,退出后重新唤醒模型
dangerouslyDisableSandbox boolean 否 契约保留字段:关闭沙箱模式

参数经两层校验:

  • JSON Schema 层 (投给模型的结构):.strict(),多余字段直接拒绝;timeout 的描述文案按超时策略动态生成(createBashInputJsonSchema)。
  • 运行时 Zod 语义层 (BashInputSchema):semanticNumber 把字符串数字(如 "120000")归一为数值;semanticBoolean 把 "true"/"1"/"yes"/"y"/"on" 与 "false"/"0"/"no"/"n"/"off" 及数字 0/1 归一为布尔------这些都是对模型偶发类型漂移的宽容,而不是放宽语义。

description 字段的描述文本本身就是一段提示词(contracts/src/tools/bash.ts:14):简单命令保持 5-10 词(git status → "Show working tree status"),管道或冷门参数命令要写清实际效果(git reset --hard origin/main → "Discard all local changes and match remote main"),且禁止出现 "complex"、"risk" 这类词------描述是给用户看的进度标签,不是风险评估。

关于 dangerouslyDisableSandbox:ExecutionSandboxPolicy 在契约中仍有定义(execution.port.ts:108),handler 也会把 sandbox.enabled 传进请求,但当前运行时已撤除沙箱强制 ------适配器中没有任何消费者读取该字段(node-execution-adapter-run.ts:276 注释明确记录了"protected-resource sandbox 时代"的历史)。该参数现在只做透传与输出回显,不代表存在沙箱边界。

4. 超时策略

ts 复制代码
// bash-timeout-policy.ts
export const DEFAULT_BASH_TIMEOUT_MS = 120_000;
export const DEFAULT_BASH_MAX_TIMEOUT_MS = 600_000;

export function resolveBashTimeoutMs(inputTimeoutMs, policy) {
  return Math.min(inputTimeoutMs || policy.defaultTimeoutMs, policy.maxTimeoutMs);
}
  • 环境变量可调 :BASH_DEFAULT_TIMEOUT_MS、BASH_MAX_TIMEOUT_MS(resolveBashTimeoutPolicy);配置后的 max 会被抬到不低于 default,避免出现"默认值大于上限"的倒挂。
  • timeout: 0 等于默认值 ,不是"无限"。|| 短路把 0 归到 default------这是与旧 watchdog 的共用语义,历史 bug(字符串数字绕过 policy、0 被压成 1ms)的修复就是让 watchdog 与 handler 走同一个 resolveBashTimeoutMs(bash.ts:542 注释)。
  • 计时器只约束子进程运行 。timeout 计时器在 spawn 完成后才启动(node-execution-adapter-run.ts:280);shell 快照采集、artifact 准备等异步准备阶段不算在 timeout 内,取消与关闭在准备期由 stoppedBeforeSpawn 兜底。
  • 杀进程有两级 :requestStop("timeout") 先 terminateProcessTree(进程组/taskkill),5 秒(FORCE_EXIT_AFTER_KILL_MS)后仍存活则 SIGKILL 并销毁输出管道------root shell 退出不代表它派生的后代已退出,必须防 orphan。executor 侧的 cleanupGraceMs: 6_000 与这 5 秒对应。
  • 超时后 status: "timed_out"、timedOut: true、returnCodeInterpretation: "Command timed out",模型收到的是结果对象而不是异常。

5. 执行主流程

#mermaid-svg-sQcK6WYTaPUgkJxk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sQcK6WYTaPUgkJxk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sQcK6WYTaPUgkJxk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sQcK6WYTaPUgkJxk .error-icon{fill:#552222;}#mermaid-svg-sQcK6WYTaPUgkJxk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sQcK6WYTaPUgkJxk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sQcK6WYTaPUgkJxk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sQcK6WYTaPUgkJxk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sQcK6WYTaPUgkJxk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sQcK6WYTaPUgkJxk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sQcK6WYTaPUgkJxk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sQcK6WYTaPUgkJxk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sQcK6WYTaPUgkJxk .marker.cross{stroke:#333333;}#mermaid-svg-sQcK6WYTaPUgkJxk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sQcK6WYTaPUgkJxk p{margin:0;}#mermaid-svg-sQcK6WYTaPUgkJxk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sQcK6WYTaPUgkJxk .cluster-label text{fill:#333;}#mermaid-svg-sQcK6WYTaPUgkJxk .cluster-label span{color:#333;}#mermaid-svg-sQcK6WYTaPUgkJxk .cluster-label span p{background-color:transparent;}#mermaid-svg-sQcK6WYTaPUgkJxk .label text,#mermaid-svg-sQcK6WYTaPUgkJxk span{fill:#333;color:#333;}#mermaid-svg-sQcK6WYTaPUgkJxk .node rect,#mermaid-svg-sQcK6WYTaPUgkJxk .node circle,#mermaid-svg-sQcK6WYTaPUgkJxk .node ellipse,#mermaid-svg-sQcK6WYTaPUgkJxk .node polygon,#mermaid-svg-sQcK6WYTaPUgkJxk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sQcK6WYTaPUgkJxk .rough-node .label text,#mermaid-svg-sQcK6WYTaPUgkJxk .node .label text,#mermaid-svg-sQcK6WYTaPUgkJxk .image-shape .label,#mermaid-svg-sQcK6WYTaPUgkJxk .icon-shape .label{text-anchor:middle;}#mermaid-svg-sQcK6WYTaPUgkJxk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sQcK6WYTaPUgkJxk .rough-node .label,#mermaid-svg-sQcK6WYTaPUgkJxk .node .label,#mermaid-svg-sQcK6WYTaPUgkJxk .image-shape .label,#mermaid-svg-sQcK6WYTaPUgkJxk .icon-shape .label{text-align:center;}#mermaid-svg-sQcK6WYTaPUgkJxk .node.clickable{cursor:pointer;}#mermaid-svg-sQcK6WYTaPUgkJxk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sQcK6WYTaPUgkJxk .arrowheadPath{fill:#333333;}#mermaid-svg-sQcK6WYTaPUgkJxk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sQcK6WYTaPUgkJxk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sQcK6WYTaPUgkJxk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sQcK6WYTaPUgkJxk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sQcK6WYTaPUgkJxk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sQcK6WYTaPUgkJxk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sQcK6WYTaPUgkJxk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sQcK6WYTaPUgkJxk .cluster text{fill:#333;}#mermaid-svg-sQcK6WYTaPUgkJxk .cluster span{color:#333;}#mermaid-svg-sQcK6WYTaPUgkJxk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sQcK6WYTaPUgkJxk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sQcK6WYTaPUgkJxk rect.text{fill:none;stroke-width:0;}#mermaid-svg-sQcK6WYTaPUgkJxk .icon-shape,#mermaid-svg-sQcK6WYTaPUgkJxk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sQcK6WYTaPUgkJxk .icon-shape p,#mermaid-svg-sQcK6WYTaPUgkJxk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sQcK6WYTaPUgkJxk .icon-shape .label rect,#mermaid-svg-sQcK6WYTaPUgkJxk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sQcK6WYTaPUgkJxk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sQcK6WYTaPUgkJxk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sQcK6WYTaPUgkJxk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 无
有
是
否
是且 run_in_background
否
是
否
是
否
Bash(command, ...)
BashInputSchema.parse
executionPort?
ConfigurationError
command 为空?
空输出,不 spawn
闲时 turn?
拒绝:闲时不支持后台
run_in_background?
显式后台生命周期

spawn 后立即提交后台
自动转后台资格?

非空命令且首 token ≠ sleep
前台先跑

超时 deadline 到 → 转后台
普通前台执行 executionPort.run
toBashOutput 结算
cwd 策略决策
readFileState 副作用

stale hint + backfill
返回 BashOutput

handler(bash.ts:104)构造 ExecutionRequest 时的关键决策:

决策 值 说明
command.mode "shell" + shellProfile: "posix-bash" 固定值;shellProfile 是内部标记,决定走合并输出文件路径,且绝不能从 hook 或用户配置注入
shellOverride session shell selection 由运行时注入的 shell 选择(第 10 节)
captureCwdAfterSuccess 仅前台为 true 后台任务不捕获 cwd
outputLimit.persistOutput 后台 "always" / 前台 "on_truncate" Bash 从 spawn 起就直接写盘,此开关只控制结算后文件是否保留
bashPrelude embedded search prelude posix/git-bash session 且 embedded search 开启时注入 find/grep shell 函数,把它们路由到内嵌搜索后端;此时工具描述的"避免命令"清单会去掉 find/grep
sandbox { enabled: !dangerouslyDisableSandbox } 见第 3 节,当前无强制语义

空命令是合法输入 :command.trim() 为空时直接返回空输出(emptyBashOutput),不 spawn、不报错------模型偶尔会发出探针式调用,按"无事发生"处理最便宜。

6. 后台执行与完成通知

6.1 两种后台路径

显式后台 (run_in_background: true):走 runBashWithBackgroundLifecycle(mode: "explicit")。子进程 spawn 出来(收到 started 事件)后立即 提交后台(commitBackground):登记 exec_<uuid> 任务、脱离 parent turn 的 AbortSignal、清掉前台 deadline,工具调用随即返回 backgrounded 结果------模型拿到 backgroundTaskId 和输出文件路径,当前 turn 继续往下走。

超时自动转后台 (auto_on_timeout):符合条件的命令(非空、首 token 不是 sleep、未显式要求后台------isBashAutoBackgroundEligible)先前台执行;deadline 到了(即调用方的 timeout 用尽)不杀进程 ,而是把整个执行转成后台任务。关键细节(node-execution-adapter-lifecycle.ts:123):

ts 复制代码
const runRequest: ExecutionRequest = {
  ...request,
  timeoutMs: 0,                    // 子进程永不被适配器超时杀掉
  outputLimit: { ..., persistOutput: "always" },
};

前台 deadline 是生命周期自己 setTimeout(commitBackground, timeoutMs) 实现的,与适配器的 timeout 计时器分离;转后台时原子提交状态(state = "backgrounded")→ 清 deadline → 脱离 parent abort → 登记任务,避免 abort/completion 在提交缝隙里误杀后台进程。

sleep 被排除在自动转后台之外:等待类命令转后台只是把"占着前台"换成"挂着后台",模型还得轮询,不如直接让超时语义生效。

6.2 后台期间的状态与事件顺序

runtime 命令队列 子进程 后台生命周期(适配器所有) Bash handler 模型(当前 turn) runtime 命令队列 子进程 后台生命周期(适配器所有) Bash handler 模型(当前 turn) #mermaid-svg-d2YnnhDWibdCUj9X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-d2YnnhDWibdCUj9X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-d2YnnhDWibdCUj9X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-d2YnnhDWibdCUj9X .error-icon{fill:#552222;}#mermaid-svg-d2YnnhDWibdCUj9X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-d2YnnhDWibdCUj9X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-d2YnnhDWibdCUj9X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-d2YnnhDWibdCUj9X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-d2YnnhDWibdCUj9X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-d2YnnhDWibdCUj9X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-d2YnnhDWibdCUj9X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-d2YnnhDWibdCUj9X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-d2YnnhDWibdCUj9X .marker.cross{stroke:#333333;}#mermaid-svg-d2YnnhDWibdCUj9X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-d2YnnhDWibdCUj9X p{margin:0;}#mermaid-svg-d2YnnhDWibdCUj9X .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-d2YnnhDWibdCUj9X text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-d2YnnhDWibdCUj9X .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-d2YnnhDWibdCUj9X .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-d2YnnhDWibdCUj9X .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-d2YnnhDWibdCUj9X .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-d2YnnhDWibdCUj9X #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-d2YnnhDWibdCUj9X .sequenceNumber{fill:white;}#mermaid-svg-d2YnnhDWibdCUj9X #sequencenumber{fill:#333;}#mermaid-svg-d2YnnhDWibdCUj9X #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-d2YnnhDWibdCUj9X .messageText{fill:#333;stroke:none;}#mermaid-svg-d2YnnhDWibdCUj9X .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-d2YnnhDWibdCUj9X .labelText,#mermaid-svg-d2YnnhDWibdCUj9X .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-d2YnnhDWibdCUj9X .loopText,#mermaid-svg-d2YnnhDWibdCUj9X .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-d2YnnhDWibdCUj9X .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-d2YnnhDWibdCUj9X .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-d2YnnhDWibdCUj9X .noteText,#mermaid-svg-d2YnnhDWibdCUj9X .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-d2YnnhDWibdCUj9X .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-d2YnnhDWibdCUj9X .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-d2YnnhDWibdCUj9X .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-d2YnnhDWibdCUj9X .actorPopupMenu{position:absolute;}#mermaid-svg-d2YnnhDWibdCUj9X .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-d2YnnhDWibdCUj9X .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-d2YnnhDWibdCUj9X .actor-man circle,#mermaid-svg-d2YnnhDWibdCUj9X line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-d2YnnhDWibdCUj9X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} turn 结束,命令继续跑 另起一轮通知 turn 重新唤醒模型 Bash(command, run_in_background=true)runBashWithBackgroundLifecycle(explicit)spawn(timeoutMs=0,输出直写文件)started 事件{ kind: "backgrounded", taskId: "exec_xxx" }BashOutput(status="backgrounded",backgroundTaskId, rawOutputPath)进程退出,结算 ExecutionResultTaskNotificationRuntimeCommand 入队runTaskNotificationBatch(合并一批通知)

状态机(lifecycle.ts:145):preparing → foreground → backgrounded → settling → terminal。前台正常跑完不经过 backgrounded,直接 settling 落回 foreground 结果------"超时了但进程其实已经自己退出"这类竞态由 onExit 先行把状态推进到 settling,防止 deadline 再把已完成的命令转成后台。

6.3 闲时 turn 的特殊约束

闲时(off-peak)turn 显式拒绝 run_in_background,并同步关闭超时自动转后台(bash.ts:136 注释):后台命令完成后通知 turn 不带 turnExecutionModel,闲时 turn 结束后会落到用户自己的套餐上跑完整 agent loop。这与 subagent runner 的 BACKGROUND_UNAVAILABLE 对称。

6.4 后台任务的配套工具

  • TaskOutput :查询后台任务状态与输出。运行中的 Bash 固定读输出文件头部 30,000 字节(task-output-bash.ts:115 注释------通用 8 MiB 尾读会让模型看到输出末尾,丢失开头的报错);已完成的任务走通用读取。执行端口还有 readBackgroundBashOutput(固定尾读 8 KiB,BACKGROUND_BASH_OUTPUT_MAX_BYTES),供协议层做轻量预览。
  • TaskStop :对 exec_<uuid> 任务 abort,状态先标 cancelled 再异步结算。
  • Read :backgroundTaskId 的输出文件路径就是普通文件,模型可直接 Read 看中间输出(模型可见文案里明确提示了这一点)。
  • Ctrl+B 手动后台 :backgroundedByUser 是契约字段,模型可见文案为 "Command was manually backgrounded by user"。assistantAutoBackgrounded 同为契约字段(assistant 模式 15 秒阻塞预算的自动后台化),当前检出的运行时代码未产生这两个标记,只有格式化层的消费文案(bash-model-content.ts:119)。

7. 输出

7.1 输出结构(BashOutput)

核心字段分五组:

组 字段 说明
正文 stdout / stderr 模型可见文本;isImage 时 stdout 是 data URL
状态 status / exitCode / signal / timedOut / cancelled / interrupted 六种 status:completed / failed / timed_out / cancelled / spawn_error / backgrounded;非零退出码以结果返回,不抛异常
截断 stdoutTruncated / stderrTruncated / stdoutBytes / stderrBytes bytes 是截断前的观测总量
持久化 rawOutputPath / persistedOutputPath / stdout/stderrPersistedOutputPath + 各自 Size 完整输出落盘位置
提示 returnCodeInterpretation / staleReadFileStateHint / ghRateLimitHint / noOutputExpected 语义化退出码、readFileState 提醒、gh 限流提醒、"该命令成功时本就无输出"标记

7.2 模型可见格式

formatBashModelContent(bash-model-content.ts)决定模型最终看到什么,按优先级四分叉:

  1. 结构化内容 :structuredContent 非空时直接透传。
  2. provider 错误 :status: "failed" 且非语义化非错误(见下)→ 纯文本 Exit code N + stdout + stderr + 后台信息 + hints。失败以纯文本进入上下文,不透出 JSON。
  3. 图片输出:stdout 是 data URL 时转成 image block。
  4. 正常文本 :stdout(去掉前导空行、掐掉尾部空白)+ stderr(被中断时追加 <error>Command was aborted before completion</error>)+ 后台信息 + stale hint + gh hint。

语义化非错误退出码 (bash-semantics.ts:98)防止把正常信号当失败:grep/rg/git grep 退出码 1 → "No matches found",diff → "Files differ",test/[ → "Condition is false",find → "Some directories were inaccessible"。这些解释命中的结果不按 provider error 处理 (不显示 Exit code 1 错误头)。判定看的是最后一条状态命令 ------命令行前面出现过 rg/grep 不会把后续 test 的 1 误判成 "No matches found"(bash-semantics.ts:146 注释)。

noOutputExpected 标记 cd、mkdir、mv、rm、export 等"成功即静默"的命令,供 UI 区分"命令没输出"与"输出丢了"。

7.3 输出限额与落盘

Bash 走合并输出文件 路径(shellProfile: "posix-bash" 触发 isBashMergedOutputRequest):stdout 和 stderr 从 spawn 起直接写入同一文件 <storage>/cli/exec/<sessionId>/<toolCallId>-stdout.log(模式 0600;ZCODE_STORAGE_DIR 可重定向存储根)。各层限额:

层 上限 说明
inline / 模型正文 30,000 字节(MAX_INLINE_OUTPUT_BYTES) 环境变量 BASH_MAX_OUTPUT_LENGTH 可调(默认 30,000,硬顶 150,000)
executor 结果预算 maxModelBytes: 30,000,artifact 策略,tail 预览 超预算时正文替换为"落盘 envelope":2,000 字符预览 + persisted 路径(MODEL_RESULT_PREVIEW_CHARS = 2_000)
落盘文件 5 GiB(BASH_RUNTIME_OUTPUT_LIMIT_BYTES) 文件 watchdog 每 5 秒检查(OUTPUT_WATCH_INTERVAL_MS),严格超出后终止进程:"Command killed: output file exceeded 5GB"
注册表元数据 maxOutputBytes: 10_000_000 ToolEntry 级预算声明

artifactBytes 是 64 MiB cap 后的实际文件大小,而模型文案里的原始观测大小用截断前的 stream bytes------bash-output.ts:41 注释明确了这组数字不能混用。

进度事件 :运行 ≥2 秒后每 1 秒发一次 progress 事件(阈值 DEFAULT_PROGRESS_THRESHOLD_MS = 2_000、间隔 1s),携带输出字节数与尾部 4 KB 文本 (DEFAULT_PROGRESS_TAIL_BYTES);文件模式下 watchProgress 每次做有界尾读,计算 outputPreview(行数/字节数 + 预估)。handler 把它转成 ToolCallProgress 会话事件,UI 据此展示"还在动"。

7.4 取消与杀进程

取消(用户打断 / turn abort / adapter 关闭)触发 terminateProcessTree:POSIX 杀进程组,Windows 用 taskkill 树杀;root shell 退出后其派生后代可能仍持有输出管道,所以 5 秒后强制 SIGKILL 并 destroyChildOutputStreams 释放读端------不能让未知进程保活 CLI。合并文件模式下以退出码 143/137 收尾。取消后模型可见文案是被中断的 stderr + "Command was aborted before completion"。

7.5 图片输出

stdout 是图片 data URL 时(含从落盘文件回读识别,源文件 ≤ 20 MB),经 imageProcessorPort.resizeToFit 压到 2,000px 内转成 image block(bash-image-output.ts)。这是 best-effort 能力:resize 失败时先回退原始 data URL(magic 校验通过的图不该因优化失败降级成文本),解码彻底失败才回退普通文本------绝不把坏图片块发给 provider。provider error 状态不做图片识别。

7.6 gh 限流提醒

命令是 gh 子命令且输出命中 rate limit 文案时,追加一条 <system-reminder>("GitHub API rate limit exceeded (5,000/hr)... Run gh api rate_limit --jq .resources and sleep until reset... use ScheduleWakeup instead of retrying"),60 秒冷却 防止重复注入(bash-gh-rate-limit.ts)。

8. 权限

8.1 只读命令自动免批准

resolveBashPermissionCapability(bash.ts:77)对运行时判定为只读 的命令返回低风险能力(readOnly: true、needsApproval: false、sideEffectScope: "none"),其余命令维持 ToolEntry 的高风险默认。判定入口是 isRuntimeReadOnlyBashCommand(bash-semantics.ts:39),四道关卡:

  1. analyzeBashCommand 解析必须"安全"(无解析错误、无不支持语法、无动态词);
  2. git + 目录切换(同现 git 与 cd/pushd/popd)组合直接不合格------git 可能在目标目录加载 hooks/config(bash-git-runtime-safety.ts:37 注释);git 命令还要求可信的 git 运行时上下文 :isGitRuntimeContextUnsafe 沿 cwd 及祖先目录检查裸仓库指示(HEAD 文件、objects/refs)与 .git 指针(symlink / gitdir: 文件)是否指向 cwd 内部或缺少 .git 段,不可信则 git 不享受只读放行;
  3. 每条子命令不能带已知写选项(hasKnownBashWriteOption),且逐条通过 readonly policy(bash-readonly-policy*.ts 十余个模块:git 只读子命令表、gh 只读多词表、文件类命令的 flag 级判定等);
  4. 必须至少命中一条只读命令。

8.2 命令解析是权限的地基

bash-command-parser.ts 用 unbash 解析命令(上限 10,000 字符),产出每条 invocation 的 argv、环境变量前缀、重定向与操作符。**"不安全"**指三种情况之一:

  • hasParseErrors:解析失败;
  • hasUnsupportedSyntax:&&/||/管道/简单命令之外的容器语法(if/for/后台 & 等);
  • hasDynamicWords:命令替换、进程替换、参数展开、算术展开等会在主命令前执行或产生不可静态确定文本的部分------权限判断不能把它们当普通 argv。

不安全的命令拿不到任何前缀规则,只能走整命令精确匹配------模型想绕过解析器的自由度被压到零。

8.3 规则评估与稳定前缀建议

Bash 的 permission 声明(bash.ts:470):

ts 复制代码
permission: {
  permission: "bash",
  patternSources: ["command"],           // 按命令维度记录/匹配模式
  alwaysAllowPatternSources: ["command"],
  denyPriority: "beforeAsk",             // deny 规则在询问前生效
}

resolveBashPermissionRulePolicy(bash-command-permission-policy.ts)做两件事:

  1. 规则评估 :把用户已保存的 allow/deny 规则应用到本次命令。可安全解析的命令会同时产出"raw subject"与"稳定 action subject"两组匹配目标------历史 bug 是 UI 保存 pnpm run lint:* 后旧 evaluator 只拿原始 invocation 比较,下一轮命中不了(bash-command-permission-policy.ts:128 注释)。
  2. 建议规则 :为"Always allow"生成稳定前缀规则 (最多 5 条,prefix:* 形式),而不是记整条命令。构造过程:
    • 先 unwrap 包装命令(sudo、env、nohup、command、time,最多两层),跳过已知选项;
    • 高危根命令永不生成前缀规则 :bash/sh/zsh/fish/cmd/powershell/pwsh(能再解释任意命令)与 rm/rmdir/chmod/chown/chgrp/dd/mkfs/mount/umount;
    • 按生成的命令注册表(generated/bash-command-registry.ts)逐级下钻子命令(如 npm run <script>、python -m <module>、aws/az/gcloud/kubectl/docker compose 的家族深度覆盖表),前缀至少要有"可执行文件 + 一个稳定 action"两个 token 才成立;
    • 走不到稳定前缀就退回整命令精确规则。

9. readFileState 双向联动

前台命令执行成功后,applyBashReadFileStateEffects(bash-read-file-state.ts)做两件事(后台任务、图片输出、被中断、provider error 一律跳过------失败结果不能污染读取状态):

stale hint(写侧) :命令命中写类特征(--write、--fix、--in-place、run format、black、cargo fmt 等正则表)时,逐个 stat 当前 readFileState 里的已读文件;mtime 晚于本次命令启动时刻且晚于已记录 mtime 的文件被判定为"被改脏"。命中则在输出追加:

[This command modified N files you've previously read: a.ts, b.ts. Call Read before editing.]

最多展示 5 个路径,多余折叠为 "and K more"。这防止模型拿 Edit 的旧 old_string 盲改被 formatter/pnpm fix 动过的文件。

backfill(读侧) :cat/head/tail/sed -n 等读类命令输出的文件,若尚未在 readFileState 中且完整未截断,回填为"已读"(10 MB 上限,isPartialView: false)------模型用 Bash 看过的文件不必再 Read 一遍就能 Edit。stdout 被截断时不回填 :bash-read-file-state.ts:97 注释点明原因------回读完整文件会绕过 Edit/Write 的 read-before-write 语义(模型只看到了一半却说"读过了")。

10. Shell 选择与会话快照

Bash 不假设 /bin/bash,而是解析出一个 ExecutionShellSelection(adapters/src/exec/bash-shell-provider.ts):

平台 优先级
Windows 用户配置(git-bash 路径 / cmd)→ 自动 Git Bash(Program Files 固定路径,或从 git.exe 反推 ..\bin\bash.exe)→ legacy "system shell"
POSIX $SHELL(bash/zsh)→ PATH 中的 bash/zsh → 固定目录(/bin、/usr/bin、/usr/local/bin、/opt/homebrew/bin)→ legacy fallback
  • 快照生命周期 :shell 是 session-start 快照------runtime 在首次真实执行前初始化一次(可以是 lazy resolver,已有快照的 session 不重复探测),持久化到 session store;resume 时恢复并处理差异(旧 Windows 会话升级后被 auto Git Bash 接管时,插入 shell_environment_change 提醒,防止模型沿用旧 shell 习惯)。
  • 防交互阻塞 :Git Bash / POSIX provider 注入 GIT_EDITOR=true 与 SHELL 环境变量,配合工具描述里"交互式 flag(git rebase -i 等)不受支持"的约定。
  • 每次调用从用户 profile 快照初始化 :shell-init-snapshot.ts 用真实 shell 跑一次初始化采集(10s 超时、1 MB 上限),产物缓存 30 天------这解释了工具描述中"Shell state does not persist; the shell is initialized from the user's profile":环境变量和函数跨调用不保留,但 profile 里的 PATH/alias 每次都在。
  • 工作目录跨调用持久 (bash-cwd-policy.ts):仅当命令 completed 且 exit 0 且主线程 scope 时,捕获实际 cwd(pwd -P 物理路径语义,symlink 经 realpath 等价判定);cwd 仍在 workspace 边界内(含 /private/var→/var 等 macOS 归一化)则保留,离开则重置回 workspaceRoot 并在 stderr 追加 Shell cwd was reset to <root>。后台任务不捕获 cwd。这也对应工具描述的建议:"prefer absolute paths --- cd in a compound command can trigger a permission prompt"。

11. 错误处理一览

场景 行为
ExecutionPort 未配置 ConfigurationError(不可恢复)
端口不支持后台生命周期却要求后台 ConfigurationError
闲时 turn 请求后台 ToolExecutionFailed(可恢复),文案要求改前台执行
空命令 空输出,不报错
非零退出码 结果对象 status: "failed" + exitCode + 语义化解释;不抛异常
超时 timed_out + "Command timed out",进程树两级击杀
取消 cancelled,stderr 追加中断说明
spawn 失败 spawn_error + 错误消息
输出超 5 GiB 终止进程:"Command killed: output file exceeded 5GB"
输出超 inline 限额 正文截断 + 落盘 envelope(预览 + persisted 路径)
stdin 写入意外失败 升级为 failed(EPIPE 等预期关闭除外)

契约还定义了完整的 BashErrorCode 表(COMMAND_NOT_PARSABLE、COMMAND_INJECTION_RISK、UNC_PATH_BLOCKED、SANDBOX_VIOLATION、OUTPUT_TOO_LARGE 等 13 项),供上层策略与遥测引用。

12. 相关文件索引

文件 职责
packages/contracts/src/tools/bash.ts 输入/输出 schema、BashErrorCode、后台任务与权限类型
packages/contracts/src/interfaces/execution.port.ts ExecutionRequest/Result、shell 选择、输出限额、后台快照契约
packages/core/src/tool/handlers/bash.ts handler 主流程、ToolEntry 定义、后台/闲时/超时分岔
.../handlers/bash-prompt.ts provider 可见描述(避免命令清单、Git 约定)
.../handlers/bash-metadata.ts UI 名称(sed -i → "Update")、活动描述、分类器输入
.../tool/bash-timeout-policy.ts 默认/上限与 env 覆盖、resolveBashTimeoutMs
.../handlers/bash-background-policy.ts 自动转后台资格(排除 sleep)
.../handlers/bash-background-lifecycle.ts 后台生命周期端口能力探测
.../handlers/bash-cwd-policy.ts cwd 捕获、workspace 边界、重置文案
.../handlers/bash-command-parser.ts unbash 解析、安全性判定(动态词/不支持语法)
.../handlers/bash-command-permission-policy.ts 规则评估与稳定前缀建议(高危根命令清单)
.../handlers/bash-readonly-policy*.ts + bash-semantics.ts 只读判定族、退出码语义、静默命令表
.../handlers/bash-output.ts / bash-model-content.ts 输出组装、模型可见格式、后台/持久化文案
.../handlers/bash-image-output.ts / bash-gh-rate-limit.ts / bash-read-file-state.ts 图片输出、gh 限流提醒、readFileState 联动
.../handlers/task-output-bash.ts TaskOutput 对后台 Bash 的投影(运行中读头部 30KB)
packages/adapters/src/exec/node-execution-adapter-run.ts 子进程 run:spawn、超时/取消/输出限额三路停止、cwd 捕获
packages/adapters/src/exec/node-execution-adapter-lifecycle.ts 后台生命周期状态机、runBashWithBackgroundLifecycle、通知结算
packages/adapters/src/exec/bash-shell-provider.ts 平台 shell 解析(Git Bash/cmd/POSIX/legacy)
packages/adapters/src/exec/bash-file-output.ts 合并输出文件、5 GiB watchdog、进度尾读
packages/adapters/src/exec/bash-output-policy.ts BASH_MAX_OUTPUT_LENGTH 解析
packages/adapters/src/exec/shell-init-snapshot.ts 用户 profile 初始化快照(30 天保留)
packages/core/src/runtime/methods/session-shell-environment.ts session shell 快照的初始化/持久化/恢复
packages/core/src/runtime/methods/runtime-command-queue.ts 后台完成通知批次(runTaskNotificationBatch)
相关推荐
熊猫钓鱼>_>1 小时前
告别手工贴发票:增值税发票识别+核验,搭一条报销自动化流水线
人工智能·ai·职场和发展·自动化·大模型·llm·agent
RuoyiOffice1 小时前
SpringBoot3+Vue3 企业 AI 工作流:Tinyflow 画布、LLM 节点与在线试跑怎么落地
spring boot·大模型·vue3·agent·ai工作流·ruoyi office·tinyflow
武子康2 小时前
PUBACK 已到,灯还没亮:Agent 设备指令何时算完成?
人工智能·后端·agent
浮生望2 小时前
用 PostgreSQL + pgvector 保存 Agent 长期记忆:从三表模型到语义检索
agent
枫叶丹42 小时前
AGENTS.md 越写越长之后:如何管理 AI 的上下文
人工智能·chatgpt·开源·agent·codex
墨心@2 小时前
CreateWorkflow 工具
agent·coding agent·harness·zcode
武子康2 小时前
ESP32 唤醒词标称 98%,为什么还要自己做验收?
人工智能·后端·agent
墨心@4 小时前
AmendWorkflow 工具
人工智能·agent·harness
老唐7774 小时前
Agent 开发入门教程:从零搭建一个 Python AI Agent
人工智能·python·深度学习·langchain·大模型·agent·智能体