diff --git a/src/agents/agent-tools-agent-config.test.ts b/src/agents/agent-tools-agent-config.test.ts index 0d689eedbae5..d101e8afc0d4 100644 --- a/src/agents/agent-tools-agent-config.test.ts +++ b/src/agents/agent-tools-agent-config.test.ts @@ -200,7 +200,7 @@ describe("Agent-specific tool filtering", () => { const toolNames = tools.map((t) => t.name); expect(toolNames).toContain("read"); expect(toolNames).not.toContain("browser"); - expect(toolNames).not.toContain("cron"); + expect(toolNames).not.toContain("automations"); expect(toolNames).not.toContain("message"); }); @@ -502,7 +502,7 @@ describe("Agent-specific tool filtering", () => { expect(ownerTools).toContain("exec"); expect(ownerTools).toContain("process"); - expect(ownerTools).toContain("cron"); + expect(ownerTools).toContain("automations"); expect(ownerTools).toContain("gateway"); expect(ownerTools).toContain("nodes"); expect(ownerTools).toContain("openclaw"); @@ -511,7 +511,7 @@ describe("Agent-specific tool filtering", () => { expect(ownerTools).toContain("conversations_turn"); expect(nonOwnerTools).not.toContain("exec"); expect(nonOwnerTools).not.toContain("process"); - expect(nonOwnerTools).not.toContain("cron"); + expect(nonOwnerTools).not.toContain("automations"); expect(nonOwnerTools).not.toContain("gateway"); expect(nonOwnerTools).not.toContain("nodes"); expect(nonOwnerTools).not.toContain("openclaw"); diff --git a/src/agents/agent-tools.availability.test.ts b/src/agents/agent-tools.availability.test.ts index 955f532b4278..fd2458c1f0d8 100644 --- a/src/agents/agent-tools.availability.test.ts +++ b/src/agents/agent-tools.availability.test.ts @@ -28,7 +28,7 @@ describe("tool availability", () => { const tools = createOpenClawCodingTools(); const toolNames = tools.map((tool) => tool.name); expect(toolNames).toContain("plugin_login"); - expect(toolNames).toContain("cron"); + expect(toolNames).toContain("automations"); expect(toolNames).toContain("gateway"); expect(toolNames).toContain("nodes"); expect(toolNames).toContain("openclaw"); diff --git a/src/agents/agent-tools.create-openclaw-coding-tools.test.ts b/src/agents/agent-tools.create-openclaw-coding-tools.test.ts index 17ac62473158..a5d5f00a8340 100644 --- a/src/agents/agent-tools.create-openclaw-coding-tools.test.ts +++ b/src/agents/agent-tools.create-openclaw-coding-tools.test.ts @@ -422,7 +422,7 @@ describe("createOpenClawCodingTools", () => { }); const names = new Set(tools.map((tool) => tool.name)); - expect(names.has("cron")).toBe(true); + expect(names.has("automations")).toBe(true); expect(names.has("gateway")).toBe(true); expect(names.has("nodes")).toBe(true); }); @@ -432,13 +432,13 @@ describe("createOpenClawCodingTools", () => { createOpenClawCodingTools({ config: testConfig, }), - ["cron"], + ["automations"], ); - expect(allowed.map((tool) => tool.name)).toEqual(["cron"]); + expect(allowed.map((tool) => tool.name)).toEqual(["automations"]); expect( buildEmptyExplicitToolAllowlistError({ - sources: [{ label: "runtime toolsAllow", entries: ["cron"] }], + sources: [{ label: "runtime toolsAllow", entries: ["automations"] }], callableToolNames: allowed.map((tool) => tool.name), toolsEnabled: true, }), @@ -1253,12 +1253,12 @@ describe("createOpenClawCodingTools", () => { createOpenClawCodingTools({ sessionKey: "agent:main:whatsapp:group:restricted-room", config: { - tools: { allow: ["read", "exec", "process", "cron"] }, + tools: { allow: ["read", "exec", "process", "automations"] }, channels: { whatsapp: { groups: { "restricted-room": { - tools: { allow: ["read", "cron"] }, + tools: { allow: ["read", "automations"] }, }, }, }, @@ -1269,7 +1269,7 @@ describe("createOpenClawCodingTools", () => { expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1); const cronAllow = latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist; const cronAllowNames = cronCreatorToolNames(cronAllow); - expectListIncludes(cronAllowNames, ["read", "cron"]); + expectListIncludes(cronAllowNames, ["read", "automations"]); expect(cronAllowNames?.includes("exec")).toBe(false); expect(cronAllowNames?.includes("process")).toBe(false); }); @@ -1284,7 +1284,7 @@ describe("createOpenClawCodingTools", () => { const cronAllowNames = cronCreatorToolNames( latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist, ); - expectListIncludes(cronAllowNames, ["read", "cron", "exec"]); + expectListIncludes(cronAllowNames, ["read", "automations", "exec"]); }); it("lets embedded attempts refresh a caller-owned cron creator tool surface", () => { @@ -1295,22 +1295,22 @@ describe("createOpenClawCodingTools", () => { > = []; createOpenClawCodingTools({ - config: { tools: { allow: ["read", "cron"] } }, + config: { tools: { allow: ["read", "automations"] } }, cronCreatorToolAllowlistRef, }); expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1); const cronAllow = latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist; expect(cronAllow).toBe(cronCreatorToolAllowlistRef); - expect(cronCreatorToolNames(cronAllow)).toEqual(["read", "cron"]); + expect(cronCreatorToolNames(cronAllow)).toEqual(["read", "automations"]); replaceWithEffectiveCronCreatorToolAllowlist(cronCreatorToolAllowlistRef, [ stubTool("read"), - stubTool("cron"), + stubTool("automations"), stubTool("bundle_mcp_search"), ]); - expect(cronCreatorToolNames(cronAllow)).toEqual(["read", "cron", "bundle_mcp_search"]); + expect(cronCreatorToolNames(cronAllow)).toEqual(["read", "automations", "bundle_mcp_search"]); }); it("passes deny-restricted tool surface to cron-created agent turns", () => { @@ -1320,7 +1320,7 @@ describe("createOpenClawCodingTools", () => { createOpenClawCodingTools({ sessionKey: "agent:main:whatsapp:group:restricted-room", config: { - tools: { allow: ["read", "exec", "process", "cron"] }, + tools: { allow: ["read", "exec", "process", "automations"] }, channels: { whatsapp: { groups: { @@ -1336,7 +1336,7 @@ describe("createOpenClawCodingTools", () => { expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1); const cronAllow = latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist; const cronAllowNames = cronCreatorToolNames(cronAllow); - expectListIncludes(cronAllowNames, ["read", "cron"]); + expectListIncludes(cronAllowNames, ["read", "automations"]); expect(cronAllowNames?.includes("exec")).toBe(false); expect(cronAllowNames?.includes("process")).toBe(false); }); @@ -1374,7 +1374,7 @@ describe("createOpenClawCodingTools", () => { it("preserves action enums in normalized schemas", () => { const defaultTools = createOpenClawCodingTools({ config: testConfig }); - const toolNames = ["canvas", "nodes", "cron", "gateway", "message"]; + const toolNames = ["canvas", "nodes", "automations", "gateway", "message"]; const missingNames = toolNames.filter( (name) => !defaultTools.some((candidate) => candidate.name === name), ); @@ -1691,7 +1691,7 @@ describe("createOpenClawCodingTools", () => { expect(names.has("browser")).toBe(true); expect(names.has("canvas")).toBe(true); expect(names.has("gateway")).toBe(true); - expect(names.has("cron")).toBe(true); + expect(names.has("automations")).toBe(true); expect(names.has("nodes")).toBe(true); }); diff --git a/src/agents/agent-tools.cron-scope.test.ts b/src/agents/agent-tools.cron-scope.test.ts index 3b1a0434d039..54b673563e2a 100644 --- a/src/agents/agent-tools.cron-scope.test.ts +++ b/src/agents/agent-tools.cron-scope.test.ts @@ -28,7 +28,7 @@ vi.mock("./openclaw-tools.js", async (importOriginal) => { return { createOpenClawTools: (options: unknown) => { mocks.createOpenClawToolsOptions(options); - return [mocks.stubTool("cron")]; + return [mocks.stubTool(AUTOMATIONS_TOOL_NAME)]; }, filterToolsByClientCaps: actual.filterToolsByClientCaps, }; @@ -37,6 +37,7 @@ vi.mock("./openclaw-tools.js", async (importOriginal) => { import "./test-helpers/fast-bash-tools.js"; import "./test-helpers/fast-coding-tools.js"; import { createOpenClawCodingTools } from "./agent-tools.js"; +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; function firstOpenClawToolsOptions(): { cronSelfRemoveOnlyJobId?: string } | undefined { return mocks.createOpenClawToolsOptions.mock.calls[0]?.[0] as @@ -55,7 +56,7 @@ describe("createOpenClawCodingTools cron scope", () => { jobId: "job-current", }); - expect(tools.map((tool) => tool.name)).toContain("cron"); + expect(tools.map((tool) => tool.name)).toContain(AUTOMATIONS_TOOL_NAME); expect(firstOpenClawToolsOptions()?.cronSelfRemoveOnlyJobId).toBe("job-current"); }); diff --git a/src/agents/agent-tools.deferred-followup.ts b/src/agents/agent-tools.deferred-followup.ts index 0c4590b0284d..552806d946dc 100644 --- a/src/agents/agent-tools.deferred-followup.ts +++ b/src/agents/agent-tools.deferred-followup.ts @@ -9,6 +9,7 @@ import { describeExecTool, describeProcessTool } from "./bash-tools.descriptions import { copyBeforeToolCallHookMarker } from "./before-tool-call-metadata.js"; import { copyChannelAgentToolMeta } from "./channel-tools.js"; import { copyToolTerminalPresentation } from "./tool-terminal-presentation.js"; +import { isAutomationsToolName } from "./tools/automations-tool-name.js"; function replaceDescription(tool: AnyAgentTool, description: string): AnyAgentTool { const updated = { ...tool, description }; @@ -24,7 +25,7 @@ export function applyDeferredFollowupToolDescriptions( tools: AnyAgentTool[], params?: { agentId?: string }, ): AnyAgentTool[] { - const hasCronTool = tools.some((tool) => tool.name === "cron"); + const hasCronTool = tools.some((tool) => isAutomationsToolName(tool.name)); return tools.map((tool) => { if (tool.name === "exec") { return replaceDescription(tool, describeExecTool({ agentId: params?.agentId, hasCronTool })); diff --git a/src/agents/agent-tools.model-provider-collision.test.ts b/src/agents/agent-tools.model-provider-collision.test.ts index dfe8f230cee3..f30a4353db1b 100644 --- a/src/agents/agent-tools.model-provider-collision.test.ts +++ b/src/agents/agent-tools.model-provider-collision.test.ts @@ -173,7 +173,7 @@ describe("applyModelProviderToolPolicy", () => { [ { name: "read" }, { name: "browser" }, - { name: "cron" }, + { name: "automations" }, { name: "message" }, { name: "exec" }, ] as unknown as AnyAgentTool[], @@ -201,7 +201,7 @@ describe("applyModelProviderToolPolicy", () => { [ { name: "read" }, { name: "browser" }, - { name: "cron" }, + { name: "automations" }, { name: "message" }, { name: "exec" }, ] as unknown as AnyAgentTool[], @@ -233,7 +233,7 @@ describe("applyModelProviderToolPolicy", () => { [ { name: "read" }, { name: "browser" }, - { name: "cron" }, + { name: "automations" }, { name: "message" }, { name: "exec" }, ] as unknown as AnyAgentTool[], @@ -265,7 +265,7 @@ describe("applyModelProviderToolPolicy", () => { [ { name: "read" }, { name: "browser" }, - { name: "cron" }, + { name: "automations" }, { name: "message" }, { name: "exec" }, ] as unknown as AnyAgentTool[], @@ -303,7 +303,7 @@ describe("applyModelProviderToolPolicy", () => { [ { name: "read" }, { name: "browser" }, - { name: "cron" }, + { name: "automations" }, { name: "message" }, { name: "exec" }, ] as unknown as AnyAgentTool[], @@ -332,7 +332,7 @@ describe("applyModelProviderToolPolicy", () => { }, ); - expect(toolNames(filtered)).toEqual(["read", "browser", "cron", "message", "exec"]); + expect(toolNames(filtered)).toEqual(["read", "browser", "automations", "message", "exec"]); }); it("keeps heavyweight tools when the experimental lean local-model flag is not enabled", () => { @@ -340,7 +340,7 @@ describe("applyModelProviderToolPolicy", () => { [ { name: "read" }, { name: "browser" }, - { name: "cron" }, + { name: "automations" }, { name: "message" }, { name: "exec" }, ] as unknown as AnyAgentTool[], @@ -360,6 +360,6 @@ describe("applyModelProviderToolPolicy", () => { }, ); - expect(toolNames(filtered)).toEqual(["read", "browser", "cron", "message", "exec"]); + expect(toolNames(filtered)).toEqual(["read", "browser", "automations", "message", "exec"]); }); }); diff --git a/src/agents/agent-tools.policy.ts b/src/agents/agent-tools.policy.ts index 0014fc2c54e8..d29f9f5ecbbb 100644 --- a/src/agents/agent-tools.policy.ts +++ b/src/agents/agent-tools.policy.ts @@ -41,6 +41,7 @@ import { normalizeToolName, resolveToolProfilePolicy, } from "./tool-policy.js"; +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; export { resolveProviderToolPolicy }; @@ -54,7 +55,7 @@ const SUBAGENT_TOOL_DENY_ALWAYS = [ "agents_list", // Status/scheduling - main agent coordinates "session_status", - "cron", + AUTOMATIONS_TOOL_NAME, // Direct session sends - subagents communicate through announce chain "sessions_send", "conversations_list", diff --git a/src/agents/core-tool-factory-descriptors.ts b/src/agents/core-tool-factory-descriptors.ts index 30ddca591931..e54ce6e9e0c7 100644 --- a/src/agents/core-tool-factory-descriptors.ts +++ b/src/agents/core-tool-factory-descriptors.ts @@ -2,6 +2,8 @@ * Static identity for names that select core agent factory families before assembly. */ +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; + export type CoreToolFactoryFamily = "base-coding" | "shell" | "openclaw"; type CoreToolFactoryDescriptor = { @@ -25,7 +27,7 @@ const CORE_TOOL_FACTORY_DESCRIPTORS = [ { name: "conversations_list", family: "openclaw" }, { name: "conversations_send", family: "openclaw" }, { name: "conversations_turn", family: "openclaw" }, - { name: "cron", family: "openclaw" }, + { name: AUTOMATIONS_TOOL_NAME, family: "openclaw" }, { name: "dashboard", family: "openclaw" }, { name: "gateway", family: "openclaw" }, { name: "get_goal", family: "openclaw" }, diff --git a/src/agents/delegation-capability.ts b/src/agents/delegation-capability.ts index 26ba1f51cb7f..aa69309d51af 100644 --- a/src/agents/delegation-capability.ts +++ b/src/agents/delegation-capability.ts @@ -1,5 +1,6 @@ import { isCompletionReportInputProvenance } from "../sessions/input-provenance.js"; import { normalizeToolName } from "./tool-policy.js"; +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; import type { AnyAgentTool } from "./tools/common.js"; import { ToolAuthorizationError } from "./tools/common.js"; @@ -14,7 +15,7 @@ const NEW_DELEGATION_TOOL_NAMES = new Set([ ]); const REPORT_ONLY_TOOL_ACTIONS: ReadonlyMap> = new Map([ - ["cron", new Set(["get", "list", "remove", "runs", "status"])], + [AUTOMATIONS_TOOL_NAME, new Set(["get", "list", "remove", "runs", "status"])], ["image_generate", new Set(["list", "status"])], ["music_generate", new Set(["list", "status"])], ["video_generate", new Set(["list", "status"])], diff --git a/src/agents/embedded-agent-subscribe.handlers.tools.ts b/src/agents/embedded-agent-subscribe.handlers.tools.ts index 4dc49152d350..c9cf8e01f7e5 100644 --- a/src/agents/embedded-agent-subscribe.handlers.tools.ts +++ b/src/agents/embedded-agent-subscribe.handlers.tools.ts @@ -106,6 +106,7 @@ import { settleAskUserPromptDelivery, waitForAskUserPromptReady, } from "./tools/ask-user-tool.js"; +import { isAutomationsToolName } from "./tools/automations-tool-name.js"; type ExecApprovalReplyModule = typeof import("../infra/exec-approval-reply.js"); type HookRunnerGlobalModule = typeof import("../plugins/hook-runner-global.js"); @@ -1580,7 +1581,7 @@ export async function handleToolExecutionEnd( // Track committed reminders only when cron.add completed successfully. if ( !isToolError && - ((toolName === "cron" && isCronAddAction(startArgs)) || + ((isAutomationsToolName(toolName) && isCronAddAction(startArgs)) || (isExecToolName(toolName) && didShellCronAddSucceed(startArgs, result))) ) { ctx.state.successfulCronAdds += 1; diff --git a/src/agents/embedded-agent-subscribe.tools.ts b/src/agents/embedded-agent-subscribe.tools.ts index 6020a9c0cb4c..938e49c114c4 100644 --- a/src/agents/embedded-agent-subscribe.tools.ts +++ b/src/agents/embedded-agent-subscribe.tools.ts @@ -34,6 +34,7 @@ import { readToolResultDetails, readToolResultStatus, } from "./tool-result-error.js"; +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; export { isToolResultError }; @@ -602,7 +603,7 @@ const TRUSTED_TOOL_RESULT_MEDIA = new Set([ "apply_patch", "browser", "canvas", - "cron", + AUTOMATIONS_TOOL_NAME, "edit", "exec", "gateway", diff --git a/src/agents/local-model-lean.ts b/src/agents/local-model-lean.ts index 24dde446c798..e4f7d4e8f2b8 100644 --- a/src/agents/local-model-lean.ts +++ b/src/agents/local-model-lean.ts @@ -10,10 +10,11 @@ import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope-config. import type { AnyAgentTool } from "./agent-tools.types.js"; import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.js"; import { expandToolGroups, normalizeToolName } from "./tool-policy.js"; +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; const LOCAL_MODEL_LEAN_DENY_TOOL_NAMES = new Set([ "browser", - "cron", + AUTOMATIONS_TOOL_NAME, "image_generate", "message", "music_generate", diff --git a/src/agents/prompt-surface.test.ts b/src/agents/prompt-surface.test.ts new file mode 100644 index 000000000000..1dcc43d024fe --- /dev/null +++ b/src/agents/prompt-surface.test.ts @@ -0,0 +1,20 @@ +// @vitest-environment node +import { describe, expect, it } from "vitest"; +import { buildOpenClawToolFallbackText } from "./prompt-surface.js"; +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; + +describe("buildOpenClawToolFallbackText", () => { + it("teaches the canonical scheduler tool, never the legacy alias", () => { + const text = buildOpenClawToolFallbackText({ + surface: "openclaw_main", + execToolName: "exec", + processToolName: "process", + }); + + expect(text).toContain(`- ${AUTOMATIONS_TOOL_NAME}:`); + // The fallback is model-facing guidance; it must not teach "cron" as a + // tool name even though the inbound alias stays accepted (RFC 0026). + expect(text).not.toMatch(/-\s*cron\b/); + expect(text.toLowerCase()).not.toContain("cron job"); + }); +}); diff --git a/src/agents/prompt-surface.ts b/src/agents/prompt-surface.ts index ffc88d32171a..99950246e968 100644 --- a/src/agents/prompt-surface.ts +++ b/src/agents/prompt-surface.ts @@ -6,6 +6,7 @@ import { isOpenClawMainPromptSurface } from "../plugins/agent-prompt-surface-kind.js"; import type { AgentPromptSurfaceKind } from "../plugins/types.js"; import { isAcpSessionKey, isSubagentSessionKey } from "../routing/session-key.js"; +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; /** Builds fallback tool guidance when a runtime cannot render the structured tool list. */ export function buildOpenClawToolFallbackText(params: { @@ -25,7 +26,7 @@ export function buildOpenClawToolFallbackText(params: { "- browser: control OpenClaw's dedicated browser", "- canvas: present/eval/snapshot the Canvas", "- nodes: list/describe/notify/camera/screen on paired nodes", - "- cron: manage cron jobs and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)", + `- ${AUTOMATIONS_TOOL_NAME}: manage automations (scheduled jobs) and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)`, "- conversations_list: list exact external conversation addresses", "- conversations_send: send directly to an external conversation", "- conversations_turn: send and wait for a correlated external reply", diff --git a/src/agents/sandbox/constants.ts b/src/agents/sandbox/constants.ts index 27264d7a6762..0b65769b29e2 100644 --- a/src/agents/sandbox/constants.ts +++ b/src/agents/sandbox/constants.ts @@ -6,6 +6,7 @@ import path from "node:path"; import { CHANNEL_IDS } from "../../channels/ids.js"; import { STATE_DIR } from "../../config/paths.js"; +import { AUTOMATIONS_TOOL_NAME } from "../tools/automations-tool-name.js"; export { DEFAULT_SANDBOX_BROWSER_NETWORK } from "./browser-network.js"; @@ -46,7 +47,7 @@ export const DEFAULT_TOOL_DENY = [ "computer", "mobile_ui", "nodes", - "cron", + AUTOMATIONS_TOOL_NAME, "gateway", ...CHANNEL_IDS, ] as const; diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index e185c4c8c892..7d3c682fa392 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -62,6 +62,7 @@ import type { ProviderSystemPromptSectionId, } from "./system-prompt-contribution.js"; import type { PromptMode, SilentReplyPromptMode } from "./system-prompt.types.js"; +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; import { buildWatchedSessionsPromptLines, type PreparedWatchedSessionsPrompt, @@ -863,7 +864,8 @@ export function buildAgentSystemPrompt(params: { "Own visible shell. Use for long/interactive jobs user should watch. exec for quiet work", canvas: "Present/eval/snapshot Canvas", nodes: "Paired node status/control/media", - cron: "Schedule/wake. Reminder text must read as reminder when fired; mention reminder for delayed gaps; include useful recent context.", + [AUTOMATIONS_TOOL_NAME]: + "Schedule/wake. Reminder text must read as reminder when fired; mention reminder for delayed gaps; include useful recent context.", message: "Message/channel actions", conversations_list: "List exact external conversation addresses", conversations_send: "Send directly to an external conversation", @@ -905,7 +907,7 @@ export function buildAgentSystemPrompt(params: { "terminal", "canvas", "nodes", - "cron", + AUTOMATIONS_TOOL_NAME, "message", "conversations_list", "conversations_send", diff --git a/src/agents/test-helpers/fast-openclaw-tools-sessions.ts b/src/agents/test-helpers/fast-openclaw-tools-sessions.ts index 70f2489a3c88..6ee313d4b536 100644 --- a/src/agents/test-helpers/fast-openclaw-tools-sessions.ts +++ b/src/agents/test-helpers/fast-openclaw-tools-sessions.ts @@ -16,7 +16,7 @@ vi.mock("../tools/computer-tool.js", () => ({ })); vi.mock("../tools/cron-tool.js", () => ({ - createCronTool: () => stubTool("cron"), + createCronTool: () => stubTool("automations"), })); vi.mock("../tools/gateway-tool.js", () => ({ diff --git a/src/agents/test-helpers/fast-openclaw-tools.ts b/src/agents/test-helpers/fast-openclaw-tools.ts index 219cf904a828..c88bb6a2bdb3 100644 --- a/src/agents/test-helpers/fast-openclaw-tools.ts +++ b/src/agents/test-helpers/fast-openclaw-tools.ts @@ -25,7 +25,7 @@ function stubActionTool(name: string, actions: string[]) { const coreTools = [ stubActionTool("canvas", ["create", "read"]), stubActionTool("nodes", ["list", "invoke"]), - stubActionTool("cron", ["schedule", "cancel"]), + stubActionTool("automations", ["schedule", "cancel"]), stubActionTool("message", ["send", "reply"]), stubTool("heartbeat_respond"), stubActionTool("gateway", ["config.get", "config.schema.lookup"]), diff --git a/src/agents/tool-catalog.test.ts b/src/agents/tool-catalog.test.ts index d2e8707f5247..140c3065c451 100644 --- a/src/agents/tool-catalog.test.ts +++ b/src/agents/tool-catalog.test.ts @@ -63,7 +63,7 @@ describe("tool-catalog", () => { "screen", "dashboard", "terminal", - "cron", + "automations", "get_goal", "create_goal", "update_goal", diff --git a/src/agents/tool-catalog.ts b/src/agents/tool-catalog.ts index e1cb83114ae3..0ad58dfcc802 100644 --- a/src/agents/tool-catalog.ts +++ b/src/agents/tool-catalog.ts @@ -23,6 +23,7 @@ import { DISMISS_TASK_TOOL_DISPLAY_SUMMARY, UPDATE_PLAN_TOOL_DISPLAY_SUMMARY, } from "./tool-description-presets.js"; +import { AUTOMATIONS_TOOL_NAME } from "./tools/automations-tool-name.js"; /** Built-in tool profile ids exposed in config and UI. */ export type ToolProfileId = "minimal" | "coding" | "messaging" | "full"; @@ -341,8 +342,8 @@ const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [ includeInOpenClawGroup: true, }, { - id: "cron", - label: "cron", + id: AUTOMATIONS_TOOL_NAME, + label: AUTOMATIONS_TOOL_NAME, description: CRON_TOOL_DISPLAY_SUMMARY, sectionId: "automation", profiles: ["coding"], diff --git a/src/agents/tool-mutation-names.ts b/src/agents/tool-mutation-names.ts index 3ccd80f70596..7af58679290e 100644 --- a/src/agents/tool-mutation-names.ts +++ b/src/agents/tool-mutation-names.ts @@ -1,4 +1,8 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; +import { + AUTOMATIONS_TOOL_NAME, + LEGACY_AUTOMATIONS_TOOL_NAMES, +} from "./tools/automations-tool-name.js"; const MUTATING_TOOL_NAMES = new Set([ "write", @@ -11,7 +15,9 @@ const MUTATING_TOOL_NAMES = new Set([ "sessions", "sessions_spawn", "sessions_send", - "cron", + AUTOMATIONS_TOOL_NAME, + // Saved transcripts predate the rename; legacy names must stay classified. + ...LEGACY_AUTOMATIONS_TOOL_NAMES, "gateway", "canvas", "computer", diff --git a/src/agents/tool-mutation.test.ts b/src/agents/tool-mutation.test.ts index fa8f394051ee..109ab167a52a 100644 --- a/src/agents/tool-mutation.test.ts +++ b/src/agents/tool-mutation.test.ts @@ -267,7 +267,10 @@ describe("tool mutation helpers", () => { plan: [{ step: "Inspect", status: "in_progress" }], }), ).toBe(true); + expect(isReplaySafeToolCall("automations", { action: "status" })).toBe(true); + // Legacy transcript entries predate the rename and must stay classified. expect(isReplaySafeToolCall("cron", { action: "status" })).toBe(true); + expect(isReplaySafeToolCall("cron", { action: "add" })).toBe(false); expect(isReplaySafeToolCall("gateway", { action: "config.get" })).toBe(true); expect(isReplaySafeToolCall("gateway", { action: "config.schema.lookup" })).toBe(true); expect(isReplaySafeToolCall("gateway", { action: "config.patch" })).toBe(false); diff --git a/src/agents/tool-mutation.ts b/src/agents/tool-mutation.ts index d1c3f4998795..aeab703906d3 100644 --- a/src/agents/tool-mutation.ts +++ b/src/agents/tool-mutation.ts @@ -9,6 +9,7 @@ import { normalizeOptionalLowercaseString, } from "@openclaw/normalization-core/string-coerce"; import { isLikelyMutatingToolName } from "./tool-mutation-names.js"; +import { isAutomationsToolName } from "./tools/automations-tool-name.js"; export { isLikelyMutatingToolName }; @@ -373,7 +374,7 @@ export function isMutatingToolCall(toolName: string, args: unknown): boolean { case "nodes": return action == null || !NODES_REPLAY_SAFE_ACTIONS.has(action); default: { - if (normalized === "cron" || normalized === "canvas") { + if (isAutomationsToolName(normalized) || normalized === "canvas") { return action == null || !READ_ONLY_ACTIONS.has(action); } if (normalized.endsWith("_actions")) { @@ -424,7 +425,7 @@ export function isReplaySafeToolCall(toolName: string, args: unknown): boolean { case "nodes": return action != null && NODES_REPLAY_SAFE_ACTIONS.has(action); default: { - if (normalized === "cron" || normalized === "canvas") { + if (isAutomationsToolName(normalized) || normalized === "canvas") { return action != null && READ_ONLY_ACTIONS.has(action); } return false; diff --git a/src/agents/tool-policy-shared.ts b/src/agents/tool-policy-shared.ts index 069f7ed87f3c..58ff336baa95 100644 --- a/src/agents/tool-policy-shared.ts +++ b/src/agents/tool-policy-shared.ts @@ -19,6 +19,8 @@ type ToolProfilePolicy = { const TOOL_NAME_ALIASES: Record = { bash: "exec", "apply-patch": "apply_patch", + // Permanent scheduler-tool alias (owner decision, RFC 0026), like bash -> exec. + cron: "automations", }; const TOOL_ALLOWLIST_INTERSECTION = Symbol.for("openclaw.toolAllowlistIntersection"); diff --git a/src/agents/tool-policy.test.ts b/src/agents/tool-policy.test.ts index de57d48de087..b86a52b9bded 100644 --- a/src/agents/tool-policy.test.ts +++ b/src/agents/tool-policy.test.ts @@ -34,7 +34,7 @@ describe("tool-policy", () => { it("resolves known profiles and ignores unknown ones", () => { const coding = resolveToolProfilePolicy("coding"); expect(coding?.allow).toContain("read"); - expect(coding?.allow).toContain("cron"); + expect(coding?.allow).toContain("automations"); expect(coding?.allow).not.toContain("gateway"); expect(resolveToolProfilePolicy("nope")).toBeUndefined(); }); @@ -52,6 +52,9 @@ describe("tool-policy", () => { expect(normalizeToolName(" BASH ")).toBe("exec"); expect(normalizeToolName("apply-patch")).toBe("apply_patch"); expect(normalizeToolName("READ")).toBe("read"); + // Pre-rename scheduler tool name from persisted config (RFC 0026). + expect(normalizeToolName("cron")).toBe("automations"); + expect(normalizeToolName("automations")).toBe("automations"); }); it("collects explicit allowlist entries", () => { @@ -180,6 +183,16 @@ describe("resolveSandboxToolPolicyForAgent", () => { }); }); +describe("isToolAllowedByPolicyName — legacy scheduler tool name (RFC 0026)", () => { + it("allows the renamed tool through persisted legacy allow lists", () => { + expect(isToolAllowedByPolicyName("automations", { allow: ["cron"] })).toBe(true); + }); + + it("denies the renamed tool through persisted legacy deny lists", () => { + expect(isToolAllowedByPolicyName("automations", { deny: ["cron"] })).toBe(false); + }); +}); + describe("isToolAllowedByPolicyName — apply_patch / write deny decoupling (#76749)", () => { it("does not deny apply_patch when write is denied", () => { expect(isToolAllowedByPolicyName("apply_patch", { deny: ["write"] })).toBe(true); diff --git a/src/agents/tool-search-ranking.ts b/src/agents/tool-search-ranking.ts index 798ae9c23100..861f2f137eab 100644 --- a/src/agents/tool-search-ranking.ts +++ b/src/agents/tool-search-ranking.ts @@ -102,7 +102,7 @@ const QUERY_EXPANSIONS: ReadonlyArray<{ terms: readonly string[]; add: readonly }, { terms: ["remind", "reminder", "later", "tomorrow", "daily", "weekly", "recurring"], - add: ["schedule", "cron", "reminder"], + add: ["schedule", "automations", "cron", "reminder"], }, { terms: ["say", "tell", "reply", "respond", "answer"], add: ["message", "send"] }, { terms: ["picture", "photo", "meme", "screenshot"], add: ["image"] }, diff --git a/src/agents/tools/automations-tool-name.ts b/src/agents/tools/automations-tool-name.ts new file mode 100644 index 000000000000..c838b2fc44c8 --- /dev/null +++ b/src/agents/tools/automations-tool-name.ts @@ -0,0 +1,22 @@ +/** + * Canonical identity of the scheduler agent tool. Single source of truth for + * every name-keyed consumer (policy lists, factory descriptors, runtime + * observers, prompts); never spell the tool name as a string literal. + */ +export const AUTOMATIONS_TOOL_NAME = "automations"; + +/** + * "cron" is a permanently accepted alias for the scheduler tool in persisted + * allow/deny config, old transcripts, and inbound calls (owner decision, + * RFC 0026; same contract as bash -> exec). Not migration debt: no doctor + * rewrite, no removal window. + */ +export const LEGACY_AUTOMATIONS_TOOL_NAMES = ["cron"] as const; + +/** True when a tool name refers to the scheduler tool, including legacy names. */ +export function isAutomationsToolName(name: string): boolean { + return ( + name === AUTOMATIONS_TOOL_NAME || + (LEGACY_AUTOMATIONS_TOOL_NAMES as readonly string[]).includes(name) + ); +} diff --git a/src/agents/tools/cron-tool-creator-cap.test.ts b/src/agents/tools/cron-tool-creator-cap.test.ts index 1c256fef7b6b..d296a39408b1 100644 --- a/src/agents/tools/cron-tool-creator-cap.test.ts +++ b/src/agents/tools/cron-tool-creator-cap.test.ts @@ -24,10 +24,11 @@ describe("cron tool creator cap", () => { capCronJobToolsAllowOnCreate(triggerJob, ["read", "cron"]); capCronJobToolsAllowOnCreate(plainJob, ["read", "cron"]); + // Legacy "cron" creator allowlists normalize to the canonical tool id. expect(triggerJob.payload).toEqual({ kind: "systemEvent", text: "wake", - toolsAllow: ["read", "cron"], + toolsAllow: ["read", "automations"], toolsAllowIsDefault: true, }); expect(plainJob.payload).toEqual({ kind: "systemEvent", text: "wake" }); @@ -103,7 +104,7 @@ describe("cron tool creator cap", () => { payload: { kind: "agentTurn", message: "updated", - toolsAllow: ["read", "cron"], + toolsAllow: ["read", "automations"], toolsAllowIsDefault: true, }, }); diff --git a/src/agents/tools/cron-tool.test.ts b/src/agents/tools/cron-tool.test.ts index 7faba39c6094..3f24b633b137 100644 --- a/src/agents/tools/cron-tool.test.ts +++ b/src/agents/tools/cron-tool.test.ts @@ -1414,7 +1414,7 @@ describe("cron tool", () => { const params = expectSingleGatewayCallMethod("cron.add") as | { payload?: { toolsAllow?: string[] } } | undefined; - expect(params?.payload?.toolsAllow).toEqual(["read", "cron"]); + expect(params?.payload?.toolsAllow).toEqual(["read", "automations"]); }); it("caps trigger-script systemEvent adds to the creator tool surface", async () => { @@ -1437,7 +1437,7 @@ describe("cron tool", () => { const params = expectSingleGatewayCallMethod("cron.add") as | { payload?: { toolsAllow?: string[] } } | undefined; - expect(params?.payload?.toolsAllow).toEqual(["read", "cron"]); + expect(params?.payload?.toolsAllow).toEqual(["read", "automations"]); }); it("infers systemEvent for implicit text payloads with toolsAllow", async () => { @@ -1492,7 +1492,7 @@ describe("cron tool", () => { trigger: { script: "return { fire: false }" }, payload: { kind: "systemEvent", - toolsAllow: ["read", "cron"], + toolsAllow: ["read", "automations"], toolsAllowIsDefault: true, }, }, @@ -1576,7 +1576,7 @@ describe("cron tool", () => { expect(params?.payload?.toolsAllow).toEqual([ "active_memory_search", "active_memory_store", - "cron", + "automations", ]); }); @@ -2973,7 +2973,7 @@ describe("cron tool", () => { | undefined; expect(params?.patch?.payload).toEqual({ kind: "agentTurn", - toolsAllow: ["read", "cron"], + toolsAllow: ["read", "automations"], toolsAllowIsDefault: true, }); }); @@ -3188,7 +3188,7 @@ describe("cron tool", () => { payload: { kind: "agentTurn", message: "run later", - toolsAllow: ["read", "cron"], + toolsAllow: ["read", "automations"], toolsAllowIsDefault: true, }, }, diff --git a/src/agents/tools/cron-tool.ts b/src/agents/tools/cron-tool.ts index 9e04f4e6924f..02e53af87034 100644 --- a/src/agents/tools/cron-tool.ts +++ b/src/agents/tools/cron-tool.ts @@ -20,6 +20,7 @@ import { resolveSessionAgentId } from "../agent-scope.js"; import { CRON_TOOL_DISPLAY_SUMMARY } from "../tool-description-presets.js"; import { normalizeToolName } from "../tool-policy.js"; import { setToolTerminalPresentation } from "../tool-terminal-presentation.js"; +import { AUTOMATIONS_TOOL_NAME } from "./automations-tool-name.js"; import { type AnyAgentTool, jsonResult, @@ -284,8 +285,8 @@ function isOlderGatewayWithoutCompactCronList(error: unknown): boolean { export function createCronTool(opts?: CronToolOptions, deps?: CronToolDeps): AnyAgentTool { const callGateway = deps?.callGatewayTool ?? callGatewayTool; const tool: AnyAgentTool = { - label: "Cron", - name: "cron", + label: "Automations", + name: AUTOMATIONS_TOOL_NAME, displaySummary: CRON_TOOL_DISPLAY_SUMMARY, description: `Gateway scheduler: reminders, delayed self-wakeups, loops, recurring work, event watchers. Never exec sleep/poll as timer. diff --git a/src/gateway/gateway-cli-backend.live-probe-helpers.test.ts b/src/gateway/gateway-cli-backend.live-probe-helpers.test.ts index b883a9cb1081..808a3f09513e 100644 --- a/src/gateway/gateway-cli-backend.live-probe-helpers.test.ts +++ b/src/gateway/gateway-cli-backend.live-probe-helpers.test.ts @@ -99,7 +99,7 @@ describe("gateway CLI backend live probe helpers", () => { activateLoopbackRuntime(port); try { await expect(verifyCliCronMcpLoopbackPreflight(preflightParams())).rejects.toThrow( - "mcp loopback tools/list did not expose cron", + "mcp loopback tools/list did not expose automations", ); expect(methods).toEqual(["initialize", "notifications/initialized", "tools/list"]); } finally { diff --git a/src/gateway/gateway-cli-backend.live-probe-helpers.ts b/src/gateway/gateway-cli-backend.live-probe-helpers.ts index d35c60444471..e73bd3f5777e 100644 --- a/src/gateway/gateway-cli-backend.live-probe-helpers.ts +++ b/src/gateway/gateway-cli-backend.live-probe-helpers.ts @@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto"; import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce"; import { renderCatFacePngBase64 } from "../../test/helpers/live-image-probe.js"; +import { AUTOMATIONS_TOOL_NAME } from "../agents/tools/automations-tool-name.js"; import { isTruthyEnvValue } from "../infra/env.js"; import { readResponseWithLimit } from "../infra/http-body.js"; import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js"; @@ -298,10 +299,10 @@ export async function verifyCliCronMcpLoopbackPreflight(params: { .filter(Boolean); logCliCronProbe("loopback-preflight:tools", { toolCount: toolNames.length, - cronVisible: toolNames.includes("cron"), + cronVisible: toolNames.includes(AUTOMATIONS_TOOL_NAME), }); - if (!toolNames.includes("cron")) { - throw new Error("mcp loopback tools/list did not expose cron"); + if (!toolNames.includes(AUTOMATIONS_TOOL_NAME)) { + throw new Error(`mcp loopback tools/list did not expose ${AUTOMATIONS_TOOL_NAME}`); } const toolCall = await callLoopbackJsonRpc({ @@ -314,7 +315,7 @@ export async function verifyCliCronMcpLoopbackPreflight(params: { id: "cron-add", method: "tools/call", params: { - name: "cron", + name: AUTOMATIONS_TOOL_NAME, arguments: JSON.parse(cronProbe.argsJson) as Record, }, }, diff --git a/src/gateway/live-agent-probes.test.ts b/src/gateway/live-agent-probes.test.ts index 51979d986854..bef3f7a19f82 100644 --- a/src/gateway/live-agent-probes.test.ts +++ b/src/gateway/live-agent-probes.test.ts @@ -67,8 +67,10 @@ describe("live-agent-probes", () => { expect(claudeRetryPrompt).toContain( "Preserve job.sessionTarget and job.sessionKey exactly as provided.", ); - expect(claudeRetryPrompt).toContain("search/load MCP tools for `openclaw cron` or `cron`"); - expect(claudeRetryPrompt).toContain("mcp__openclaw__cron"); + expect(claudeRetryPrompt).toContain( + "search/load MCP tools for `openclaw automations` or `automations`", + ); + expect(claudeRetryPrompt).toContain("mcp__openclaw__automations"); expect(claudeRetryPrompt).toContain("Do not use Claude native `CronCreate`"); expect(claudeRetryPrompt).not.toContain("openclaw-tools"); expect( @@ -86,7 +88,7 @@ describe("live-agent-probes", () => { attempt: 1, exactReply: spec.name, }), - ).toContain("previous OpenClaw cron MCP tool call was cancelled"); + ).toContain("previous OpenClaw automations MCP tool call was cancelled"); const args = JSON.parse(spec.argsJson) as { job?: { sessionTarget?: string; diff --git a/src/gateway/live-agent-probes.ts b/src/gateway/live-agent-probes.ts index dedc798b2f59..250d7a95985c 100644 --- a/src/gateway/live-agent-probes.ts +++ b/src/gateway/live-agent-probes.ts @@ -113,9 +113,9 @@ export function buildLiveCronProbeMessage(params: { const claudeLike = isClaudeLikeLiveAgent(params.agent); if (params.attempt === 0) { return ( - "Use the OpenClaw MCP cron tool from server `openclaw`. " + - "If it is not already visible, search/load MCP tools for `openclaw cron` or `cron`, " + - "then call the matching OpenClaw MCP tool; Claude-style names may appear as `mcp__openclaw__cron`. " + + "Use the OpenClaw MCP automations tool from server `openclaw`. " + + "If it is not already visible, search/load MCP tools for `openclaw automations` or `automations`, " + + "then call the matching OpenClaw MCP tool; Claude-style names may appear as `mcp__openclaw__automations`. " + "Do not use Claude native `CronCreate`, `CronList`, or `CronDelete`; those are not OpenClaw proof. " + `Call it with JSON arguments ${params.argsJson}. ` + "Preserve the JSON exactly, including job.sessionTarget and job.sessionKey; do not omit, rename, or flatten those fields. " + @@ -125,9 +125,9 @@ export function buildLiveCronProbeMessage(params: { } if (claudeLike) { return ( - "Retry the OpenClaw MCP cron tool from server `openclaw` now. " + - "If it is not already visible, search/load MCP tools for `openclaw cron` or `cron`, " + - "then call the matching OpenClaw MCP tool; Claude-style names may appear as `mcp__openclaw__cron`. " + + "Retry the OpenClaw MCP automations tool from server `openclaw` now. " + + "If it is not already visible, search/load MCP tools for `openclaw automations` or `automations`, " + + "then call the matching OpenClaw MCP tool; Claude-style names may appear as `mcp__openclaw__automations`. " + "Do not use Claude native `CronCreate`, `CronList`, or `CronDelete`; those are not OpenClaw proof. " + `Use these exact JSON arguments: ${params.argsJson}. ` + "Preserve job.sessionTarget and job.sessionKey exactly as provided. " + @@ -138,9 +138,9 @@ export function buildLiveCronProbeMessage(params: { ); } return ( - "Your previous OpenClaw cron MCP tool call was cancelled before the job was created. " + - "Retry the OpenClaw MCP cron tool from server `openclaw` now. " + - "If the harness shows Claude-style MCP names, use `mcp__openclaw__cron`. " + + "Your previous OpenClaw automations MCP tool call was cancelled before the job was created. " + + "Retry the OpenClaw MCP automations tool from server `openclaw` now. " + + "If the harness shows Claude-style MCP names, use `mcp__openclaw__automations`. " + `Use these exact JSON arguments: ${params.argsJson}. ` + "Preserve job.sessionTarget and job.sessionKey exactly as provided. " + `If the cron job is created, reply exactly: ${params.exactReply}. ` + diff --git a/src/gateway/mcp-http.handlers.ts b/src/gateway/mcp-http.handlers.ts index 933b46be0229..3494056c1f8b 100644 --- a/src/gateway/mcp-http.handlers.ts +++ b/src/gateway/mcp-http.handlers.ts @@ -9,6 +9,7 @@ import { resolveToolExecutionErrorKind, resolveToolResultFailureKind, } from "../agents/tool-result-error.js"; +import { isAutomationsToolName } from "../agents/tools/automations-tool-name.js"; import type { McpLoopbackToolCallOutcome } from "./mcp-http.loopback-runtime.js"; import { MCP_LOOPBACK_SERVER_NAME, @@ -97,7 +98,17 @@ export async function handleMcpJsonRpc(params: { case "tools/list": return jsonRpcResult(id, { tools: params.toolSchema }); case "tools/call": { - const toolName = typeof methodParams?.name === "string" ? methodParams.name.trim() : ""; + const requestedToolName = + typeof methodParams?.name === "string" ? methodParams.name.trim() : ""; + // "cron" is a permanently accepted inbound alias for the scheduler tool + // (owner decision, RFC 0026; same contract as bash -> exec). Resolve it to + // the published canonical tool without re-advertising it in tools/list. + const toolName = + !params.toolSchema.some((tool) => tool.name === requestedToolName) && + isAutomationsToolName(requestedToolName) + ? (params.toolSchema.find((tool) => isAutomationsToolName(tool.name))?.name ?? + requestedToolName) + : requestedToolName; const rawToolArgs = methodParams?.arguments; if (rawToolArgs !== undefined && !isRecord(rawToolArgs)) { return jsonRpcError(id, -32602, "Invalid params: tools/call arguments must be an object"); diff --git a/src/gateway/mcp-http.test.ts b/src/gateway/mcp-http.test.ts index 4d18d2b4bbd3..4e5e2beb5a35 100644 --- a/src/gateway/mcp-http.test.ts +++ b/src/gateway/mcp-http.test.ts @@ -2834,6 +2834,44 @@ describe("mcp loopback server", () => { expect(signal).toBeInstanceOf(AbortSignal); }); + it("resolves legacy cron tools/call names to the renamed automations tool", async () => { + const execute = vi.fn(async () => ({ + content: [{ type: "text", text: "SCHEDULED" }], + })); + const tool = makeMockTool({ name: "automations", description: "manage schedules", execute }); + + const payload = await handleMcpJsonRpc({ + message: { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "cron", arguments: {} }, + }, + tools: [tool as unknown as AnyAgentTool], + toolSchema: buildMockMcpToolSchema([tool]), + }); + + expectMcpResultText(payload as McpToolResultPayload, "SCHEDULED", false); + expect(execute).toHaveBeenCalledTimes(1); + }); + + it("still rejects legacy cron calls when the automations tool is not exposed", async () => { + const tool = makeMessageTool(); + + const payload = await handleMcpJsonRpc({ + message: { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "cron", arguments: {} }, + }, + tools: [tool as unknown as AnyAgentTool], + toolSchema: buildMockMcpToolSchema([tool]), + }); + + expectMcpResultText(payload as McpToolResultPayload, "Tool not available: cron", true); + }); + it("preserves request-disconnect evidence without classifying a tool failure", async () => { const controller = new AbortController(); controller.abort(); diff --git a/src/gateway/mcp-http.ts b/src/gateway/mcp-http.ts index 621d4699fafb..6194ccb48d11 100644 --- a/src/gateway/mcp-http.ts +++ b/src/gateway/mcp-http.ts @@ -8,6 +8,7 @@ import { } from "node:http"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { resolveToolLoopDetectionConfig } from "../agents/tool-loop-detection-config.js"; +import { isAutomationsToolName } from "../agents/tools/automations-tool-name.js"; import { getRuntimeConfig } from "../config/io.js"; import { resolveSessionEntryAccessTarget } from "../config/sessions/session-accessor.js"; import { isTruthyEnvValue } from "../infra/env.js"; @@ -340,7 +341,7 @@ async function startMcpLoopbackServer(port = 0): Promise<{ inboundEventKind: requestContext.inboundEventKind, senderIsOwner: requestContext.senderIsOwner, toolCount: scopedTools.toolSchema.length, - cronVisible: scopedTools.toolSchema.some((tool) => tool.name === "cron"), + cronVisible: scopedTools.toolSchema.some((tool) => isAutomationsToolName(tool.name)), }); const responses: object[] = []; for (const [messageIndex, message] of messages.entries()) { diff --git a/src/gateway/tool-resolution.exclude.test.ts b/src/gateway/tool-resolution.exclude.test.ts index c5e691b55483..d4fc3a0337da 100644 --- a/src/gateway/tool-resolution.exclude.test.ts +++ b/src/gateway/tool-resolution.exclude.test.ts @@ -78,7 +78,7 @@ const hoisted = vi.hoisted(() => { createOpenClawToolsMock: vi.fn((_args: CreateOpenClawToolsArg) => [ makeTool("read"), makeTool("sessions_spawn"), - makeTool("cron"), + makeTool("automations"), makeTool("gateway"), makeTool("nodes"), ]), @@ -144,7 +144,7 @@ describe("resolveGatewayScopedTools excludeToolNames", () => { expect(result.tools.map((tool) => tool.name)).toEqual([ "sessions_spawn", - "cron", + "automations", "gateway", "nodes", ]); @@ -255,7 +255,7 @@ describe("resolveGatewayScopedTools excludeToolNames", () => { it("does not fall back when policy removes a mediated coding tool", () => { hoisted.createOpenClawToolsMock.mockReturnValueOnce([ hoisted.makeTool("write"), - hoisted.makeTool("cron"), + hoisted.makeTool("automations"), ]); const result = resolveGatewayScopedTools({ @@ -266,7 +266,7 @@ describe("resolveGatewayScopedTools excludeToolNames", () => { excludeToolNames: ["read", "edit", "apply_patch", "exec", "process"], }); - expect(result.tools.map((tool) => tool.name)).toEqual(["cron"]); + expect(result.tools.map((tool) => tool.name)).toEqual(["automations"]); }); it("keeps owner-only core tools visible only for owner loopback callers", () => { @@ -290,14 +290,14 @@ describe("resolveGatewayScopedTools excludeToolNames", () => { expect(ownerResult.tools.map((tool) => tool.name)).toEqual([ "read", "sessions_spawn", - "cron", + "automations", "gateway", "nodes", ]); expect(nonOwnerResult.tools.map((tool) => tool.name)).toEqual(["read", "sessions_spawn"]); const args = readCreateToolsArgs(1); expect(args.pluginToolDenylist).toEqual([ - "cron", + "automations", "gateway", "sessions", "screen", @@ -311,7 +311,7 @@ describe("resolveGatewayScopedTools excludeToolNames", () => { "openclaw", ]); expect(args.inheritedToolDenylist).toEqual([ - "cron", + "automations", "gateway", "sessions", "screen", @@ -804,7 +804,7 @@ describe("resolveGatewayScopedTools excludeToolNames", () => { it("does not inherit node-only exec as a generic child or cron capability", () => { const result = resolveGatewayScopedTools({ - cfg: { tools: { allow: ["exec", "sessions_spawn", "cron"] } } as OpenClawConfig, + cfg: { tools: { allow: ["exec", "sessions_spawn", "automations"] } } as OpenClawConfig, sessionKey: "agent:main:direct:test", surface: "loopback", senderIsOwner: true, @@ -820,7 +820,7 @@ describe("resolveGatewayScopedTools excludeToolNames", () => { const result = resolveGatewayScopedTools({ cfg: { agents: { defaults: { sandbox: { mode: "all" } } }, - tools: { sandbox: { tools: { deny: ["cron"] } } }, + tools: { sandbox: { tools: { deny: ["automations"] } } }, } as OpenClawConfig, sessionKey: "agent:main:direct:test", surface: "loopback", @@ -829,36 +829,36 @@ describe("resolveGatewayScopedTools excludeToolNames", () => { expect(result.tools.map((tool) => tool.name)).toEqual(["read", "sessions_spawn"]); const args = readCreateToolsArgs(); expect(args.sandboxed).toBe(true); - expect(args.pluginToolDenylist).toEqual(["cron"]); - expect(args.inheritedToolDenylist).toEqual(["cron"]); + expect(args.pluginToolDenylist).toEqual(["automations"]); + expect(args.inheritedToolDenylist).toEqual(["automations"]); }); it("passes final filtered tool surface to gateway cron jobs", () => { hoisted.createOpenClawToolsMock.mockReturnValueOnce([ hoisted.makeTool("read"), - hoisted.makeTool("cron"), + hoisted.makeTool("automations"), hoisted.makeTool("exec"), ]); const result = resolveGatewayScopedTools({ cfg: { - tools: { allow: ["read", "cron"] }, + tools: { allow: ["read", "automations"] }, } as OpenClawConfig, sessionKey: "agent:main:direct:test", surface: "loopback", }); - expect(result.tools.map((tool) => tool.name)).toEqual(["read", "cron"]); + expect(result.tools.map((tool) => tool.name)).toEqual(["read", "automations"]); expect(readCreateToolsArgs().cronCreatorToolAllowlist).toEqual([ { name: "read" }, - { name: "cron" }, + { name: "automations" }, ]); }); it("passes unrestricted gateway tool surfaces to cron jobs", () => { hoisted.createOpenClawToolsMock.mockReturnValueOnce([ hoisted.makeTool("read"), - hoisted.makeTool("cron"), + hoisted.makeTool("automations"), hoisted.makeTool("exec"), ]); @@ -869,10 +869,10 @@ describe("resolveGatewayScopedTools excludeToolNames", () => { senderIsOwner: true, }); - expect(result.tools.map((tool) => tool.name)).toEqual(["read", "cron", "exec"]); + expect(result.tools.map((tool) => tool.name)).toEqual(["read", "automations", "exec"]); expect(readCreateToolsArgs().cronCreatorToolAllowlist).toEqual([ { name: "read" }, - { name: "cron" }, + { name: "automations" }, { name: "exec" }, ]); }); diff --git a/src/gateway/tool-resolution.ts b/src/gateway/tool-resolution.ts index 883a4f49ff0a..80849b91acf7 100644 --- a/src/gateway/tool-resolution.ts +++ b/src/gateway/tool-resolution.ts @@ -193,7 +193,14 @@ export function resolveGatewayScopedTools(params: { const gatewayToolsCfg = params.cfg.gateway?.tools; const defaultGatewayDeny = surface === "http" - ? DEFAULT_GATEWAY_HTTP_TOOL_DENY.filter((name) => !gatewayToolsCfg?.allow?.includes(name)) + ? DEFAULT_GATEWAY_HTTP_TOOL_DENY.filter( + // Config allow entries may use legacy tool names (e.g. "cron"); + // normalize both sides so they still lift the matching default deny. + (name) => + !gatewayToolsCfg?.allow?.some( + (allowed) => normalizeToolName(allowed) === normalizeToolName(name), + ), + ) : []; const ownerOnlyGatewayDeny = params.senderIsOwner === false || (surface === "http" && params.senderIsOwner !== true) diff --git a/src/gateway/tools-invoke-http.cron-regression.test.ts b/src/gateway/tools-invoke-http.cron-regression.test.ts index cabc3e700761..20d4f463bde4 100644 --- a/src/gateway/tools-invoke-http.cron-regression.test.ts +++ b/src/gateway/tools-invoke-http.cron-regression.test.ts @@ -60,9 +60,9 @@ vi.mock("../plugins/tools.js", () => ({ vi.mock("../agents/openclaw-tools.js", () => { const tools = [ { - name: "cron", + name: "automations", parameters: { type: "object", properties: { action: { type: "string" } } }, - execute: async () => ({ ok: true, via: "cron" }), + execute: async () => ({ ok: true, via: "automations" }), }, { name: "gateway", diff --git a/src/gateway/tools-invoke-http.test.ts b/src/gateway/tools-invoke-http.test.ts index c5e1c1c999f1..f176a13f710d 100644 --- a/src/gateway/tools-invoke-http.test.ts +++ b/src/gateway/tools-invoke-http.test.ts @@ -141,9 +141,9 @@ vi.mock("../agents/openclaw-tools.js", () => { }, }, { - name: "cron", + name: "automations", parameters: { type: "object", properties: {} }, - execute: async () => ({ ok: true, result: "cron" }), + execute: async () => ({ ok: true, result: "automations" }), }, { name: "exec", @@ -797,7 +797,7 @@ describe("POST /tools/invoke", () => { const body = await expectOkInvokeResponse(res); expect(body.result?.inheritedToolDenylist).toEqual( - expect.arrayContaining(["cron", "gateway", "nodes"]), + expect.arrayContaining(["automations", "gateway", "nodes"]), ); }); @@ -1227,7 +1227,8 @@ describe("tools.invoke Gateway RPC", () => { expect(call?.[0], tool).toBe(true); expect(call?.[1]?.ok, tool).toBe(false); - expect(call?.[1]?.toolName, tool).toBe(tool); + // Legacy "cron" requests canonicalize before dispatch and report the canonical id. + expect(call?.[1]?.toolName, tool).toBe(tool === "cron" ? "automations" : tool); const error = call?.[1]?.error as { code?: string; message?: string } | undefined; expect(error?.code, tool).toBe("not_found"); } diff --git a/src/gateway/tools-invoke-shared.ts b/src/gateway/tools-invoke-shared.ts index 701f54cf6630..d0ad2c0f2df0 100644 --- a/src/gateway/tools-invoke-shared.ts +++ b/src/gateway/tools-invoke-shared.ts @@ -8,6 +8,10 @@ import { runBeforeToolCallHook } from "../agents/agent-tools.before-tool-call.js import { resolveToolLoopDetectionConfig } from "../agents/agent-tools.js"; import { getChannelAgentToolMeta } from "../agents/channel-tools.js"; import { isKnownCoreToolId } from "../agents/tool-catalog.js"; +import { + AUTOMATIONS_TOOL_NAME, + isAutomationsToolName, +} from "../agents/tools/automations-tool-name.js"; import { ToolInputError, type AnyAgentTool } from "../agents/tools/common.js"; import { normalizeConversationReadInvocationOrigin, @@ -174,7 +178,13 @@ export async function invokeGatewayTool(params: { const conversationReadOrigin = normalizeConversationReadInvocationOrigin( params.conversationReadOrigin, ); - const toolName = normalizeOptionalString(params.input.name ?? params.input.tool) ?? ""; + const requestedToolName = normalizeOptionalString(params.input.name ?? params.input.tool) ?? ""; + // "cron" is a permanently accepted inbound alias for the scheduler tool + // (owner decision, RFC 0026; same contract as bash -> exec). Canonicalize + // before core-id checks and exact-name dispatch below. + const toolName = isAutomationsToolName(requestedToolName) + ? AUTOMATIONS_TOOL_NAME + : requestedToolName; if (!toolName) { return { ok: false, diff --git a/src/mcp/openclaw-tools-serve.test.ts b/src/mcp/openclaw-tools-serve.test.ts index 1f45c0018445..e7bc614d4f3c 100644 --- a/src/mcp/openclaw-tools-serve.test.ts +++ b/src/mcp/openclaw-tools-serve.test.ts @@ -28,7 +28,7 @@ describe("OpenClaw tools MCP server", () => { ); const listed = await handlers.listTools(); - expect(listed.tools.map((tool) => tool.name)).toContain("cron"); + expect(listed.tools.map((tool) => tool.name)).toContain("automations"); }); it("requires the managed bridge to pass a real agent session key", () => { diff --git a/src/mcp/openclaw-tools-serve.ts b/src/mcp/openclaw-tools-serve.ts index 9730524bcade..eb92b0b87dd5 100644 --- a/src/mcp/openclaw-tools-serve.ts +++ b/src/mcp/openclaw-tools-serve.ts @@ -6,6 +6,7 @@ */ import { pathToFileURL } from "node:url"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { AUTOMATIONS_TOOL_NAME } from "../agents/tools/automations-tool-name.js"; import type { AnyAgentTool } from "../agents/tools/common.js"; import { createCronTool } from "../agents/tools/cron-tool.js"; import { createSystemAgentTool } from "../agents/tools/system-agent-tool.js"; @@ -57,7 +58,10 @@ export function resolveOpenClawToolsForMcp( if (!agentSessionKey) { throw new Error(`${OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY_ENV} is required`); } - return createCronTool({ agentSessionKey, creatorToolAllowlist: [{ name: "cron" }] }); + return createCronTool({ + agentSessionKey, + creatorToolAllowlist: [{ name: AUTOMATIONS_TOOL_NAME }], + }); }); } diff --git a/src/mcp/plugin-tools-handlers.ts b/src/mcp/plugin-tools-handlers.ts index fc7ea7ab67b9..ffd06cf3e0eb 100644 --- a/src/mcp/plugin-tools-handlers.ts +++ b/src/mcp/plugin-tools-handlers.ts @@ -8,6 +8,7 @@ import { wrapToolWithBeforeToolCallHook, } from "../agents/agent-tools.before-tool-call.js"; import { BEFORE_TOOL_CALL_HOOK_CONTEXT } from "../agents/before-tool-call-metadata.js"; +import { isAutomationsToolName } from "../agents/tools/automations-tool-name.js"; import type { AnyAgentTool } from "../agents/tools/common.js"; import { formatErrorMessage } from "../infra/errors.js"; import { coerceChatContentText } from "../shared/chat-content.js"; @@ -82,7 +83,14 @@ export function createPluginToolsMcpHandlers(tools: AnyAgentTool[]) { })), }), callTool: async (params: CallPluginToolParams, signal?: AbortSignal) => { - const entry = toolMap.get(params.name); + // "cron" is a permanently accepted inbound alias for the scheduler tool + // (owner decision, RFC 0026; same contract as bash -> exec). Resolve it to + // the published canonical tool without re-advertising it in listTools. + const entry = + toolMap.get(params.name) ?? + (isAutomationsToolName(params.name) + ? Array.from(toolMap.entries()).find(([name]) => isAutomationsToolName(name))?.[1] + : undefined); if (!entry) { return { content: [{ type: "text", text: `Unknown tool: ${params.name}` }], diff --git a/src/security/audit-trust-model.test.ts b/src/security/audit-trust-model.test.ts index eff13f12967a..c6672843b2a3 100644 --- a/src/security/audit-trust-model.test.ts +++ b/src/security/audit-trust-model.test.ts @@ -244,8 +244,8 @@ describe("security audit trust model findings", () => { ); expect(finding?.severity).toBe("critical"); expect(finding?.detail).toContain("channels.whatsapp.groupPolicy"); - expect(finding?.detail).toContain("controlPlane=[cron]"); - expect(finding?.detail).not.toContain("controlPlane=[gateway, cron]"); + expect(finding?.detail).toContain("controlPlane=[automations]"); + expect(finding?.detail).not.toContain("controlPlane=[automations, gateway]"); }, }, { @@ -278,7 +278,7 @@ describe("security audit trust model findings", () => { (entry) => entry.checkId === "security.exposure.open_groups_with_control_plane_tools", ); expect(finding?.detail).toContain( - "agents.defaults (profile=messaging; controlPlane=[cron])", + "agents.defaults (profile=messaging; controlPlane=[automations])", ); }, }, diff --git a/src/security/dangerous-tools.ts b/src/security/dangerous-tools.ts index 4b39ed20dbe6..0f755fc0a3a8 100644 --- a/src/security/dangerous-tools.ts +++ b/src/security/dangerous-tools.ts @@ -1,5 +1,6 @@ // Shared tool-risk constants. // Keep these centralized so gateway HTTP restrictions and security audits don't drift. +import { AUTOMATIONS_TOOL_NAME } from "../agents/tools/automations-tool-name.js"; /** * Tools denied via Gateway HTTP `POST /tools/invoke` by default. @@ -32,7 +33,7 @@ export const DEFAULT_GATEWAY_HTTP_TOOL_DENY = [ "conversations_send", "conversations_turn", // Persistent automation control plane — can create/update/remove scheduled runs - "cron", + AUTOMATIONS_TOOL_NAME, // Gateway config can expose secrets and host topology "gateway", // Node command relay can reach system.run on paired hosts @@ -45,10 +46,10 @@ export const DEFAULT_GATEWAY_HTTP_TOOL_DENY = [ ] as const; /** - * Sensitive control-plane tools. `cron` can persist automation; `gateway` + * Sensitive control-plane tools. `automations` can persist scheduled runs; `gateway` * exposes configuration and schema details even though its agent actions are read-only. */ -export const GATEWAY_CONTROL_PLANE_TOOLS = ["cron", "gateway"] as const; +export const GATEWAY_CONTROL_PLANE_TOOLS = [AUTOMATIONS_TOOL_NAME, "gateway"] as const; /** * Core tools that require sender owner identity on Gateway-scoped surfaces. diff --git a/src/skills/runtime/tool-dispatch.test.ts b/src/skills/runtime/tool-dispatch.test.ts index f14c40062d75..3f2ecf67cc58 100644 --- a/src/skills/runtime/tool-dispatch.test.ts +++ b/src/skills/runtime/tool-dispatch.test.ts @@ -59,7 +59,7 @@ describe("resolveSkillDispatchTools", () => { const args = hoisted.createOpenClawToolsMock.mock.calls[0]?.[0]; expect(tools.map((tool) => tool.name)).toEqual(["read", "cron"]); - expect(args?.cronCreatorToolAllowlist).toEqual([{ name: "read" }, { name: "cron" }]); + expect(args?.cronCreatorToolAllowlist).toEqual([{ name: "read" }, { name: "automations" }]); expect(args?.nativeChannelId).toBe("native-room-1"); }); @@ -78,7 +78,7 @@ describe("resolveSkillDispatchTools", () => { expect(tools.map((tool) => tool.name)).toEqual(["read", "cron", "exec"]); expect(args?.cronCreatorToolAllowlist).toEqual([ { name: "read" }, - { name: "cron" }, + { name: "automations" }, { name: "exec" }, ]); }); diff --git a/src/talk/client-voice-confirmation.ts b/src/talk/client-voice-confirmation.ts index 5c3790e3eb4f..b25da18b92cf 100644 --- a/src/talk/client-voice-confirmation.ts +++ b/src/talk/client-voice-confirmation.ts @@ -1,6 +1,7 @@ /** In-memory spoken confirmation binding for high-impact Talk actions. */ import { createHash, randomUUID } from "node:crypto"; import { buildToolMutationState } from "../agents/tool-mutation.js"; +import { AUTOMATIONS_TOOL_NAME } from "../agents/tools/automations-tool-name.js"; const CONFIRMATION_TTL_MS = 2 * 60_000; @@ -72,7 +73,7 @@ function requiresHighImpactVoiceConfirmation(toolName: string, params: unknown): "computer", "mobile_ui", "canvas", - "cron", + AUTOMATIONS_TOOL_NAME, "process", ].includes(normalizedTool) ) { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json index b5a6fe047733..9dc8d5b3bc8c 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.discord-group.json @@ -1100,7 +1100,7 @@ "required": ["action"], "type": "object" }, - "name": "cron", + "name": "automations", "type": "function" }, { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json index 05680f896e37..9ab390452ba6 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.heartbeat-turn.json @@ -1096,7 +1096,7 @@ "required": ["action"], "type": "object" }, - "name": "cron", + "name": "automations", "type": "function" }, { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json index 2e12b2a23fde..1009dd7a792d 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/codex-dynamic-tools.telegram-direct.json @@ -1096,7 +1096,7 @@ "required": ["action"], "type": "object" }, - "name": "cron", + "name": "automations", "type": "function" }, { diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md index 445cb9df58c2..04381ac62540 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/discord-group-codex-message-tool.md @@ -78,7 +78,7 @@ "agents_list", "message", "sessions_spawn", - "cron", + "automations", "gateway", "nodes", "session_status", @@ -221,20 +221,20 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 61437, - "roughTokens": 15360 + "chars": 61444, + "roughTokens": 15361 }, "openClawDeveloperInstructions": { - "chars": 3804, - "roughTokens": 951 + "chars": 3811, + "roughTokens": 953 }, "totalTextOnly": { - "chars": 28187, - "roughTokens": 7047 + "chars": 28194, + "roughTokens": 7049 }, "totalWithDynamicToolsJson": { - "chars": 89626, - "roughTokens": 22407 + "chars": 89640, + "roughTokens": 22410 }, "userInputText": { "chars": 1300, @@ -421,7 +421,7 @@ Approval policy is currently never. Do not provide the `sandbox_permissions` for ````text You are a personal agent running inside OpenClaw. OpenClaw has dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes. -Deferred searchable OpenClaw dynamic tools available: cron, gateway, nodes, session_status, sessions_history, sessions_list, sessions_search, sessions_send, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. +Deferred searchable OpenClaw dynamic tools available: automations, gateway, nodes, session_status, sessions_history, sessions_list, sessions_search, sessions_send, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. Use Codex native `spawn_agent` for Codex subagents. `spawn_agent` and the other native collaboration tools may be deferred: when `spawn_agent` is not directly listed, load it with `tool_search` before spawning. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation, never as a substitute for `spawn_agent`. @@ -526,7 +526,7 @@ Full JSON: `codex-dynamic-tools.discord-group.json` "agents_list", "message", "sessions_spawn", - "cron", + "automations", "gateway", "nodes", "session_status", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md index 01050db150bf..a92aa0f63441 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md @@ -78,7 +78,7 @@ "agents_list", "message", "sessions_spawn", - "cron", + "automations", "gateway", "nodes", "session_status", @@ -221,20 +221,20 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 61129, - "roughTokens": 15283 + "chars": 61136, + "roughTokens": 15284 }, "openClawDeveloperInstructions": { - "chars": 2695, - "roughTokens": 674 + "chars": 2702, + "roughTokens": 676 }, "totalTextOnly": { - "chars": 26707, - "roughTokens": 6677 + "chars": 26714, + "roughTokens": 6679 }, "totalWithDynamicToolsJson": { - "chars": 87838, - "roughTokens": 21960 + "chars": 87852, + "roughTokens": 21963 }, "userInputText": { "chars": 929, @@ -421,7 +421,7 @@ Approval policy is currently never. Do not provide the `sandbox_permissions` for ````text You are a personal agent running inside OpenClaw. OpenClaw has dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes. -Deferred searchable OpenClaw dynamic tools available: cron, gateway, nodes, session_status, sessions_history, sessions_list, sessions_search, sessions_send, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. +Deferred searchable OpenClaw dynamic tools available: automations, gateway, nodes, session_status, sessions_history, sessions_list, sessions_search, sessions_send, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. Use Codex native `spawn_agent` for Codex subagents. `spawn_agent` and the other native collaboration tools may be deferred: when `spawn_agent` is not directly listed, load it with `tool_search` before spawning. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation, never as a substitute for `spawn_agent`. @@ -520,7 +520,7 @@ Full JSON: `codex-dynamic-tools.telegram-direct.json` "agents_list", "message", "sessions_spawn", - "cron", + "automations", "gateway", "nodes", "session_status", diff --git a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md index 79e9c4119bb1..05c13e6e77be 100644 --- a/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md +++ b/test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-heartbeat-codex-tool.md @@ -78,7 +78,7 @@ "agents_list", "message", "sessions_spawn", - "cron", + "automations", "gateway", "nodes", "session_status", @@ -222,20 +222,20 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the "roughTokens": 0 }, "dynamicToolsJson": { - "chars": 62661, - "roughTokens": 15666 + "chars": 62668, + "roughTokens": 15667 }, "openClawDeveloperInstructions": { - "chars": 2695, - "roughTokens": 674 + "chars": 2702, + "roughTokens": 676 }, "totalTextOnly": { - "chars": 27136, - "roughTokens": 6784 + "chars": 27143, + "roughTokens": 6786 }, "totalWithDynamicToolsJson": { - "chars": 89799, - "roughTokens": 22450 + "chars": 89813, + "roughTokens": 22454 }, "userInputText": { "chars": 1284, @@ -422,7 +422,7 @@ Approval policy is currently never. Do not provide the `sandbox_permissions` for ````text You are a personal agent running inside OpenClaw. OpenClaw has dynamic tools for OpenClaw-owned messaging, cron, sessions, media, gateway, and nodes. -Deferred searchable OpenClaw dynamic tools available: cron, gateway, nodes, session_status, sessions_history, sessions_list, sessions_search, sessions_send, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. +Deferred searchable OpenClaw dynamic tools available: automations, gateway, nodes, session_status, sessions_history, sessions_list, sessions_search, sessions_send, subagents, tts, web_fetch, web_search. Use `tool_search` to load exact callable specs before use. Use Codex native `spawn_agent` for Codex subagents. `spawn_agent` and the other native collaboration tools may be deferred: when `spawn_agent` is not directly listed, load it with `tool_search` before spawning. Use OpenClaw `sessions_spawn` only for OpenClaw or ACP delegation, never as a substitute for `spawn_agent`. @@ -521,7 +521,7 @@ Full JSON: `codex-dynamic-tools.heartbeat-turn.json` "agents_list", "message", "sessions_spawn", - "cron", + "automations", "gateway", "nodes", "session_status", diff --git a/test/helpers/agents/happy-path-prompt-snapshots.ts b/test/helpers/agents/happy-path-prompt-snapshots.ts index 6029cbdfb780..6689987bb1ea 100644 --- a/test/helpers/agents/happy-path-prompt-snapshots.ts +++ b/test/helpers/agents/happy-path-prompt-snapshots.ts @@ -59,7 +59,7 @@ const CODEX_YOLO_PERMISSION_INSTRUCTIONS = [ ].join("\n"); const HAPPY_PATH_TOOL_NAMES = new Set([ "nodes", - "cron", + "automations", "message", "heartbeat_respond", "tts",