Skip to content

cron: add lifecycle hooks for job execution#45465

Open
Arry8 wants to merge 3 commits intoopenclaw:mainfrom
Arry8:feature/cron-lifecycle-hooks
Open

cron: add lifecycle hooks for job execution#45465
Arry8 wants to merge 3 commits intoopenclaw:mainfrom
Arry8:feature/cron-lifecycle-hooks

Conversation

@Arry8
Copy link
Copy Markdown

@Arry8 Arry8 commented Mar 13, 2026

Summary

  • Adds a hook system that runs user-defined scripts at cron job lifecycle points (beforeRun, afterComplete, onFailure, afterRun)
  • Hooks are configured globally via openclaw.json cron.hooks section, or per-job via hooks field in the job store
  • Supports priority ordering, job/agent ID filtering, workflow filtering, skipGlobal overrides, and abort capability on beforeRun hooks
  • Hook failures are isolated — they log warnings but never crash the job

Supersedes #45239 (closed) with fixes for all three issues identified in Greptile review.

Fixes from review (#45239)

  1. filter.workflow now enforcedmatchesFilter() checks entry.filter.workflow against the workflow string passed from the caller
  2. agentId filter correct for agent-less jobs — undefined agentId is treated as non-matching when the filter is set
  3. No more setTimeout leakcreateTimeout() returns a clear() handle, called in a finally block after each hook

Config example

{
  "cron": {
    "hooks": {
      "beforeRun": [
        { "script": "workspace/scripts/hooks/audit-log.cjs", "priority": 5 }
      ],
      "onFailure": [
        { "script": "workspace/scripts/hooks/alert.cjs", "filter": { "workflow": ["cron"] } }
      ]
    }
  }
}

Hook script contract

module.exports = async function(ctx) {
  // ctx.hookPoint, ctx.workflow, ctx.job, ctx.status,
  // ctx.error, ctx.durationMs, ctx.meta, ctx.log
  // beforeRun only: return { abort: true, reason: '...' } to skip the job
};

Files changed

  • src/config/types.cron.ts — New types: CronHookEntry, CronLifecycleHookPoint, CronHooksConfig, CronJobHooksConfig; added hooks? to CronConfig
  • src/cron/types.ts — Added hooks?: CronJobHooksConfig to CronJob
  • src/cron/hooks.ts — Hook runner module: loadHookEntries(), runCronHooks(), filter matching, timeout with cleanup
  • src/cron/hooks.test.ts — 14 unit tests covering loading, merging, priority, all three filter types, abort, error isolation
  • src/cron/service/timer.ts — Integrated hook calls into runDueJob() at all 4 lifecycle points

Test plan

  • pnpm build passes
  • pnpm check passes (lint + format)
  • pnpm test src/cron/hooks.test.ts — 14/14 tests pass
  • Manual: configure a hook script in openclaw.json, trigger a cron job, verify hook fires

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation scripts Repository scripts size: XL labels Mar 13, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 13, 2026

Greptile Summary

This PR introduces a lifecycle hook system for cron jobs, allowing user-defined scripts to run at beforeRun, afterComplete, onFailure, and afterRun points. The implementation is largely solid — filter logic, priority sorting, timeout cleanup, and error isolation are well-designed and match the previous review's fixes. However, there is one logic inconsistency and a few smaller concerns worth addressing before merge.

  • afterRun hooks do not fire when beforeRun aborts — the early return in timer.ts exits before reaching the afterRun block, contradicting the stated guarantee that afterRun always fires. Hook authors relying on afterRun for cleanup or audit logging will silently miss it when a beforeRun hook aborts.
  • CronHookContext.result field is defined but never populated — hook scripts that access ctx.result will always receive undefined; the field should be removed or actually set in the context builder.
  • runCronHooks abort path has no unit test — the core beforeRun abort behaviour (returning { abort: true }) is not directly exercised in the test suite despite being the most security-sensitive path in the feature.

Confidence Score: 3/5

  • Mostly safe to merge but the afterRun gap creates a correctness risk for hook authors relying on its "always fires" guarantee.
  • The core implementation is solid and addresses all three issues from the prior review. The afterRun issue is a real logic bug that could silently break hook-based cleanup or audit workflows, not just a theoretical concern. The dead result field and missing abort test reduce confidence in the completeness of the change.
  • src/cron/service/timer.ts — the beforeRun abort early-return path needs to be restructured so afterRun hooks still fire.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/cron/service/timer.ts
Line: 657-664

Comment:
**`afterRun` hooks silently skip on `beforeRun` abort**

The early `return` on a `beforeRun` abort (lines 658–664) exits the function before `afterRun` hooks are reached (line 710). The comment at line 710 states `afterRun` hooks "always fire regardless of outcome", but a `beforeRun` abort is itself an outcome — and `afterRun` never fires in this path.

If an `afterRun` hook is used for cleanup, audit logging, or metrics recording, it will be silently missed whenever a `beforeRun` hook aborts the job. This could lead to incomplete audit trails or unreleased resources that the hook was expected to clean up.

To fix this, extract the abort-result build into a variable and fall through to the shared `afterRun` block rather than returning early:

```typescript
let abortResult: TimedCronRunOutcome | undefined;
if (beforeEntries.length > 0) {
  const beforeResult = await runCronHooks("beforeRun", makeHookCtx("beforeRun"), beforeEntries);
  if (beforeResult.aborted) {
    abortResult = {
      jobId: id,
      status: "skipped",
      error: `hook aborted: ${beforeResult.reason ?? "unknown"}`,
      startedAt,
      endedAt: state.deps.nowMs(),
    };
  }
}
if (abortResult) {
  // still run afterRun hooks before returning
  const afterRunEntries = loadHookEntries("afterRun", state.deps.cronConfig, job);
  if (afterRunEntries.length > 0) {
    await runCronHooks("afterRun", makeHookCtx("afterRun", { status: "skipped", durationMs: abortResult.endedAt - abortResult.startedAt }), afterRunEntries);
  }
  return abortResult;
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/cron/hooks.ts
Line: 13

Comment:
**`result` field is defined but never populated**

`CronHookContext` declares a `result?: unknown` field, but it is never set when `makeHookCtx` builds the context object in `timer.ts`. Hook script authors who reference `ctx.result` expecting the job's core result will always receive `undefined`.

Either remove the field to avoid confusion, or populate it in `timer.ts` with `coreResult` (or the raw `executeJobCoreWithTimeout` return value) so hook scripts can actually use it. The PR description's hook contract comment (`ctx.hookPoint, ctx.workflow, ctx.job, ctx.status …`) doesn't mention `ctx.result` either, which suggests it was left in the type by accident.

```suggestion
  job: Pick<CronJob, "id" | "name" | "agentId" | "schedule">;
  error?: string;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/cron/hooks.test.ts
Line: 166-206

Comment:
**`runCronHooks` abort path is untested**

The three `runCronHooks` tests cover empty entries and a missing module, but the core `beforeRun` abort contract — a hook returning `{ abort: true, reason: '...' }` — has no test. Given that abort is the most security-sensitive behaviour (it gates whether a job executes at all), this path should be explicitly covered:

```typescript
it("aborts execution when a beforeRun hook returns { abort: true }", async () => {
  const abortHook = vi.fn().mockResolvedValue({ abort: true, reason: "test abort" });
  vi.doMock("./abort-hook.cjs", () => ({ default: abortHook }));
  // or use a loadHookModule mock strategy consistent with the test setup
});

it("returns aborted:false when abort is false", async () => {
  // hook returns { abort: false } — should not abort
});
```

Also consider a test where `reason` is omitted — `isAbortResult` still triggers but `reason` should default to `"aborted by hook"` per line 108 of `hooks.ts`.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: a8043de

Comment thread src/cron/service/timer.ts
Comment thread src/cron/hooks.ts Outdated
Comment thread src/cron/hooks.test.ts
@Arry8 Arry8 force-pushed the feature/cron-lifecycle-hooks branch from a8043de to 0a65002 Compare March 13, 2026 21:30
@openclaw-barnacle openclaw-barnacle Bot added size: M and removed docs Improvements or additions to documentation scripts Repository scripts size: XL labels Mar 13, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8043de1af

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/config/types.cron.ts
Comment thread src/cron/hooks.ts Outdated
Comment thread src/cron/service/timer.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1660fbeb30

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/service/timer.ts
@Arry8
Copy link
Copy Markdown
Author

Arry8 commented Mar 13, 2026

@coderabbitai review

Copy link
Copy Markdown

@xkonjin xkonjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Summary: Well-structured addition. The type definitions are clean, the filter system is expressive, and the test coverage is thorough. A few issues worth addressing before merge:

🔴 Security: loadHookModule accepts arbitrary data: URIs in tests but likely require()/import() from disk in production

The implementation dynamically imports scripts via path. If cron job configs are user-editable (jobs.json), a malicious per-job hook entry could load any script on the filesystem. Since CronJobHooksConfig accepts plain string paths with no validation, there's no guard against:

{ "hooks": { "beforeRun": ["../../secrets.env"] } }

Recommendation: validate hook script paths against a configured allowlist or restrict them to workspace-relative paths within a specific directory (e.g. hooks/). At minimum, reject absolute paths and ../ traversal in per-job hook entries.

🟡 HOOK_TIMEOUT_MS = 10_000 is hardcoded and not configurable

A 10-second timeout per hook may be too short for some post-completion hooks (e.g. sending summaries, writing to external services) and too long for beforeRun guard hooks. Consider exposing this as a configurable field on CronHookEntry or CronConfig.

🟡 Hook execution errors are swallowed silently for non-beforeRun points

From the test:

it("logs warning and continues when script module is not found", ...)

This is the right behavior for resilience, but the log level should be error when the script exists but throws at runtime (vs. warn for module-not-found). Users debugging a broken onFailure hook would benefit from a clearer signal.

🟡 afterRun vs afterComplete/onFailure ordering not documented

The type CronLifecycleHookPoint = "beforeRun" | "afterComplete" | "onFailure" | "afterRun" implies afterRun fires regardless of success/failure (like a finally block), while afterComplete and onFailure are conditional. This invariant should be documented in the type definition and in the README/config docs, otherwise users will misuse afterRun expecting conditional behavior.

💡 meta object is shared but not typed

The meta: Record<string, unknown> on CronHookContext is a useful escape hatch for hook-to-hook communication within a run. Consider adding a brief JSDoc comment noting this is the intended use, and whether hooks can mutate it for downstream hooks to read.

💡 inlineHook test helper is clever but fragile

The data-URI import trick works in Node.js but may not work in all environments (Bun, edge runtimes). If the test suite is ever run in a non-Node context, these tests will silently fail or throw. A comment noting the Node-only requirement would help future contributors.

Test coverage is excellent — the filter combinations, skipGlobal, and abort semantics are all well exercised. The main gaps are the path traversal security concern and the error logging level distinction.

@Arry8
Copy link
Copy Markdown
Author

Arry8 commented Mar 13, 2026

@coderabbitai review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cdfea188b3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/hooks.ts Outdated
@Arry8
Copy link
Copy Markdown
Author

Arry8 commented Mar 13, 2026

All review findings addressed

Greptile (3 findings — fixed in 1660fbe)

  • afterRun silent skip on abort: afterRun hooks now fire before the early return
  • Unused result field: removed from CronHookContext
  • Abort path untested: 4 new tests using data: URI inline hooks

Codex Connector P1 (2 findings — fixed in cdfea18)

  • Zod schema missing hooks: CronHookEntrySchema added; hooks wired into cron object schema
  • Relative path resolution: loadHookModule uses path.resolve + importFileModule; URL schemes (data:, file:) pass through directly

Codex Connector P2 (3 findings — fixed in 1660fbe and faab903)

  • afterRun on beforeRun abort: same fix as Greptile finding above
  • Catch-up run hooks: runStartupCatchupCandidate now runs the full beforeRun/afterComplete/onFailure/afterRun lifecycle
  • Windows absolute paths: path.isAbsolute() checked before URL-scheme regex

Local integration test (passed)

Hooks confirmed firing on a live OC instance. Hook log files created for beforeRun, afterComplete, and afterRun across multiple cron jobs.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: faab90352b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/hooks.ts Outdated
Comment thread src/cron/service/timer.ts
@openclaw-barnacle openclaw-barnacle Bot added app: web-ui App: web-ui gateway Gateway runtime labels Mar 14, 2026
@Arry8
Copy link
Copy Markdown
Author

Arry8 commented Mar 14, 2026

Review findings addressed (41c7282)

xkonjin (human review) — all 6 items fixed

  • Security (path traversal): per-job hook paths now validated — absolute paths and ../ traversal rejected via isValidJobHookPath()
  • Configurable timeout: added timeoutMs field to CronHookEntry (default 10s); wired into Zod schema
  • Error vs warn log level: runtime throws log at error level; module-not-found stays at warn
  • Type docs for lifecycle ordering: CronLifecycleHookPoint now has JSDoc explaining afterRun fires like a finally block
  • JSDoc for meta: expanded comment explaining hook-to-hook communication pattern
  • inlineHook Node-only: added comment noting data: URI imports require Node.js

Latest Codex review (faab903) — both P1/P2 fixed

  • P1: Stable base directory: hook scripts now resolved from OC home (derived from storePath) instead of process.cwd()
  • P2: Manual run hooks: finishPreparedManualRun in ops.ts now runs full beforeRun/afterComplete/onFailure/afterRun lifecycle

Additional improvements

  • Gateway schema: added hooks field to TypeBox CronJobSchema — fixes "not a recognized key" rejection
  • jobName filter: hooks can now filter by job name (case-insensitive substring match) in addition to job ID
  • Tests: 7 new tests (path traversal x2, absolute path rejection, isValidJobHookPath x3, jobName filter x2) — total 25

All checks pass: pnpm build, pnpm check, pnpm test src/cron/hooks.test.ts (25/25).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41c7282452

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/service/ops.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e88721b09e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/hooks.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ece1ec2097

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/hooks.ts
Comment on lines +204 to +208
const normalized = path.normalize(scriptPath);
if (normalized.startsWith("..")) {
return false;
}
return true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject non-filesystem per-job hook specifiers

Per-job hooks are described as restricted because jobs.json may be less trusted, but this validator only blocks absolute paths and .. traversal. Values like data:text/javascript,... (or ~-prefixed paths) still pass here, then loadHookModule() imports URL schemes directly / expands ~, so per-job config can escape the intended path guard and execute arbitrary modules outside the workspace. Restrict per-job entries to safe relative filesystem paths (for example, reject URI schemes and ~ prefixes).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f13017: isValidJobHookPath now rejects URL-scheme specifiers (npm:, node:, data:, https:, etc.) before the path-traversal check. Per-job hooks are restricted to relative filesystem paths; global hooks in openclaw.json remain unrestricted since they are admin-controlled. Added four test cases to the suite.

Comment thread src/cron/service/ops.ts
await persist(state);
armTimer(state);
});
return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit finished event on manual beforeRun aborts

This early return on the manual-run abort path skips the later emit(... action: "finished") block in finishPreparedManualRun, so hook-aborted cron.run executions are persisted as skipped but never broadcast as completed. That makes downstream consumers miss these runs (for example, run-log appends in the cron event handler), creating inconsistent observability versus timer/catch-up executions that always emit a finished event.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8f13017: the beforeRun abort path in finishPreparedManualRun now emits a finished event (status: skipped, error, runAtMs, durationMs, nextRunAtMs) inside the locked block after persisting, so gateway/UI listeners receive the completion signal.

Add a hook system that runs user-defined scripts at cron job lifecycle
points (beforeRun, afterComplete, onFailure, afterRun). Hooks are
configured via openclaw.json cron.hooks (global) and per-job hooks
in the job store. Supports priority ordering, job/agent filtering,
skipGlobal overrides, and abort capability on beforeRun hooks.
@Arry8 Arry8 force-pushed the feature/cron-lifecycle-hooks branch from ece1ec2 to 6145a07 Compare March 14, 2026 20:49
@Arry8
Copy link
Copy Markdown
Author

Arry8 commented Mar 14, 2026

Rebased and squashed this branch onto the current upstream/main.

What changed:

  • Branch was originally based on a commit 735 commits behind upstream/main, causing the PR diff to show ~1840 unrelated files
  • Rebased all 9 fixup commits onto current upstream/main and squashed them into a single clean commit
  • Resolved one conflict in src/utils.ts: merged upstream's resolveHomeRelativePath refactor with the base parameter added by this PR for relative hook script path resolution
  • PR now shows only the 9 cron-related files it actually touches

No functional changes — same behavior, cleaner history.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6145a07b4d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/service/timer.ts

const hookMeta: Record<string, unknown> = {};
// Derive OC home from the cron store path for stable hook script resolution.
const hookBasePath = path.resolve(path.dirname(state.deps.storePath), "..");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve hook scripts against config/workspace root

This base path calculation makes relative hook imports depend on where cron.store is located, not on a stable OpenClaw workspace/config root: hookBasePath is computed from state.deps.storePath and then used by loadHookModule for all relative scripts. Since cron.store is user-configurable and hook scripts are documented as workspace-relative, moving the cron store file (for backups/volumes) will silently remap paths like workspace/scripts/audit.cjs to the wrong directory and cause hooks to stop loading unless users switch to absolute paths.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed: both runDueJob (timer.ts) and finishPreparedManualRun (ops.ts) compute hookBasePath = path.resolve(path.dirname(state.deps.storePath), "..") which resolves to OC home (~/.openclaw/) independently of process.cwd(). Relative hook paths are resolved against this stable root via resolveUserPath.

- isValidJobHookPath: reject URL-scheme specifiers (npm:, node:, data:,
  https:, etc.) in per-job hook paths; only relative filesystem paths allowed
- finishPreparedManualRun: emit finished event on beforeRun abort so
  gateway/UI listeners see the run as complete
- add URL-scheme rejection tests to isValidJobHookPath suite
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f130174bc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/hooks.ts
Comment on lines +201 to +203
if (path.isAbsolute(scriptPath)) {
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Trim per-job hook paths before safety checks

The per-job path guard checks the raw scriptPath, but hook loading later resolves the same value through resolveUserPath(...), which trims whitespace first. A jobs.json value like " /etc/passwd" (or a spaced traversal path) bypasses path.isAbsolute(...) here and is then trimmed into an absolute path during import, so per-job hooks can execute files outside the intended relative workspace scope.

Useful? React with 👍 / 👎.

Comment thread src/cron/hooks.ts
Comment on lines +65 to +69
const jobScripts = job.hooks?.[hookPoint];
const jobEntries: ResolvedEntry[] = [];
if (jobScripts) {
for (const script of jobScripts) {
if (!isValidJobHookPath(script)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate hook entry types before iterating

This loop assumes job.hooks[hookPoint] is a string array, but cron jobs are persisted in jobs.json and hook values are not runtime-normalized before this read path. If a malformed entry like [123] is present, isValidJobHookPath(123) throws in path.isAbsolute, which aborts the run before normal result finalization and can leave jobs marked as running until stale-run recovery clears them.

Useful? React with 👍 / 👎.

beforeRun/afterComplete/onFailure/afterRun hooks now receive ctx.payload
so scripts can inspect kind, command, message, etc. and make decisions
(abort, conditional logic, audit) without guessing from the job name.

- Add payload: CronPayload to CronHookContext type (hooks.ts)
- Pass structuredClone(job.payload) in all three makeHookCtx sites
  (timer.ts scheduled runs, timer.ts catch-up runs, ops.ts manual runs)
- hooks.test.ts: 3 new tests — kind-based abort, non-matching kind,
  payload field access via ctx.meta
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbe6470ada

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/hooks.ts
Comment on lines +203 to +204
if (path.isAbsolute(scriptPath)) {
return false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Trim per-job hook paths before enforcing path restrictions

isValidJobHookPath performs its absolute/traversal checks on the raw scriptPath, so entries like " /etc/passwd" or " ../outside/hook.cjs" pass validation here, but later loadHookModule resolves the same value through resolveUserPath, which trims first (src/utils.ts) and then imports the escaped path. That bypasses the per-job safety boundary for jobs.json hooks and allows loading arbitrary filesystem modules outside the intended relative scope.

Useful? React with 👍 / 👎.

Comment thread src/cron/hooks.ts
Comment on lines +69 to +71
if (jobScripts) {
for (const script of jobScripts) {
if (!isValidJobHookPath(script)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate per-job hook entry types before path checks

loadHookEntries assumes every job.hooks[hookPoint] element is a string and calls isValidJobHookPath directly, but jobs.json is not schema-validated at load time, so malformed entries like [123] make path.isAbsolute throw. In timer/manual execution paths this exception bubbles out before normal result finalization, which can leave runningAtMs markers uncleared and block subsequent runs until stale-run recovery.

Useful? React with 👍 / 👎.

@Arry8
Copy link
Copy Markdown
Author

Arry8 commented Mar 17, 2026

Hey @vincentkoc — you recently merged some cron work (#45459). This PR adds lifecycle hooks for cron job execution and has been sitting open with all Codex findings addressed. Would you be able to take a look when you get a chance? Happy to address any feedback.

@Arry8
Copy link
Copy Markdown
Author

Arry8 commented Mar 18, 2026

Hey @ImLukeF — saw you recently merged some cron work (#16511). This PR adds lifecycle hooks for cron job execution and has been sitting open with all Codex findings addressed. Would you be able to take a look when you get a chance? Happy to address any feedback.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 28, 2026

Codex review: found issues before merge.

Summary
The PR adds cron lifecycle hook config/types, a hook runner, protocol schema, tests, and scheduled/startup/manual execution wiring for beforeRun, afterComplete, onFailure, and afterRun scripts.

Reproducibility: yes. Source inspection shows current main has no cron lifecycle hook config or dispatch, and the PR defects are reproducible by following persisted job hook values through loadHookEntries, isValidJobHookPath, resolveUserPath, and importFileModule.

Next step before merge
Needs maintainer/security approval for the new core cron code-execution surface before repair automation; the mechanical fixes are clear, but the product and trust-boundary decision is not.

Security
Needs attention: The diff introduces a new cron-driven local script execution surface and still has a concrete per-job path restriction bypass.

Review findings

  • [P1] Trim paths before enforcing per-job hook restrictions — src/cron/hooks.ts:201-203
  • [P2] Validate persisted job hooks before iterating — src/cron/hooks.ts:67-71
  • [P2] Reuse stable hook import specifiers — src/cron/hooks.ts:192
Review details

Best possible solution:

Keep this as an implementation candidate only after maintainer approval, then harden persisted hook validation, normalize paths before safety checks, use stable imports or bounded reload semantics, and document the trust boundary before merge.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main has no cron lifecycle hook config or dispatch, and the PR defects are reproducible by following persisted job hook values through loadHookEntries, isValidJobHookPath, resolveUserPath, and importFileModule.

Is this the best way to solve the issue?

No. The feature direction may be useful, but this patch is not the best mergeable form until the path guard, persisted input validation, import caching behavior, docs/changelog, and code-execution trust boundary are resolved.

Full review comments:

  • [P1] Trim paths before enforcing per-job hook restrictions — src/cron/hooks.ts:201-203
    isValidJobHookPath checks the raw persisted string, but loadHookModule later resolves the same value through resolveUserPath, which trims first. A value like " /abs/hook.cjs" or " ../outside/hook.cjs" can pass the guard and then import outside the intended per-job relative scope.
    Confidence: 0.93
  • [P2] Validate persisted job hooks before iterating — src/cron/hooks.ts:67-71
    job.hooks?.[hookPoint] comes from jobs.json and is not proven to be a string array before this loop calls isValidJobHookPath. Malformed persisted values such as [123] can throw in path handling before normal hook isolation/finalization, so guard the array and element types first.
    Confidence: 0.88
  • [P2] Reuse stable hook import specifiers — src/cron/hooks.ts:192
    Every file-backed hook import passes cacheBust: true, and the shared loader appends a timestamp query for that mode. Recurring cron hooks therefore create a fresh ESM module instance on every run, which can grow Gateway memory in long-lived processes.
    Confidence: 0.82
  • [P2] Document the cron hook contract — src/config/types.cron.ts:101-102
    This adds user-facing cron.hooks config and persisted job.hooks code execution, but the PR does not update docs or the changelog. Operators need the trust boundary, path rules, hook points, timeout behavior, abort semantics, and examples documented before this ships.
    Confidence: 0.78

Overall correctness: patch is incorrect
Overall confidence: 0.9

Security concerns:

  • [medium] Per-job hook path restriction can be bypassed — src/cron/hooks.ts:201
    Per-job hooks are intended to be relative filesystem paths, but the safety check uses the raw string while import resolution trims it later. Whitespace-prefixed absolute or traversal paths can evade the restriction and load code outside the intended scope.
    Confidence: 0.93
  • [medium] Dynamic cron script execution needs approval — src/cron/hooks.ts:181
    Global and per-job cron hook settings can execute local modules during scheduled, startup catch-up, and manual runs. This is a new code-execution surface and should be approved with documented operator trust boundaries before merge.
    Confidence: 0.8

What I checked:

  • current-main-no-cron-hooks-config: Current CronConfig has retry, webhook, retention, runLog, failureAlert, and failureDestination fields, but no cron.hooks or cron lifecycle hook types. (src/config/types.cron.ts:31, ecd562b2b5f0)
  • current-main-schema-rejects-cron-hooks: The strict current-main cron Zod schema lists cron settings without a hooks key, so cron.hooks is not currently accepted by config validation. (src/config/zod-schema.ts:620, ecd562b2b5f0)
  • current-main-executes-without-hook-dispatch: The scheduled, startup catch-up, and manual run paths call executeJobCoreWithTimeout directly on current main with no loadHookEntries/runCronHooks lifecycle dispatch. (src/cron/service/timer.ts:887, ecd562b2b5f0)
  • pr-adds-code-execution-surface: The PR head adds CronHookEntry/CronLifecycleHookPoint types and cron.hooks, with hook scripts that can be absolute or workspace-relative for global config. (src/config/types.cron.ts:30, dbe6470ada33)
  • pr-path-trim-bypass: PR head checks raw per-job hook paths in isValidJobHookPath, while resolveUserPath later trims the same script path before import, allowing whitespace-prefixed absolute or traversal paths to evade the guard. (src/cron/hooks.ts:201, dbe6470ada33)
  • pr-unvalidated-persisted-job-hooks: PR head iterates job.hooks[hookPoint] and calls path.isAbsolute through isValidJobHookPath without first proving the persisted value is an array of strings. (src/cron/hooks.ts:67, dbe6470ada33)

Likely related people:

  • vincentkoc: Recent current-main history includes cron dispatch/task boundary refactors, and the PR author explicitly routed review to @vincentkoc for nearby cron work. (role: recent cron and task-boundary maintainer; confidence: high; commits: 1c9053802a, 80ed55332d, 338d313043; files: src/cron/service/timer.ts, src/cron/service/ops.ts)
  • steipete: History shows the original cron scheduler plus many later cron runtime, config, delivery, and cleanup refactors by Peter Steinberger. (role: original and major cron maintainer; confidence: high; commits: f9409cbe43, 115591c5b6, 5d90e31807; files: src/cron/service/timer.ts, src/cron/service/ops.ts, src/cron/types.ts)
  • Tyler Yust: Current-main history includes several cron scheduler reliability, delivery-mode, and job-configuration commits across the central cron types/protocol area. (role: cron configuration and delivery contributor; confidence: medium; commits: d90cac990c, 3f82daefd8, 511c656cbc; files: src/cron/types.ts, src/gateway/protocol/schema/cron.ts, src/cron/service/timer.ts)
  • Tak Hoffman: Current-main history includes cron UI edit parity plus timeout, manual-run, and due-job preservation work near the execution paths this PR changes. (role: cron runtime and UI parity contributor; confidence: medium; commits: 77c3b142a9, 556af3f08b, 73e5bb7635; files: src/cron/service/timer.ts, src/cron/service/ops.ts)
  • ImLukeF: The current PR discussion asks @ImLukeF for review based on recently merged cron work, and related PR feat(cron): support custom session IDs and auto-bind to current session #16511 credits @ImLukeF for helping finish and land cron session-target changes. (role: recent cron reviewer/merger context; confidence: medium; commits: e7d9648fba; files: src/cron/types.ts, src/gateway/protocol/schema/cron.ts)

Remaining risk / open question:

  • The core-vs-plugin direction and operator trust boundary for cron-driven dynamic script execution still need explicit maintainer/security approval.
  • The PR lacks user-facing docs and changelog coverage for path rules, hook ordering, timeout behavior, abort semantics, and the code-execution risk.

Codex review notes: model gpt-5.5, reasoning high; reviewed against ecd562b2b5f0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui gateway Gateway runtime size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants