diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f6ffc3cc3f..6690a5dedd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/tasks: reject partially numeric `openclaw tasks audit --limit` values so audit limits must be real positive integers instead of accepting strings like `5abc`. (#84901) Thanks @jbetala7. - Twitch: keep stale message-handler cleanup callbacks from removing newer handler registrations for the same account, preserving inbound message delivery after reconnects. Fixes #83888. (#85425) Thanks @alkor2000. - Memory/LanceDB: expose public memory artifacts through the active memory provider bridge so memory-wiki imports durable memory files, daily notes, dream reports, and event logs without depending on memory-core internals. Fixes #83604. (#85060) Thanks @brokemac79. - Docker setup: stop printing the Gateway bearer token in setup logs and printed follow-up commands. diff --git a/src/cli/program/helpers.test.ts b/src/cli/program/helpers.test.ts index 596a8672941..0d7b7fa3890 100644 --- a/src/cli/program/helpers.test.ts +++ b/src/cli/program/helpers.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { collectOption, parsePositiveIntOrUndefined, + parseStrictPositiveIntOrUndefined, resolveActionArgs, resolveCommandOptionArgs, } from "./helpers.js"; @@ -31,6 +32,27 @@ describe("program helpers", () => { expect(parsePositiveIntOrUndefined(value)).toBe(expected); }); + it.each([ + { value: undefined, expected: undefined }, + { value: null, expected: undefined }, + { value: "", expected: undefined }, + { value: 5, expected: 5 }, + { value: 5.9, expected: undefined }, + { value: 0, expected: undefined }, + { value: -1, expected: undefined }, + { value: Number.NaN, expected: undefined }, + { value: "10", expected: 10 }, + { value: " 10 ", expected: 10 }, + { value: "+10", expected: 10 }, + { value: "10ms", expected: undefined }, + { value: "1.5", expected: undefined }, + { value: "0", expected: undefined }, + { value: "nope", expected: undefined }, + { value: true, expected: undefined }, + ])("parseStrictPositiveIntOrUndefined(%j)", ({ value, expected }) => { + expect(parseStrictPositiveIntOrUndefined(value)).toBe(expected); + }); + it("resolveActionArgs returns args when command has arg array", () => { const command = new Command(); (command as Command & { args?: string[] }).args = ["one", "two"]; diff --git a/src/cli/program/helpers.ts b/src/cli/program/helpers.ts index 77c768c34dc..453d04f0a22 100644 --- a/src/cli/program/helpers.ts +++ b/src/cli/program/helpers.ts @@ -1,4 +1,5 @@ import type { Command } from "commander"; +import { parseStrictPositiveInteger } from "../../infra/parse-finite-number.js"; export function collectOption(value: string, previous: string[] = []): string[] { return [...previous, value]; @@ -25,6 +26,10 @@ export function parsePositiveIntOrUndefined(value: unknown): number | undefined return undefined; } +export function parseStrictPositiveIntOrUndefined(value: unknown): number | undefined { + return parseStrictPositiveInteger(value); +} + export function resolveActionArgs(actionCommand?: Command): string[] { if (!actionCommand) { return []; diff --git a/src/cli/program/register.status-health-sessions.test.ts b/src/cli/program/register.status-health-sessions.test.ts index 3decb75d9ed..737c6540e88 100644 --- a/src/cli/program/register.status-health-sessions.test.ts +++ b/src/cli/program/register.status-health-sessions.test.ts @@ -433,6 +433,16 @@ describe("registerStatusHealthSessionsCommands", () => { }); }); + it("rejects partially numeric tasks audit limits", async () => { + await runCli(["tasks", "--json", "audit", "--limit", "5abc"]); + + expect(runtime.error).toHaveBeenCalledWith( + "--limit must be a positive integer, for example --limit 25.", + ); + expect(runtime.exit).toHaveBeenCalledWith(1); + expect(tasksAuditCommand).not.toHaveBeenCalled(); + }); + it("routes tasks flow commands through the TaskFlow handlers", async () => { await runCli(["tasks", "flow", "list", "--json", "--status", "blocked"]); expectCommandOptions(flowsListCommand, {}); diff --git a/src/cli/program/register.status-health-sessions.ts b/src/cli/program/register.status-health-sessions.ts index c686a6a83fb..841fe47cabf 100644 --- a/src/cli/program/register.status-health-sessions.ts +++ b/src/cli/program/register.status-health-sessions.ts @@ -5,7 +5,7 @@ import { formatDocsLink } from "../../terminal/links.js"; import { theme } from "../../terminal/theme.js"; import { runCommandWithRuntime } from "../cli-utils.js"; import { formatHelpExamples } from "../help-format.js"; -import { parsePositiveIntOrUndefined } from "./helpers.js"; +import { parsePositiveIntOrUndefined, parseStrictPositiveIntOrUndefined } from "./helpers.js"; function resolveVerbose(opts: { verbose?: boolean; debug?: boolean }): boolean { return Boolean(opts.verbose || opts.debug); @@ -73,6 +73,16 @@ function parseTimeoutMs(timeout: unknown): number | null | undefined { return parsed; } +function parseTasksAuditLimit(limit: unknown): number | null | undefined { + const parsed = parseStrictPositiveIntOrUndefined(limit); + if (limit !== undefined && parsed === undefined) { + defaultRuntime.error("--limit must be a positive integer, for example --limit 25."); + defaultRuntime.exit(1); + return null; + } + return parsed; +} + async function runWithVerboseAndTimeout( opts: { verbose?: boolean; debug?: boolean; timeout?: unknown }, action: (params: { verbose: boolean; timeoutMs: number | undefined }) => Promise, @@ -444,6 +454,10 @@ export function registerStatusHealthSessionsCommands(program: Command) { .option("--limit ", "Limit displayed findings") .action(async (opts, command) => { const parentOpts = command.parent?.opts() as { json?: boolean } | undefined; + const limit = parseTasksAuditLimit(opts.limit); + if (limit === null) { + return; + } await runCommandWithRuntime(defaultRuntime, async () => { const { tasksAuditCommand } = await import("../../commands/tasks.js"); await tasksAuditCommand( @@ -464,7 +478,7 @@ export function registerStatusHealthSessionsCommands(program: Command) { | "missing_linked_tasks" | "blocked_task_missing" | undefined, - limit: parsePositiveIntOrUndefined(opts.limit), + limit, }, defaultRuntime, ); diff --git a/src/cli/program/route-args.ts b/src/cli/program/route-args.ts index aedd2903757..acc5be690be 100644 --- a/src/cli/program/route-args.ts +++ b/src/cli/program/route-args.ts @@ -6,6 +6,7 @@ import { getVerboseFlag, hasFlag, } from "../argv.js"; +import { parseStrictPositiveIntOrUndefined } from "./helpers.js"; type OptionalFlagParse = { ok: boolean; @@ -352,8 +353,12 @@ export function parseTasksAuditRouteArgs(argv: string[]) { if (!code.ok) { return null; } - const limit = getPositiveIntFlagValue(argv, "--limit"); - if (limit === null) { + const rawLimit = getFlagValue(argv, "--limit"); + if (rawLimit === null) { + return null; + } + const limit = rawLimit === undefined ? undefined : parseStrictPositiveIntOrUndefined(rawLimit); + if (rawLimit !== undefined && limit === undefined) { return null; } return { diff --git a/src/cli/program/routes.test.ts b/src/cli/program/routes.test.ts index 74f9a9a7e69..daef522a02a 100644 --- a/src/cli/program/routes.test.ts +++ b/src/cli/program/routes.test.ts @@ -592,6 +592,10 @@ describe("program routes", () => { ["tasks", "audit"], ["node", "openclaw", "tasks", "audit", "--json", "--limit"], ); + await expectRunFalse( + ["tasks", "audit"], + ["node", "openclaw", "tasks", "audit", "--json", "--limit", "5abc"], + ); await expectRunFalse( ["tasks", "audit"], ["node", "openclaw", "tasks", "audit", "--json", "--unknown"],