Skip to content

Commit b02ab0c

Browse files
mmapseleqtrizit
authored andcommitted
fix(cron): cap edited scheduled turns
1 parent 49b18dd commit b02ab0c

6 files changed

Lines changed: 256 additions & 24 deletions

File tree

src/agents/agent-tools.create-openclaw-coding-tools.test.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ function expectListIncludes(
154154
}
155155
}
156156

157+
function cronCreatorToolNames(
158+
list: OpenClawToolsOptions["cronCreatorToolAllowlist"] | undefined,
159+
): string[] | undefined {
160+
return list?.map((entry) => (typeof entry === "string" ? entry : entry.name));
161+
}
162+
157163
describe("createOpenClawCodingTools", () => {
158164
const testConfig: OpenClawConfig = {};
159165

@@ -761,9 +767,10 @@ describe("createOpenClawCodingTools", () => {
761767

762768
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
763769
const cronAllow = latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist;
764-
expectListIncludes(cronAllow, ["read", "cron"]);
765-
expect(cronAllow?.includes("exec")).toBe(false);
766-
expect(cronAllow?.includes("process")).toBe(false);
770+
const cronAllowNames = cronCreatorToolNames(cronAllow);
771+
expectListIncludes(cronAllowNames, ["read", "cron"]);
772+
expect(cronAllowNames?.includes("exec")).toBe(false);
773+
expect(cronAllowNames?.includes("process")).toBe(false);
767774
});
768775

769776
it("passes deny-restricted tool surface to cron-created agent turns", () => {
@@ -788,9 +795,10 @@ describe("createOpenClawCodingTools", () => {
788795

789796
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
790797
const cronAllow = latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist;
791-
expectListIncludes(cronAllow, ["read", "cron"]);
792-
expect(cronAllow?.includes("exec")).toBe(false);
793-
expect(cronAllow?.includes("process")).toBe(false);
798+
const cronAllowNames = cronCreatorToolNames(cronAllow);
799+
expectListIncludes(cronAllowNames, ["read", "cron"]);
800+
expect(cronAllowNames?.includes("exec")).toBe(false);
801+
expect(cronAllowNames?.includes("process")).toBe(false);
794802
});
795803

796804
it("records core tool-prep stages for hot-path diagnostics", () => {

src/agents/agent-tools.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,34 @@ import { resolveWorkspaceRoot } from "./workspace-dir.js";
121121
const MEMORY_FLUSH_ALLOWED_TOOL_NAMES = new Set(["read", "write"]);
122122

123123
function hasExplicitDenyPolicy(policy?: { deny?: string[] }): boolean {
124-
return Array.isArray(policy?.deny) && policy.deny.some((entry) => entry.trim());
124+
return (
125+
Array.isArray(policy?.deny) &&
126+
policy.deny.some((entry) => typeof entry === "string" && entry.trim())
127+
);
128+
}
129+
130+
type EffectiveCronCreatorTool = {
131+
name: string;
132+
pluginId?: string;
133+
};
134+
135+
function replaceWithEffectiveCronCreatorToolAllowlist(
136+
target: EffectiveCronCreatorTool[],
137+
tools: AnyAgentTool[],
138+
): void {
139+
target.length = 0;
140+
const seen = new Set<string>();
141+
for (const tool of tools) {
142+
const name = normalizeToolName(tool.name);
143+
if (!name || seen.has(name)) {
144+
continue;
145+
}
146+
seen.add(name);
147+
const meta = getPluginToolMeta(tool);
148+
const pluginId =
149+
typeof meta?.pluginId === "string" ? normalizeToolName(meta.pluginId) : undefined;
150+
target.push(pluginId ? { name, pluginId } : { name });
151+
}
125152
}
126153

127154
type GuardContainerMount = {
@@ -942,7 +969,7 @@ export function createOpenClawCodingTools(options?: {
942969
];
943970
const shouldInheritEffectiveToolAllowlist =
944971
toolPolicyInheritanceSources.some(hasRestrictiveAllowPolicy);
945-
const cronCreatorToolAllowlist: string[] = [];
972+
const cronCreatorToolAllowlist: EffectiveCronCreatorTool[] = [];
946973
const shouldCaptureCronCreatorToolAllowlist = toolPolicyInheritanceSources.some(
947974
(policy) => hasRestrictiveAllowPolicy(policy) || hasExplicitDenyPolicy(policy),
948975
);
@@ -1179,7 +1206,7 @@ export function createOpenClawCodingTools(options?: {
11791206
replaceWithEffectiveToolAllowlist(inheritedToolAllowlist, subagentFiltered);
11801207
}
11811208
if (shouldCaptureCronCreatorToolAllowlist) {
1182-
replaceWithEffectiveToolAllowlist(cronCreatorToolAllowlist, subagentFiltered);
1209+
replaceWithEffectiveCronCreatorToolAllowlist(cronCreatorToolAllowlist, subagentFiltered);
11831210
}
11841211
options?.recordToolPrepStage?.("authorization-policy");
11851212
// Always normalize tool JSON Schemas before handing them to OpenClaw model runtime.

src/agents/openclaw-tools.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import type { ToolFsPolicy } from "./tool-fs-policy.js";
4141
import { resolveToolLoopDetectionConfig } from "./tool-loop-detection-config.js";
4242
import { createAgentsListTool } from "./tools/agents-list-tool.js";
4343
import type { AnyAgentTool } from "./tools/common.js";
44-
import { createCronTool } from "./tools/cron-tool.js";
44+
import { createCronTool, type CronCreatorToolAllowlistEntry } from "./tools/cron-tool.js";
4545
import { createEmbeddedCallGateway } from "./tools/embedded-gateway-stub.js";
4646
import { createGatewayTool } from "./tools/gateway-tool.js";
4747
import {
@@ -110,7 +110,7 @@ export function createOpenClawTools(
110110
pluginToolAllowlist?: string[];
111111
pluginToolDenylist?: string[];
112112
/** Effective caller tool surface to persist on isolated cron agentTurn jobs. */
113-
cronCreatorToolAllowlist?: string[];
113+
cronCreatorToolAllowlist?: CronCreatorToolAllowlistEntry[];
114114
/** Current channel ID for auto-threading. */
115115
currentChannelId?: string;
116116
/** Routable target for the current conversation when it differs from the native channel ID. */

src/agents/tools/cron-tool.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,65 @@ describe("cron tool", () => {
900900
expect(params?.payload?.toolsAllow).toEqual(["read", "cron"]);
901901
});
902902

903+
it("expands plugin selectors against the creator tool surface on agentTurn adds", async () => {
904+
const tool = createTestCronTool({
905+
agentSessionKey: "agent:main:telegram:group:restricted-room",
906+
creatorToolAllowlist: [
907+
{ name: "active_memory_search", pluginId: "active-memory" },
908+
{ name: "active_memory_store", pluginId: "active-memory" },
909+
{ name: "cron" },
910+
],
911+
});
912+
913+
await tool.execute("call-capped-add-plugin-tools", {
914+
action: "add",
915+
job: {
916+
...buildReminderAgentTurnJob(),
917+
payload: {
918+
kind: "agentTurn",
919+
message: "hello",
920+
toolsAllow: ["active-memory", "cron", "exec"],
921+
},
922+
},
923+
});
924+
925+
const params = expectSingleGatewayCallMethod("cron.add") as
926+
| { payload?: { toolsAllow?: string[] } }
927+
| undefined;
928+
expect(params?.payload?.toolsAllow).toEqual([
929+
"active_memory_search",
930+
"active_memory_store",
931+
"cron",
932+
]);
933+
});
934+
935+
it("expands group:plugins against the creator tool surface on agentTurn adds", async () => {
936+
const tool = createTestCronTool({
937+
agentSessionKey: "agent:main:telegram:group:restricted-room",
938+
creatorToolAllowlist: [
939+
{ name: "active_memory_search", pluginId: "active-memory" },
940+
{ name: "cron" },
941+
],
942+
});
943+
944+
await tool.execute("call-capped-add-plugin-group", {
945+
action: "add",
946+
job: {
947+
...buildReminderAgentTurnJob(),
948+
payload: {
949+
kind: "agentTurn",
950+
message: "hello",
951+
toolsAllow: ["group:plugins"],
952+
},
953+
},
954+
});
955+
956+
const params = expectSingleGatewayCallMethod("cron.add") as
957+
| { payload?: { toolsAllow?: string[] } }
958+
| undefined;
959+
expect(params?.payload?.toolsAllow).toEqual(["active_memory_search"]);
960+
});
961+
903962
it("recovers concatenated cron add keys from local tool-call parsers", async () => {
904963
const tool = createTestCronTool();
905964
await tool.execute("call-concatenated-add", {
@@ -2020,6 +2079,44 @@ describe("cron tool", () => {
20202079
});
20212080
});
20222081

2082+
it("adds the creator tool surface when updating an existing agentTurn without a payload patch", async () => {
2083+
callGatewayMock
2084+
.mockResolvedValueOnce({
2085+
id: "job-9",
2086+
payload: { kind: "agentTurn", message: "hello" },
2087+
})
2088+
.mockResolvedValueOnce({ ok: true });
2089+
2090+
const tool = createTestCronTool({
2091+
agentSessionKey: "agent:main:telegram:group:restricted-room",
2092+
creatorToolAllowlist: ["read", "cron"],
2093+
});
2094+
await tool.execute("call-update-capped-no-payload", {
2095+
action: "update",
2096+
id: "job-9",
2097+
patch: { enabled: false },
2098+
});
2099+
2100+
expect(callGatewayMock).toHaveBeenCalledTimes(2);
2101+
expect(readGatewayCall(0)).toEqual({
2102+
method: "cron.get",
2103+
params: { id: "job-9" },
2104+
});
2105+
expect(readGatewayCall(1)).toEqual({
2106+
method: "cron.update",
2107+
params: {
2108+
id: "job-9",
2109+
patch: {
2110+
enabled: false,
2111+
payload: {
2112+
kind: "agentTurn",
2113+
toolsAllow: ["read", "cron"],
2114+
},
2115+
},
2116+
},
2117+
});
2118+
});
2119+
20232120
it("preserves null model payload patches on update", async () => {
20242121
callGatewayMock.mockResolvedValueOnce({ ok: true });
20252122

0 commit comments

Comments
 (0)