blob: 9c02e81320267e4c8d29532340fe5d91b4f23ba6 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import test from 'node:test';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { jsonSchema, tool } from 'ai';
import {
DEFAULT_CODE_MODE_EXECUTION_POLICY,
type ExecuteCodeCellInput,
executeCodeCell,
} from '../code-mode.js';
function execute(code: string, input: Partial<Omit<ExecuteCodeCellInput, 'code'>> = {}) {
return executeCodeCell({
code,
tools: [],
callTool: async () => null,
...input,
});
}
test('excludes host waiting from the execution budget and preserves dependent Promise.race progress', async () => {
let releaseSlow!: (value: string) => void;
const slow = new Promise<string>((resolve) => {
releaseSlow = resolve;
});
const calls: string[] = [];
const result = await execute(
`
const slow = tools.slow({});
const fast = tools.fast({});
const winner = await Promise.race([slow, fast]);
await tools.after({});
await Promise.all([slow, fast]);
return winner;
`,
{
tools: ['slow', 'fast', 'after'].map((name) => ({ name })),
signal: AbortSignal.timeout(5_000),
executionPolicy: { ...DEFAULT_CODE_MODE_EXECUTION_POLICY, timeoutMs: 200 },
callTool: async (name, _input, signal) => {
calls.push(name);
if (name === 'slow') {
signal.addEventListener('abort', () => releaseSlow('cancelled'), { once: true });
return slow;
}
if (name === 'fast') {
await new Promise((resolve) => setTimeout(resolve, 500));
return 'fast';
}
releaseSlow('slow');
return null;
},
},
).finally(() => releaseSlow('cleanup'));
assert.equal(result.ok, true);
if (result.ok) assert.equal(result.value, 'fast');
assert.deepEqual(calls, ['slow', 'fast', 'after']);
});
test('executes standard JavaScript without an interpreter subset', async () => {
const result = await execute(`
const key = 'answer';
const message = await Promise.reject(new Error('expected'))
.catch((error) => error.message);
return { [key]: 42, message };
`);
assert.deepEqual(result, {
ok: true,
value: { answer: 42, message: 'expected' },
toolCalls: [],
});
});
test('executes TypeScript syntax', async () => {
const result = await execute(`
interface Answer { value: number }
const answer: Answer = { value: 42 };
return answer;
`);
assert.deepEqual(result, { ok: true, value: { value: 42 }, toolCalls: [] });
});
test('does not read the execution result through a guest-controlled global', async () => {
const result = await execute(`
Object.defineProperty(globalThis, '__runResult', {
get() { throw new Error('Guest getter must not run'); },
set() {},
});
return 'actual result';
`);
assert.equal(result.ok, true);
if (result.ok) assert.equal(result.value, 'actual result');
});
test('reports invalid source as a parse error', async () => {
const result = await execute('const value = ;');
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.error.kind, 'parse_error');
});
test('reports a runtime SyntaxError as an execution error', async () => {
const result = await execute(`return JSON.parse('not json');`);
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.error.kind, 'execution_error');
});
test('runs nested tools concurrently inside one cell', async () => {
const calls: Array<{ name: string; input: unknown }> = [];
let active = 0;
let maxActive = 0;
let releaseTools!: () => void;
let observeConcurrency!: () => void;
const toolsReleased = new Promise<void>((resolve) => {
releaseTools = resolve;
});
const concurrencyObserved = new Promise<void>((resolve) => {
observeConcurrency = resolve;
});
const execution = execute(
`return await Promise.all([
tools.lookup({ id: 'a' }),
tools.lookup({ id: 'b' }),
]);`,
{
tools: [{ name: 'lookup' }],
callTool: async (name, input) => {
calls.push({ name, input });
active += 1;
maxActive = Math.max(maxActive, active);
if (active === 2) observeConcurrency();
await toolsReleased;
active -= 1;
return input;
},
},
);
const overlapped = await Promise.race([
concurrencyObserved.then(() => true),
execution.then(() => false),
]);
assert.equal(overlapped, true);
releaseTools();
const result = await execution;
assert.equal(maxActive, 2);
assert.deepEqual(calls, [
{ name: 'lookup', input: { id: 'a' } },
{ name: 'lookup', input: { id: 'b' } },
]);
assert.deepEqual(result, {
ok: true,
value: [{ id: 'a' }, { id: 'b' }],
toolCalls: [
{ index: 1, name: 'lookup' },
{ index: 2, name: 'lookup' },
],
});
});
test('does not expose Node capabilities to cell code', async () => {
const result = await execute(`
let functionConstructorBlocked = false;
try {
Function('return 1')();
} catch {
functionConstructorBlocked = true;
}
return {
process: typeof globalThis.process,
require: typeof globalThis.require,
fetch: typeof globalThis.fetch,
webAssembly: typeof globalThis.WebAssembly,
eval: typeof globalThis.eval,
functionConstructorBlocked,
};
`);
assert.equal(result.ok, true);
if (!result.ok) return;
assert.deepEqual(result.value, {
process: 'undefined',
require: 'undefined',
fetch: 'undefined',
webAssembly: 'undefined',
eval: 'undefined',
functionConstructorBlocked: true,
});
});
test('keeps sandbox console output out of the host process stdout', async () => {
// The product policy's `maxConsoleOutputBytes: 1` is the only thing standing
// between cell code and the host's stdout, which the CLI writes its TUI and
// command output to. The sandbox writes from a worker thread that Node pipes
// into the parent's stdout, so this has to be observed from outside the
// process rather than by patching `process.stdout.write` here.
const moduleUrl = new URL('../code-mode.js', import.meta.url).href;
const child = spawn(
process.execPath,
[
'--input-type=module',
'-e',
`const { executeCodeCell } = await import(${JSON.stringify(moduleUrl)});
const result = await executeCodeCell({
code: "console.log('sandbox-console-marker'); return 1;",
tools: [],
callTool: async () => null,
});
if (!result.ok || result.value !== 1) process.exit(2);`,
],
{ stdio: ['ignore', 'pipe', 'ignore'] },
);
let stdout = '';
child.stdout.setEncoding('utf8');
child.stdout.on('data', (chunk: string) => {
stdout += chunk;
});
const [code] = (await once(child, 'exit')) as [number | null];
assert.equal(code, 0, 'the probe cell did not run to completion');
assert.equal(stdout, '');
});
test('starts each cell in a fresh global context', async () => {
const first = await execute('globalThis.transient = 42; return globalThis.transient;');
const second = await execute('return globalThis.transient ?? null;');
assert.equal(first.ok ? first.value : undefined, 42);
assert.equal(second.ok ? second.value : undefined, null);
});
test('uses normal partial-execution semantics before an unknown tool failure', async () => {
const calls: string[] = [];
const result = await execute(
`
await tools.allowed({});
return await tools.missing({});
`,
{
tools: [{ name: 'allowed' }],
callTool: async (name) => {
calls.push(name);
return null;
},
},
);
assert.deepEqual(calls, ['allowed']);
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.error.kind, 'unknown_tool');
});
test('does not dispatch tools inherited from Object.prototype', async () => {
let inheritedCalls = 0;
Object.defineProperty(Object.prototype, 'inheritedCodeModeTool', {
configurable: true,
value: tool({
inputSchema: jsonSchema({}),
execute: async () => {
inheritedCalls += 1;
return 'escaped';
},
}),
});
try {
const result = await execute('return await tools.inheritedCodeModeTool({});');
assert.equal(result.ok ? undefined : result.error.kind, 'unknown_tool');
assert.equal(inheritedCalls, 0);
} finally {
delete (Object.prototype as Record<string, unknown>).inheritedCodeModeTool;
}
});
test('does not start an unobserved tool call', async () => {
let calls = 0;
const result = await execute('tools.echo({}); return null;', {
tools: [{ name: 'echo' }],
callTool: async () => {
calls += 1;
return null;
},
});
assert.equal(calls, 0);
assert.equal(result.ok, false);
});
test('lets cell code handle an ordinary tool failure', async () => {
const result = await execute(
`
try {
await tools.fail({});
} catch (error) {
return error.message;
}
`,
{
tools: [{ name: 'fail' }],
callTool: async () => {
throw new Error('expected failure');
},
},
);
assert.deepEqual(result, {
ok: true,
value: 'expected failure',
toolCalls: [{ index: 1, name: 'fail' }],
});
});
test('reports uncaught runtime and tool failures', async (t) => {
await t.test('runtime', async () => {
const result = await execute("throw new Error('out of memory');");
assert.equal(result.ok ? undefined : result.error.kind, 'execution_error');
});
await t.test('tool', async () => {
const result = await execute('return await tools.fail({});', {
tools: [{ name: 'fail' }],
callTool: async () => {
throw new Error('stack overflow');
},
});
assert.equal(result.ok ? undefined : result.error.kind, 'tool_failure');
});
await t.test('non-serializable tool output', async () => {
const circular: Record<string, unknown> = {};
circular.self = circular;
const result = await execute('return await tools.fail({});', {
tools: [{ name: 'fail' }],
callTool: async () => circular,
});
assert.equal(result.ok ? undefined : result.error.kind, 'tool_failure');
});
});
test('enforces byte and bridge limits', async (t) => {
await t.test('source', async () => {
const result = await execute('return null;', {
executionPolicy: { ...DEFAULT_CODE_MODE_EXECUTION_POLICY, maxSourceBytes: 1 },
});
assert.equal(result.ok ? undefined : result.error.kind, 'limit_exceeded');
});
await t.test('tool input', async () => {
const result = await execute("return await tools.echo({ value: '12345' });", {
tools: [{ name: 'echo' }],
executionPolicy: { ...DEFAULT_CODE_MODE_EXECUTION_POLICY, maxToolInputBytes: 4 },
callTool: async () => null,
});
assert.equal(result.ok ? undefined : result.error.kind, 'limit_exceeded');
});
await t.test('tool output', async () => {
const result = await execute('return await tools.echo({});', {
tools: [{ name: 'echo' }],
executionPolicy: { ...DEFAULT_CODE_MODE_EXECUTION_POLICY, maxToolOutputBytes: 4 },
callTool: async () => '12345',
});
assert.equal(result.ok ? undefined : result.error.kind, 'limit_exceeded');
});
await t.test('cell output', async () => {
const result = await execute("return '12345';", {
executionPolicy: { ...DEFAULT_CODE_MODE_EXECUTION_POLICY, maxResultBytes: 4 },
});
assert.equal(result.ok ? undefined : result.error.kind, 'limit_exceeded');
});
await t.test('tool calls', async () => {
const result = await execute('await tools.echo({}); return await tools.echo({});', {
tools: [{ name: 'echo' }],
executionPolicy: { ...DEFAULT_CODE_MODE_EXECUTION_POLICY, maxBridgeRequests: 1 },
callTool: async () => null,
});
assert.equal(result.ok ? undefined : result.error.kind, 'limit_exceeded');
});
await t.test('tool concurrency', async () => {
let started = 0;
const result = await execute('return await Promise.all([tools.echo({}), tools.echo({})]);', {
tools: [{ name: 'echo' }],
executionPolicy: {
...DEFAULT_CODE_MODE_EXECUTION_POLICY,
maxInFlightBridgeRequests: 1,
timeoutMs: 500,
},
callTool: async (_name, _input, signal) => {
started += 1;
await new Promise<void>((resolve) => {
if (signal.aborted) resolve();
else signal.addEventListener('abort', () => resolve(), { once: true });
});
return null;
},
});
assert.equal(result.ok ? undefined : result.error.kind, 'limit_exceeded');
assert.ok(started <= 1);
});
});
test('enforces the configured VM stack limit', async () => {
const result = await execute('function recurse() { return recurse(); } return recurse();', {
executionPolicy: { ...DEFAULT_CODE_MODE_EXECUTION_POLICY, maxStackSizeBytes: 64 * 1024 },
});
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.error.kind, 'limit_exceeded');
});
test('enforces the configured VM memory limit', async () => {
const result = await execute('return new ArrayBuffer(16 * 1024 * 1024).byteLength;', {
executionPolicy: {
...DEFAULT_CODE_MODE_EXECUTION_POLICY,
memoryLimitBytes: 8 * 1024 * 1024,
timeoutMs: 5_000,
},
});
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.error.kind, 'limit_exceeded');
});
test('preempts a pure compute loop at the sandbox-time limit', async () => {
const result = await execute('while (true) {}', {
executionPolicy: { ...DEFAULT_CODE_MODE_EXECUTION_POLICY, timeoutMs: 20 },
});
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.error.kind, 'limit_exceeded');
});
test('still preempts compute while a host tool is pending', async () => {
let aborted = false;
const result = await execute(
'const pending = tools.wait({}).then(value => value); await tools.started({}); while (true) {}',
{
tools: [{ name: 'wait' }, { name: 'started' }],
signal: AbortSignal.timeout(5_000),
executionPolicy: { ...DEFAULT_CODE_MODE_EXECUTION_POLICY, timeoutMs: 100 },
callTool: async (name, _input, signal) => {
if (name === 'started') return null;
await new Promise<void>((resolve) => {
const onAbort = () => {
aborted = true;
resolve();
};
if (signal.aborted) onAbort();
else signal.addEventListener('abort', onAbort, { once: true });
});
return null;
},
},
);
assert.equal(result.ok, false);
if (!result.ok) assert.equal(result.error.kind, 'limit_exceeded');
assert.equal(aborted, true);
});
test('accumulates compute across awaits instead of resetting the budget', async () => {
// Keep the single slice well below the budget on a loaded runner, with
// enough repeated slices to exercise cumulative accounting on faster hosts.
const work = 'let total = 0; for (let i = 0; i < 10_000; i++) total += Math.sqrt(i);';
const executionPolicy = {
...DEFAULT_CODE_MODE_EXECUTION_POLICY,
timeoutMs: 1_000,
maxBridgeRequests: 8_192,
};
const single = await execute(`${work} return total;`, { executionPolicy });
assert.equal(single.ok, true, JSON.stringify(single));
let calls = 0;
const result = await execute(
`for (let step = 0; step < 5_000; step++) { ${work} await tools.tick({}); }`,
{
executionPolicy,
tools: [{ name: 'tick' }],
signal: AbortSignal.timeout(15_000),
callTool: async () => {
calls++;
return null;
},
},
);
assert.equal(result.ok, false);
if (!result.ok) assert.match(result.error.message, /timed out after 1000ms/);
assert.ok(calls > 1 && calls < 5_000, `completed ${calls} slices`);
});
test('waits for an aborted host operation to settle before rejecting', async () => {
const controller = new AbortController();
const reason = new Error('stop requested');
let toolStarted!: () => void;
let releaseTool!: () => void;
const started = new Promise<void>((resolve) => {
toolStarted = resolve;
});
const released = new Promise<void>((resolve) => {
releaseTool = resolve;
});
const execution = execute('return await tools.wait({});', {
tools: [{ name: 'wait' }],
signal: controller.signal,
callTool: async (_name, _input, signal) => {
toolStarted();
await new Promise<void>((resolve) => signal.addEventListener('abort', () => resolve()));
await released;
return null;
},
});
await started;
controller.abort(reason);
const early = await Promise.race([
execution.then(
() => 'settled' as const,
() => 'settled' as const,
),
new Promise<'pending'>((resolve) => setImmediate(() => resolve('pending'))),
]);
assert.equal(early, 'pending');
releaseTool();
await assert.rejects(execution, (error) => error === reason);
});
test('does not start later tools after a fatal host failure', async () => {
const fatalError = new Error('durable commit failed');
const calls: string[] = [];
const execution = execute(
`
try { await tools.first({}); } catch {}
try { await tools.second({}); } catch {}
return 'ignored';
`,
{
tools: [{ name: 'first' }, { name: 'second' }],
callTool: async (name) => {
calls.push(name);
if (name === 'first') throw fatalError;
return null;
},
isFatalToolError: (error) => error === fatalError,
},
);
await assert.rejects(execution, (error) => error === fatalError);
assert.deepEqual(calls, ['first']);
});
test('aborts and drains concurrent tools while preserving the first fatal failure', async () => {
const firstFatal = new Error('first durable failure');
const secondFatal = new Error('second durable failure');
const calls: string[] = [];
let peerStarted!: () => void;
let peerAborted!: () => void;
let releasePeer!: () => void;
const peerHasStarted = new Promise<void>((resolve) => {
peerStarted = resolve;
});
const peerWasAborted = new Promise<void>((resolve) => {
peerAborted = resolve;
});
const peerCanFinish = new Promise<void>((resolve) => {
releasePeer = resolve;
});
const execution = execute('return await Promise.all([tools.peer({}), tools.fail({})]);', {
tools: [{ name: 'peer' }, { name: 'fail' }],
callTool: async (name, _input, signal) => {
calls.push(name);
if (name === 'fail') {
await peerHasStarted;
throw firstFatal;
}
peerStarted();
await new Promise<void>((resolve) => {
if (signal.aborted) resolve();
else signal.addEventListener('abort', () => resolve(), { once: true });
});
peerAborted();
await peerCanFinish;
throw secondFatal;
},
isFatalToolError: (error) => error === firstFatal || error === secondFatal,
});
await peerWasAborted;
try {
const beforeRelease = await Promise.race([
execution.then(
() => 'settled' as const,
() => 'settled' as const,
),
new Promise<'pending'>((resolve) => setImmediate(() => resolve('pending'))),
]);
assert.equal(beforeRelease, 'pending');
assert.deepEqual(calls, ['peer', 'fail']);
releasePeer();
await assert.rejects(execution, (error) => error === firstFatal);
} finally {
releasePeer();
await Promise.allSettled([execution]);
}
});