diff --git a/src/agents/tools/common.ts b/src/agents/tools/common.ts index 22e271b7ccd6..9bcab8a49cec 100644 --- a/src/agents/tools/common.ts +++ b/src/agents/tools/common.ts @@ -1,8 +1,12 @@ import type { TSchema } from "typebox"; import { readLocalFileSafely } from "../../infra/fs-safe.js"; -import { parseStrictFiniteNumber } from "../../infra/parse-finite-number.js"; import { detectMime } from "../../media/mime.js"; import { readSnakeCaseParamRaw } from "../../param-key.js"; +import { + asPositiveSafeInteger, + asSafeIntegerInRange, + parseStrictFiniteNumber, +} from "../../shared/number-coercion.js"; import { normalizeStringEntries } from "../../shared/string-normalization.js"; import type { ImageSanitizationLimits } from "../image-sanitization.js"; import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "../runtime/index.js"; @@ -194,10 +198,10 @@ export function readNumberParam( return undefined; } if (positiveInteger) { - return Number.isSafeInteger(value) && value > 0 ? value : undefined; + return asPositiveSafeInteger(value); } if (nonNegativeInteger) { - return Number.isSafeInteger(value) && value >= 0 ? value : undefined; + return asSafeIntegerInRange(value, { min: 0 }); } return integer ? Math.trunc(value) : value; } diff --git a/src/infra/parse-finite-number.ts b/src/infra/parse-finite-number.ts index 6084559708fc..29e990245405 100644 --- a/src/infra/parse-finite-number.ts +++ b/src/infra/parse-finite-number.ts @@ -1,54 +1,7 @@ -function normalizeNumericString(value: string): string | undefined { - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - -export function parseFiniteNumber(value: unknown): number | undefined { - if (typeof value === "number" && Number.isFinite(value)) { - return value; - } - if (typeof value === "string") { - return parseStrictFiniteNumber(value); - } - return undefined; -} - -export function parseStrictInteger(value: unknown): number | undefined { - if (typeof value === "number") { - return Number.isSafeInteger(value) ? value : undefined; - } - if (typeof value !== "string") { - return undefined; - } - const normalized = normalizeNumericString(value); - if (!normalized || !/^[+-]?\d+$/.test(normalized)) { - return undefined; - } - const parsed = Number(normalized); - return Number.isSafeInteger(parsed) ? parsed : undefined; -} - -export function parseStrictFiniteNumber(value: unknown): number | undefined { - if (typeof value === "number") { - return Number.isFinite(value) ? value : undefined; - } - if (typeof value !== "string") { - return undefined; - } - const normalized = normalizeNumericString(value); - if (!normalized || !/^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(normalized)) { - return undefined; - } - const parsed = Number(normalized); - return Number.isFinite(parsed) ? parsed : undefined; -} - -export function parseStrictPositiveInteger(value: unknown): number | undefined { - const parsed = parseStrictInteger(value); - return parsed !== undefined && parsed > 0 ? parsed : undefined; -} - -export function parseStrictNonNegativeInteger(value: unknown): number | undefined { - const parsed = parseStrictInteger(value); - return parsed !== undefined && parsed >= 0 ? parsed : undefined; -} +export { + parseFiniteNumber, + parseStrictFiniteNumber, + parseStrictInteger, + parseStrictNonNegativeInteger, + parseStrictPositiveInteger, +} from "../shared/number-coercion.js"; diff --git a/src/plugin-sdk/number-runtime.ts b/src/plugin-sdk/number-runtime.ts index 5f34108baee7..e3bb9d85ced3 100644 --- a/src/plugin-sdk/number-runtime.ts +++ b/src/plugin-sdk/number-runtime.ts @@ -6,4 +6,4 @@ export { parseStrictFiniteNumber, parseStrictNonNegativeInteger, parseStrictPositiveInteger, -} from "../infra/parse-finite-number.js"; +} from "../shared/number-coercion.js"; diff --git a/src/plugin-sdk/string-coerce-runtime.ts b/src/plugin-sdk/string-coerce-runtime.ts index 311f5c06642f..b7299319497f 100644 --- a/src/plugin-sdk/string-coerce-runtime.ts +++ b/src/plugin-sdk/string-coerce-runtime.ts @@ -20,6 +20,10 @@ export { asPositiveSafeInteger, asSafeIntegerInRange, parseFiniteNumber, + parseStrictFiniteNumber, + parseStrictInteger, + parseStrictNonNegativeInteger, + parseStrictPositiveInteger, } from "../shared/number-coercion.js"; export { asBoolean, parseBooleanValue } from "../utils/boolean.js"; export { diff --git a/src/shared/number-coercion.test.ts b/src/shared/number-coercion.test.ts index 297fc80007d7..1e7a5d185092 100644 --- a/src/shared/number-coercion.test.ts +++ b/src/shared/number-coercion.test.ts @@ -4,6 +4,10 @@ import { asFiniteNumberInRange, asSafeIntegerInRange, parseFiniteNumber, + parseStrictFiniteNumber, + parseStrictInteger, + parseStrictNonNegativeInteger, + parseStrictPositiveInteger, } from "./number-coercion.js"; describe("number-coercion", () => { @@ -37,4 +41,28 @@ describe("number-coercion", () => { expect(parseFiniteNumber("")).toBeUndefined(); expect(parseFiniteNumber("nope")).toBeUndefined(); }); + + test("parseStrictInteger accepts only safe integer tokens", () => { + expect(parseStrictInteger("42")).toBe(42); + expect(parseStrictInteger(" -7 ")).toBe(-7); + expect(parseStrictInteger("+9")).toBe(9); + expect(parseStrictInteger("1.5")).toBeUndefined(); + expect(parseStrictInteger("1e3")).toBeUndefined(); + expect(parseStrictInteger(Number.MAX_SAFE_INTEGER + 1)).toBeUndefined(); + }); + + test("parseStrictFiniteNumber rejects partial numeric strings", () => { + expect(parseStrictFiniteNumber("42")).toBe(42); + expect(parseStrictFiniteNumber(".5")).toBe(0.5); + expect(parseStrictFiniteNumber("1e3")).toBe(1000); + expect(parseStrictFiniteNumber("3.14ms")).toBeUndefined(); + expect(parseStrictFiniteNumber("0x10")).toBeUndefined(); + }); + + test("strict integer range helpers enforce sign", () => { + expect(parseStrictPositiveInteger("9")).toBe(9); + expect(parseStrictPositiveInteger("0")).toBeUndefined(); + expect(parseStrictNonNegativeInteger("0")).toBe(0); + expect(parseStrictNonNegativeInteger("-1")).toBeUndefined(); + }); }); diff --git a/src/shared/number-coercion.ts b/src/shared/number-coercion.ts index 1a2025fc9e4d..c886a053dab9 100644 --- a/src/shared/number-coercion.ts +++ b/src/shared/number-coercion.ts @@ -47,21 +47,58 @@ export function asSafeIntegerInRange( return value; } +function normalizeNumericString(value: string): string | undefined { + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; +} + export function parseFiniteNumber(value: unknown): number | undefined { + if (typeof value === "number") { + return Number.isFinite(value) ? value : undefined; + } + return parseStrictFiniteNumber(value); +} + +export function parseStrictInteger(value: unknown): number | undefined { + if (typeof value === "number") { + return Number.isSafeInteger(value) ? value : undefined; + } + if (typeof value !== "string") { + return undefined; + } + const normalized = normalizeNumericString(value); + if (!normalized || !/^[+-]?\d+$/.test(normalized)) { + return undefined; + } + const parsed = Number(normalized); + return Number.isSafeInteger(parsed) ? parsed : undefined; +} + +export function parseStrictFiniteNumber(value: unknown): number | undefined { if (typeof value === "number") { return Number.isFinite(value) ? value : undefined; } if (typeof value !== "string") { return undefined; } - const trimmed = value.trim(); - if (!trimmed || !/^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(trimmed)) { + const normalized = normalizeNumericString(value); + if (!normalized || !/^[+-]?(?:(?:\d+\.?\d*)|(?:\.\d+))(?:e[+-]?\d+)?$/i.test(normalized)) { return undefined; } - const parsed = Number(trimmed); + const parsed = Number(normalized); return Number.isFinite(parsed) ? parsed : undefined; } export function asPositiveSafeInteger(value: unknown): number | undefined { return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined; } + +export function parseStrictPositiveInteger(value: unknown): number | undefined { + const parsed = parseStrictInteger(value); + return parsed !== undefined && parsed > 0 ? parsed : undefined; +} + +export function parseStrictNonNegativeInteger(value: unknown): number | undefined { + const parsed = parseStrictInteger(value); + return parsed !== undefined && parsed >= 0 ? parsed : undefined; +}