cron: add script payload kind for native deterministic job execution#25
cron: add script payload kind for native deterministic job execution#25
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds a new cron payload kind "script" with native execFile execution, CLI/UI support for script payloads, hook/context payload wiring, delivery integration for script output, type/schema updates, and unit tests for script execution behavior. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
src/cli/cron-cli/register.cron-add.ts (1)
249-257: Simplify ternary for session target inference.The nested ternary can be simplified since both
agentTurnandscriptmap to"isolated".♻️ Suggested simplification
- const inferredSessionTarget = - payload.kind === "agentTurn" - ? "isolated" - : payload.kind === "script" - ? "isolated" - : "main"; + const inferredSessionTarget = + payload.kind === "systemEvent" ? "main" : "isolated";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cli/cron-cli/register.cron-add.ts` around lines 249 - 257, The nested ternary creating inferredSessionTarget is verbose because both payload.kind values "agentTurn" and "script" map to "isolated"; replace the nested ternary in inferredSessionTarget with a single condition that checks if payload.kind === "agentTurn" || payload.kind === "script" and returns "isolated", otherwise "main", and keep subsequent sessionTarget assignment (using sessionSource, sessionTargetRaw) unchanged; update the symbol inferredSessionTarget and ensure logic around sessionTarget and sessionSource remains identical.src/cron/exec-script.ts (1)
38-39: Consider validating cwd directory existence.If
payload.cwdresolves to a non-existent directory,execFilewill fail with a less clear error (e.g.,ENOENT). Validating upfront would provide a clearer message.🛡️ Optional validation for cwd
const resolvedCwd = payload.cwd ? resolveScriptPath(payload.cwd, basePath) : basePath; + if (payload.cwd && !fs.existsSync(resolvedCwd)) { + return { + status: "error", + error: `script cwd not found: ${resolvedCwd} (cwd: ${payload.cwd})`, + }; + } const childEnv = payload.env ? { ...process.env, ...payload.env } : process.env;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cron/exec-script.ts` around lines 38 - 39, resolvedCwd may point to a non-existent path causing execFile to fail with unclear ENOENT; after computing resolvedCwd (using resolveScriptPath(payload.cwd, basePath)), validate that it exists and is a directory (e.g., fs.stat or fs.existsSync + isDirectory) before calling execFile, and if the check fails, throw or return a descriptive error indicating the invalid cwd (include payload.cwd and resolvedCwd) so callers of exec-script (and logs) get a clear message; keep childEnv logic unchanged.src/cron/exec-script.test.ts (1)
98-111: Potential test flakiness with fixed timeout.The 50ms delay before aborting may be too short on slow CI runners, potentially causing the test to abort before the process fully starts, which could lead to intermittent failures.
♻️ Consider a more robust approach
it("kills script and returns error when aborted mid-run", async () => { - const script = writeScript("slow.sh", "#!/bin/sh\nsleep 30"); + // Write a script that signals readiness before sleeping + const script = writeScript("slow.sh", "#!/bin/sh\necho started\nsleep 30"); const controller = new AbortController(); const resultPromise = execCronScript({ payload: { kind: "script", command: script }, basePath: tmpDir, abortSignal: controller.signal, }); - // Abort after a short delay to let the process start. - setTimeout(() => controller.abort(), 50); + // Give the process time to start; 100ms is more reliable across CI environments + setTimeout(() => controller.abort(), 100); const result = await resultPromise; expect(result.status).toBe("error"); expect(result.error).toContain("aborted"); });Alternatively, you could poll for process existence or use a more sophisticated synchronization mechanism, but increasing the timeout is the simplest fix.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cron/exec-script.test.ts` around lines 98 - 111, The test "kills script and returns error when aborted mid-run" uses a fixed 50ms setTimeout to abort which can be flaky on slow CI; update the test that calls execCronScript (and uses writeScript, AbortController, tmpDir) to wait more robustly before aborting — either poll/wait until the child process is confirmed started (e.g., detect the process or a readiness marker) or simply increase the abort delay to a safer value (e.g., 500ms) to avoid aborting before the script actually begins.src/cron/service/timer.ts (1)
1193-1218: Script execution path looks well-structured; consider logging delivery failures.The script payload execution logic correctly bypasses session/LLM flows and integrates with the existing delivery pipeline. However, the empty
catchblock on line 1212 silently swallows delivery errors, which could make troubleshooting difficult.🔧 Proposed fix to log delivery errors
try { const deliveryResult = await state.deps.announceScriptOutput({ job, text: scriptResult.summary, }); return { ...scriptResult, ...deliveryResult }; - } catch { + } catch (err) { + state.deps.log.warn( + { jobId: job.id, err: String(err) }, + "cron: script output delivery failed", + ); return { ...scriptResult, delivered: false, deliveryAttempted: true }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cron/service/timer.ts` around lines 1193 - 1218, The empty catch in the script payload delivery path swallows errors from announceScriptOutput; update the catch to capture the thrown error and log it via the existing logging mechanism (e.g., use state.deps.logger or an available logger) and still return { ...scriptResult, delivered: false, deliveryAttempted: true }; locate the block handling script payloads around execCronScript and the announceScriptOutput call (refer to job.payload, scriptResult, and state.deps.announceScriptOutput) and add error capture and a descriptive process/log call including the job id and error details.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cron/normalize.ts`:
- Around line 97-102: The normalization branch for next.command should treat
whitespace-only strings as missing; after trimming the string in
src/cron/normalize.ts (the block that currently checks typeof next.command ===
"string" and computes trimmed), if trimmed is non-empty set next.command to
trimmed, otherwise remove the property (or set it to undefined) so downstream
validation sees it as absent; update the branch that references next.command to
delete next.command when trimmed is empty.
In `@src/cron/types.ts`:
- Around line 102-105: The CronPayloadPatch union allows
Partial<CronScriptPayload> which makes the discriminant kind optional, but the
runtime expects kind: "script"; update the script variant to require kind:
"script" while keeping other fields optional by replacing
Partial<CronScriptPayload> with (Partial<Omit<CronScriptPayload, "kind">> & {
kind: "script" }) so the type aligns with the runtime discriminated schema;
refer to CronPayloadPatch, CronScriptPayload, and the kind property when making
this change.
In `@src/gateway/server-cron.ts`:
- Around line 305-334: The announceScriptOutput handler should catch errors from
deliverOutboundPayloads so a thrown exception doesn't prevent returning a
delivery status; wrap the await deliverOutboundPayloads call in a try/catch, on
error call cronLogger.error with context (e.g., { jobId: job.id, err:
String(err) }) and return { delivered: false, deliveryAttempted: true },
otherwise return { delivered: true, deliveryAttempted: true }; reference
announceScriptOutput, deliverOutboundPayloads, createOutboundSendDeps and
cronLogger when applying the fix.
---
Nitpick comments:
In `@src/cli/cron-cli/register.cron-add.ts`:
- Around line 249-257: The nested ternary creating inferredSessionTarget is
verbose because both payload.kind values "agentTurn" and "script" map to
"isolated"; replace the nested ternary in inferredSessionTarget with a single
condition that checks if payload.kind === "agentTurn" || payload.kind ===
"script" and returns "isolated", otherwise "main", and keep subsequent
sessionTarget assignment (using sessionSource, sessionTargetRaw) unchanged;
update the symbol inferredSessionTarget and ensure logic around sessionTarget
and sessionSource remains identical.
In `@src/cron/exec-script.test.ts`:
- Around line 98-111: The test "kills script and returns error when aborted
mid-run" uses a fixed 50ms setTimeout to abort which can be flaky on slow CI;
update the test that calls execCronScript (and uses writeScript,
AbortController, tmpDir) to wait more robustly before aborting — either
poll/wait until the child process is confirmed started (e.g., detect the process
or a readiness marker) or simply increase the abort delay to a safer value
(e.g., 500ms) to avoid aborting before the script actually begins.
In `@src/cron/exec-script.ts`:
- Around line 38-39: resolvedCwd may point to a non-existent path causing
execFile to fail with unclear ENOENT; after computing resolvedCwd (using
resolveScriptPath(payload.cwd, basePath)), validate that it exists and is a
directory (e.g., fs.stat or fs.existsSync + isDirectory) before calling
execFile, and if the check fails, throw or return a descriptive error indicating
the invalid cwd (include payload.cwd and resolvedCwd) so callers of exec-script
(and logs) get a clear message; keep childEnv logic unchanged.
In `@src/cron/service/timer.ts`:
- Around line 1193-1218: The empty catch in the script payload delivery path
swallows errors from announceScriptOutput; update the catch to capture the
thrown error and log it via the existing logging mechanism (e.g., use
state.deps.logger or an available logger) and still return { ...scriptResult,
delivered: false, deliveryAttempted: true }; locate the block handling script
payloads around execCronScript and the announceScriptOutput call (refer to
job.payload, scriptResult, and state.deps.announceScriptOutput) and add error
capture and a descriptive process/log call including the job id and error
details.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1f740c64-32cf-4bf5-a980-edda60b903ab
📥 Commits
Reviewing files that changed from the base of the PR and between 9c4eaf6 and 5f3cd231fbee094fccbb9f18167b8e693f9a6354.
⛔ Files ignored due to path filters (1)
docs/repo-map.jsonis excluded by!docs/repo-map.json
📒 Files selected for processing (21)
src/cli/cron-cli/register.cron-add.tssrc/cron/exec-script.test.tssrc/cron/exec-script.tssrc/cron/hooks.test.tssrc/cron/hooks.tssrc/cron/normalize.tssrc/cron/service/jobs.tssrc/cron/service/normalize.tssrc/cron/service/ops.tssrc/cron/service/state.tssrc/cron/service/timer.tssrc/cron/types.tssrc/gateway/protocol/schema/cron.tssrc/gateway/server-cron.tsui/src/i18n/locales/en.tsui/src/ui/app-defaults.tsui/src/ui/controllers/cron.tsui/src/ui/presenter.tsui/src/ui/types.tsui/src/ui/ui-types.tsui/src/ui/views/cron.ts
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/cron/exec-script.ts (2)
6-7: Useimport typefor type-only imports.These imports are only used as types, not values. Per coding guidelines, use
import type { X }for type-only imports.Suggested fix
-import type { CronRunOutcome } from "./types.js"; -import type { CronScriptPayload } from "./types.js"; +import type { CronRunOutcome, CronScriptPayload } from "./types.js";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cron/exec-script.ts` around lines 6 - 7, The two imports for CronRunOutcome and CronScriptPayload are type-only and should use TypeScript's type-only import syntax; update the import statements that bring in CronRunOutcome and CronScriptPayload so they read "import type { CronRunOutcome } from './types.js';" and "import type { CronScriptPayload } from './types.js';" (ensure there are no value imports of those symbols elsewhere in exec-script.ts).
136-140: Clarify fallback behavior for TypeScript files.The comment states "falls back to node+tsx if bun is unavailable," but no fallback logic is implemented—the spawn will fail with a "command not found" error if
bunisn't installed. Consider either:
- Updating the comment to reflect actual behavior (spawn error surfaced to user)
- Adding runtime probing to check for
bunavailabilityThe current behavior is acceptable (clear spawn error), but the comment is misleading.
Suggested comment update
case ".ts": - // Prefer bun for TypeScript; falls back to node+tsx if bun is unavailable, - // but we let the caller surface that as a spawn error rather than probing. + // Uses bun for TypeScript. If bun is not installed, the spawn will fail + // with an error message. Users can ensure bun is available or use a shebang. return "bun";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cron/exec-script.ts` around lines 136 - 140, The comment in the switch branch for TypeScript (case ".ts") claims it "falls back to node+tsx if bun is unavailable" but no such fallback exists; update the comment in exec-script.ts at the case ".ts" branch (the logic that returns "bun") to accurately state that we prefer bun and that if bun is not installed the spawn will fail and the caller will surface the error, or alternatively implement a runtime probe for bun availability and return "node+tsx" when bun is absent—pick one approach and make the comment and/or code consistent with that choice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/cron/exec-script.ts`:
- Around line 6-7: The two imports for CronRunOutcome and CronScriptPayload are
type-only and should use TypeScript's type-only import syntax; update the import
statements that bring in CronRunOutcome and CronScriptPayload so they read
"import type { CronRunOutcome } from './types.js';" and "import type {
CronScriptPayload } from './types.js';" (ensure there are no value imports of
those symbols elsewhere in exec-script.ts).
- Around line 136-140: The comment in the switch branch for TypeScript (case
".ts") claims it "falls back to node+tsx if bun is unavailable" but no such
fallback exists; update the comment in exec-script.ts at the case ".ts" branch
(the logic that returns "bun") to accurately state that we prefer bun and that
if bun is not installed the spawn will fail and the caller will surface the
error, or alternatively implement a runtime probe for bun availability and
return "node+tsx" when bun is absent—pick one approach and make the comment
and/or code consistent with that choice.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 03e2c63f-ed21-42ac-b68b-cb6b3305d75e
📥 Commits
Reviewing files that changed from the base of the PR and between 5f3cd231fbee094fccbb9f18167b8e693f9a6354 and ec8818beb8e45222a93341802bb872768ab66fdf.
⛔ Files ignored due to path filters (1)
docs/repo-map.jsonis excluded by!docs/repo-map.json
📒 Files selected for processing (9)
.coderabbit.yamlsrc/cli/cron-cli/register.cron-add.tssrc/cron/exec-script.test.tssrc/cron/exec-script.tssrc/cron/normalize.tssrc/cron/service/jobs.tssrc/cron/service/timer.tssrc/cron/types.tssrc/gateway/server-cron.ts
💤 Files with no reviewable changes (1)
- .coderabbit.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
- src/cron/exec-script.test.ts
- src/gateway/server-cron.ts
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/cron/exec-script.ts (1)
27-35: Consider validating non-empty command.If
payload.commandis empty or whitespace-only,resolveScriptPathwill returnbasePathitself. SincebasePathis a directory,fs.existsSyncwould returntrue, andexecFilewould then fail with a confusing error (e.g.,EACCESorEISDIR).🛡️ Proposed fix to add early validation
const resolvedCommand = resolveScriptPath(payload.command, basePath); + + if (!payload.command.trim()) { + return { status: "error", error: "script command is empty" }; + } // Validate file exists before spawning to give a clear error message.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cron/exec-script.ts` around lines 27 - 35, The code fails to validate that payload.command is non-empty/whitespace and may treat basePath (a directory) as a valid script because resolveScriptPath returns basePath; update the pre-check before calling resolveScriptPath/execFile to reject empty or whitespace-only payload.command and/or ensure the resolved path is a regular file (not a directory) using fs.stat or similar; specifically add validation around payload.command and after resolveScriptPath confirm the path is a file (e.g., using fs.statSync(resolvedCommand).isFile()) and return a clear error object if the command is empty or the resolved path is not a file so execFile won’t produce confusing EISDIR/EACCES errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cron/exec-script.ts`:
- Around line 127-130: The switch in exec-script.ts currently maps the case
labels ".bash" and ".zsh" to return "sh", which prevents bash/zsh-specific
features from working; update the case handling so that ".bash" returns "bash"
and ".zsh" returns "zsh" (leave ".sh" returning "sh"), ensuring the interpreter
selection logic (the case ".sh"/".bash"/".zsh" block) uses the correct
interpreter strings instead of defaulting both to "sh".
---
Nitpick comments:
In `@src/cron/exec-script.ts`:
- Around line 27-35: The code fails to validate that payload.command is
non-empty/whitespace and may treat basePath (a directory) as a valid script
because resolveScriptPath returns basePath; update the pre-check before calling
resolveScriptPath/execFile to reject empty or whitespace-only payload.command
and/or ensure the resolved path is a regular file (not a directory) using
fs.stat or similar; specifically add validation around payload.command and after
resolveScriptPath confirm the path is a file (e.g., using
fs.statSync(resolvedCommand).isFile()) and return a clear error object if the
command is empty or the resolved path is not a file so execFile won’t produce
confusing EISDIR/EACCES errors.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ab0eeed8-428a-4318-8cb8-c99e8f2b6887
📥 Commits
Reviewing files that changed from the base of the PR and between ec8818beb8e45222a93341802bb872768ab66fdf and edab5a686a3d9dd8ce5f41cd704174565604d7cd.
📒 Files selected for processing (1)
src/cron/exec-script.ts
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
2e57f9b to
920ce3a
Compare
Summary
payload.kind: "script"— a new cron payload type that runs a command viachild_process.execFile(shell injection-safe), wires script stdout as deliverable output to channels, extends the dashboard UI with script-specific form fields, and trims whitespace in normalize for consistency.resolveDeliveryTarget+deliverOutboundPayloadsinfrastructure.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
openclaw cron add --script <path> [--args <a> <b>] [--env KEY=VAL] [--cwd <dir>] [--timeout <s>] [--announce]--announceroute stdout summary to the configured delivery channel.Security Impact (required)
execFileruns an arbitrary command path the user specifies in the job config.execFile(notexec) is used explicitly to prevent shell injection — command and args are passed as separate argv, no shell interpolation.execFileargv split prevents shell metacharacter injection. Command path is stored verbatim in the job record (same trust boundary as existing system event payloads). No new network surface.Repro + Verification
Environment
Steps
pnpm build && pnpm check && pnpm test -- src/cron/exec-script.test.ts src/cron/hooks.test.tsopenclaw cron add --name test-script --every 1m --script ~/.openclaw/workspace/scripts/test.cjs --announce --channel lastSummary by CodeRabbit
New Features
Bug Fixes
Tests