fix(cli): reject malformed timeout options

This commit is contained in:
Peter Steinberger
2026-05-24 01:03:58 +01:00
parent 96959ec3d7
commit 459cee5315
5 changed files with 50 additions and 12 deletions

View File

@@ -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";

View File

@@ -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",
);
});
});

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;