执行边界:子进程、环境与网络
所有子进程与网络请求都收敛到 adapters 的两个入口:执行端口怎样选 shell、取登录 Shell 快照、在 Windows 上起 .cmd 与 Git Bash;环境变量怎样在入口清洗、把代理与证书封存再只还给子进程;输出直写、进程树终止与退出码归一;以及为什么没有操作系统沙箱。
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 行),例外见网络一节。上一篇讲了 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):
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。
选哪个 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)。选择随会话固定、冷恢复时怎样复原,见上一篇。恢复时如果没能按快照复原(快照缺失、失效或读取失败),而当前 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
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 初始化快照(下一节)。prepareChildSpawn(apps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-process.ts:62)拿到快照就去掉 -l(node-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 一个定义 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 及其启动配置,这些脚本可能早于模型任务运行。
环境变量:清洗、封存与恢复
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_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):
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 规则、模型目录与选项映射)。
输出、终止与退出码
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。模型请求的网关改写见模型适配层。
AGENTS.md 要求统一请求入口集中管理“超时、重试、退避、鉴权、代理、自定义证书……”(apps/zcode-cli/AGENTS.md:55),实际上 network/ 与 http/ 两个目录里没有任何重试逻辑。模型请求的重试在模型层自己做,见一次模型请求。
没有操作系统沙箱
根目录 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 的防线只有权限层:只读判定、模式、规则与审批(上一篇与权限模式与规则),加上 Hook(生命周期 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 | 内置 sandbox-runtime 分叉,默认关闭;打开后只包 Bash,而且只有 macOS 后端,网络一律不限 |
| Grok Build | Linux 用 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、桌面和无头模式下各自怎样收场。