refactor(sandbox): await cancellable preparation in process consumers

This commit is contained in:
Tianyi Cui committed 2026-09-12 15:03:02 +08:00
1 parent fc7f7ddda6
commit caa69608fb
132 files changed
+2801 -426

No files matched your search

@@ -138,21 +138,21 @@ describe('Node runtime host failures', () => {
it('fails a required unavailable sandbox before spawning', async () => {
const h = await setup({}, 'read-only')
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(() => { throw new SandboxUnavailableError('read-only') })
vi.spyOn(h.ctx.sandbox, 'confine').mockRejectedValue(new SandboxUnavailableError('read-only'))
expect((await h.start()).error?.kind).toBe('sandbox-unavailable')
expect(h.spawn).not.toHaveBeenCalled()
})
it('keeps partial enforcement separate from a successful program', async () => {
const h = await setup({}, 'read-only')
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([...argv]))
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(async argv => confinement([...argv]))
h.onBoot(() => { h.emit({ type: 'done', value: encodeCodeJsonWire(42) }) })
expect(await h.start()).toEqual({ logs: [], value: 42, sandbox: { mode: 'read-only', denied: false, enforcement: 'partial' } })
})
it.each([['EACCES: blocked', true], ['EPERM: unrelated dialect', false]] as const)('uses only the selected denial dialect for %s', async (message, denied) => {
const h = await setup({}, 'read-only')
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([...argv]))
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(async argv => confinement([...argv]))
h.onBoot(() => { h.emit({ type: 'done', error: { kind: 'exception', message } }) })
const result = await h.start()
expect(result.error).toEqual({ kind: 'exception', message })
@@ -161,7 +161,7 @@ describe('Node runtime host failures', () => {
it('distinguishes fatal sandbox startup output from a program denial', async () => {
const h = await setup({}, 'read-only')
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([...argv]))
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(async argv => confinement([...argv]))
h.onBoot(() => {
h.stderr.write('sandbox-fatal: runner could not initialize')
h.direct.resolve({ exitCode: 1, signal: null })
@@ -211,6 +211,30 @@ describe('Node runtime host failures', () => {
expect(h.spawn).not.toHaveBeenCalled()
})
it('does not launch when confinement resolves after cancellation', async () => {
const h = await setup({}, 'read-only')
const entered = Promise.withResolvers<AbortSignal>()
const response = Promise.withResolvers<ConfinedArgv>()
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation((_argv, _policy, signal) => {
entered.resolve(signal!)
return response.promise
})
const controller = new AbortController()
const pending = h.start({ ...request, signal: controller.signal }, NO_INITIAL_FRAME)
try {
const signal = await entered.promise
expect(signal.aborted).toBe(false)
controller.abort('confinement cancelled')
expect(signal.aborted).toBe(true)
response.resolve(confinement([process.execPath]))
expect((await pending).error).toEqual({ kind: 'abort', message: 'confinement cancelled' })
expect(h.spawn).not.toHaveBeenCalled()
} finally {
response.resolve(confinement([process.execPath]))
await pending
}
})
it('reports an early control EOF using the direct process result', async () => {
const h = await setup()
h.spawn.mockImplementation(() => {
@@ -422,7 +446,7 @@ describe('Node runtime host failures', () => {
it.each([true, false])('attributes a failed confined spawn only with runner evidence (%s)', async (runnerFailed) => {
const h = await setup({}, 'read-only')
const runner = '/sandbox-runner'
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([runner, ...argv]))
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(async argv => confinement([runner, ...argv]))
h.onBoot(() => {
h.direct.reject(Object.assign(new Error('spawn rejected'), runnerFailed ? { code: 'ENOENT', path: runner, syscall: `spawn ${runner}` } : {}))
})
@@ -15,7 +15,7 @@ const sandboxUsable = await (async () => {
const probe = new Context()
try {
await probe.plugin(Sandbox, {})
probe.sandbox.confine([process.execPath, '--version'], { mode: 'read-only', workspaceRoot: process.cwd() })
await probe.sandbox.confine([process.execPath, '--version'], { mode: 'read-only', workspaceRoot: process.cwd() })
return true
} catch (error: unknown) {
if (error instanceof SandboxUnavailableError) return false
@@ -255,15 +255,15 @@ describe('Node program process', () => {
const confine = ctx.sandbox.confine.bind(ctx.sandbox)
const policy = runtime.resolve({ program: '', bindings: [] }).sandboxPolicy
if (policy === undefined || policy.mode === 'danger-full-access') throw new Error('expected confined policy')
const wrapped = confine([process.execPath, '--version'], { ...policy, mode: policy.mode })
const wrapped = await confine([process.execPath, '--version'], { ...policy, mode: policy.mode })
const original = wrapped.argv[0]
if (original === undefined) throw new Error('expected sandbox launcher')
const executable = await ctx.subprocess.resolveExecutable(original)
const alias = 'ptc-private-sandbox-launcher'
await symlink(executable, join(root, alias))
const previousPath = process.env.PATH
const substitute = vi.spyOn(ctx.sandbox, 'confine').mockImplementation((argv, selected) => {
const result = confine(argv, selected)
const substitute = vi.spyOn(ctx.sandbox, 'confine').mockImplementation(async (argv, selected, signal) => {
const result = await confine(argv, selected, signal)
return { ...result, argv: [alias, ...result.argv.slice(1)] }
})
try {