diff --git a/src/cli/cron-cli/register.cron-edit.ts b/src/cli/cron-cli/register.cron-edit.ts index 0afda904570..b7afc0d9bc9 100644 --- a/src/cli/cron-cli/register.cron-edit.ts +++ b/src/cli/cron-cli/register.cron-edit.ts @@ -186,7 +186,11 @@ export function registerCronEditCommand(cron: Command) { patch.sessionTarget = sessionTarget; } if (typeof opts.wake === "string") { - patch.wakeMode = opts.wake; + const wakeMode = opts.wake.trim(); + if (wakeMode !== "now" && wakeMode !== "next-heartbeat") { + throw new Error("--wake must be now or next-heartbeat"); + } + patch.wakeMode = wakeMode; } if (opts.agent && opts.clearAgent) { throw new Error("Use --agent or --clear-agent, not both"); @@ -229,10 +233,20 @@ export function registerCronEditCommand(cron: Command) { const model = normalizeOptionalString(opts.model); const thinking = normalizeOptionalString(opts.thinking); const toolsAllow = parseCronToolsAllow(opts.tools); - const timeoutSeconds = opts.timeoutSeconds - ? Number.parseInt(String(opts.timeoutSeconds), 10) - : undefined; - const hasTimeoutSeconds = Boolean(timeoutSeconds && Number.isFinite(timeoutSeconds)); + const rawTimeoutSeconds = + opts.timeoutSeconds === undefined ? undefined : String(opts.timeoutSeconds).trim(); + if (rawTimeoutSeconds !== undefined && !/^\d+$/u.test(rawTimeoutSeconds)) { + throw new Error("Invalid --timeout-seconds (must be a positive integer)."); + } + const timeoutSeconds = + rawTimeoutSeconds === undefined ? undefined : Number(rawTimeoutSeconds); + const hasTimeoutSeconds = + typeof timeoutSeconds === "number" && + Number.isSafeInteger(timeoutSeconds) && + timeoutSeconds > 0; + if (rawTimeoutSeconds !== undefined && !hasTimeoutSeconds) { + throw new Error("Invalid --timeout-seconds (must be a positive integer)."); + } const hasDeliveryModeFlag = opts.announce || typeof opts.deliver === "boolean"; const threadId = parseCronThreadIdOption(opts.threadId); const hasDeliveryThreadId = typeof threadId === "number"; diff --git a/src/cli/parse-timeout.test.ts b/src/cli/parse-timeout.test.ts index d478827de15..f29d51e2a84 100644 --- a/src/cli/parse-timeout.test.ts +++ b/src/cli/parse-timeout.test.ts @@ -10,6 +10,9 @@ describe("parseTimeoutMs", () => { expect(parseTimeoutMs(undefined)).toBeUndefined(); expect(parseTimeoutMs("")).toBeUndefined(); expect(parseTimeoutMs("nope")).toBeUndefined(); + expect(parseTimeoutMs("10abc")).toBeUndefined(); + expect(parseTimeoutMs("1.5")).toBeUndefined(); + expect(parseTimeoutMs("0")).toBeUndefined(); }); }); @@ -40,4 +43,12 @@ describe("parseTimeoutMsWithFallback", () => { expect(() => parseTimeoutMsWithFallback("0", 3000)).toThrow('Received: "0"'); expect(() => parseTimeoutMsWithFallback("-1", 3000)).toThrow('Received: "-1"'); }); + + it("throws on malformed or unsafe parsed values", () => { + expect(() => parseTimeoutMsWithFallback("10abc", 3000)).toThrow('Received: "10abc"'); + expect(() => parseTimeoutMsWithFallback("1.5", 3000)).toThrow('Received: "1.5"'); + expect(() => parseTimeoutMsWithFallback(String(Number.MAX_SAFE_INTEGER + 1), 3000)).toThrow( + "Received", + ); + }); }); diff --git a/src/cli/parse-timeout.ts b/src/cli/parse-timeout.ts index 712dc8c757a..d53a4d43ec6 100644 --- a/src/cli/parse-timeout.ts +++ b/src/cli/parse-timeout.ts @@ -12,9 +12,12 @@ export function parseTimeoutMs(raw: unknown): number | undefined { if (!trimmed) { return undefined; } - value = Number.parseInt(trimmed, 10); + if (!/^\d+$/u.test(trimmed)) { + return undefined; + } + value = Number(trimmed); } - return Number.isFinite(value) ? value : undefined; + return Number.isSafeInteger(value) && value > 0 ? value : undefined; } function invalidTimeout(value?: string): Error { @@ -53,8 +56,11 @@ export function parseTimeoutMsWithFallback( return fallbackMs; } - const parsed = Number.parseInt(value, 10); - if (!Number.isFinite(parsed) || parsed <= 0) { + if (!/^\d+$/u.test(value)) { + throw invalidTimeout(value); + } + const parsed = Number(value); + if (!Number.isSafeInteger(parsed) || parsed <= 0) { throw invalidTimeout(value); } return parsed; diff --git a/src/cli/update-cli/shared.command-runner.test.ts b/src/cli/update-cli/shared.command-runner.test.ts index 7e1f3ab7f37..2d097c3e010 100644 --- a/src/cli/update-cli/shared.command-runner.test.ts +++ b/src/cli/update-cli/shared.command-runner.test.ts @@ -60,10 +60,11 @@ describe("createGlobalCommandRunner", () => { expect(parseTimeoutMsOrExit("0")).toBeNull(); expect(parseTimeoutMsOrExit("-1")).toBeNull(); expect(parseTimeoutMsOrExit(" ")).toBeNull(); + expect(parseTimeoutMsOrExit(String(Number.MAX_SAFE_INTEGER))).toBeNull(); - expect(error).toHaveBeenCalledTimes(5); + expect(error).toHaveBeenCalledTimes(6); expect(error).toHaveBeenCalledWith("--timeout must be a positive integer (seconds)"); - expect(exit).toHaveBeenCalledTimes(5); + expect(exit).toHaveBeenCalledTimes(6); expect(exit).toHaveBeenCalledWith(1); } finally { error.mockRestore(); diff --git a/src/cli/update-cli/shared.ts b/src/cli/update-cli/shared.ts index 0b1d153da1f..2ac30f6b7c9 100644 --- a/src/cli/update-cli/shared.ts +++ b/src/cli/update-cli/shared.ts @@ -52,6 +52,7 @@ export type UpdateWizardOptions = { }; const INVALID_TIMEOUT_ERROR = "--timeout must be a positive integer (seconds)"; +const MAX_SAFE_TIMEOUT_SECONDS = Math.floor(Number.MAX_SAFE_INTEGER / 1000); export function parseTimeoutMsOrExit(timeout?: string): number | undefined | null { if (timeout === undefined) { @@ -59,7 +60,12 @@ export function parseTimeoutMsOrExit(timeout?: string): number | undefined | nul } const trimmed = timeout.trim(); const seconds = Number(trimmed); - if (!/^\d+$/u.test(trimmed) || !Number.isSafeInteger(seconds) || seconds <= 0) { + if ( + !/^\d+$/u.test(trimmed) || + !Number.isSafeInteger(seconds) || + seconds <= 0 || + seconds > MAX_SAFE_TIMEOUT_SECONDS + ) { defaultRuntime.error(INVALID_TIMEOUT_ERROR); defaultRuntime.exit(1); return null;