openclaw / src /cli /cron-cli /register.cron-edit-options.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
f778c12 verified
Raw History Blame Contribute Delete
12.1 kB
import {
normalizeOptionalString,
readNonBlankString,
} from "@openclaw/normalization-core/string-coerce";
import { isSystemMonitorDeclaration } from "../../cron/system-owned-declaration.js";
import type { CronJob } from "../../cron/types.js";
import { isSystemOwnedCronPayloadKind } from "../../cron/types.js";
import { CronCliError } from "./cron-cli-error.js";
import {
parseCronCommandArgv,
parseCronCommandEnv,
parseCronIntegerOption,
parseCronNoOutputTimeoutOption,
parseCronStringList,
} from "./shared.js";
import { parseCronThreadIdOption } from "./thread-id-shared.js";
import { readCronPayloadScript } from "./trigger-options.js";
const assignIf = (
target: Record<string, unknown>,
key: string,
value: unknown,
shouldAssign: boolean,
) => {
if (shouldAssign) {
target[key] = value;
}
};
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";
const scriptPath = readNonBlankString(opts.script);
const commandShell = normalizeOptionalString(opts.command);
const commandArgv = parseCronCommandArgv(opts.commandArgv);
if (commandShell && commandArgv) {
throw new CronCliError(
"Pass command payload either with --command or --command-argv, not both.",
);
}
// Raw flag presence owns the set/clear mutex even when normalization omits a blank value.
const hasModel = typeof opts.model === "string";
const model = normalizeOptionalString(opts.model);
if (hasModel && opts.clearModel) {
throw new CronCliError("Use --model or --clear-model, not both");
}
const hasThinking = typeof opts.thinking === "string";
const thinking = normalizeOptionalString(opts.thinking);
if (hasThinking && opts.clearThinking) {
throw new CronCliError("Use --thinking or --clear-thinking, not both");
}
const fallbacks = parseCronStringList(opts.fallbacks);
if (typeof opts.fallbacks === "string" && opts.clearFallbacks) {
throw new CronCliError("Use --fallbacks or --clear-fallbacks, not both");
}
const toolsAllow = parseCronStringList(opts.tools);
const timeoutSeconds = parseCronIntegerOption(
opts.timeoutSeconds,
"--timeout-seconds",
"non-negative",
);
const hasTimeoutSeconds = timeoutSeconds !== undefined;
const noOutputTimeoutSeconds = parseCronNoOutputTimeoutOption(opts);
const outputMaxBytes = parseCronIntegerOption(opts.outputMaxBytes, "--output-max-bytes");
const scriptTimeoutSeconds = parseCronIntegerOption(
opts.scriptTimeoutSeconds,
"--script-timeout-seconds",
);
const scriptToolBudget = parseCronIntegerOption(opts.scriptToolBudget, "--script-tool-budget");
const hasWebhookDelivery = Boolean(webhookUrl);
const hasDeliveryModeFlag =
opts.announce || typeof opts.deliver === "boolean" || hasWebhookDelivery;
const threadId = parseCronThreadIdOption(opts.threadId);
const hasDeliveryThreadId = typeof threadId === "number";
const hasDeliveryTarget =
typeof opts.channel === "string" ||
typeof opts.to === "string" ||
hasDeliveryThreadId ||
Boolean(opts.clearChannel) ||
Boolean(opts.clearTo) ||
Boolean(opts.clearThreadId);
const hasDeliveryAccount = typeof opts.account === "string" || Boolean(opts.clearAccount);
const hasBestEffort = typeof opts.bestEffortDeliver === "boolean";
if (hasWebhookDelivery && (hasDeliveryTarget || hasDeliveryAccount)) {
throw new CronCliError("--webhook cannot be combined with chat delivery options.");
}
if (typeof opts.channel === "string" && opts.clearChannel) {
throw new CronCliError("Use --channel or --clear-channel, not both");
}
if (typeof opts.to === "string" && opts.clearTo) {
throw new CronCliError("Use --to or --clear-to, not both");
}
if (hasDeliveryThreadId && opts.clearThreadId) {
throw new CronCliError("Use --thread-id or --clear-thread-id, not both");
}
if (typeof opts.account === "string" && opts.clearAccount) {
throw new CronCliError("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) ||
Boolean(commandCwd) ||
hasCommandInput ||
opts.commandEnv !== undefined ||
noOutputTimeoutSeconds !== undefined ||
outputMaxBytes !== undefined;
const hasToolsAllowPatch =
typeof opts.tools === "string" || Array.isArray(opts.tools) || Boolean(opts.clearTools);
const hasAgentTurnSpecificPayloadField =
typeof opts.message === "string" ||
Boolean(model) ||
Boolean(opts.clearModel) ||
typeof opts.fallbacks === "string" ||
Boolean(opts.clearFallbacks) ||
Boolean(thinking) ||
Boolean(opts.clearThinking) ||
typeof opts.lightContext === "boolean";
const hasScriptSpecificPayloadField =
Boolean(scriptPath) || scriptTimeoutSeconds !== undefined || scriptToolBudget !== undefined;
if (hasTimeoutSeconds && hasScriptSpecificPayloadField) {
throw new CronCliError("Use --script-timeout-seconds for script jobs, not --timeout-seconds.");
}
if (hasTimeoutSeconds && hasSystemEventPatch) {
throw new CronCliError("--timeout-seconds is not supported for systemEvent jobs.");
}
let timeoutOnlyPayloadKind: "agentTurn" | "command" | undefined;
if (hasTimeoutSeconds && !hasCommandSpecificPayloadField && !hasAgentTurnSpecificPayloadField) {
const existingJob = await loadExistingJob();
const existingKind = existingJob.payload.kind;
if (existingKind === "script") {
throw new CronCliError(
"Use --script-timeout-seconds for script jobs, not --timeout-seconds.",
);
}
if (
existingKind === "systemEvent" ||
isSystemOwnedCronPayloadKind(existingKind) ||
isSystemMonitorDeclaration(existingJob.declarationKey)
) {
throw new CronCliError(`--timeout-seconds is not supported for ${existingKind} jobs.`);
}
timeoutOnlyPayloadKind = existingKind;
}
let toolsOnlyPayloadKind: CronJob["payload"]["kind"] | undefined;
if (
hasToolsAllowPatch &&
!hasSystemEventPatch &&
!hasAgentTurnSpecificPayloadField &&
!hasCommandSpecificPayloadField &&
!hasScriptSpecificPayloadField &&
!hasTimeoutSeconds
) {
// Tool grants are shared by every payload kind; a policy-only edit must
// preserve the stored execution kind instead of creating an agent turn.
const existingJob = await loadExistingJob();
if (isSystemMonitorDeclaration(existingJob.declarationKey)) {
throw new CronCliError("System-owned cron jobs cannot be edited by cron clients.");
}
toolsOnlyPayloadKind = existingJob.payload.kind;
}
const hasAgentTurnPayloadField =
hasAgentTurnSpecificPayloadField ||
timeoutOnlyPayloadKind === "agentTurn" ||
(hasToolsAllowPatch && toolsOnlyPayloadKind === "agentTurn");
const hasCommandPayloadField =
hasCommandSpecificPayloadField ||
timeoutOnlyPayloadKind === "command" ||
toolsOnlyPayloadKind === "command";
const hasAgentTurnPatch = hasAgentTurnPayloadField;
const hasCommandPatch = hasCommandPayloadField;
const hasScriptPatch = hasScriptSpecificPayloadField || toolsOnlyPayloadKind === "script";
const hasSystemEventOrToolsPatch = hasSystemEventPatch || toolsOnlyPayloadKind === "systemEvent";
if (
[hasSystemEventOrToolsPatch, hasAgentTurnPatch, hasCommandPatch, hasScriptPatch].filter(Boolean)
.length > 1
) {
throw new CronCliError("Choose at most one payload change");
}
const assignToolsAllowPatch = (payload: Record<string, unknown>): void => {
if (opts.clearTools) {
// Clearing a restriction means an explicit unrestricted grant. Persisting
// a wildcard avoids creating a new capless legacy job at the upgrade boundary.
payload.toolsAllow = ["*"];
} else if (toolsAllow) {
payload.toolsAllow = toolsAllow;
}
};
if (hasSystemEventOrToolsPatch) {
const payload: Record<string, unknown> = { kind: "systemEvent" };
assignIf(payload, "text", String(opts.systemEvent), hasSystemEventPatch);
assignToolsAllowPatch(payload);
patch.payload = payload;
} else if (hasAgentTurnPatch) {
const payload: Record<string, unknown> = { kind: "agentTurn" };
assignIf(payload, "message", String(opts.message), typeof opts.message === "string");
if (opts.clearModel) {
payload.model = null;
} else {
assignIf(payload, "model", model, Boolean(model));
}
assignIf(payload, "fallbacks", fallbacks, typeof opts.fallbacks === "string");
assignIf(payload, "fallbacks", null, Boolean(opts.clearFallbacks));
if (opts.clearThinking) {
payload.thinking = null;
} else {
assignIf(payload, "thinking", thinking, Boolean(thinking));
}
assignIf(payload, "timeoutSeconds", timeoutSeconds, hasTimeoutSeconds);
assignIf(payload, "lightContext", opts.lightContext, typeof opts.lightContext === "boolean");
assignToolsAllowPatch(payload);
patch.payload = payload;
} else if (hasCommandPatch) {
const payload: Record<string, unknown> = { kind: "command" };
assignIf(payload, "argv", commandArgv, Boolean(commandArgv));
assignIf(payload, "argv", ["sh", "-lc", commandShell], Boolean(commandShell));
assignIf(payload, "cwd", commandCwd, Boolean(commandCwd));
assignIf(payload, "env", parseCronCommandEnv(opts.commandEnv), opts.commandEnv !== undefined);
assignIf(payload, "input", opts.commandInput, hasCommandInput);
assignIf(payload, "timeoutSeconds", timeoutSeconds, hasTimeoutSeconds);
assignIf(
payload,
"noOutputTimeoutSeconds",
noOutputTimeoutSeconds,
noOutputTimeoutSeconds !== undefined,
);
assignIf(payload, "outputMaxBytes", outputMaxBytes, outputMaxBytes !== undefined);
assignToolsAllowPatch(payload);
patch.payload = payload;
} else if (hasScriptPatch) {
const payload: Record<string, unknown> = { kind: "script" };
if (scriptPath) {
payload.script = await readCronPayloadScript(scriptPath);
}
assignIf(payload, "timeoutSeconds", scriptTimeoutSeconds, scriptTimeoutSeconds !== undefined);
assignIf(payload, "toolBudget", scriptToolBudget, scriptToolBudget !== undefined);
assignToolsAllowPatch(payload);
patch.payload = payload;
}
if (hasDeliveryModeFlag || hasDeliveryTarget || hasDeliveryAccount || hasBestEffort) {
const delivery: Record<string, unknown> = {};
if (hasDeliveryModeFlag) {
delivery.mode = hasWebhookDelivery
? "webhook"
: opts.announce || opts.deliver === true
? "announce"
: "none";
} else if (opts.bestEffortDeliver === true) {
// Back-compat: enabling best-effort historically implied announce mode.
delivery.mode = "announce";
}
if (opts.clearChannel) {
delivery.channel = null;
} else if (typeof opts.channel === "string") {
const channel = opts.channel.trim();
delivery.channel = channel ? channel : undefined;
}
if (hasWebhookDelivery) {
delivery.to = webhookUrl;
} else if (opts.clearTo) {
delivery.to = null;
} else if (typeof opts.to === "string") {
const to = opts.to.trim();
delivery.to = to ? to : undefined;
}
if (opts.clearThreadId) {
delivery.threadId = null;
} else if (hasDeliveryThreadId) {
delivery.threadId = threadId;
}
if (opts.clearAccount) {
delivery.accountId = null;
} else if (typeof opts.account === "string") {
const account = opts.account.trim();
delivery.accountId = account ? account : undefined;
}
if (typeof opts.bestEffortDeliver === "boolean") {
delivery.bestEffort = opts.bestEffortDeliver;
}
patch.delivery = delivery;
}
return patch;
}