执行边界:子进程、环境与网络

所有子进程与网络请求都收敛到 adapters 的两个入口:执行端口怎样选 shell、取登录 Shell 快照、在 Windows 上起 .cmd 与 Git Bash;环境变量怎样在入口清洗、把代理与证书封存再只还给子进程;输出直写、进程树终止与退出码归一;以及为什么没有操作系统沙箱。

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

ZCode 的 Agent 运行时内核(core 包)不直接调用 child_processfetch。子进程一律经过执行端口 ExecutionPort,它在 CLI 里的实现是 apps/zcode-cli/packages/adapters/src/exec/ 下的 NodeExecutionAdapter(24 个文件、约 4300 行);网络请求基本都经过 adapters/src/network/ 的代理与证书解析,以及 adapters/src/http/ 的 HTTP 客户端(6 个文件、约 1400 行),例外见网络一节。上一篇讲了 Bash 工具怎样决定跑什么、要不要审批,这一篇讲命令最终怎样被跑起来:用哪个 shell、带什么环境、输出去哪、怎样停止,以及网络出口怎样统一。

apps/zcode-cli/AGENTS.md:57 对这个边界的期望是:

子进程执行应通过统一执行入口完成,便于集中管理 sandbox、权限审批、环境变量、超时、取消、输出截断、流式输出和退出码归一化。

除了“sandbox”一项,其余在代码里都能找到落点。沙箱的缺席放在最后一节讲。

位置(apps/zcode-cli/packages/adapters/src/ 下)职责
exec/node-execution-adapter*.ts适配器本体,按基础、结果、进程、运行、生命周期五层继承
exec/execution-command.tsexec/bash-shell-provider.tsexec/windows-executable.ts环境组装、选 shell、解析可执行文件
exec/shell-init-snapshot.tsexec/bash-startup-script.tsexec/cwd-capture.ts登录 Shell 快照、前置脚本、工作目录回传
exec/bash-file-output.tsexec/output-collector.tsexec/outputEncoding.tsexec/process-tree.ts输出直写与收集、编码、进程树终止
network/subprocess-env.tsnetwork/http-config.tsnetwork/proxy-fetch.ts子进程网络变量、代理与证书解析、带代理的 fetch
http/index.tshttp/public-egress-policy.tshttp/response-body.ts通用 HTTP 客户端、公网出口校验、响应体上限
device/process-probe*.tsdevice/cli-device-mid.ts进程资源采样、设备标识(点到为止)

执行端口:两种命令

端口只认两种命令(apps/zcode-cli/packages/contracts/src/interfaces/execution.port.ts:8):

export type ExecutionCommand =
  | {
      mode: "argv";
      file: string;
      args?: string[];
    }
  | {
      mode: "shell";
      command: string;
      shell?: true | string;
      /**
       * Internal Bash tool shell selection profile. This must not be exposed through hooks,
       * user config, or generic shell execution.
       */
      shellProfile?: "posix-bash";
      /**
       * ZCode runtime-provided Bash shell selection. Current consumers must only use
       * this when shellProfile === "posix-bash"; generic shell execution must ignore it.
       */
      shellOverride?: ExecutionShellSelection;
    };

shellProfile: "posix-bash" 是 Bash 工具专用的标记,只有带着它的请求才会走下面的 shell 选择、登录 Shell 快照和合并输出文件。其他调用方:

调用方模式出处
Bash 工具shellposix-bashapps/zcode-cli/packages/core/src/tool/handlers/bash.ts:409
命令型 Hookshell,shell 由 Hook 配置决定,stdin 传 JSONapps/zcode-cli/packages/core/src/hooks/configured-runner-callback.ts:34
自定义命令里的 shell 展开shellapps/zcode-cli/packages/bootstrap/src/custom-command-shell-expansion.ts:122
PDF 读取(pdfinfopdftoppmargvapps/zcode-cli/packages/adapters/src/pdf/index.ts:43
动态工作流读取 git 状态argvapps/zcode-cli/packages/bootstrap/src/app/workflow-world-read.ts:211

适配器在 createApp 里创建,网络配置与输出根目录 ~/.zcode/cli/exec 在这里注入(apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:392apps/zcode-cli/packages/adapters/src/exec/execution-utils.ts:18)。Hook 的协议与超时见生命周期 Hooks

选哪个 shell

resolveEffectiveBashShellSelectionapps/zcode-cli/packages/adapters/src/exec/bash-shell-provider.ts:33)先看会话快照:快照来源不是“用户配置”时直接沿用(bash-shell-provider.ts:115),否则按平台重新解析。

  • macOS 与 Linux$SHELL 本身是 bash 或 zsh 就用它;否则在 PATH/bin/usr/bin/usr/local/bin/opt/homebrew/bin 里找,默认先 zsh 后 bash,$SHELL 是 bash 时反过来(bash-shell-provider.ts:46:15:284)。都找不到就退回 system shell,交给 Node 的默认 shell。
  • Windows:用户在桌面端设置里选了 Git Bash 或 CMD,且路径可用,就用它(bash-shell-provider.ts:143);否则自动找 Git Bash:先查 C:\Program Files\Git\bin\bash.exe 与 x86 目录,再从 PATH 上的 git.exe 反推安装目录(bash-shell-provider.ts:16:74);还找不到就退回 system shell,即 ComSpec 指向的 cmd.exeapps/zcode-cli/packages/adapters/src/exec/execution-command.ts:213)。Bash 工具不会选 PowerShell。
  • 选中的 bash、zsh、Git Bash 都附带一个环境覆盖:GIT_EDITOR=true,让 git 需要编辑器时直接返回,不会挂在交互编辑器上;SHELL 指向实际用的 shell(bash-shell-provider.ts:91:103)。

用户配置的来源是桌面端的集成终端设置,经 ZCode Protocol 作为 user-config 覆盖传进来(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server-operations.ts:3139);纯 CLI 只做自动探测(create-app.ts:435)。选择随会话固定、冷恢复时怎样复原,见上一篇。恢复时如果没能按快照复原(快照缺失、失效或读取失败),而当前 shell 是自动探测到的 Git Bash,或者名字与历史里记录的不同,运行时会往历史里补一条附件提醒模型,比如 The Bash tool shell is Git Bash.apps/zcode-cli/packages/core/src/runtime/methods/shell-environment.ts:24apps/zcode-cli/packages/core/src/runtime/methods/session-shell-environment.ts:156);快照正常恢复时不插,保证“shell 设置只对新会话生效”。

有一处不一致:给模型的 git 状态快照在 Windows 上提示“用 PowerShell 运行 git status”(apps/zcode-cli/packages/adapters/src/context/git-snapshot.ts:172),但内置工具里没有 PowerShell 工具,Bash 也不会用 PowerShell。

一条命令怎样 spawn

图表加载中…

bash 和 zsh 都以 -c -l 命令 启动,即非交互的登录 shell(execution-command.ts:142):

function createShellProviderCommand(
  provider: BashShellProvider,
  command: string,
): ResolvedSpawnCommand {
  if (provider.dialect === "cmd") {
    return {
      args: [],
      cwdDialect: provider.dialect,
      envOverlay: provider.envOverlay,
      file: command,
      shell: provider.shell,
    };
  }

  return {
    args: ["-c", "-l", command],
    cwdDialect: provider.dialect,
    envOverlay: provider.envOverlay,
    file: provider.file,
    shell: provider.shell,
    usesLoginShell: true,
  };
}

每条命令都跑一遍登录配置太慢,于是有了 shell 初始化快照(下一节)。prepareChildSpawnapps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-process.ts:62)拿到快照就去掉 -lnode-execution-adapter-process.ts:79):

    const shellInitSnapshot =
      request.command.mode === "shell" &&
      request.command.shellProfile === "posix-bash" &&
      resolvedCommand.shell === false &&
      supportsShellInitSnapshot(resolvedCommand.cwdDialect)
        ? await revalidateShellInitSnapshotForExecution(
            await this.shellInitSnapshots.getOrCreate({
              env: snapshotEnv,
              rootDir,
              shellDialect: resolvedCommand.cwdDialect,
              shellPath: resolvedCommand.file,
            }),
          )
        : undefined;
    const useLoginShell = shellInitSnapshot ? false : true;
    const resolvedShell = setResolvedShellLoginMode(resolvedCommand, useLoginShell);

最终交给 shell 的脚本由三段拼成:先 . 快照文件 2>/dev/null || true,快照坏了也不影响命令;开启内嵌搜索时再 source 一个定义 findgrep 函数的前置脚本,按内容哈希存在 bash-startup/会话 ID/ 下、权限 0600(apps/zcode-cli/packages/adapters/src/exec/bash-startup-script.ts:82bash-startup-script.ts:104);最后是用户命令加 cwd 捕获(见上一篇)。spawn 的选项是:POSIX 上 detached: true,让命令自成进程组以便整组终止;windowsHide: true;没有 stdin 时 stdin 为 ignorenode-execution-adapter-process.ts:125)。

Windows 的 argv 执行argv 模式在 POSIX 上直接 spawn(file, args),不经 shell;在 Windows 上先按 PATHPATHEXT(缺省 .COM.EXE.BAT.CMD)找到真实文件(apps/zcode-cli/packages/adapters/src/exec/windows-executable.ts:14)。.exe 仍然不经 shell;.cmd.bat 无法在 shell: false 下直接启动,于是改由 ComSpec/d /s /c 执行:含空白或特殊字符的参数整体加双引号,其中的 "%&()<>^| 再用 ^ 转义(execution-command.ts:118execution-command.ts:242)。Windows 的环境变量名不分大小写,设置与删除时会把大小写不同的同名键一并处理(execution-command.ts:263)。

登录 Shell 快照

“快照”在两个地方各有一份,作用不同。

CLI:shell 初始化快照ShellInitSnapshotManagerapps/zcode-cli/packages/adapters/src/exec/shell-init-snapshot.ts:95)为每个 shell 生成一个可 source 的脚本:

  • 怎么取:以 -c -l 跑一段生成脚本,先 source ~/.zshrc~/.bashrc~/.profile,再把函数定义(bash 用 base64 编码后 eval)、shell 选项、别名(各最多 1000 行)和 PATH 写进快照文件;开头先 unalias -a,因为别名会在函数定义时被“冻结”(shell-init-snapshot.ts:252shell-init-snapshot.ts:305)。Git Bash 下过滤掉 winpty 别名,它在没有 TTY 时会报错;Git Bash 的 PATH 另跑一次 bash -lc 'echo "$PATH"' 取得(shell-init-snapshot.ts:175)。注意快照只带函数、选项、别名和 PATH,不带其他环境变量。
  • 超时:生成命令 10 秒超时,输出缓冲 1 MiB(shell-init-snapshot.ts:14:15);失败或文件没生成,就当没有快照,退回 -l
  • 缓存:按“根目录、方言、shell 路径”缓存 Promise,同一个适配器实例只生成一次(shell-init-snapshot.ts:104:376);每次使用前确认文件还在(shell-init-snapshot.ts:292)。
  • 清理:文件放在 ~/.zcode/cli/exec/shell-snapshots/,名字带时间戳和随机串;适配器关闭时删掉本进程生成的,构造时顺带清理 30 天前的旧文件(shell-init-snapshot.ts:17shell-init-snapshot.ts:192apps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-base.ts:50)。

桌面端:登录 Shell 环境快照。GUI 进程和远端服务进程的 PATH 往往没经过登录 shell 初始化(注释见 packages/services/src/runtime-tools/runtimeLoginShellEnvCapture.ts:68),桌面主进程启动时就异步采一份(packages/desktop/src/main/index.ts:572)。captureLoginShellEnvSnapshotruntimeLoginShellEnvCapture.ts:173)依次尝试 $SHELL/bin/zsh/bin/bash/bin/shruntimeLoginShellEnvCapture.ts:39),zsh 与 bash 用 -ilc,在两个标记之间执行 env -0;在探测 shell 的 PATH 前面补上一组系统目录,并设 TERM=dumbCI=1;4 秒超时,输出上限 2 MiB,超时连同进程组一起杀掉(runtimeLoginShellEnvCapture.ts:53runtimeLoginShellEnvCapture.ts:60)。同步版本的结果在进程内缓存(runtimeLoginShellEnvCapture.ts:220),Windows 上直接跳过。采到的环境并不全盘照搬:只取登录 shell 的 PATH 和 54 个白名单模式匹配到的变量(KUBECONFIGSSH_AUTH_SOCKAWS_PROFILEJAVA_HOMENVM_DIRVIRTUAL_ENVMISE_*DIRENV_* 等),代理与证书变量照样走下文的封存(packages/services/src/runtime-tools/runtimeCommandEnv.ts:48runtimeCommandEnv.ts:155)。采集失败就用不跑 shell 的兜底补丁。

NOTICE.md:20 对此有一句提示:桌面或终端为获取 Shell 环境可能执行登录 Shell 及其启动配置,这些脚本可能早于模型任务运行。

环境变量:清洗、封存与恢复

图表加载中…

CLI 进程的第一件事就是清洗环境,注释写着目的(apps/zcode-cli/packages/cli/src/main.ts:20):入口先清洗用户 shell 注入的 NODE_ENV、代理和证书变量,网络变量只封存给后续 Bash、工具子进程恢复。清单在 packages/shared/src/runtimeEnv.ts:43

const SANITIZED_RUNTIME_ENV_KEYS = [
  "NODE_ENV",
  "ELECTRON_RUN_AS_NODE",
  "NODE_NO_WARNINGS",
  "HTTP_PROXY",
  "HTTPS_PROXY",
  "ALL_PROXY",
  "NO_PROXY",
  "NODE_EXTRA_CA_CERTS",
  "SSL_CERT_FILE",
  "SSL_CERT_DIR",
  "REQUESTS_CA_BUNDLE",
  "CURL_CA_BUNDLE",
  "GIT_SSL_CAINFO",
  // ...
] as const;

省略的部分是远端网络授权、Computer Use 的 broker 凭据、OpenTelemetry 与遥测身份变量,注释逐条解释了为什么不能漏给 Bash 和 MCP 子进程,比如 broker socket 泄漏会让恶意 MCP 或被提示注入的命令直接驱动已授权的 Helper(runtimeEnv.ts:60)。比较不分大小写,小写的 http_proxy 同样被删;npm_config_yarn_pnpm_ 开头的代理与 CA 配置用正则一并处理(runtimeEnv.ts:109)。

被删的变量分两种命运:

  • 封存:代理、NO_PROXY、证书与包管理器的代理配置,原值被序列化进 ZCODE_TOOL_ENV_PASSTHROUGH_JSONruntimeEnv.ts:227),挂在运行时自己的环境上(apps/zcode-cli/packages/cli/src/env.ts:40)。
  • 丢弃NODE_ENVELECTRON_RUN_AS_NODENODE_NO_WARNINGS、broker 凭据、遥测变量不进封存(runtimeEnv.ts:96runtimeEnv.ts:294)。所以用户终端里 exportNODE_ENV 也到不了 Bash 子进程,除非登录 shell 的配置文件自己再设一遍。CLI 判断自己是开发形态还是生产形态,看的是 ZCODE_RUNTIME_ENV 和入口路径,不再读 NODE_ENVenv.ts:125)。

桌面 Host 在任何 spawn 之前做同样的事(packages/services/src/runtime-tools/runtimeCommandEnv.ts:265);协议服务器只在开发形态下加载 .env,之后无论是否加载都会再清洗一遍(apps/zcode-cli/packages/cli/src/run.ts:255)。

到了子进程边界,buildExecutionEnvexecution-command.ts:28)复制当前环境、再清洗一次,然后调用 applyNetworkEgressEnvapps/zcode-cli/packages/adapters/src/network/subprocess-env.ts:42):

export function applyNetworkEgressEnv(
  env: Record<string, string>,
  options: NetworkEgressEnvOptions,
): Record<string, string> {
  const platform = options.platform ?? process.platform;
  const sourceEnv = options.sourceEnv ?? {};
  const network = options.network ?? {};

  deleteEnvKey(env, ZCODE_TOOL_ENV_PASSTHROUGH_ENV_KEY, platform);
  if (options.toolEnvPassthrough !== false) {
    applyToolEnvPassthroughEnv(env, sourceEnv, platform);
  }
  applyProxyEnv(env, sourceEnv, network, platform);
  applyNoProxyEnv(env, sourceEnv, network, platform);
  applyCaEnv(env, sourceEnv, network, platform);
  return env;
}

顺序是:去掉封存用的 JSON 变量本身;按原名恢复封存的值;若配置了 network.httpProxyZCODE_HTTP_PROXY,用它覆盖全部 6 个代理变量(大小写各三个),没写协议头的补上 http://subprocess-env.ts:71);NO_PROXYno_proxynetwork.noProxyZCODE_NO_PROXY;证书文件取 network.caCertFileZCODE_AGENT_CA_CERT,同时写进 NODE_EXTRA_CA_CERTSSSL_CERT_FILEREQUESTS_CA_BUNDLECURL_CA_BUNDLEGIT_SSL_CAINFO 五个变量(subprocess-env.ts:113),让 Node、Python、curl、git 都认。MCP 的 stdio 服务器用同一个函数组装环境(apps/zcode-cli/packages/adapters/src/mcp/network.ts:9)。

此外 applyExecutionTextEnv 给子进程补文本环境:LANGLC_CTYPE 缺失或是 C/POSIX 时改成 UTF-8 locale(macOS 用 en_US.UTF-8,其余用 C.UTF-8),否则 wcls 会把中文路径打成问号;PYTHONIOENCODING=utf-8PYTHONUTF8=1 则总是设置(apps/zcode-cli/packages/adapters/src/exec/outputEncoding.ts:68)。

CLI 入口还做两件与环境有关、但不属于清洗的事:设置 ZCODE_BETA=1 或以 zcode-beta 名字启动时,数据目录改到 ~/.zcode-betaenv.ts:133);对需要模型的子命令,prepareCliProviderRuntimeEnv 把内置与个人 Provider 配置文件的路径写进环境变量,SEA 单文件包则先把内嵌的配置落盘(apps/zcode-cli/packages/cli/src/provider-runtime-env.ts:53,配置本身见Provider 规则、模型目录与选项映射)。

输出、终止与退出码

Bash 与其他命令的输出走两条不同的路:

Bash(posix-bash其他命令
收集方式子进程的 stdout、stderr 直接指向同一个文件描述符,不经 Node 管道(apps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-run.ts:197管道加 OutputCollector,内联默认 10 MiB,超出部分按请求的 persistOutput 决定是否落盘(execution-utils.ts:9apps/zcode-cli/packages/adapters/src/exec/output-collector.ts:51
文件会话 ID/工具调用 ID-stdout.log,权限 0600,POSIX 上带 O_NOFOLLOWnode-execution-adapter-base.ts:100apps/zcode-cli/packages/adapters/src/exec/bash-file-output.ts:40stdout、stderr 各一个文件
上限文件每 5 秒检查一次,超过 5 GB 终止(bash-file-output.ts:65execution-utils.ts:11落盘默认 50 MiB(execution-utils.ts:10
进度2 秒后每秒读文件尾部 4 KiB,附 5 行与 100 行两档预览(bash-file-output.ts:98apps/zcode-cli/packages/adapters/src/exec/bash-output-preview.ts:3同样的阈值与间隔,读收集器的尾部

输出为空、退出码非零且不是 137 时,适配器会查一下输出目录所在文件系统:可用空间不足 10 MB 或 inode 不足 1000,就把“磁盘满导致输出丢失”写进结果,免得模型对着空输出瞎猜(bash-file-output.ts:185)。Windows 上的非 UTF-8 输出按活动代码页解码:可用 ZCODE_WINDOWS_OUTPUT_ENCODING 指定;否则用 chcp 查询,1 秒超时;代码页是 65001 时按区域猜 gb18030cp932cp949 等,只对不是合法 UTF-8 的字节生效(outputEncoding.ts:248outputEncoding.ts:204)。

终止。超时、取消或输出超限时(node-execution-adapter-run.ts:130):

  • POSIX 上的 Bash:用 ps -A -o pid= -o ppid= 取一次进程表,找出所有后代,对进程组和每个后代发 SIGTERM;1.5 秒后再对进程组和重新找到的后代发 SIGKILLapps/zcode-cli/packages/adapters/src/exec/process-tree.ts:5node-execution-adapter-process.ts:179)。要这样做,是因为 job control、管道、xargs 可以把子进程放进别的进程组。查进程表最多等 500 毫秒(process-tree.ts:4)。
  • POSIX 上的其他命令:只对进程组发 SIGTERM,750 毫秒后组还活着再 SIGKILLprocess-tree.ts:15)。
  • Windows:taskkill /pid 进程号 /T /F 杀整棵树(node-execution-adapter-process.ts:152)。

管道模式下,退出后最多再等 1 秒读完输出,还不结束就杀树并销毁读端;请求终止后 5 秒仍未退出,就按 SIGKILL 结算(execution-utils.ts:12:13)。

退出码归一。状态只有五种:completedfailedtimed_outcancelledspawn_error,优先级依次是 spawn 错误、超时、取消、输出超限,最后看退出码是否为 0(apps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-results.ts:181)。Bash 的直写模式不等进程真正退出:请求终止后立即以退出码 143(超时)或 137(取消)结算(node-execution-adapter-run.ts:140)。超时的错误文本形如 Command timed out after 2mnode-execution-adapter-results.ts:200)。没有传超时的请求默认 300000 毫秒(execution-utils.ts:5),Bash 工具总是显式传。

网络:统一出口

网络配置只有四个键,配置文件与环境变量两种来源(apps/zcode-cli/packages/adapters/src/config/schema.ts:26apps/zcode-cli/packages/adapters/src/config/env-config.adapter.ts:34):

配置键环境变量默认
network.httpProxyZCODE_HTTP_PROXY
network.noProxyZCODE_NO_PROXY
network.caCertFileZCODE_AGENT_CA_CERT
network.timeoutZCODE_HTTP_TIMEOUTZCODE_TIMEOUT180000 毫秒(apps/zcode-cli/packages/contracts/src/config/index.ts:306

代理解析在 resolveProxyForRequestInternalapps/zcode-cli/packages/adapters/src/network/http-config.ts:57):先看 noProxy(支持 *.example.com*.example.com、带端口与 IPv6 方括号的写法,http-config.ts:190),再用 network.httpProxy,再用 ZCODE_HTTP_PROXY。用户 shell 里的 HTTPS_PROXY 已经被封存,参与运行时自己的请求。唯一的例外是 WebFetch 这类 HTTP 客户端:它们在前两者都没有时,会回退到封存的 https_proxyHTTPS_PROXYhttp_proxy 等原值和原来的 no_proxyhttp-config.ts:94)。

出口实现代理来源
模型请求先过官方 Coding Plan 网关改写,再进带代理的 fetch(apps/zcode-cli/packages/adapters/src/model/model-execution.ts:516配置、ZCODE_HTTP_PROXY
MCP 的 HTTP 传输createNetworkProxyFetchapps/zcode-cli/packages/adapters/src/mcp/network.ts:22配置、ZCODE_HTTP_PROXY
WebFetch、插件市场与插件包下载createNodeWebFetchHttpClientAdapterapps/zcode-cli/packages/adapters/src/http/index.ts:150配置、ZCODE_HTTP_PROXY,再回退到用户原来的代理
登录与授权createNodeHttpClientAdapterapps/zcode-cli/packages/bootstrap/src/auth-login.ts:381配置、ZCODE_HTTP_PROXY
内置 Provider 配置的远程刷新直接用全局 fetchapps/zcode-cli/packages/bootstrap/src/app/process-provider-registry-runtime.ts:80不走代理与自定义证书

带代理的 fetch(apps/zcode-cli/packages/adapters/src/network/proxy-fetch.ts:29)有代理或自定义证书时,改用 Node 的 http/httpsproxy-agent 发请求,证书文件读一次后缓存;两者都没有就直接用全局 fetch。通用 HTTP 客户端 NodeHttpClientAdapter 另有几条规矩(http/index.ts:31):默认超时 180000 毫秒、响应体上限 10 MiB(content-length 超限直接拒,流式读取时累计超限也拒,apps/zcode-cli/packages/adapters/src/http/response-body.ts:18)、自动带上 x-zcode-trace-id 请求头、默认不跟随重定向。请求声明 egressPolicy: "public" 时,先解析 DNS,拒绝 localhost.local 与所有解析到非公网地址的主机,而且此时不许走代理,因为代理侧的 DNS 解析无法验证(http/index.ts:272apps/zcode-cli/packages/adapters/src/http/public-egress-policy.ts:79),细节见 WebFetch 与 WebSearch。模型请求的网关改写见模型适配层

AGENTS.md 要求统一请求入口集中管理“超时、重试、退避、鉴权、代理、自定义证书……”(apps/zcode-cli/AGENTS.md:55),实际上 network/http/ 两个目录里没有任何重试逻辑。模型请求的重试在模型层自己做,见一次模型请求

没有操作系统沙箱

根目录 NOTICE.md:9 写得很直接:

当前共享 Agent 执行适配器不提供默认的操作系统沙箱;工作目录、工作区身份、Git worktree、浏览器页面隔离和 Node REPL 的运行上下文,均不应被当作所有工具的系统级隔离保证。

代码与这句话一致,而且留着沙箱的“壳”:

  • 执行契约里有 ExecutionSandboxPolicyenabledprofiledangerouslyDisableSandbox)和 sandbox_violation 这个失败类型(execution.port.ts:108execution.port.ts:163),Bash 工具每次都填 sandbox.enabledhandlers/bash.ts:428),但 NodeExecutionAdapter 从不读取 request.sandbox,整个 adapters/src/exec 里“sandbox”只出现在一句注释中。
  • 那句注释说,把超时计时器提前到准备阶段是 “protected-resource sandbox 时代”的做法,“sandbox 撤除后”才恢复为只约束子进程运行(node-execution-adapter-run.ts:276)。从这句看,早先的版本有过某种沙箱,开源版里已经没有了;仓库里也找不到 sandbox-exec、bubblewrap、seccomp、Landlock 之类的调用。
  • 模型可见的 dangerouslyDisableSandbox 参数仍在 schema 里(apps/zcode-cli/packages/contracts/src/tools/bash.ts:66),传不传结果都一样;遥测里的 sandboxed 字段默认记为真(apps/zcode-cli/packages/core/src/tool/handlers/bash.ts:298),与事实不符。
  • apps/zcode-cli/AGENTS.md:63 还要求“权限系统、sandbox 和审批流程应读取这些声明”,这是目标而不是现状。

所以 ZCode 的防线只有权限层:只读判定、模式、规则与审批(上一篇权限模式与规则),加上 Hook(生命周期 Hooks)。一旦命令被放行,它就以当前系统账号的全部权限运行:能读 ~/.ssh、能联网、能改工作区外的文件。yolo 模式下连审批都没有,而 -p 无头模式不指定 --mode 时默认就是 yoloNOTICE.md:11apps/zcode-cli/packages/cli/src/run.ts:42)。NOTICE 给出的缓解建议是:处理不可信项目时限制运行账号、凭据和网络权限(NOTICE.md:28)。

同类产品的做法可以对照:

产品Bash 的隔离
ZCode无,只靠权限层
MiniMax Code内置 sandbox-runtime 分叉,默认关闭;打开后只包 Bash,而且只有 macOS 后端,网络一律不限
Grok BuildLinux 用 Landlock 加 bubblewrap,macOS 用 Seatbelt,子进程网络用 seccomp 封
Codex操作系统沙箱与审批策略都是用户可见的一等配置

设备探针

adapters/src/device 只点到为止。process-probe 按进程组采 RSS 与累计 CPU 时间,供 Bash 慢命令和 MCP 的资源遥测使用:macOS 调一次 ps,Windows 调 tasklist,Linux 只读 /proc;每次采样 1 秒超时,连续 3 次失败就停用(apps/zcode-cli/packages/adapters/src/device/process-probe.ts:30)。Bash 命令每 15 秒采一次、最多 20 次(packages/shared/src/zcode-protocol/index.ts:478),只有宿主注入了资源回调才启用(node-execution-adapter-process.ts:42)。cli-device-mid 维护一个设备标识,用于反馈与 Provider 请求头,与桌面端共用同一个状态文件(apps/zcode-cli/packages/adapters/src/device/cli-device-mid.ts:30),遥测部分见遥测、调试与提示词轨迹

下一篇:Todo、提问与 Plan 模式——模型怎样记待办、向用户发问卷、进入与退出只读的规划模式,这些交互在 TUI、桌面和无头模式下各自怎样收场。

本页目录