Conversation
5e465aa to
852d377
Compare
c5464b6 to
d074005
Compare
🔒 Aisle Security AnalysisWe found 5 potential security issue(s) in this PR:
1. 🟠 Untrusted plugins can obtain gateway CronService handle via ctx.getCron() and manipulate cron jobs
DescriptionIn
Vulnerable code: const hookCtx: PluginHookGatewayContext = {
config: getRuntimeConfig(),
getCron: () => cron as PluginHookGatewayCronService,
};RecommendationDo not expose the concrete
Example (capability wrapper): const cronApi: PluginHookGatewayCronService = {
list: (opts) => cron.list(opts).then(jobs => jobs.map(toPluginCronJob)),
add: (input) => { throw new Error("not permitted"); },
update: (id, patch) => { throw new Error("not permitted"); },
remove: (id) => { throw new Error("not permitted"); },
};
const hookCtx: PluginHookGatewayContext = {
config: getRuntimeConfig(),
getCron: () => cronApi,
};If mutation must be supported, add explicit policy checks in the wrapper before calling into 2. 🟠 Sensitive runtime configuration exposed to plugins via cron_changed hook context
DescriptionThe
Vulnerable code: const hookCtx: PluginHookGatewayContext = {
config: getRuntimeConfig(),
getCron: () => cron as PluginHookGatewayCronService,
};
void hookRunner.runCronChanged(evt, hookCtx)RecommendationAvoid passing the full runtime config into plugin hooks. Recommended options:
import { toPluginSafeConfig } from "../plugins/config-sanitize.js";
const hookCtx: PluginHookGatewayContext = {
config: toPluginSafeConfig(getRuntimeConfig()),
getCron: () => cron as PluginHookGatewayCronService,
};Where
Also document clearly in the plugin SDK whether hooks can access secrets, and treat any plugin that receives secrets as fully trusted. 3. 🟡 Sensitive CronJob payload fields leaked to plugins via cron_changed hook
Description
Because This is an information disclosure risk when plugins are untrusted or have weaker trust boundaries than the core gateway. Vulnerable code: payload: job.payload ? structuredClone(job.payload) : undefined,Relevant internal payload shape includes additional fields beyond export type CronPayload = { kind: "systemEvent"; text: string } | CronAgentTurnPayload;
// CronAgentTurnPayloadFields includes model/fallbacks/toolsAllow/externalContentSource/etc.RecommendationDo not clone and forward the internal payload verbatim. Instead, explicitly construct the public payload shape and drop/deny-list internal fields. Example (allow only systemEvent payload): function toPluginCronPayload(payload: CronPayload | undefined): { kind: string; text?: string } | undefined {
if (!payload) return undefined;
if (payload.kind === "systemEvent") {
return { kind: payload.kind, text: payload.text };
}
// For agentTurn or other internal kinds, either omit entirely or provide a redacted summary
return { kind: payload.kind };
}
function toPluginCronJob(job: CronJob): PluginHookGatewayCronJob {
return {
id: job.id,
// ...
payload: toPluginCronPayload(job.payload),
};
}If some agentTurn fields are intended to be public, define a separate public payload type and whitelist only those keys. Add tests to ensure extra keys (e.g. 4. 🟡 Sensitive CronJob data broadcast over gateway "cron" event (CronEvent.job snapshot)
Description
This creates a risk of information disclosure to any connected/authorized UI clients (and any other consumers of the gateway broadcast channel) that were previously only receiving the smaller Vulnerable code: onEvent: (evt) => {
params.broadcast("cron", evt, { dropIfSlow: true });
// ...
}RecommendationDo not broadcast the internal Options:
onEvent: (evt) => {
const publicEvt = {
action: evt.action,
jobId: evt.jobId,
// include only minimal safe fields
...(evt.job ? { job: { id: evt.job.id, name: evt.job.name, enabled: evt.job.enabled } } : {}),
runAtMs: evt.runAtMs,
durationMs: evt.durationMs,
status: evt.status,
summary: evt.summary,
nextRunAtMs: evt.nextRunAtMs,
};
params.broadcast("cron", publicEvt, { dropIfSlow: true });
};
Also consider adding a unit test asserting that 5. 🟡 Unbounded async execution of cron_changed plugin hooks can exhaust resources
DescriptionThe gateway fires
Vulnerable code: void hookRunner.runCronChanged(evt, hookCtx).catch((err) => {
cronLogger.warn({ err: formatErrorMessage(err), jobId: evt.jobId }, "cron_changed hook failed");
});RecommendationAdd backpressure for Options (choose one):
import pLimit from "p-limit";
const limit = pLimit(1); // or small N
const runCronChangedHook = (evt: PluginHookCronChangedEvent) => {
const hookRunner = getGlobalHookRunner();
if (!hookRunner?.hasHooks("cron_changed")) return;
const hookCtx: PluginHookGatewayContext = {
config: getRuntimeConfig(),
getCron: () => cron as PluginHookGatewayCronService,
};
void limit(() => hookRunner.runCronChanged(evt, hookCtx))
.catch((err) => cronLogger.warn({ err: formatErrorMessage(err), jobId: evt.jobId }, "cron_changed hook failed"));
};
const DEFAULT_VOID_HOOK_TIMEOUT_MS_BY_HOOK = {
agent_end: 30_000,
cron_changed: 5_000,
};This prevents unbounded buildup of pending promises when hooks are slow or never resolve. Analyzed PR: #72773 at commit Last updated on: 2026-04-28T06:26:34Z |
Greptile SummaryThis PR adds a typed Confidence Score: 4/5Safe to merge — no logic bugs found; all findings are style/documentation P2s. Implementation is correct: job snapshots are taken synchronously, the src/plugins/hook-types.ts — the Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/plugins/hook-types.ts
Line: 621-624
Comment:
**`agentTurn` payload fields silently dropped from the public type**
The public `payload` type only exposes `{ kind?: string; text?: string }`, but `CronPayload` also includes `CronAgentTurnPayload` which carries `message`, `model`, `fallbacks`, `thinking`, `timeoutSeconds`, and other fields. `toPluginCronJob` assigns `job.payload` directly, so those extra fields are present at runtime but invisible to the type system — plugin authors querying an `agentTurn` job's payload won't get typed access to `message` or the model override. Consider widening the union or at minimum documenting which variant fields are exposed.
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/gateway/server-cron.test.ts
Line: 194-197
Comment:
**Assert mock called before indexing into `mock.calls`**
`calls[0]?.[1]` uses optional chaining, so if `runCronChangedMock` is never invoked, `hookCtx` is `undefined` and the test fails with `expect(undefined).toBe(runtimeCfg)` rather than a clear "mock was not called" message. Adding `expect(runCronChangedMock).toHaveBeenCalledTimes(1)` before the `mock.calls` access would surface the real failure immediately.
```suggestion
expect(runCronChangedMock).toHaveBeenCalledTimes(1);
const calls = runCronChangedMock.mock.calls as unknown[][];
```
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/service/state.ts
Line: 19-20
Comment:
**JSDoc comment overstates guarantee**
The comment says `job` is "Included for all actions", but the field is typed as `job?: CronJob`. In the `remove` path in `ops.ts`, `job` is set to `removedJob` which TypeScript types as `CronJob | undefined` (even though logically it's always defined when the event fires). Consider softening the comment to "Included for all actions where the job is accessible" or narrowing the type to `CronJob` and asserting non-null at the call site.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix: improve cron_changed hook correctne..." | Re-trigger Greptile |
| payload?: { | ||
| kind?: string; | ||
| text?: string; | ||
| }; |
There was a problem hiding this comment.
agentTurn payload fields silently dropped from the public type
The public payload type only exposes { kind?: string; text?: string }, but CronPayload also includes CronAgentTurnPayload which carries message, model, fallbacks, thinking, timeoutSeconds, and other fields. toPluginCronJob assigns job.payload directly, so those extra fields are present at runtime but invisible to the type system — plugin authors querying an agentTurn job's payload won't get typed access to message or the model override. Consider widening the union or at minimum documenting which variant fields are exposed.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/hook-types.ts
Line: 621-624
Comment:
**`agentTurn` payload fields silently dropped from the public type**
The public `payload` type only exposes `{ kind?: string; text?: string }`, but `CronPayload` also includes `CronAgentTurnPayload` which carries `message`, `model`, `fallbacks`, `thinking`, `timeoutSeconds`, and other fields. `toPluginCronJob` assigns `job.payload` directly, so those extra fields are present at runtime but invisible to the type system — plugin authors querying an `agentTurn` job's payload won't get typed access to `message` or the model override. Consider widening the union or at minimum documenting which variant fields are exposed.
How can I resolve this? If you propose a fix, please make it concise.| ); | ||
| } finally { | ||
| state.cron.stop(); | ||
| } |
There was a problem hiding this comment.
Assert mock called before indexing into
mock.calls
calls[0]?.[1] uses optional chaining, so if runCronChangedMock is never invoked, hookCtx is undefined and the test fails with expect(undefined).toBe(runtimeCfg) rather than a clear "mock was not called" message. Adding expect(runCronChangedMock).toHaveBeenCalledTimes(1) before the mock.calls access would surface the real failure immediately.
| ); | |
| } finally { | |
| state.cron.stop(); | |
| } | |
| expect(runCronChangedMock).toHaveBeenCalledTimes(1); | |
| const calls = runCronChangedMock.mock.calls as unknown[][]; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-cron.test.ts
Line: 194-197
Comment:
**Assert mock called before indexing into `mock.calls`**
`calls[0]?.[1]` uses optional chaining, so if `runCronChangedMock` is never invoked, `hookCtx` is `undefined` and the test fails with `expect(undefined).toBe(runtimeCfg)` rather than a clear "mock was not called" message. Adding `expect(runCronChangedMock).toHaveBeenCalledTimes(1)` before the `mock.calls` access would surface the real failure immediately.
```suggestion
expect(runCronChangedMock).toHaveBeenCalledTimes(1);
const calls = runCronChangedMock.mock.calls as unknown[][];
```
How can I resolve this? If you propose a fix, please make it concise.| /** Snapshot of the job at the time of the event. Included for all actions. */ | ||
| job?: CronJob; |
There was a problem hiding this comment.
JSDoc comment overstates guarantee
The comment says job is "Included for all actions", but the field is typed as job?: CronJob. In the remove path in ops.ts, job is set to removedJob which TypeScript types as CronJob | undefined (even though logically it's always defined when the event fires). Consider softening the comment to "Included for all actions where the job is accessible" or narrowing the type to CronJob and asserting non-null at the call site.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/service/state.ts
Line: 19-20
Comment:
**JSDoc comment overstates guarantee**
The comment says `job` is "Included for all actions", but the field is typed as `job?: CronJob`. In the `remove` path in `ops.ts`, `job` is set to `removedJob` which TypeScript types as `CronJob | undefined` (even though logically it's always defined when the event fires). Consider softening the comment to "Included for all actions where the job is accessible" or narrowing the type to `CronJob` and asserting non-null at the call site.
How can I resolve this? If you propose a fix, please make it concise.- Fix PluginHookGatewayCronDeliveryStatus: replace 'error' with 'unknown' to match internal CronDeliveryStatus enum - Add job snapshot to CronEvent so removed events carry the deleted job - Extract pickDefined helper, replace 14-field verbose spread mapping - Add toPluginCronJob mapper for explicit internal→public type boundary - Fix schedule union: use literal-only kind discriminants for TS narrowing - Use loadConfig() (runtime) instead of params.cfg (startup) in hook ctx - Use formatErrorMessage instead of String(err) for stack preservation - Fix pre-existing getCron TS2322 with explicit cast (matches gateway_start) - Re-export supporting types from hooks.ts for plugin consumers - Add tests: removed events with job, finished with full fields, runtime cfg
7837b07 to
71ef0da
Compare
- docs/plugins/hooks.md: add `cron_changed` to the Lifecycle hook catalog and a Gateway lifecycle paragraph describing its typed event payload, run status, delivery status, and removed-event job snapshot, so plugin authors picking up f155a5f (#72773) have a canonical reference beyond the sdk-overview bullet that already shipped in the same SHA. - docs/help/environment.md: add a "Legacy environment variables" section for aa1834a so users see that `CLAWDBOT_*` and `MOLTBOT_*` prefixes are now ignored and trigger an `OPENCLAW_LEGACY_ENV_VARS` deprecation warning, with a rename example to `OPENCLAW_*`.
* feat: add cron changed plugin hook * fix: improve cron_changed hook correctness and code quality - Fix PluginHookGatewayCronDeliveryStatus: replace 'error' with 'unknown' to match internal CronDeliveryStatus enum - Add job snapshot to CronEvent so removed events carry the deleted job - Extract pickDefined helper, replace 14-field verbose spread mapping - Add toPluginCronJob mapper for explicit internal→public type boundary - Fix schedule union: use literal-only kind discriminants for TS narrowing - Use loadConfig() (runtime) instead of params.cfg (startup) in hook ctx - Use formatErrorMessage instead of String(err) for stack preservation - Fix pre-existing getCron TS2322 with explicit cast (matches gateway_start) - Re-export supporting types from hooks.ts for plugin consumers - Add tests: removed events with job, finished with full fields, runtime cfg
- docs/plugins/hooks.md: add `cron_changed` to the Lifecycle hook catalog and a Gateway lifecycle paragraph describing its typed event payload, run status, delivery status, and removed-event job snapshot, so plugin authors picking up cd03596 (openclaw#72773) have a canonical reference beyond the sdk-overview bullet that already shipped in the same SHA. - docs/help/environment.md: add a "Legacy environment variables" section for b832211 so users see that `CLAWDBOT_*` and `MOLTBOT_*` prefixes are now ignored and trigger an `OPENCLAW_LEGACY_ENV_VARS` deprecation warning, with a rename example to `OPENCLAW_*`.
Summary
cron_changedplugin hook emitted from gateway cron lifecycle events