From 7d257b75549bfc53fdafeafe86149b3a3a4b6e4e Mon Sep 17 00:00:00 2001 From: Ayush7614 Date: Thu, 13 Aug 2026 18:52:33 +0530 Subject: [PATCH 1/2] Reject non-finite numbers in cloud output schema validation validateCloudOutput accepted Infinity/-Infinity as a type 'number' instance and skipped the minimum/maximum bounds, so a JSON numeral overflow like JSON.parse('1e400') passed validation and was emitted as null in the returned structured output. Require Number.isFinite in the number type checks (JSON Schema branch and shorthand branch) and explicitly reject non-finite instances when minimum/maximum bounds are present, which also covers constraint-only schemas that have no type keyword. Fixes #2779 --- src/chrome/src/agent/cloud-output.js | 17 +++++--- src/firefox/src/agent/cloud-output.js | 17 +++++--- test/run.js | 62 +++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/chrome/src/agent/cloud-output.js b/src/chrome/src/agent/cloud-output.js index be439d32f..e9ff559bd 100644 --- a/src/chrome/src/agent/cloud-output.js +++ b/src/chrome/src/agent/cloud-output.js @@ -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)) @@ -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; }); @@ -284,9 +284,16 @@ 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 numerals cannot overflow to a non-finite value, so a non-finite + // instance is never a valid bound. Reject it explicitly so a constraint-only + // schema (no `type`) cannot 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`); diff --git a/src/firefox/src/agent/cloud-output.js b/src/firefox/src/agent/cloud-output.js index be439d32f..e9ff559bd 100644 --- a/src/firefox/src/agent/cloud-output.js +++ b/src/firefox/src/agent/cloud-output.js @@ -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)) @@ -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; }); @@ -284,9 +284,16 @@ 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 numerals cannot overflow to a non-finite value, so a non-finite + // instance is never a valid bound. Reject it explicitly so a constraint-only + // schema (no `type`) cannot 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`); diff --git a/test/run.js b/test/run.js index ce2950035..79b3d6a0c 100644 --- a/test/run.js +++ b/test/run.js @@ -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, @@ -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', From 87def746b125052d67944b36ab1958b8e720f10c Mon Sep 17 00:00:00 2001 From: Ayush7614 Date: Thu, 13 Aug 2026 19:35:00 +0530 Subject: [PATCH 2/2] Clarify the non-finite overflow comment per review feedback The previous comment said JSON numerals cannot overflow to a non-finite value, which reverses the actual failure mode: a parser can overflow a numeral like 1e400 into Infinity. Reword to describe the parser overflow and note the result is never a valid JSON instance, so the branch is not mistaken for unreachable code. --- src/chrome/src/agent/cloud-output.js | 7 ++++--- src/firefox/src/agent/cloud-output.js | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/chrome/src/agent/cloud-output.js b/src/chrome/src/agent/cloud-output.js index e9ff559bd..7de4791bf 100644 --- a/src/chrome/src/agent/cloud-output.js +++ b/src/chrome/src/agent/cloud-output.js @@ -284,9 +284,10 @@ 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)}`); } - // JSON numerals cannot overflow to a non-finite value, so a non-finite - // instance is never a valid bound. Reject it explicitly so a constraint-only - // schema (no `type`) cannot let Infinity slip past minimum/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'); diff --git a/src/firefox/src/agent/cloud-output.js b/src/firefox/src/agent/cloud-output.js index e9ff559bd..7de4791bf 100644 --- a/src/firefox/src/agent/cloud-output.js +++ b/src/firefox/src/agent/cloud-output.js @@ -284,9 +284,10 @@ 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)}`); } - // JSON numerals cannot overflow to a non-finite value, so a non-finite - // instance is never a valid bound. Reject it explicitly so a constraint-only - // schema (no `type`) cannot let Infinity slip past minimum/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');