Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 13 additions & 5 deletions src/chrome/src/agent/cloud-output.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export function validateCloudOutput(value, schema) {
}
const valid = shorthand === 'any'
|| (shorthand === 'string' && typeof item === 'string')
|| (shorthand === 'number' && typeof item === 'number' && !Number.isNaN(item))
|| (shorthand === 'number' && Number.isFinite(item))
|| (shorthand === 'integer' && Number.isInteger(item))
|| (shorthand === 'boolean' && typeof item === 'boolean')
|| (shorthand === 'object' && isObject(item))
Expand Down Expand Up @@ -272,7 +272,7 @@ export function validateCloudOutput(value, schema) {
if (type === 'array') return Array.isArray(item);
if (type === 'object') return isObject(item);
if (type === 'integer') return Number.isInteger(item);
if (type === 'number') return typeof item === 'number' && !Number.isNaN(item);
if (type === 'number') return Number.isFinite(item);
if (type === 'null') return item === null;
return typeof item === type;
});
Expand All @@ -284,9 +284,17 @@ export function validateCloudOutput(value, schema) {
if (Number.isInteger(spec.maxLength) && length > spec.maxLength) push(path, `expected at most ${spec.maxLength} characters`);
if (typeof spec.pattern === 'string' && !new RegExp(spec.pattern).test(item)) push(path, `expected to match ${JSON.stringify(spec.pattern)}`);
}
if (typeof item === 'number' && Number.isFinite(item)) {
if (typeof spec.minimum === 'number' && item < spec.minimum) push(path, `expected at least ${spec.minimum}`);
if (typeof spec.maximum === 'number' && item > spec.maximum) push(path, `expected at most ${spec.maximum}`);
// JSON has no Infinity literal, but a parser can still overflow a numeral
// like 1e400 into a non-finite value. Such a value is never a valid JSON
// instance, so reject it explicitly — a constraint-only schema (no `type`)
// would otherwise let Infinity slip past minimum/maximum.
if (typeof item === 'number' && (typeof spec.minimum === 'number' || typeof spec.maximum === 'number')) {
if (!Number.isFinite(item)) {
push(path, 'expected a finite number');
} else {
if (typeof spec.minimum === 'number' && item < spec.minimum) push(path, `expected at least ${spec.minimum}`);
if (typeof spec.maximum === 'number' && item > spec.maximum) push(path, `expected at most ${spec.maximum}`);
}
}
if (Array.isArray(item)) {
if (Number.isInteger(spec.minItems) && item.length < spec.minItems) push(path, `expected at least ${spec.minItems} items`);
Expand Down
18 changes: 13 additions & 5 deletions src/firefox/src/agent/cloud-output.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export function validateCloudOutput(value, schema) {
}
const valid = shorthand === 'any'
|| (shorthand === 'string' && typeof item === 'string')
|| (shorthand === 'number' && typeof item === 'number' && !Number.isNaN(item))
|| (shorthand === 'number' && Number.isFinite(item))
|| (shorthand === 'integer' && Number.isInteger(item))
|| (shorthand === 'boolean' && typeof item === 'boolean')
|| (shorthand === 'object' && isObject(item))
Expand Down Expand Up @@ -272,7 +272,7 @@ export function validateCloudOutput(value, schema) {
if (type === 'array') return Array.isArray(item);
if (type === 'object') return isObject(item);
if (type === 'integer') return Number.isInteger(item);
if (type === 'number') return typeof item === 'number' && !Number.isNaN(item);
if (type === 'number') return Number.isFinite(item);
if (type === 'null') return item === null;
return typeof item === type;
});
Expand All @@ -284,9 +284,17 @@ export function validateCloudOutput(value, schema) {
if (Number.isInteger(spec.maxLength) && length > spec.maxLength) push(path, `expected at most ${spec.maxLength} characters`);
if (typeof spec.pattern === 'string' && !new RegExp(spec.pattern).test(item)) push(path, `expected to match ${JSON.stringify(spec.pattern)}`);
}
if (typeof item === 'number' && Number.isFinite(item)) {
if (typeof spec.minimum === 'number' && item < spec.minimum) push(path, `expected at least ${spec.minimum}`);
if (typeof spec.maximum === 'number' && item > spec.maximum) push(path, `expected at most ${spec.maximum}`);
// JSON has no Infinity literal, but a parser can still overflow a numeral
// like 1e400 into a non-finite value. Such a value is never a valid JSON
// instance, so reject it explicitly — a constraint-only schema (no `type`)
// would otherwise let Infinity slip past minimum/maximum.
if (typeof item === 'number' && (typeof spec.minimum === 'number' || typeof spec.maximum === 'number')) {
if (!Number.isFinite(item)) {
push(path, 'expected a finite number');
} else {
if (typeof spec.minimum === 'number' && item < spec.minimum) push(path, `expected at least ${spec.minimum}`);
if (typeof spec.maximum === 'number' && item > spec.maximum) push(path, `expected at most ${spec.maximum}`);
}
}
if (Array.isArray(item)) {
if (Number.isInteger(spec.minItems) && item.length < spec.minItems) push(path, `expected at least ${spec.minItems} items`);
Expand Down
62 changes: 62 additions & 0 deletions test/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -14841,6 +14841,19 @@ test('done_json accepts free-form and shorthand output schemas it advertises', a
[{ items: false }, [1], false],
[{ anyOf: [false, { minLength: 3 }] }, 'abc', true],
[{ anyOf: [false, { minLength: 3 }] }, 'a', false],
// A JSON numeral can overflow to Infinity (JSON.parse('1e400')). A
// non-finite number is never a valid JSON instance, so it must fail the
// type check and the minimum/maximum bounds instead of passing both.
[{ type: 'number', maximum: 100 }, Infinity, false],
[{ type: 'number', maximum: 100 }, -Infinity, false],
[{ type: 'number', minimum: 0 }, Infinity, false],
[{ type: 'number', minimum: 0 }, -Infinity, false],
[{ type: 'number' }, Infinity, false],
[{ type: 'number', maximum: 100 }, JSON.parse('1e400'), false],
// Constraint-only schemas have no type check, so the bounds themselves
// have to reject a non-finite instance.
[{ maximum: 100 }, Infinity, false],
[{ minimum: 0 }, -Infinity, false],
]) {
assert.equal(
cloudModule.validateCloudOutput(value, spec).ok,
Expand Down Expand Up @@ -14923,6 +14936,55 @@ test('done_json accepts free-form and shorthand output schemas it advertises', a
}
});

test('done_json and validateCloudOutput reject non-finite numbers instead of completing with null', async () => {
const cloudModules = [];
for (const label of ['chrome', 'firefox']) {
cloudModules.push(await import(
pathToFileURL(path.join(ROOT, `src/${label}/src/agent/cloud-output.js`)).href
));
}
for (const [label, handle, cloudModule] of [
['chrome', handleDoneJsonCh, cloudModules[0]],
['firefox', handleDoneJsonFx, cloudModules[1]],
]) {
const schema = {
type: 'object',
properties: { count: { type: 'number', maximum: 100 } },
required: ['count'],
additionalProperties: false,
};
const overflow = JSON.parse('1e400');
const underflow = JSON.parse('-1e400');

// A bounded number must not accept a numeral-overflow instance.
const first = handle({ outputSchema: schema, schemaRepairUsed: false }, {
result: { count: overflow },
summary: 's',
});
assert.equal(first.done, undefined, `[${label}] Infinity passed number maximum bounds`);
assert.equal(first.schemaValidationError, true, `[${label}] Infinity was not a schema failure`);
assert.equal(first.cloudResult, undefined, `[${label}] Infinity completed a run with a corrupt result`);

// The one repair attempt must not complete the run either.
const terminal = handle({ outputSchema: schema, schemaRepairUsed: true }, {
result: { count: underflow },
summary: 's',
});
assert.equal(terminal.done, true, `[${label}] -Infinity did not terminate the run after repair`);
assert.equal(terminal.cloudFailed, true, `[${label}] -Infinity completed a failed cloud run`);

// The shorthand number token has the same finiteness contract.
assert.equal(cloudModule.validateCloudOutput(overflow, 'number').ok, false,
`[${label}] shorthand number accepted Infinity`);
assert.equal(cloudModule.validateCloudOutput(underflow, 'number').ok, false,
`[${label}] shorthand number accepted -Infinity`);

// A finite value that respects the bounds still validates.
assert.equal(cloudModule.validateCloudOutput(100, { type: 'number', maximum: 100 }).ok, true,
`[${label}] a finite bound-respecting number was rejected`);
}
});

test('structured Ask cloud runs recover prose finals through done_json', async () => {
const schema = {
type: 'object',
Expand Down
Loading