mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:00:42 +00:00
fix: parse PowerShell cron tools allow-list (#68858) (thanks @chen-zhang-cs-code)
* fix(cron): parse PowerShell tools allow list * fix(cron): clarify tools allow-list help * fix: parse PowerShell cron tools allow-list (#68858) (thanks @chen-zhang-cs-code) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Control UI/cron: keep the runtime-only `last` delivery sentinel from being materialized into persisted cron delivery and failure-alert channel configs when jobs are created or edited. (#68829) Thanks @tianhaocui.
|
||||
- OpenAI/Responses: strip orphaned reasoning blocks before outbound Responses API calls so compacted or restored histories no longer fail on standalone reasoning items. (#55787) Thanks @suboss87.
|
||||
- Cron/CLI: parse PowerShell-style `--tools` allow-lists the same way as comma-separated input, so `cron add` and `cron edit` no longer persist `exec read write` as one combined tool entry on Windows. (#68858) Thanks @chen-zhang-cs-code.
|
||||
|
||||
## 2026.4.19-beta.2
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ type CronUpdatePatch = {
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
lightContext?: boolean;
|
||||
toolsAllow?: string[];
|
||||
};
|
||||
delivery?: {
|
||||
mode?: string;
|
||||
@@ -73,7 +74,12 @@ type CronUpdatePatch = {
|
||||
|
||||
type CronAddParams = {
|
||||
schedule?: { kind?: string; staggerMs?: number };
|
||||
payload?: { model?: string; thinking?: string; lightContext?: boolean };
|
||||
payload?: {
|
||||
model?: string;
|
||||
thinking?: string;
|
||||
lightContext?: boolean;
|
||||
toolsAllow?: string[];
|
||||
};
|
||||
delivery?: { mode?: string; accountId?: string };
|
||||
deleteAfterRun?: boolean;
|
||||
agentId?: string;
|
||||
@@ -418,6 +424,23 @@ describe("cron cli", () => {
|
||||
expect(params?.payload?.lightContext).toBe(true);
|
||||
});
|
||||
|
||||
it("splits PowerShell-style space-separated --tools on cron add", async () => {
|
||||
const params = await runCronAddAndGetParams([
|
||||
"--name",
|
||||
"Tools",
|
||||
"--cron",
|
||||
"* * * * *",
|
||||
"--session",
|
||||
"isolated",
|
||||
"--message",
|
||||
"hello",
|
||||
"--tools",
|
||||
"exec read write",
|
||||
]);
|
||||
|
||||
expect(params?.payload?.toolsAllow).toEqual(["exec", "read", "write"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "omits empty model and thinking",
|
||||
@@ -437,6 +460,17 @@ describe("cron cli", () => {
|
||||
expect(patch?.patch?.payload?.thinking).toBe(expectedThinking);
|
||||
});
|
||||
|
||||
it("splits PowerShell-style space-separated --tools on cron edit", async () => {
|
||||
const patch = await runCronEditAndGetPatch([
|
||||
"--message",
|
||||
"hello",
|
||||
"--tools",
|
||||
"exec read write",
|
||||
]);
|
||||
|
||||
expect(patch?.patch?.payload?.toolsAllow).toEqual(["exec", "read", "write"]);
|
||||
});
|
||||
|
||||
it("sets and clears agent id on cron edit", async () => {
|
||||
await runCronCommand(["cron", "edit", "job-1", "--agent", " Ops ", "--message", "hello"]);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { resolveCronCreateSchedule } from "./schedule-options.js";
|
||||
import {
|
||||
getCronChannelOptions,
|
||||
handleCronCliError,
|
||||
parseCronToolsAllow,
|
||||
printCronJson,
|
||||
printCronList,
|
||||
warnIfCronSchedulerDisabled,
|
||||
@@ -93,7 +94,7 @@ export function registerCronAddCommand(cron: Command) {
|
||||
.option("--model <model>", "Model override for agent jobs (provider/model or alias)")
|
||||
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
||||
.option("--light-context", "Use lightweight bootstrap context for agent jobs", false)
|
||||
.option("--tools <csv>", "Comma-separated tool allow-list (e.g. exec,read,write)")
|
||||
.option("--tools <list>", "Tool allow-list (e.g. exec,read,write or exec read write)")
|
||||
.option("--announce", "Announce summary to a chat (subagent-style)", false)
|
||||
.option("--deliver", "Deprecated (use --announce). Announces a summary to a chat.")
|
||||
.option("--no-deliver", "Disable announce delivery and skip main-session summary")
|
||||
@@ -150,13 +151,7 @@ export function registerCronAddCommand(cron: Command) {
|
||||
timeoutSeconds:
|
||||
timeoutSeconds && Number.isFinite(timeoutSeconds) ? timeoutSeconds : undefined,
|
||||
lightContext: opts.lightContext === true ? true : undefined,
|
||||
toolsAllow:
|
||||
typeof opts.tools === "string" && opts.tools.trim()
|
||||
? opts.tools
|
||||
.split(",")
|
||||
.map((t: string) => normalizeOptionalString(t))
|
||||
.filter((t): t is string => Boolean(t))
|
||||
: undefined,
|
||||
toolsAllow: parseCronToolsAllow(opts.tools),
|
||||
};
|
||||
})();
|
||||
|
||||
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
applyExistingCronSchedulePatch,
|
||||
resolveCronEditScheduleRequest,
|
||||
} from "./schedule-options.js";
|
||||
import { getCronChannelOptions, parseDurationMs, warnIfCronSchedulerDisabled } from "./shared.js";
|
||||
import {
|
||||
getCronChannelOptions,
|
||||
parseCronToolsAllow,
|
||||
parseDurationMs,
|
||||
warnIfCronSchedulerDisabled,
|
||||
} from "./shared.js";
|
||||
|
||||
const assignIf = (
|
||||
target: Record<string, unknown>,
|
||||
@@ -59,7 +64,7 @@ export function registerCronEditCommand(cron: Command) {
|
||||
.option("--timeout-seconds <n>", "Timeout seconds for agent jobs")
|
||||
.option("--light-context", "Enable lightweight bootstrap context for agent jobs")
|
||||
.option("--no-light-context", "Disable lightweight bootstrap context for agent jobs")
|
||||
.option("--tools <csv>", "Comma-separated tool allow-list (e.g. exec,read,write)")
|
||||
.option("--tools <list>", "Tool allow-list (e.g. exec,read,write or exec read write)")
|
||||
.option("--clear-tools", "Remove tool allow-list (use all tools)", false)
|
||||
.option("--announce", "Announce summary to a chat (subagent-style)")
|
||||
.option("--deliver", "Deprecated (use --announce). Announces a summary to a chat.")
|
||||
@@ -175,6 +180,7 @@ export function registerCronEditCommand(cron: Command) {
|
||||
const hasSystemEventPatch = typeof opts.systemEvent === "string";
|
||||
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;
|
||||
@@ -190,6 +196,7 @@ export function registerCronEditCommand(cron: Command) {
|
||||
hasTimeoutSeconds ||
|
||||
typeof opts.lightContext === "boolean" ||
|
||||
typeof opts.tools === "string" ||
|
||||
Array.isArray(opts.tools) ||
|
||||
opts.clearTools ||
|
||||
hasDeliveryModeFlag ||
|
||||
hasDeliveryTarget ||
|
||||
@@ -217,11 +224,8 @@ export function registerCronEditCommand(cron: Command) {
|
||||
);
|
||||
if (opts.clearTools) {
|
||||
payload.toolsAllow = null;
|
||||
} else if (typeof opts.tools === "string" && opts.tools.trim()) {
|
||||
payload.toolsAllow = opts.tools
|
||||
.split(",")
|
||||
.map((t: string) => t.trim())
|
||||
.filter(Boolean);
|
||||
} else if (toolsAllow) {
|
||||
payload.toolsAllow = toolsAllow;
|
||||
}
|
||||
patch.payload = payload;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { CronJob } from "../../cron/types.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { getCronChannelOptions, printCronList } from "./shared.js";
|
||||
import { getCronChannelOptions, parseCronToolsAllow, printCronList } from "./shared.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
listChannelPluginsMock: vi.fn(),
|
||||
@@ -192,3 +192,19 @@ describe("getCronChannelOptions", () => {
|
||||
expect(getCronChannelOptions()).toBe("last|telegram|signal");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseCronToolsAllow", () => {
|
||||
it.each([
|
||||
{ input: "exec,read,write", expected: ["exec", "read", "write"] },
|
||||
{ input: "exec, read, write", expected: ["exec", "read", "write"] },
|
||||
{ input: "exec read write", expected: ["exec", "read", "write"] },
|
||||
{ input: " exec read,write ", expected: ["exec", "read", "write"] },
|
||||
{ input: ["exec", "read", "write"], expected: ["exec", "read", "write"] },
|
||||
])("parses $input", ({ input, expected }) => {
|
||||
expect(parseCronToolsAllow(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("returns undefined for empty input", () => {
|
||||
expect(parseCronToolsAllow(" , ")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
parseOffsetlessIsoDateTimeInTimeZone,
|
||||
} from "../../infra/format-time/parse-offsetless-zoned-datetime.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { colorize, isRich, theme } from "../../terminal/theme.js";
|
||||
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||
import { callGatewayFromCli } from "../gateway-rpc.js";
|
||||
@@ -99,6 +102,19 @@ export function parseCronStaggerMs(params: {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function parseCronToolsAllow(input: unknown): string[] | undefined {
|
||||
const raw = Array.isArray(input)
|
||||
? input.map((value) => String(value)).join(" ")
|
||||
: typeof input === "string"
|
||||
? input
|
||||
: "";
|
||||
const tools = raw
|
||||
.split(/[,\s]+/u)
|
||||
.map((tool) => normalizeOptionalString(tool))
|
||||
.filter((tool): tool is string => Boolean(tool));
|
||||
return tools.length > 0 ? tools : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a one-shot `--at` value into an ISO string (UTC).
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user