mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(agents): rename scheduler agent tool cron -> automations (#114841)
* refactor(agents): route scheduler tool-name consumers through canonical identity Introduce AUTOMATIONS_TOOL_NAME + isAutomationsToolName() in src/agents/tools/automations-tool-name.ts as the single source of truth for the scheduler agent tool's name, and convert every exact-name consumer: factory descriptors, deferred-followup availability detection, add-counting, policy deny lists, mutation classification, trusted media set, tool catalog id, system-prompt tool order and tool-line map, sandbox deny defaults, delegation capability map, MCP loopback probes, and local-model lean deny. Behavior-neutral: the constant still resolves to "cron". Prepares the rename in RFC openclaw/rfcs#50 so the flip is a one-line change with no scattered literals. * feat(agents): rename scheduler agent tool cron -> automations Flip AUTOMATIONS_TOOL_NAME to "automations" and register the legacy name: - TOOL_NAME_ALIASES gains cron -> automations, so persisted toolsAllow/ toolsDeny lists, tool groups, and creator allowlists written before the rename keep matching through the same shipped mechanism as bash -> exec. No doctor rewrite needed. - isAutomationsToolName() accepts legacy names so saved transcripts keep their mutation/replay-safety classification; MUTATING_TOOL_NAMES retains the legacy entry for the same reason. - Tool label, catalog label, and tool-search keywords follow the rename ("cron" kept as a search synonym). - Regression tests cover old-name policy matching (allow and deny), legacy transcript replay classification, and legacy creator allowlists normalizing to the canonical id. Model-facing description strings still say cron; those move in the follow-up strings PR. Part of RFC openclaw/rfcs#50 Phase 1. * test(agents): update creator-cap expectations for canonical automations id The creator tool surface derives from normalized live tool names, so derived toolsAllow outputs now emit "automations". Passthrough paths without a creator cap keep storing user input verbatim; those expectations stay on the legacy name as stored-data coverage. * fix(gateway): canonicalize legacy cron tool calls and restore scheduler deny protection Review follow-ups from ClawSweeper and Codex on the rename (RFC 0026): - MCP loopback tools/call resolves legacy "cron" names to the published automations tool without re-advertising the old name in tools/list. - Gateway /tools/invoke canonicalizes legacy names before core-id checks and exact-name dispatch, so pre-rename integrations keep working. - Security fix: dangerous-tools deny lists (owner-only HTTP deny and control-plane set) were keyed on the literal "cron", so the renamed tool silently lost default-deny and owner-only protection on the HTTP invoke surface. Lists now use the canonical constant, and the gateway.tools.allow un-deny filter normalizes both sides so legacy allow entries still lift it. - Voice high-impact confirmation list and MCP serve creator allowlist follow the canonical name. Existing cron-regression suite now proves the legacy path end to end: default deny 404 for both names, legacy allow entry lifts the deny, and non-owner protection holds. * fix(agents): cover stdio MCP legacy calls, probe prompts, and prompt snapshots for the rename - stdio MCP servers (openclaw-tools-serve / plugin tools handlers) resolve legacy "cron" callTool names to the published canonical tool, matching the HTTP loopback behavior; listTools stays canonical-only. - Live probe prompts instruct harnesses to load/call the automations MCP tool (mcp__openclaw__automations) instead of the retired name. - Prompt snapshot fixture filter follows the canonical name (the renamed tool had silently dropped out of the Codex dynamic-tools snapshots); snapshots regenerated as a clean rename. - Type-cast the new mcp-http handler test payloads for check-test-types. * test(agents): update tool-surface expectations for the automations rename CI-surfaced fallout in shards not covered by the focused local runs: tool availability, agent-config filtering, coding-tools construction, model-provider lean policy, and skill dispatch all assert the scheduler tool's surface name. Mock fixtures and expectations follow the canonical id; legacy-name coverage stays in the dedicated policy/creator-cap/invoke regression suites. * test(gateway): update tool-resolution exclude expectations for automations rename * test(security): update trust-model audit expectations for automations rename * docs(agents): declare cron a permanent scheduler-tool alias per owner decision Maintainer decision (Omar): cron is not being retired anywhere — config keys, RPC methods, schedule syntax, and the CLI token all keep it, and the tool alias follows the same permanent contract as bash -> exec. No doctor rewrite and no removal window; comments updated to state the contract instead of a deprecation plan. * chore(agents): regen prompt snapshots after rebase onto main * fix(agents): teach canonical automations tool in fallback guidance and reuse the identity source Review follow-ups: the structured-list fallback still taught models the cron tool; the cron-scope test echoed its own stub; MCP serve allowlist and voice confirmation hardcoded the name instead of the canonical constant. * fix(mcp): place automations identity import outside the header comment --------- Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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<string, ReadonlySet<string>> = 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"])],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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"]),
|
||||
|
||||
@@ -63,7 +63,7 @@ describe("tool-catalog", () => {
|
||||
"screen",
|
||||
"dashboard",
|
||||
"terminal",
|
||||
"cron",
|
||||
"automations",
|
||||
"get_goal",
|
||||
"create_goal",
|
||||
"update_goal",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,6 +19,8 @@ type ToolProfilePolicy = {
|
||||
const TOOL_NAME_ALIASES: Record<string, string> = {
|
||||
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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"] },
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}. ` +
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<MockGatewayTool["execute"]>(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();
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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" },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 }],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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}` }],
|
||||
|
||||
@@ -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])",
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
) {
|
||||
|
||||
+1
-1
@@ -1100,7 +1100,7 @@
|
||||
"required": ["action"],
|
||||
"type": "object"
|
||||
},
|
||||
"name": "cron",
|
||||
"name": "automations",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -1096,7 +1096,7 @@
|
||||
"required": ["action"],
|
||||
"type": "object"
|
||||
},
|
||||
"name": "cron",
|
||||
"name": "automations",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -1096,7 +1096,7 @@
|
||||
"required": ["action"],
|
||||
"type": "object"
|
||||
},
|
||||
"name": "cron",
|
||||
"name": "automations",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
|
||||
Vendored
+11
-11
@@ -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",
|
||||
|
||||
test/fixtures/agents/prompt-snapshots/codex-runtime-happy-path/telegram-direct-codex-message-tool.md
Vendored
+11
-11
@@ -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",
|
||||
|
||||
Vendored
+11
-11
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user