Skip to content

Commit 12c5296

Browse files
authored
fix: allow cron self-removal in isolated runs (#73028)
1 parent 46783d4 commit 12c5296

22 files changed

Lines changed: 515 additions & 13 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ Docs: https://docs.openclaw.ai
357357
- Discord/gateway: count failed health-monitor restart attempts toward cooldown and hourly caps, and evict stale account lifecycle state during channel reloads so repeated Discord gateway recovery cannot loop on old status. Fixes #38596. (#40413) Thanks @jellyAI-dev and @vashquez.
358358
- TTS/BlueBubbles: pre-transcode synthesized MP3 audio to opus-in-CAF (mono, 24 kHz — validated against macOS 15.x Messages.app's native voice-memo CAF descriptor) on macOS hosts before handing the file to BlueBubbles, so iMessage renders the result as a native voice-memo bubble with proper duration and waveform UI instead of a plain file attachment. Adds an opt-in `tts.voice.preferAudioFileFormat` channel capability and a magic-byte sniff for the CAF container so the host-local-media validator (which uses `file-type` and didn't recognize CAF natively) can verify the pre-transcoded buffer. Channels that don't opt in are unaffected. (#72586) Fixes #72506. Thanks @omarshahine.
359359
- Feishu: retry WebSocket startup failures with monitor-owned backoff while preserving SDK-local heartbeat defaults, so persistent-connection startup failures no longer leave the monitor hung. Fixes #68766; related #42354 and #55532. Thanks @alex-xuweilong, @120106835, @sirfengyu, and @tianhaocui.
360+
- Cron: normalize isolated job tool allowlists before granting the narrow self-removal cron tool path, keeping scheduled jobs aligned with shared tool policy normalization. (#73028) Thanks @jalehman.
360361

361362
## 2026.4.26
362363

src/agents/openclaw-tools.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export function createOpenClawTools(
8989
allowMediaInvokeCommands?: boolean;
9090
/** Explicit agent ID override for cron/hook sessions. */
9191
requesterAgentIdOverride?: string;
92+
/** Restrict the cron tool to self-removing this active cron job. */
93+
cronSelfRemoveOnlyJobId?: string;
9294
/** Require explicit message targets (no implicit last-route sends). */
9395
requireExplicitMessageTarget?: boolean;
9496
/** If true, omit the message tool from the tool list. */
@@ -247,6 +249,9 @@ export function createOpenClawTools(
247249
accountId: options?.agentAccountId,
248250
threadId: options?.currentThreadTs ?? options?.agentThreadId,
249251
},
252+
...(options?.cronSelfRemoveOnlyJobId
253+
? { selfRemoveOnlyJobId: options.cronSelfRemoveOnlyJobId }
254+
: {}),
250255
}),
251256
]),
252257
...(!embedded && messageTool ? [messageTool] : []),

src/agents/openclaw-tools.tts-config.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,26 @@ describe("createOpenClawTools cron context wiring", () => {
302302
},
303303
});
304304
});
305+
306+
it("passes self-remove scope into the cron tool", async () => {
307+
const { createOpenClawTools } = await import("./openclaw-tools.js");
308+
309+
createOpenClawTools({
310+
agentSessionKey: "agent:main:cron:job-current",
311+
cronSelfRemoveOnlyJobId: "job-current",
312+
disableMessageTool: true,
313+
disablePluginTools: true,
314+
});
315+
316+
expect(mocks.createCronToolOptions).toHaveBeenCalledWith({
317+
agentSessionKey: "agent:main:cron:job-current",
318+
currentDeliveryContext: {
319+
channel: undefined,
320+
to: undefined,
321+
accountId: undefined,
322+
threadId: undefined,
323+
},
324+
selfRemoveOnlyJobId: "job-current",
325+
});
326+
});
305327
});

src/agents/pi-embedded-runner/effective-tool-policy.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type FinalEffectiveToolPolicyParams = {
5656
senderUsername?: string | null;
5757
senderE164?: string | null;
5858
senderIsOwner?: boolean;
59+
ownerOnlyToolAllowlist?: string[];
5960
warn: (message: string) => void;
6061
};
6162

@@ -152,6 +153,7 @@ export function applyFinalEffectiveToolPolicy(
152153
const ownerFiltered = applyOwnerOnlyToolPolicy(
153154
params.bundledTools,
154155
params.senderIsOwner === true,
156+
params.ownerOnlyToolAllowlist,
155157
);
156158
// Suppress unavailable-core-tool warnings on every step of this pass.
157159
// `applyToolPolicyPipeline` infers `coreToolNames` from the `tools` array

src/agents/pi-embedded-runner/run.overflow-compaction.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ function makeForwardingCase(internalEvents: AgentInternalEvent[]) {
4343
runId: "forward-attempt-params",
4444
params: {
4545
toolsAllow: ["exec", "read"],
46+
ownerOnlyToolAllowlist: ["cron"],
4647
bootstrapContextMode: "lightweight",
4748
bootstrapContextRunKind: "cron",
4849
disableMessageTool: true,
@@ -52,6 +53,7 @@ function makeForwardingCase(internalEvents: AgentInternalEvent[]) {
5253
},
5354
expected: {
5455
toolsAllow: ["exec", "read"],
56+
ownerOnlyToolAllowlist: ["cron"],
5557
bootstrapContextMode: "lightweight",
5658
bootstrapContextRunKind: "cron",
5759
disableMessageTool: true,

src/agents/pi-embedded-runner/run.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,9 @@ export async function runEmbeddedPiAgent(
988988
silentExpected: params.silentExpected,
989989
bootstrapContextMode: params.bootstrapContextMode,
990990
bootstrapContextRunKind: params.bootstrapContextRunKind,
991+
jobId: params.jobId,
991992
toolsAllow: params.toolsAllow,
993+
ownerOnlyToolAllowlist: params.ownerOnlyToolAllowlist,
992994
disableMessageTool: params.disableMessageTool,
993995
forceMessageTool: params.forceMessageTool,
994996
requireExplicitMessageTarget: params.requireExplicitMessageTarget,

src/agents/pi-embedded-runner/run/attempt.memory-flush-forwarding.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ describe("runEmbeddedAttempt memory flush tool forwarding", () => {
4949
}
5050
});
5151

52+
it("forwards cron job id into tool creation so self-removal can be scoped", () => {
53+
expect(
54+
buildEmbeddedAttemptToolRunContext({
55+
trigger: "cron",
56+
jobId: "job-current",
57+
}),
58+
).toMatchObject({
59+
trigger: "cron",
60+
jobId: "job-current",
61+
});
62+
});
63+
5264
it("activates the memory flush append-only write wrapper", async () => {
5365
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-attempt-memory-flush-"));
5466
const memoryFile = path.join(workspaceDir, MEMORY_RELATIVE_PATH);

src/agents/pi-embedded-runner/run/attempt.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ describe("applyEmbeddedAttemptToolsAllow", () => {
7272
applyEmbeddedAttemptToolsAllow(tools, ["exec", "read"]).map((tool) => tool.name),
7373
).toEqual(["exec", "read"]);
7474
});
75+
76+
it("normalizes explicit toolsAllow entries before filtering", () => {
77+
const tools = [{ name: "cron" }, { name: "read" }, { name: "message" }];
78+
79+
expect(
80+
applyEmbeddedAttemptToolsAllow(tools, [" cron ", "READ"]).map((tool) => tool.name),
81+
).toEqual(["cron", "read"]);
82+
});
7583
});
7684

7785
describe("normalizeMessagesForLlmBoundary", () => {

src/agents/pi-embedded-runner/run/attempt.tool-run-context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import type { EmbeddedRunTrigger } from "./params.js";
66

77
export function buildEmbeddedAttemptToolRunContext(params: {
88
trigger?: EmbeddedRunTrigger;
9+
jobId?: string;
910
memoryFlushWritePath?: string;
1011
trace?: DiagnosticTraceContext;
1112
}): {
1213
trigger?: EmbeddedRunTrigger;
14+
jobId?: string;
1315
memoryFlushWritePath?: string;
1416
trace?: DiagnosticTraceContext;
1517
} {
1618
return {
1719
trigger: params.trigger,
20+
jobId: params.jobId,
1821
memoryFlushWritePath: params.memoryFlushWritePath,
1922
...(params.trace ? { trace: freezeDiagnosticTraceContext(params.trace) } : {}),
2023
};

src/agents/pi-embedded-runner/run/attempt.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ import {
157157
collectExplicitToolAllowlistSources,
158158
} from "../../tool-allowlist-guard.js";
159159
import { UNKNOWN_TOOL_THRESHOLD } from "../../tool-loop-detection.js";
160+
import { normalizeToolName } from "../../tool-policy.js";
160161
import { shouldAllowProviderOwnedThinkingReplay } from "../../transcript-policy.js";
161162
import { normalizeUsage, type NormalizedUsage } from "../../usage.js";
162163
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
@@ -480,8 +481,8 @@ export function applyEmbeddedAttemptToolsAllow<T extends { name: string }>(
480481
if (!toolsAllow || toolsAllow.length === 0) {
481482
return tools;
482483
}
483-
const allowSet = new Set(toolsAllow);
484-
return tools.filter((tool) => allowSet.has(tool.name));
484+
const allowSet = new Set(toolsAllow.map((name) => normalizeToolName(name)));
485+
return tools.filter((tool) => allowSet.has(normalizeToolName(tool.name)));
485486
}
486487

487488
export function normalizeMessagesForLlmBoundary(messages: AgentMessage[]): AgentMessage[] {
@@ -718,6 +719,7 @@ export async function runEmbeddedAttempt(
718719
senderUsername: params.senderUsername,
719720
senderE164: params.senderE164,
720721
senderIsOwner: params.senderIsOwner,
722+
ownerOnlyToolAllowlist: params.ownerOnlyToolAllowlist,
721723
allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
722724
sessionKey: sandboxSessionKey,
723725
sessionId: params.sessionId,
@@ -929,6 +931,7 @@ export async function runEmbeddedAttempt(
929931
senderUsername: params.senderUsername,
930932
senderE164: params.senderE164,
931933
senderIsOwner: params.senderIsOwner,
934+
ownerOnlyToolAllowlist: params.ownerOnlyToolAllowlist,
932935
warn: (message) => log.warn(message),
933936
});
934937
const effectiveTools = [...tools, ...filteredBundledTools];

0 commit comments

Comments
 (0)