Bash:解析、只读判定与后台任务

Bash 工具的输入与提示词、unbash 解析与只读判定规则体系、判定结果在权限层的含义、命令注册表生成、工作目录与 git 护栏、超时与自动转后台、输出截断与图片、TaskOutput 与 TaskStop,以及 Bash 读文件怎样回填读取状态。

作者 David更新于 18 篇(共 47 篇)

Bash 是 ZCode 里权限面最宽的内置工具之一。它的静态声明是 readOnly: falsesideEffectScope: "system"riskLevel: "high"needsApproval: trueapps/zcode-cli/packages/core/src/tool/handlers/bash.ts:446),但每次调用都会先被解析一遍:能证明是只读的命令当场降级为低风险、免审批;证明不了的,才按高风险走审批。这套判定加上后台生命周期、输出投影和读取状态联动,占了 apps/zcode-cli/packages/core/src/tool/handlers/ 下 37 个 bash*.ts 文件(约 6100 行),外加一个 1.9 MB 的生成文件。

本篇只讲 Bash 这一侧。子进程怎样起、用哪个 shell、环境变量怎样清洗,见下一篇执行边界;后台任务的统一注册表与完成通知见后台任务与通知;权限规则的语法与匹配顺序见权限模式与规则

位置(core/src/tool/handlers/ 下)职责
bash.tsbash-prompt.tsbash-background-*.ts工具入口:组装执行请求,选前台、显式后台或超时转后台
bash-command-parser.ts用 unbash 把命令拆成简单命令序列
bash-semantics.tsbash-readonly-policy*.ts(20 个)只读判定、退出码语义
bash-command-permission-policy.tsbash-command-rule-evaluator.tsgenerated/bash-command-registry.ts权限规则的匹配主语与“始终允许”建议
bash-git-runtime-safety.tsbash-cwd-policy.tsgit 运行时护栏、工作目录保留与重置
bash-output.tsbash-model-content.tsbash-image-output.tsbash-gh-rate-limit.ts结果对象与模型可见文本
bash-read-file-sources.tsbash-read-file-state.ts与读取状态联动
task-output*.tstask-stop.ts读后台输出、停止后台任务

输入与提示词

输入 schema 在 apps/zcode-cli/packages/contracts/src/tools/bash.ts:32,是一个 strict 对象:

字段含义
command要执行的命令字符串
timeout毫秒,描述里写明上限 600000(contracts/src/tools/bash.ts:11);字符串数字也会被转成数字(contracts/src/tools/bash.ts:78
description一句主动语态的说明,简单命令 5 到 10 个词,字段描述里带了 lsgit status 等示例(contracts/src/tools/bash.ts:14
run_in_background显式后台运行
dangerouslyDisableSandbox字面意思是“绕过沙箱”,但下一篇会看到它没有实际效果

两个布尔字段接受 "true""yes""1""on" 这类字符串(contracts/src/tools/bash.ts:12contracts/src/tools/bash.ts:88),对模型偶尔吐出的字符串布尔值很宽容。

工具描述由 apps/zcode-cli/packages/core/src/tool/handlers/bash-prompt.ts:1 拼出,篇幅不长,核心是这两句(bash-prompt.ts:13bash-prompt.ts:16):

Working directory persists between calls, but prefer absolute paths — cd in a compound command can trigger a permission prompt.

run_in_background runs the command detached: it keeps running across turns and re-invokes you when it exits. No & needed.

其余几条:不要用 Bash 跑 findgrepcatheadtailsedawkecho,改用专用工具(开启内嵌搜索时 findgrep 从名单里去掉,因为 shell 里的这两个命令已被替换,见读、写、改、搜);超时的默认值与上限写成具体数字;Git 一节禁止交互式参数(git rebase -i 之类),要求用 gh 操作 GitHub,只在用户要求时提交或推送,在默认分支上先建分支(bash-prompt.ts:18)。

一次调用的路径

图表加载中…

入口 executeBashHandlerapps/zcode-cli/packages/core/src/tool/handlers/bash.ts:104)先组装执行请求(handlers/bash.ts:388):命令以 mode: "shell"shellProfile: "posix-bash" 交给执行端口,带上会话固定的 shell 选择;前台命令要求成功后回传最终工作目录(captureCwdAfterSuccesshandlers/bash.ts:420);内联输出上限 30000 字节,后台一律落盘、前台只在截断时保留落盘文件(handlers/bash.ts:422)。然后在三条路里选一条(handlers/bash.ts:172):

  const runCommand = async () =>
    parsed.run_in_background && backgroundLifecyclePort
      ? backgroundLifecyclePort.runBashWithBackgroundLifecycle(
          request,
          { mode: "explicit" },
          runOptions,
        )
      : eligibleForAutoBackground && backgroundLifecyclePort
        ? backgroundLifecyclePort.runBashWithBackgroundLifecycle(
            request,
            { mode: "auto_on_timeout" },
            runOptions,
          )
        : {
            kind: "foreground" as const,
            result: await executionPort.run(request, runOptions),
          };

注意中间那条:普通前台命令默认也走后台生命周期,模式是 auto_on_timeout。只有两种情况退回纯前台:命令首词是 sleepapps/zcode-cli/packages/core/src/tool/handlers/bash-background-policy.ts:3),或者当前是闲时回合。闲时回合还会直接拒绝 run_in_background,注释解释了原因:后台命令完成后会另起一轮通知回合,而那一轮不带闲时模型,会落到用户自己的套餐上跑完整的 Agent 循环(handlers/bash.ts:136)。闲时任务本身见定时任务与闲时任务

命令运行 2 秒后,适配器每秒读一次输出文件尾部(apps/zcode-cli/packages/adapters/src/exec/execution-utils.ts:14),工具把它转成 ToolCallProgress 事件,带已用时间、字节数和输出预览,界面据此滚动显示(handlers/bash.ts:359)。

命令解析:unbash

解析用的是 bash 解析库 unbash,版本锁在 4.0.1(apps/zcode-cli/packages/core/package.json:44)。analyzeBashCommandapps/zcode-cli/packages/core/src/tool/handlers/bash-command-parser.ts:51)的产出是一串“简单命令调用”,每条记下 argv、原文片段、前缀环境赋值、重定向和它前面的连接符(&&||||& 或顺序执行,bash-command-parser.ts:14)。几个处理原则:

  • 只展开四种节点StatementAndOrPipeline 是容器,Command 是简单命令;其余任何节点类型都只记进 unsupportedNodeTypes,不再往里走(bash-command-parser.ts:129)。从代码看,括号子 shell、if/for/while、函数定义这类复合结构都不在这四种之内。语句末尾的 & 也记为一种不支持的语法(bash-command-parser.ts:115)。
  • 长度与错误:超过 10000 个字符直接按解析失败处理(bash-command-parser.ts:48);parse 抛异常,或返回的 errors 非空,同样记为解析失败(bash-command-parser.ts:64bash-command-parser.ts:76)。
  • 重定向:语句级与命令级的重定向合并到每条命令上,目标和 here-doc 正文也要检查是否动态(bash-command-parser.ts:166bash-command-parser.ts:207)。
  • 动态词:命令替换、进程替换会在主命令之前执行,权限判断不能把它们当普通参数。判定按词的组成部分逐一看:只有字面量、单引号和 ANSI-C 引号($'...')算静态,双引号要看里面的内容;命令替换、进程替换、变量展开、算术展开、花括号展开、扩展 glob 与未知类型一律当动态(bash-command-parser.ts:221)。

解析失败、有不支持的语法、有动态词,三项之一成立,命令就不“权限安全”(bash-command-parser.ts:92),此后既不可能判为只读,也不会被拆开去匹配前缀规则。

只读判定

整条命令的判定在 isRuntimeReadOnlyBashCommandapps/zcode-cli/packages/core/src/tool/handlers/bash-semantics.ts:39):先要求权限安全、至少有一条命令;同一条命令里既有 git 又有 cdpushdpopd 时直接否决,因为 git 可能在目标目录加载 hooks 与配置,而 cd && grep 这种仍可放行(apps/zcode-cli/packages/core/src/tool/handlers/bash-git-runtime-safety.ts:37);含 git 且当前目录的 git 环境可疑时也否决(见下文“git 护栏”)。然后逐条判定,每一条都必须明确判为只读,有一条未知就整体不是只读(bash-semantics.ts:52)。

单条命令的判定是一串短路(apps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-argv.ts:25):

export function evaluateBashReadonlyPolicy(
  commandPart: BashCommandInvocation,
): boolean | undefined {
  if (!areEnvAssignmentsAllowed(commandPart)) return false;
  if (!areRedirectsAllowed(commandPart)) return false;

  const argv = stripSafeCommandWrappers(commandPart.argv);
  if (argv.length === 0) return false;
  if (argv.some(isUnsafeWindowsUncPath)) return false;
  if (argv[0] === "git") return isGitReadOnlyCommand(argv);

  const directArgvResult = evaluateDirectReadonlyArgv(argv);
  if (directArgvResult !== undefined) return directArgvResult;

  const prefixPolicyResult = evaluateReadonlyPrefixPolicy(argv, commandPart.commandText);
  if (prefixPolicyResult !== undefined) return prefixPolicyResult;

  if (READONLY_ALLOW_ANY_ARG_COMMANDS.has(argv[0] ?? "")) return true;
  if (process.platform === "win32" && argv[0] === "xargs") return undefined;

  const policy = READONLY_COMMAND_POLICIES.get(argv[0] ?? "");
  if (!policy) return undefined;
  if (argv[0] === "cd" && argv.length > 2) return false;
  if (policy.additionalCommandIsDangerousCallback?.(commandPart.commandText, argv.slice(1)))
    return false;
  if (!isArgvAllowedByPolicy(argv, policy, argv[0] ?? "")) return false;
  if (policy.regex && !policy.regex.test(commandPart.commandText)) return false;
  return true;
}

在它之前还有一道 hasKnownBashWriteOptionsed -ifind-delete/-exec/-fprint 等 10 个写选项、tree -o、git 的危险全局参数,出现即否决(bash-readonly-policy-argv.ts:55)。各层的内容:

规则出处
环境赋值前缀只许 39 个无害变量,如 LANGTZNO_COLORCIGIT_TERMINAL_PROMPTapps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-argv-io.ts:3
重定向只许输入重定向 <<<<&<<<;输出只许写到 /dev/null>&N/dev/tcp/dev/udp 一律拒bash-readonly-policy-argv-io.ts:53
包装词剥掉 commandbuiltinnoglob 再看真正的命令;Windows UNC 路径拒bash-readonly-policy-argv-io.ts:75
精确 argvip addrnode -vpython3 --version 等;docker images/ps 另查危险全局参数;printffindhistoryarchifconfig 各有专门检查apps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-argv-direct.ts:69
多词前缀24 条:docker inspectdocker logsgh auth status,以及 21 条 gh 只读命令(gh pr viewgh run listgh search code 等)apps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-commands.ts:21
任意参数46 个命令不看参数:catheadtailwcdiffstatunamewhichsleepapps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-simple-commands.ts:171
标志表60 个命令按安全标志表逐个检查参数,部分附加回调bash-readonly-policy-simple-commands.ts:59
git24 个只读子命令,各有标志表bash-readonly-policy-commands.ts:16

标志表是这套规则的主体。每个安全标志声明取值类型:nonenumberstringoptionalStringchar,以及只能是字面 {}EOF 的两种(apps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-types.ts:1)。检查器支持 --flag=value-n5 这类粘连写法和 -la 这类短标志簇(簇里每个字母都必须是 none 类型),head/tail-20 特判放行,位置参数不限,遇到 -- 停止检查;表里没有的标志一律拒(apps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-argv-flags.ts:18)。标志表管不住的语义交给回调:

  • sed 脚本里出现 w 写文件命令即否决(apps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-callbacks.ts:82);
  • jq-f-L--rawfile 等读外部文件的选项,过滤器里出现 $ENVenvincludeimport 也否决(bash-readonly-policy-callbacks.ts:5);
  • date 的位置参数必须以 + 开头,否则可能是在设置系统时间(bash-readonly-policy-callbacks.ts:86);ps 禁含 e 的 BSD 风格参数,它会显示进程的环境变量(bash-readonly-policy-callbacks.ts:104);
  • xargs 只能驱动 8 个命令:echoprintfwcgrepegrepfgrepheadtailbash-readonly-policy-callbacks.ts:269),在 Windows 上 xargs 一律算未知;
  • gh 的参数值若含 ://@ 或两个以上 /,就可能指向别的主机,否决(bash-readonly-policy-callbacks.ts:298)。

从代码看有一处重叠:catheadtailwc 等既在标志表里,又在“任意参数”集合里。任意参数的检查排在标志表之前(bash-readonly-policy-argv.ts:42),所以这些命令的标志表实际不生效。

git 先规范化全局参数:只允许 --no-pager--paginate-c-C--git-dir--work-tree--exec-path--config-env--namespace--super-prefix--shallow-file--attr-source--bare 这 11 个会改变 git 读哪份配置、在哪个仓库执行的参数,出现即否决(bash-readonly-policy-simple-commands.ts:45)。子命令按最长前缀匹配(apps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-argv-git.ts:12),再过回调:git loggit showgit rev-listgit shortloggit for-each-ref 的格式串里出现 %G%(signature 会触发 GPG 验签,否决(apps/zcode-cli/packages/core/src/tool/handlers/bash-readonly-policy-git-callbacks.ts:15);git branchgit tag 不带 --list/-l 又有位置参数,就是在创建分支或标签,否决(bash-readonly-policy-git-callbacks.ts:92);git reflog 只许 showlistgit ls-remote 带位置参数会联网,否决;git remote show 必须带 -nbash-readonly-policy-git-callbacks.ts:21:32:52)。

判定为只读意味着什么

判定结果通过工具入口的 resolvePermissionCapability 生效(handlers/bash.ts:77):只读命令的能力被覆盖为 readOnly: trueneedsApproval: falseriskLevel: "low"sideEffectScope: "none"。权限服务据此在各模式下分流(apps/zcode-cli/packages/core/src/permission/service.ts:97):

模式只读 Bash非只读 Bash
buildedit直接放行,规则 mode.build.readOnlyservice.ts:456询问,规则 mode.build.highRiskservice.ts:474
Plan(planEnabled 为真)放行,规则 mode.plan.readOnlyservice.ts:408拒绝而不是询问,规则 mode.plan.nonReadOnlyservice.ts:443
yolo 且不在 Plan放行(service.ts:136放行

Plan 模式的分支排在项目 allow 规则之前(service.ts:176),所以审批时选“始终允许”存下的项目规则(比如 npm test:*)在规划期间也不生效;项目级 denyask 规则则更早(service.ts:158)。

还要注意,只读判定只看命令与参数的形态,不看路径:从代码看,cathead 读工作区之外的绝对路径同样算只读,在 build 模式下也免审批。

同一套判定还用在别处:动态工作流在 ToolCallStarted 事件上读“这一笔是否会改写工作区”(apps/zcode-cli/packages/core/src/tool/executor/permission-capability.ts:28);记忆 Agent 只放行只读 Bash(apps/zcode-cli/packages/core/src/memory/memory-agent-loop.ts:147,见项目记忆)。

规则匹配的主语resolveBashPermissionRulePolicyapps/zcode-cli/packages/core/src/tool/handlers/bash-command-permission-policy.ts:85)把命令拆成每条简单命令的“主语”,交给 evaluateBashRulesapps/zcode-cli/packages/core/src/tool/handlers/bash-command-rule-evaluator.ts:13):

  • 规则内容与整条命令逐字相等,总是命中;
  • 命令不权限安全,或含重定向、非静态的环境赋值,就认逐字相等(bash-command-permission-policy.ts:169);
  • allow 规则要覆盖每一条非只读的子命令,只读的部分自动豁免,所以 cd app && npm test 只需要一条 npm test:*
  • denyask 规则命中任意一条子命令即生效;
  • 前缀:* 形式按词边界匹配,其余带 * 的走通配(bash-command-rule-evaluator.ts:40)。

命令注册表:给“始终允许”找稳定前缀

用户在审批时选“始终允许”,ZCode 要提议一条规则。逐字规则太窄,整个 git:* 又太宽,所以需要知道一条命令的哪几个词是“动作”。buildSuggestedUpdatesbash-command-permission-policy.ts:135)为每条非只读子命令算一个稳定前缀,拼成 前缀:*,最多 5 条,超过或算不出就退回逐字规则(bash-command-permission-policy.ts:16)。前缀的算法在 resolveStableCommandPrefixbash-command-permission-policy.ts:187):剥掉最多两层 envsudonohuptimecommand 包装;rmchmodddmkfsshbashpowershell 等 16 个高风险根命令永远不给前缀(bash-command-permission-policy.ts:19);几类有固定深度:python -m 模块npm/pnpm/yarn/bun run 脚本deno taskmake/just 目标aws/az 取两词、gcloud 取三词(bash-command-permission-policy.ts:68);其余查注册表,跳过已知选项(带参数的选项连参数一起跳),沿子命令树往下走。

命令建议的规则
pnpm --dir app run lintpnpm run lint:*--dir 被识别为带参数的全局选项并丢弃)
git -C sub commit -m "x"git commit:*
python3 -m pytest testspython3 -m pytest:*
rm -rf dist逐字规则 rm -rf dist

注册表来自 Fig 的命令行补全库。生成脚本 apps/zcode-cli/scripts/generate-bash-command-registry.mjs:15 要求 @withfig/autocomplete 恰好是 2.692.3(版本检查在 generate-bash-command-registry.mjs:52),逐个 import 包里的补全规格,压成四元组 [名字, 选项, 参数标志, 子命令]:选项只记是否带参数;参数标志用位表示是否是命令、模块、可变长、可选、危险、文件、目录(generate-bash-command-registry.mjs:23:162);函数式的动态子命令与只含 loadSpec 的节点被跳过(generate-bash-command-registry.mjs:141:149)。输出按名字排序、附上构建文件的 sha256,体积超过 3 MiB 就报错(generate-bash-command-registry.mjs:16:86)。--check 模式在临时目录重新生成、与仓库里的文件逐字节比较(generate-bash-command-registry.mjs:92),挂在 pnpm check 上(apps/zcode-cli/package.json:10:23)。仓库里的产物 1906984 字节,含 707 个根命令名(含别名),文件头记录跳过了 522 个 loadSpec 节点(apps/zcode-cli/packages/core/src/tool/handlers/generated/bash-command-registry.ts:4)。

同类思路可以对照 OpenCode 的 Shell 工具:那边用 tree-sitter 解析命令,按命令元数生成“总是允许”规则。

工作目录与 git 护栏

工作目录。Bash 每次都起新 shell,只把最终目录带回来:前台命令在原命令后追加一段,退出码为 0 时把 pwd -P 写进临时文件(apps/zcode-cli/packages/adapters/src/exec/cwd-capture.ts:51),注释明说环境变量、别名、函数都不会保留(cwd-capture.ts:32)。decideBashCwdPolicyapps/zcode-cli/packages/core/src/tool/handlers/bash-cwd-policy.ts:33)只在命令成功、且是主会话(子 Agent 不算)时生效(bash-cwd-policy.ts:37):新目录在工作区内就保留;离开工作区则重置回工作区根,并在 stderr 末尾追加 Shell cwd was reset to …bash-cwd-policy.ts:47)。比较时同时看字面路径与真实路径,macOS 的 /private/tmp/private/var 先归一(bash-cwd-policy.ts:104)。

git 运行时护栏。代码里没有“禁止 git push --force”这类硬拦截:破坏性 git 命令只是判不成只读,按普通高风险命令走审批,yolo 下照样执行。护栏针对的是另一类风险:一个恶意仓库的 .git 配置可以让看似只读的 git status 执行任意程序。isGitRuntimeContextUnsafebash-git-runtime-safety.ts:71)从当前目录往上找 .git

  • .git 是符号链接或 gitdir: 文件时,目标解析后若落在当前目录之内、或路径里没有 .git 这一段,判为不安全;gitdir 文件超过 32 KiB 或含 NUL 也不安全(bash-git-runtime-safety.ts:98:124);
  • .git 目录要有合法的 HEAD、可进入的 objectsrefs、且没有 commondir,才算可信(bash-git-runtime-safety.ts:141);
  • 在找到可信的 .git 之前,路上任何一层出现 HEAD 文件或 objectsrefs 目录,就当成裸仓库,不安全(bash-git-runtime-safety.ts:172)。

判为不安全后,这条命令里的任何 git 调用都不再是只读,回到审批。

超时与后台

数字出处
默认超时120000 毫秒,环境变量 BASH_DEFAULT_TIMEOUT_MS 可改apps/zcode-cli/packages/core/src/tool/bash-timeout-policy.ts:6:18
超时上限600000 毫秒,BASH_MAX_TIMEOUT_MS 可改,但不低于默认值bash-timeout-policy.ts:7:23
本次超时timeout 与默认值取其一,再与上限取小;0 视同未给bash-timeout-policy.ts:34
执行器看门狗上面的值再加 6000 毫秒清理宽限handlers/bash.ts:497apps/zcode-cli/packages/core/src/tool/executor/timeout.ts:203
子 Agent 的后台 Bash最长 3600000 毫秒,到点取消apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts:25

两个环境变量在组装运行时配置时读取(apps/zcode-cli/packages/bootstrap/src/app/runtime-config.ts:120);代码里没有按命令名调整超时的表。

关键在于“超时”对前台命令意味着什么。runBashWithBackgroundLifecycleapps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-lifecycle.ts:102)把真正的进程超时设为 0、输出改为始终落盘(node-execution-adapter-lifecycle.ts:123),另挂一个“前台期限”计时器;期限一到,进程并不被杀,而是原地转为后台任务(node-execution-adapter-lifecycle.ts:190):

    const commitBackground = () => {
      if (state !== "foreground" || controller.signal.aborted) return false;

      // 旧 explicit background 复用了通用 start(),foreground timeout 与
      // parent turn abort 会继续挂在子进程上。这里先原子提交状态,再同步清理 deadline、
      // 脱离 parent abort 并登记 task,避免 abort/completion 在提交缝隙里误杀后台进程。
      state = "backgrounded";
      bashLifecycle?.onBackgrounded?.();
      clearForegroundDeadline();
      removeExternalAbort();
      this.backgroundTasks.set(taskId, record);
      if (persistedLimitReached) {
        controller.abort("output_limit");
      }
      resolveOutcome({
        kind: "backgrounded",
        task: {
          taskId,
          status: "running",
          startedAt,
          pid: record.pid,
          ...outputPaths,
        },
      });
      return true;
    };

显式后台的区别只是:进程一启动(收到 started 事件)就立即提交(node-execution-adapter-lifecycle.ts:245)。两种情况下,工具都返回 status: "backgrounded" 与任务 ID,模型看到 Command running in background with ID: …,外加输出文件路径和“用 Read 看中间输出”的提示(apps/zcode-cli/packages/core/src/tool/handlers/bash-model-content.ts:119)。所以在 ZCode 里,只有 sleep 开头的命令和闲时回合里的命令会真正“超时被杀”,普通命令超时的结果是变成后台任务;转入后台后主会话里不再有超时,只剩输出文件 5 GB 的上限(见下一篇)。执行器随后把它登记进运行时任务表,完成时注入 <task-notification> 再起一轮(apps/zcode-cli/packages/core/src/tool/executor/background-tasks.ts:91),细节见后台任务与通知

同一个格式化函数里还有两段文案:“超过 assistant 模式 15 秒阻塞预算,被移到后台”与“用户手动转后台”(bash-model-content.ts:122:125)。对应的 assistantAutoBackgroundedbackgroundedByUser 字段只在契约里声明,全仓库没有任何代码给它们赋值,TUI 里也没有手动转后台的按键。从代码看,这两段是没有接线的遗留文案。

读后台输出与停止

TaskOutput 的契约在 apps/zcode-cli/packages/contracts/src/tools/task-output.ts:4,带着 AgentOutputToolBashOutputToolAgentOutputBashOutput 四个别名(contracts/src/tools/task-output.ts:5)。它的描述开头就是 DEPRECATED,建议对 bash 任务直接用 Read 读输出文件(contracts/src/tools/task-output.ts:12)。参数是 task_idblock(默认 true)和 timeout(默认 30000、最大 600000 毫秒,contracts/src/tools/task-output.ts:29)。处理逻辑在 apps/zcode-cli/packages/core/src/tool/handlers/task-output.ts:40

  • block=false 且任务仍在运行,返回 not_readyblock=true 时每 100 毫秒轮询一次,到时仍未结束返回 timeouthandlers/task-output.ts:34handlers/task-output.ts:229);
  • Bash 任务运行中只读输出文件开头 30000 字节,结束后读结尾 8 MiB(apps/zcode-cli/packages/core/src/tool/handlers/task-output-projection.ts:10apps/zcode-cli/packages/core/src/tool/handlers/task-output-bash.ts:120);
  • 给模型的文本最多 32000 字符,保留尾部并在前面注明完整文件路径;TASK_MAX_OUTPUT_LENGTH 可调,上限 160000(handlers/task-output.ts:199);
  • 读到终态才把任务标记为 notified,而且先投影、再检查中止信号、最后写标记,避免吞掉完成通知(handlers/task-output.ts:60)。

描述里还说任务 ID 可以用 /tasks 命令查看(contracts/src/tools/task-output.ts:22),但 ZCode 的内置斜杠命令表里没有 /taskspackages/shared/src/zcode-slash-command-help.ts:9)。

TaskStopapps/zcode-cli/packages/core/src/tool/handlers/task-stop.ts:96)别名 KillShellKillBash,参数 task_id,旧名 shell_id 仍兼容(task-stop.ts:29)。它调用后台任务控制端口,发起方标为 model,这样终态通知会写“被你停止”而不是“被用户停止”(task-stop.ts:50);自身超时 10000 毫秒,不可覆盖(task-stop.ts:136)。

界面上的后台任务详情走另一条路:运行时方法 readBackgroundBashOutputapps/zcode-cli/packages/core/src/runtime/methods/background-bash-output.ts:5)由 ZCode Protocol 的 backgroundBashOutput 查询调用(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/v4-gateway.ts:1906),适配器按启动时的会话 ID 校验归属,只返回输出文件结尾 8192 字节(node-execution-adapter-lifecycle.ts:300packages/shared/src/background-bash-output.ts:3)。

输出:截断、退出码与图片

Bash 的 stdout 与 stderr 由子进程直接写进同一个文件(下一篇细说),所以结果里 stderr 通常为空,stdout 是合并后的输出。给模型的内容由 formatBashModelContentbash-model-content.ts:14)生成:

  • 截断:内联部分取输出开头 30000 字节(apps/zcode-cli/packages/adapters/src/exec/bash-file-output.ts:132),适配器侧可用 BASH_MAX_OUTPUT_LENGTH 调到最多 150000(apps/zcode-cli/packages/adapters/src/exec/bash-output-policy.ts:1)。超出时完整文件保留,模型看到一个 <persisted-output> 信封:总大小、完整路径、开头 2000 字符预览(bash-model-content.ts:10apps/zcode-cli/packages/core/src/tool/result-persistence-format.ts:29)。没超出的前台输出文件在结算时删除(apps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-results.ts:94)。
  • 退出码:非零退出码不抛异常,作为结果字段返回(contracts/src/tools/bash.ts:123)。interpretBashReturnCode 按链上最后一条命令给出语义:greprg 退出 1 是 “No matches found”,diff 是 “Files differ”,find 是 “Some directories were inaccessible”,test/[ 是 “Condition is false”,git grepgit diff 同样识别(bash-semantics.ts:134)。这四种不算错误;其余非零退出码在模型内容开头写 Exit code N,并把工具结果标为错误(bash-model-content.ts:50apps/zcode-cli/packages/core/src/runtime/helpers/tool-result.ts:74)。被中断的命令追加 <error>Command was aborted before completion</error>bash-model-content.ts:105)。
  • 图片:如果整个 stdout 恰好是一个 data:image/…;base64,… 形式的 URL,就作为图片块交给模型(apps/zcode-cli/packages/core/src/tool/handlers/bash-image-output.ts:20)。落盘的输出最多读 20 MiB;有图像处理端口时缩放到长宽都不超过 2000 像素(bash-image-output.ts:12apps/zcode-cli/packages/contracts/src/tools/read.ts:20);缩放失败但图片头校验通过时用原图,解码失败则退回文本。
  • gh 限流提示:命令里调用了 ghauthhelpversionaliascompletionconfig 除外),输出又匹配 “API rate limit exceeded” 等字样时,追加一条 system-reminder,说明 5000 次每小时的配额由所有工具与 Agent 共享,要求先查 gh api rate_limit 再睡到重置;同一进程 60 秒内只提示一次(apps/zcode-cli/packages/core/src/tool/handlers/bash-gh-rate-limit.ts:1)。提示还让模型轮询时改用 ScheduleWakeup,但 ZCode 并没有注册这个工具,它只出现在一张工具排序名单里(apps/zcode-cli/packages/core/src/tool/provider-visible-order.ts:19)。

Bash 读文件与读取状态

Edit、Write 要求“先读后写”(见读、写、改、搜)。模型常用 Bash 看文件,所以 Bash 结束后 applyBashReadFileStateEffects 做两件事(apps/zcode-cli/packages/core/src/tool/handlers/bash-read-file-state.ts:51)。

回填collectBashReadFileSourcesapps/zcode-cli/packages/core/src/tool/handlers/bash-read-file-sources.ts:44)只认几种一眼能看懂的形态,且整条命令不能含 |<>cat 文件(可带 -n)、head/tail -n N 文件(默认 10 行)、sed -nN,MpNp 脚本,以及单独一条的 grep 模式 文件(必须退出码为 0);多条命令时夹在中间的 echoprintftrue: 可以忽略。满足条件、stdout 没被截断、文件不超过 10 MiB、之前没有记录,就按命令实际展示的内容写一条读取记录(bash-read-file-state.ts:95):headtailsed -n 记下行范围,catgrep 不带范围,等同整文件读取。这些记录一律不标记为部分视图(bash-read-file-state.ts:149),而 Edit 判断“没读过”只看有没有记录、是不是部分视图(apps/zcode-cli/packages/core/src/tool/handlers/edit.ts:429),所以一次 head、甚至一次命中的 grep 之后,模型就可以直接 Edit 这个文件。

过期提示。命令匹配格式化类标记(--write--fix--in-placeblackcargo fmtgo fmtterraform fmt 等 19 种,bash-read-file-state.ts:20)时,逐个 stat 已读过的文件,修改时间晚于命令开始、且晚于记录的,汇总成一句 [This command modified N files you've previously read: … Call Read before editing.],最多列 5 个路径(bash-read-file-state.ts:182)。后台命令、图片输出、被中断或出错的结果,这两件事都不做(bash-read-file-state.ts:168)。

Shell 选择随会话固定

用哪个 shell 在会话创建时决定,之后不再变。persistBashShellSelectionSnapshot 把选择写成一条会话条目,ID 是会话 ID 加 :runtime:bash_shell_selectionapps/zcode-cli/packages/core/src/runtime/methods/bash-shell-snapshot.ts:27),注释说明理由:shell 设置变更只影响新会话,冷恢复必须继续用同一个 shell(bash-shell-snapshot.ts:48)。恢复时读最新一条:快照里的路径仍可执行就是 restored;不可用了且当前有候选就 fallback 到当前候选,否则 stalebash-shell-snapshot.ts:104)。Windows 上字面的 cmd.exe 不做可执行检查(bash-shell-snapshot.ts:196)。shell 候选怎样挑出来、恢复后怎样提醒模型 shell 变了,见下一篇。

下一篇:执行边界:子进程、环境与网络——命令最终怎样 spawn、登录 Shell 快照怎么取、代理与证书变量怎样封存再还给子进程,以及为什么没有操作系统沙箱。

本页目录