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

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

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.net/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.net/manual/zcode/exec-boundary
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

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

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

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

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

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

## 执行端口：两种命令

端口只认两种命令（`apps/zcode-cli/packages/contracts/src/interfaces/execution.port.ts:8`）：

```ts
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 工具 | `shell` 加 `posix-bash` | `apps/zcode-cli/packages/core/src/tool/handlers/bash.ts:409` |
| 命令型 Hook | `shell`，shell 由 Hook 配置决定，stdin 传 JSON | `apps/zcode-cli/packages/core/src/hooks/configured-runner-callback.ts:34` |
| 自定义命令里的 shell 展开 | `shell` | `apps/zcode-cli/packages/bootstrap/src/custom-command-shell-expansion.ts:122` |
| PDF 读取（`pdfinfo`、`pdftoppm`） | `argv` | `apps/zcode-cli/packages/adapters/src/pdf/index.ts:43` |
| 动态工作流读取 git 状态 | `argv` | `apps/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:392`、`apps/zcode-cli/packages/adapters/src/exec/execution-utils.ts:18`）。Hook 的协议与超时见[生命周期 Hooks](https://daiw.net/manual/zcode/hooks)。

## 选哪个 shell

`resolveEffectiveBashShellSelection`（`apps/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.exe`（`apps/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`）。选择随会话固定、冷恢复时怎样复原，见[上一篇](https://daiw.net/manual/zcode/bash)。恢复时如果没能按快照复原（快照缺失、失效或读取失败），而当前 shell 是自动探测到的 Git Bash，或者名字与历史里记录的不同，运行时会往历史里补一条附件提醒模型，比如 `The Bash tool shell is Git Bash.`（`apps/zcode-cli/packages/core/src/runtime/methods/shell-environment.ts:24`、`apps/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

```mermaid
flowchart TD
  A["ExecutionRequest"] --> B["buildExecutionEnv：清洗、补 UTF-8 locale、恢复网络变量、叠加 overlay"]
  B --> C{"命令模式"}
  C -->|argv| D["POSIX 直接 spawn，Windows 按 PATHEXT 找文件，.cmd 与 .bat 经 cmd.exe"]
  C -->|"shell 加 posix-bash"| E["选 shell：zsh、bash、Git Bash 或 CMD"]
  C -->|其他 shell| F["交给 Node 的 shell 选项"]
  E --> G{"有 shell 初始化快照"}
  G -->|有| H["-c，并先 source 快照"]
  G -->|没有| I["-c -l，登录 shell"]
  H --> J["前置内嵌搜索脚本，追加 cwd 捕获"]
  I --> J
  J --> K["spawn：POSIX 独立进程组，stdout 与 stderr 直写同一文件"]
```

bash 和 zsh 都以 `-c -l 命令` 启动，即非交互的登录 shell（`execution-command.ts:142`）：

```ts
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 初始化快照（下一节）。`prepareChildSpawn`（`apps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-process.ts:62`）拿到快照就去掉 `-l`（`node-execution-adapter-process.ts:79`）：

```ts
    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 一个定义 `find`、`grep` 函数的前置脚本，按内容哈希存在 `bash-startup/会话 ID/` 下、权限 0600（`apps/zcode-cli/packages/adapters/src/exec/bash-startup-script.ts:82`、`bash-startup-script.ts:104`）；最后是用户命令加 cwd 捕获（见上一篇）。spawn 的选项是：POSIX 上 `detached: true`，让命令自成进程组以便整组终止；`windowsHide: true`；没有 stdin 时 stdin 为 `ignore`（`node-execution-adapter-process.ts:125`）。

**Windows 的 argv 执行**。`argv` 模式在 POSIX 上直接 `spawn(file, args)`，不经 shell；在 Windows 上先按 `PATH` 与 `PATHEXT`（缺省 `.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:118`、`execution-command.ts:242`）。Windows 的环境变量名不分大小写，设置与删除时会把大小写不同的同名键一并处理（`execution-command.ts:263`）。

## 登录 Shell 快照

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

**CLI：shell 初始化快照**。`ShellInitSnapshotManager`（`apps/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:252`、`shell-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:17`、`shell-init-snapshot.ts:192`、`apps/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`）。`captureLoginShellEnvSnapshot`（`runtimeLoginShellEnvCapture.ts:173`）依次尝试 `$SHELL`、`/bin/zsh`、`/bin/bash`、`/bin/sh`（`runtimeLoginShellEnvCapture.ts:39`），zsh 与 bash 用 `-ilc`，在两个标记之间执行 `env -0`；在探测 shell 的 `PATH` 前面补上一组系统目录，并设 `TERM=dumb`、`CI=1`；4 秒超时，输出上限 2 MiB，超时连同进程组一起杀掉（`runtimeLoginShellEnvCapture.ts:53`、`runtimeLoginShellEnvCapture.ts:60`）。同步版本的结果在进程内缓存（`runtimeLoginShellEnvCapture.ts:220`），Windows 上直接跳过。采到的环境并不全盘照搬：只取登录 shell 的 `PATH` 和 54 个白名单模式匹配到的变量（`KUBECONFIG`、`SSH_AUTH_SOCK`、`AWS_PROFILE`、`JAVA_HOME`、`NVM_DIR`、`VIRTUAL_ENV`、`MISE_*`、`DIRENV_*` 等），代理与证书变量照样走下文的封存（`packages/services/src/runtime-tools/runtimeCommandEnv.ts:48`、`runtimeCommandEnv.ts:155`）。采集失败就用不跑 shell 的兜底补丁。

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

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

```mermaid
flowchart LR
  A["用户 shell 的环境"] --> B["CLI 入口清洗"]
  B -->|"删除 NODE_ENV、代理、证书、遥测变量"| C["运行时 process.env"]
  B -->|"代理与证书原值"| D["ZCODE_TOOL_ENV_PASSTHROUGH_JSON"]
  C --> E["模型与 MCP 的 HTTP：只认 network 配置与 ZCODE_HTTP_PROXY"]
  C --> F["applyNetworkEgressEnv"]
  D --> F
  F --> G["Bash、Hook、MCP stdio 子进程：按原名恢复，再叠加配置"]
  D -.->|"仅作回退"| H["WebFetch 与插件下载的 HTTP 客户端"]
```

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

```ts
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_JSON`（`runtimeEnv.ts:227`），挂在运行时自己的环境上（`apps/zcode-cli/packages/cli/src/env.ts:40`）。
- **丢弃**：`NODE_ENV`、`ELECTRON_RUN_AS_NODE`、`NODE_NO_WARNINGS`、broker 凭据、遥测变量不进封存（`runtimeEnv.ts:96`、`runtimeEnv.ts:294`）。所以用户终端里 `export` 的 `NODE_ENV` 也到不了 Bash 子进程，除非登录 shell 的配置文件自己再设一遍。CLI 判断自己是开发形态还是生产形态，看的是 `ZCODE_RUNTIME_ENV` 和入口路径，不再读 `NODE_ENV`（`env.ts:125`）。

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

到了子进程边界，`buildExecutionEnv`（`execution-command.ts:28`）复制当前环境、再清洗一次，然后调用 `applyNetworkEgressEnv`（`apps/zcode-cli/packages/adapters/src/network/subprocess-env.ts:42`）：

```ts
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.httpProxy` 或 `ZCODE_HTTP_PROXY`，用它覆盖全部 6 个代理变量（大小写各三个），没写协议头的补上 `http://`（`subprocess-env.ts:71`）；`NO_PROXY` 与 `no_proxy` 取 `network.noProxy` 或 `ZCODE_NO_PROXY`；证书文件取 `network.caCertFile` 或 `ZCODE_AGENT_CA_CERT`，同时写进 `NODE_EXTRA_CA_CERTS`、`SSL_CERT_FILE`、`REQUESTS_CA_BUNDLE`、`CURL_CA_BUNDLE`、`GIT_SSL_CAINFO` 五个变量（`subprocess-env.ts:113`），让 Node、Python、curl、git 都认。MCP 的 stdio 服务器用同一个函数组装环境（`apps/zcode-cli/packages/adapters/src/mcp/network.ts:9`）。

此外 `applyExecutionTextEnv` 给子进程补文本环境：`LANG`、`LC_CTYPE` 缺失或是 `C`/`POSIX` 时改成 UTF-8 locale（macOS 用 `en_US.UTF-8`，其余用 `C.UTF-8`），否则 `wc`、`ls` 会把中文路径打成问号；`PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1` 则总是设置（`apps/zcode-cli/packages/adapters/src/exec/outputEncoding.ts:68`）。

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

## 输出、终止与退出码

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:9`、`apps/zcode-cli/packages/adapters/src/exec/output-collector.ts:51`） |
| 文件 | `会话 ID/工具调用 ID-stdout.log`，权限 0600，POSIX 上带 `O_NOFOLLOW`（`node-execution-adapter-base.ts:100`、`apps/zcode-cli/packages/adapters/src/exec/bash-file-output.ts:40`） | stdout、stderr 各一个文件 |
| 上限 | 文件每 5 秒检查一次，超过 5 GB 终止（`bash-file-output.ts:65`、`execution-utils.ts:11`） | 落盘默认 50 MiB（`execution-utils.ts:10`） |
| 进度 | 2 秒后每秒读文件尾部 4 KiB，附 5 行与 100 行两档预览（`bash-file-output.ts:98`、`apps/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 时按区域猜 `gb18030`、`cp932`、`cp949` 等，只对不是合法 UTF-8 的字节生效（`outputEncoding.ts:248`、`outputEncoding.ts:204`）。

**终止**。超时、取消或输出超限时（`node-execution-adapter-run.ts:130`）：

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

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

**退出码归一**。状态只有五种：`completed`、`failed`、`timed_out`、`cancelled`、`spawn_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 2m`（`node-execution-adapter-results.ts:200`）。没有传超时的请求默认 300000 毫秒（`execution-utils.ts:5`），Bash 工具总是显式传。

## 网络：统一出口

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

| 配置键 | 环境变量 | 默认 |
| --- | --- | --- |
| `network.httpProxy` | `ZCODE_HTTP_PROXY` | 无 |
| `network.noProxy` | `ZCODE_NO_PROXY` | 无 |
| `network.caCertFile` | `ZCODE_AGENT_CA_CERT` | 无 |
| `network.timeout` | `ZCODE_HTTP_TIMEOUT` 或 `ZCODE_TIMEOUT` | 180000 毫秒（`apps/zcode-cli/packages/contracts/src/config/index.ts:306`） |

代理解析在 `resolveProxyForRequestInternal`（`apps/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_proxy`、`HTTPS_PROXY`、`http_proxy` 等原值和原来的 `no_proxy`（`http-config.ts:94`）。

| 出口 | 实现 | 代理来源 |
| --- | --- | --- |
| 模型请求 | 先过官方 Coding Plan 网关改写，再进带代理的 fetch（`apps/zcode-cli/packages/adapters/src/model/model-execution.ts:516`） | 配置、`ZCODE_HTTP_PROXY` |
| MCP 的 HTTP 传输 | `createNetworkProxyFetch`（`apps/zcode-cli/packages/adapters/src/mcp/network.ts:22`） | 配置、`ZCODE_HTTP_PROXY` |
| WebFetch、插件市场与插件包下载 | `createNodeWebFetchHttpClientAdapter`（`apps/zcode-cli/packages/adapters/src/http/index.ts:150`） | 配置、`ZCODE_HTTP_PROXY`，再回退到用户原来的代理 |
| 登录与授权 | `createNodeHttpClientAdapter`（`apps/zcode-cli/packages/bootstrap/src/auth-login.ts:381`） | 配置、`ZCODE_HTTP_PROXY` |
| 内置 Provider 配置的远程刷新 | 直接用全局 `fetch`（`apps/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`/`https` 加 `proxy-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:272`、`apps/zcode-cli/packages/adapters/src/http/public-egress-policy.ts:79`），细节见 [WebFetch 与 WebSearch](https://daiw.net/manual/zcode/web-tools)。模型请求的网关改写见[模型适配层](https://daiw.net/manual/zcode/model-adapters)。

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

## 没有操作系统沙箱

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

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

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

- 执行契约里有 `ExecutionSandboxPolicy`（`enabled`、`profile`、`dangerouslyDisableSandbox`）和 `sandbox_violation` 这个失败类型（`execution.port.ts:108`、`execution.port.ts:163`），Bash 工具每次都填 `sandbox.enabled`（`handlers/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 的防线只有权限层：只读判定、模式、规则与审批（[上一篇](https://daiw.net/manual/zcode/bash)与[权限模式与规则](https://daiw.net/manual/zcode/permission)），加上 Hook（[生命周期 Hooks](https://daiw.net/manual/zcode/hooks)）。一旦命令被放行，它就以当前系统账号的全部权限运行：能读 `~/.ssh`、能联网、能改工作区外的文件。`yolo` 模式下连审批都没有，而 `-p` 无头模式不指定 `--mode` 时默认就是 `yolo`（`NOTICE.md:11`、`apps/zcode-cli/packages/cli/src/run.ts:42`）。NOTICE 给出的缓解建议是：处理不可信项目时限制运行账号、凭据和网络权限（`NOTICE.md:28`）。

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

| 产品 | Bash 的隔离 |
| --- | --- |
| ZCode | 无，只靠权限层 |
| [MiniMax Code](https://daiw.net/manual/minimax-code/sandbox) | 内置 sandbox-runtime 分叉，默认关闭；打开后只包 Bash，而且只有 macOS 后端，网络一律不限 |
| [Grok Build](https://daiw.net/manual/grok-build/sandbox-permissions) | Linux 用 Landlock 加 bubblewrap，macOS 用 Seatbelt，子进程网络用 seccomp 封 |
| [Codex](https://daiw.net/manual/codex/sandbox-approvals) | 操作系统沙箱与审批策略都是用户可见的一等配置 |

## 设备探针

`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`），遥测部分见[遥测、调试与提示词轨迹](https://daiw.net/manual/zcode/telemetry-debug)。

下一篇：[Todo、提问与 Plan 模式](https://daiw.net/manual/zcode/interaction-tools)——模型怎样记待办、向用户发问卷、进入与退出只读的规划模式，这些交互在 TUI、桌面和无头模式下各自怎样收场。
