fix(code-runtime): preserve native temp setup and preview activation

This commit is contained in:
Tianyi Cui committed 2026-09-12 15:03:02 +08:00
1 parent 2344830cfd
commit 091cbcdbfb
14 files changed
+70 -13

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-node/README.md
README.md: 76e3e4a7cb2d758797ca130f498fe6ec3f697b35
README.zh.md: 9835128af67d4b57266818aa06ab6cee21412e15
README.md: 357173bf93a0e5d1e0048b6ee06fac36db856a62
README.zh.md: 5808193e493b1a8dcf1e404dcc938f62b4b246db
@@ -83,7 +83,7 @@ The host owns policy, deadlines, binding lookup and process cleanup. The child o
### Launch and control
The host strips erasable types, resolves the executable and bootstrap in the configured execution world, wraps the argv through `ctx.sandbox`, and spawns through `ctx.subprocess`. After adopting the inherited control channel, the child retains only executable-search and Windows system paths in its OS environment and replaces the program-visible `process.env` with an empty dictionary. The retained native paths keep nested Windows process creation functional. The heap limit uses Node argv or a provider-created `NODE_OPTIONS` value for packaged executables; ambient loader and inspector flags are discarded.
The host strips erasable types, resolves the executable and bootstrap in the configured execution world, wraps the argv through `ctx.sandbox`, and spawns through `ctx.subprocess`. After adopting the inherited control channel, the child retains only executable-search, Windows system, and temporary paths in its OS environment and replaces the program-visible `process.env` with an empty dictionary. Windows ACL setup receives the parent's distinct `TEMP` and `TMP` values for shared grant locks, then replaces both with its private directory before starting the program. These native paths keep nested process creation and native temporary-file APIs functional. The heap limit uses Node argv or a provider-created `NODE_OPTIONS` value for packaged executables; ambient loader and inspector flags are discarded.
Length-framed JSON travels separately from stdout/stderr. The host bounds frames and queued writes, validates call identity and declared binding names before dispatch, and refuses invalid traffic. Output capture meters serialized logs plus the completion or diagnostic; fixed result-envelope fields and sandbox metadata are outside that ledger.
@@ -83,7 +83,7 @@ Host 负责策略、截止时间、绑定查找和进程清理。子进程负责
### 启动与控制
Host 擦除可擦除类型,在配置的执行世界中解析可执行文件与 bootstrap,通过 `ctx.sandbox` 包装 argv,并通过 `ctx.subprocess` 启动。接管继承的控制通道后,子进程在 OS 环境中只保留可执行文件搜索路径和 Windows 系统路径,并将程序可见的 `process.env` 替换为空字典。保留的原生路径使嵌套 Windows 进程创建仍可正常工作。堆上限通过 Node argv 或为打包可执行文件由提供方构造的 `NODE_OPTIONS` 值传递;环境中的加载器和调试器标志会被丢弃。
Host 擦除可擦除类型,在配置的执行世界中解析可执行文件与 bootstrap,通过 `ctx.sandbox` 包装 argv,并通过 `ctx.subprocess` 启动。接管继承的控制通道后,子进程在 OS 环境中只保留可执行文件搜索路径、Windows 系统路径和临时路径,并将程序可见的 `process.env` 替换为空字典。Windows ACL 初始化接收父进程各自的 `TEMP` 和 `TMP` 值以使用共享授权锁,然后在启动程序前将二者替换为私有目录。这些原生路径使嵌套进程创建和原生临时文件 API 仍可正常工作。堆上限通过 Node argv 或为打包可执行文件由提供方构造的 `NODE_OPTIONS` 值传递;环境中的加载器和调试器标志会被丢弃。
带长度分帧的 JSON 与 stdout/stderr 分开传输。Host 限制帧与排队写入,在分派前验证调用身份和已声明的绑定名,并拒绝无效通信。输出捕获计量序列化日志加完成值或诊断;固定结果信封字段与沙箱元数据不计入该账本。
@@ -1,4 +1,4 @@
/** Startup variables required by native executables before model evaluation. */
/** Native executable search and Windows system paths retained in the OS environment. */
export const STARTUP_ENVIRONMENT_NAMES: ReadonlySet<string> = new Set(['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR'])
/** Native executable search, Windows system paths, and sandbox temporary paths retained in the OS environment. */
export const STARTUP_ENVIRONMENT_NAMES: ReadonlySet<string> = new Set(['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP'])
@@ -429,6 +429,21 @@ describe('Node runtime host failures', () => {
expect((await h.start()).error).toEqual({ kind: runnerFailed ? 'sandbox-unavailable' : 'worker-exit', message: 'spawn rejected' })
})
it('retains distinct native temp paths for the trusted launcher while removing other ambient values', async () => {
const h = await setup()
onTestFinished(() => { vi.unstubAllEnvs() })
vi.stubEnv('TEMP', 'fixture-temp-first')
vi.stubEnv('TMP', 'fixture-tmp-second')
vi.stubEnv('DSH_TEST_RUNTIME_SECRET', 'must-not-inherit')
h.onBoot(() => { h.emit({ type: 'done' }) })
expect((await h.start()).error).toBeUndefined()
const env = h.spawn.mock.calls[0]?.[0].env ?? {}
expect(Object.hasOwn(env, 'TEMP')).toBe(false)
expect(Object.hasOwn(env, 'TMP')).toBe(false)
expect(Object.hasOwn(env, 'DSH_TEST_RUNTIME_SECRET')).toBe(true)
expect(env.DSH_TEST_RUNTIME_SECRET).toBeUndefined()
})
it('selects the private packaged bootstrap without leaking ambient environment', async () => {
const h = await setup()
h.onBoot(() => { h.emit({ type: 'done' }) })
@@ -23,6 +23,8 @@ it('clears process environment, dispatches a binding reply and flushes the termi
const nativeEnvironment = state.env
nativeEnvironment.SystemRoot = 'C:\\Windows'
nativeEnvironment.PATH = '/native/bin'
nativeEnvironment.TMP = 'C:\\sandbox-temp'
nativeEnvironment.TEMP = 'C:\\sandbox-temp'
const messages: Record<string, unknown>[] = []
const peer = new JsonChannel(host, 4096, (raw) => {
const message = raw as Record<string, unknown>
@@ -35,7 +37,7 @@ it('clears process environment, dispatches a binding reply and flushes the termi
expect(state.env).toEqual({})
expect(state.env).not.toBe(nativeEnvironment)
expect(Object.getPrototypeOf(state.env)).toBeNull()
expect(nativeEnvironment).toEqual({ SystemRoot: 'C:\\Windows', PATH: '/native/bin' })
expect(nativeEnvironment).toEqual({ SystemRoot: 'C:\\Windows', PATH: '/native/bin', TMP: 'C:\\sandbox-temp', TEMP: 'C:\\sandbox-temp' })
expect(state.exitCode).toBeUndefined()
expect(decodeCodeJsonWire(messages.find(message => message.type === 'done')?.value)).toBe(42)
})
@@ -1,4 +1,4 @@
import { mkdtemp, mkdir, readFile, rm, symlink } from 'node:fs/promises'
import { mkdtemp, mkdir, readFile, readdir, rm, symlink } from 'node:fs/promises'
import { homedir } from 'node:os'
import { createServer, type Socket } from 'node:net'
import { delimiter, join } from 'node:path'
@@ -59,12 +59,40 @@ describe('Node program process', () => {
expect(value.env).toEqual([])
expect(value.status).toBe(0)
expect(value.error).toBeNull()
const nativeKeys = ['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR']
const nativeKeys = ['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP']
// CoreFoundation initializes this entry independently when a macOS child starts.
if (process.platform === 'darwin') nativeKeys.push('__CF_USER_TEXT_ENCODING')
expect(value.childKeys.filter(key => !nativeKeys.includes(key.toUpperCase()))).toEqual([])
})
it.skipIf(process.platform !== 'win32' || !sandboxUsable)('uses the common Windows grant lock and private native temp without exposing ambient values', async () => {
const { run, root } = await setup({}, 'workspace-write')
const temp = join(root, 'node-temp')
const tmp = join(root, 'win32-temp')
await mkdir(temp)
await mkdir(tmp)
onTestFinished(() => { vi.unstubAllEnvs() })
vi.stubEnv('TEMP', temp)
vi.stubEnv('TMP', tmp)
vi.stubEnv('DSH_TEST_RUNTIME_SECRET', 'must-not-inherit')
const childCode = 'const fs=require("node:fs"); const path=require("node:path"); const temp=require("node:os").tmpdir(); const file=path.join(temp,"native-temp.txt"); fs.writeFileSync(file,"native-temp"); process.stdout.write(JSON.stringify({file,temp,env:Object.keys(process.env)}));'
const result = await run({
program: `const {spawnSync}=await import("node:child_process"); const child=spawnSync(process.execPath,["-e",${JSON.stringify(childCode)}],{encoding:"utf8"}); if(child.status!==0) throw new Error(child.error?.message ?? child.stderr); const native=JSON.parse(child.stdout); return {env:Object.keys(process.env),native,observed:await tools.inspect({path:native.file})};`,
bindings: bindings({ inspect: async (args) => {
const path = (args as { path: string }).path
expect(path.startsWith(`${temp}\\dsh-`)).toBe(true)
return await readFile(path, 'utf8')
} }),
})
expect(result.error).toBeUndefined()
const value = result.value as { env: string[]; native: { env: string[]; temp: string }; observed: string }
expect(value.env).toEqual([])
expect(value.native.env).not.toContain('DSH_TEST_RUNTIME_SECRET')
expect(value.native.temp.startsWith(`${temp}\\dsh-`)).toBe(true)
expect(value.observed).toBe('native-temp')
expect((await readdir(join(tmp, 'dsh-acl-locks'))).some(name => name.endsWith('.lock'))).toBe(true)
})
it('returns binding values and preserves typed binding rejection', async () => {
const { run } = await setup()
const result = await run({