Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions packages/core/src/agents/runtime/workflow-sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,210 @@ describe('extractAndStripMeta', () => {
expect(() => extractAndStripMeta(src)).toThrow(/unbalanced/i);
});

// The meta literal is model-authored source, and every caller reaches it on
// a path where a wedged thread is unrecoverable: the run path (before the
// sandbox's own 30s body timeout is armed) and, in follow-up work, the tool
// confirmation dialog and the saved-workflow palette. A field value that
// never returns must surface as an ordinary malformed-meta error.
//
// Each case asserts a generous wall-clock bound rather than a precise one,
// so the assertion stays stable on a loaded CI runner. Without the bound
// these hang the worker until vitest's own timeout kills it.
describe('bounded evaluation', () => {
const BOUND_MS = 5_000;

function timed(fn: () => unknown): number {
const startedAt = Date.now();
try {
fn();
} catch {
/* the throw is asserted separately */
}
return Date.now() - startedAt;
}

it('bounds a field value that loops on evaluation', () => {
const src = `export const meta = { name: (function () { while (true) {} })(), description: 'd' }\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/failed to evaluate meta object literal/,
);
expect(timed(() => extractAndStripMeta(src))).toBeLessThan(BOUND_MS);
});

// The case a timeout on the literal's own evaluation does NOT catch: a
// getter defers its work to property-read time, so the literal itself
// evaluates instantly and only spins when the value is walked. Walking on
// the host would run it on the host thread, unbounded.
it('bounds a getter that loops when the value is walked', () => {
const src = `export const meta = { name: 'x', description: 'd', get phases() { while (true) {} } }\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/failed to evaluate meta object literal/,
);
expect(timed(() => extractAndStripMeta(src))).toBeLessThan(BOUND_MS);
});

it('bounds a getter nested inside phases', () => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复:新增无限循环 then getter 的超时回归,以及 phases 自定义 Symbol.iterator 不被调用的回归。验证:workflow-sandbox.test.ts 167/167 通过。

const src = `export const meta = { name: 'x', description: 'd', phases: [{ get title() { while (true) {} } }] }\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/failed to evaluate meta object literal/,
);
expect(timed(() => extractAndStripMeta(src))).toBeLessThan(BOUND_MS);
});

it('refuses a meta literal that exceeds the serialized size cap', () => {
const src = `export const meta = { name: 'x'.repeat(200000), description: 'd' }\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/failed to evaluate meta object literal/,
);
});

it('enforces the cap after JSON escaping', () => {
const src = `export const meta = { name: '\\0'.repeat(20000), description: 'd' }\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/failed to evaluate meta object literal/,
);
});

it('enforces the cap for string-free containers', () => {
const src = `export const meta = { name: 'x', description: 'd', phases: Array.from({ length: 40000 }, () => ({})) }\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/failed to evaluate meta object literal/,
);
});

it('bounds a looping then getter', () => {
const src = `export const meta = { name: 'x', description: 'd', extra: { get then() { while (true) {} } } }\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/failed to evaluate meta object literal/,
);
expect(timed(() => extractAndStripMeta(src))).toBeLessThan(BOUND_MS);
});

it('does not invoke a phases iterator', () => {
const src = `export const meta = {
name: 'x',
description: 'd',
phases: Object.assign([{ title: 'one' }], {
[Symbol.iterator]: function () { while (true) {} },
}),
}\nreturn 1`;
expect(extractAndStripMeta(src).meta?.phases).toEqual([{ title: 'one' }]);
});

it('leaves a well-formed meta literal unaffected', () => {
const src = `export const meta = { name: 'w', description: 'd', phases: [{ title: 'One' }] }\nreturn 1`;
const { meta } = extractAndStripMeta(src);
expect(meta).toEqual({
name: 'w',
description: 'd',
phases: [{ title: 'One' }],
});
});
});

// The literal is evaluated as its own program, so it never shares a lexical
// scope with the serializer that walks it. Interpolating it into the
// serializer's scope would let it read the helpers and overwrite the flag
// that decides whether a thenable was found — i.e. disarm the check that
// keeps a stray rejected Promise from killing the host process.
it('meta source cannot observe or mutate the serializer scope', () => {
const src = `export const meta = { name: String(typeof copy) + ':' + String(typeof hasThenable), description: 'd' }\nreturn 1`;
const { meta } = extractAndStripMeta(src);
expect(meta?.name).toBe('undefined:undefined');
});

it('a thenable stays rejected even when the literal predefines the flag name', () => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复:测试 fixture 现显式设置 hasThenable: false,覆盖预定义标志仍不能隐藏 Promise 的场景。验证:workflow-sandbox.test.ts 167/167 通过。

const src = `export const meta = { name: 'x', description: 'd', hasThenable: false, phases: Promise.resolve(1) }\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/meta values must not be Promises/,
);
});

it('isolates serializer intrinsics from the meta literal', () => {
const src = `export const meta = {
name: (JSON.stringify = () => ({ toString() { return '@'; } }), 'x'),
description: 'd',
}\nreturn 1`;
expect(extractAndStripMeta(src).meta).toEqual({
name: 'x',
description: 'd',
});
});

it('rejects Promises after the literal mutates serializer helpers', () => {
const src = `export const meta = {
name: (
Object.keys = () => ['name', 'description'],
Promise.prototype.then = () => undefined,
'x'
),
description: 'd',
extra: Promise.resolve(1),
}\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/meta values must not be Promises/,
);
});

it('does not expose host helpers to serializer getters', () => {
const src = `export const meta = {
name: 'x',
description: 'd',
extra: Object.defineProperty({}, 'value', { enumerable: true, get: function () {
const serializerGlobal = arguments.callee.caller.constructor('return globalThis')();
if (serializerGlobal.__qwenWorkflowMetaIsPromise) throw new Error('host helper exposed');

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复。验证证据:workflow-sandbox 183/183 通过;隔离探测现检查 serializer realm 的 copy helper。

return 'safe';
} }),
}\nreturn 1`;
expect(extractAndStripMeta(src).meta).toEqual({
name: 'x',
description: 'd',
});
});

it('prefers a Promise error after the size budget is exceeded', () => {
const src = `export const meta = {
name: 'x'.repeat(200000),
description: 'd',
extra: Promise.resolve(1),
}\nreturn 1`;
expect(() => extractAndStripMeta(src)).toThrow(
/meta values must not be Promises/,
);
});

it('copies shared phase objects at each array position', () => {
const src = `export const meta = {
name: 'x',
description: 'd',
phases: (function () {
const phase = { title: 'one' };
return [phase, phase];
})(),
}\nreturn 1`;
expect(extractAndStripMeta(src).meta?.phases).toEqual([
{ title: 'one' },
{ title: 'one' },
]);
});

it.each([
[
`export const meta = { name: 'x', description: 'd', whenToUse: () => {} }\nreturn 1`,
/meta.whenToUse must be a string/,
],
[
`export const meta = { name: 'x', description: 'd', phases: [{ title: 'one', detail: Symbol('x') }] }\nreturn 1`,
/meta.phases\[\].detail must be a string/,
],
[
`export const meta = { name: 'x', description: 'd', phases: [{ title: 'one', model: 1n }] }\nreturn 1`,
/meta.phases\[\].model must be a string/,
],
])('preserves invalid optional field types for validation', (src, error) => {
expect(() => extractAndStripMeta(src)).toThrow(error);
});

// P4a adversarial review (HIGH × 3 lenses): the docstring at
// workflow-sandbox.ts:283-294 promises the returned meta is HOST-realm —
// a per-field copy that defends against T1/T8/T14-style vm-realm escape
Expand Down
Loading
Loading