mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix(cron): reject blank --command-cwd before kind forge (#121535)
* fix(cron): reject blank --command-cwd/--command-input before kind forge
Presence-only typeof checks counted blank cwd/input as command-specific
edits, forging {kind:"command"} patches that could convert agentTurn or
script jobs into empty command payloads. Require non-blank values first.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cron): allow empty --command-input while rejecting blank cwd
ClawSweeper: Gateway command stdin is an unrestricted string, so empty
or whitespace --command-input must still patch through. Keep the blank
--command-cwd reject that prevents forging a command payload with no cwd.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(cron): focus command blank option coverage
Amp-Thread-ID: https://ampcode.com/threads/T-01a0220d-eaa0-76b4-adb9-68841f015b75
* fix(cron): reject blank cwd before edit reads
Amp-Thread-ID: https://ampcode.com/threads/T-01a0220d-eaa0-76b4-adb9-68841f015b75
---------
Co-authored-by: zyw02 <zyw02@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -25,6 +25,7 @@ export async function resolveCronEditPayloadDeliveryPatch(
|
||||
opts: Record<string, unknown>,
|
||||
loadExistingJob: () => Promise<CronJob>,
|
||||
webhookUrl: string | undefined,
|
||||
commandCwd: string | undefined,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const patch: Record<string, unknown> = {};
|
||||
const hasSystemEventPatch = typeof opts.systemEvent === "string";
|
||||
@@ -120,11 +121,13 @@ export async function resolveCronEditPayloadDeliveryPatch(
|
||||
throw new Error("Use --account or --clear-account, not both");
|
||||
}
|
||||
|
||||
// Unlike cwd, command stdin intentionally accepts empty and whitespace strings.
|
||||
const hasCommandInput = typeof opts.commandInput === "string";
|
||||
const hasCommandSpecificPayloadField =
|
||||
Boolean(commandShell) ||
|
||||
Boolean(commandArgv) ||
|
||||
typeof opts.commandCwd === "string" ||
|
||||
typeof opts.commandInput === "string" ||
|
||||
Boolean(commandCwd) ||
|
||||
hasCommandInput ||
|
||||
opts.commandEnv !== undefined ||
|
||||
noOutputTimeoutSeconds !== undefined ||
|
||||
outputMaxBytes !== undefined;
|
||||
@@ -228,14 +231,9 @@ export async function resolveCronEditPayloadDeliveryPatch(
|
||||
const payload: Record<string, unknown> = { kind: "command" };
|
||||
assignIf(payload, "argv", commandArgv, Boolean(commandArgv));
|
||||
assignIf(payload, "argv", ["sh", "-lc", commandShell], Boolean(commandShell));
|
||||
assignIf(
|
||||
payload,
|
||||
"cwd",
|
||||
normalizeOptionalString(opts.commandCwd),
|
||||
typeof opts.commandCwd === "string",
|
||||
);
|
||||
assignIf(payload, "cwd", commandCwd, Boolean(commandCwd));
|
||||
assignIf(payload, "env", parseCronCommandEnv(opts.commandEnv), opts.commandEnv !== undefined);
|
||||
assignIf(payload, "input", opts.commandInput, typeof opts.commandInput === "string");
|
||||
assignIf(payload, "input", opts.commandInput, hasCommandInput);
|
||||
assignIf(payload, "timeoutSeconds", timeoutSeconds, hasTimeoutSeconds);
|
||||
assignIf(
|
||||
payload,
|
||||
|
||||
@@ -1033,6 +1033,28 @@ describe("cron edit command", () => {
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it.each(["", " "])("rejects blank --command-cwd %j", async (value) => {
|
||||
await expectCronEditRejection(["--command-cwd", value], "--command-cwd must not be blank");
|
||||
});
|
||||
|
||||
it("rejects blank --command-cwd before loading an existing job", async () => {
|
||||
await expectCronEditRejection(
|
||||
["--pacing-min", "30m", "--command-cwd", " "],
|
||||
"--command-cwd must not be blank",
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["", " "])("preserves --command-input %j as command stdin", async (value) => {
|
||||
await createCronProgram().parseAsync(["edit", "job-1", "--command-input", value], {
|
||||
from: "user",
|
||||
});
|
||||
|
||||
expect(callGatewayFromCli).toHaveBeenCalledWith("cron.update", expect.anything(), {
|
||||
id: "job-1",
|
||||
patch: { payload: { kind: "command", input: value } },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects --webhook combined with a delivery clear flag", async () => {
|
||||
const errorSpy = vi.spyOn(defaultRuntime, "error").mockImplementation(() => {});
|
||||
const exitSpy = vi.spyOn(defaultRuntime, "exit").mockImplementation((() => undefined) as never);
|
||||
|
||||
@@ -171,6 +171,10 @@ export function registerCronEditCommand(cron: Command) {
|
||||
if (opts.clearTools && opts.tools !== undefined) {
|
||||
throw new Error("Use --tools or --clear-tools, not both");
|
||||
}
|
||||
const commandCwd = normalizeOptionalString(opts.commandCwd);
|
||||
if (typeof opts.commandCwd === "string" && !commandCwd) {
|
||||
throw new Error("--command-cwd must not be blank");
|
||||
}
|
||||
let existingJobPromise: Promise<CronJobForEdit> | undefined;
|
||||
let expectedConfigRevision: string | undefined;
|
||||
const readExistingCronJob = async (): Promise<CronJobForEdit> => {
|
||||
@@ -421,7 +425,12 @@ export function registerCronEditCommand(cron: Command) {
|
||||
|
||||
Object.assign(
|
||||
patch,
|
||||
await resolveCronEditPayloadDeliveryPatch(opts, readExistingCronJob, webhookUrl),
|
||||
await resolveCronEditPayloadDeliveryPatch(
|
||||
opts,
|
||||
readExistingCronJob,
|
||||
webhookUrl,
|
||||
commandCwd,
|
||||
),
|
||||
);
|
||||
|
||||
const hasFailureAlertAfter = typeof opts.failureAlertAfter === "string";
|
||||
|
||||
Reference in New Issue
Block a user