Files
openclaw/src/agents/agent-tools.create-openclaw-coding-tools.test.ts
Yuval Dinodia edf4aca7bc fix(agents): apply_patch destroys an existing file when a patch creates that path (#114911)
* fix(agents): stop apply_patch from silently overwriting existing files

An "*** Add File:" hunk wrote its target unconditionally. When the path
already existed, apply_patch replaced the entire file, returned Success,
and listed the path under "added", so neither the model nor the UI got
any signal that existing content had been destroyed. The "*** Move to:"
destination of an update hunk had the same gap and reported the clobbered
path as merely modified.

The add and move-to branches now check the destination through the patch
file ops before writing and fail closed when it exists. Routing the check
through fileOps keeps it correct on all three backends (workspace-scoped
fs-safe root, raw fs, sandbox bridge). The check runs per hunk in patch
order, so deleting a path earlier in the same patch and recreating it
still works.

* fix(agents): make apply_patch destination creation atomic

The previous guard checked that an add or move-to destination was absent
and then wrote it. A competing writer could create the path in that gap,
after which the write still replaced it, so the no-clobber guarantee did
not hold under contention.

Destination creation now goes through a single exclusive create-if-absent
operation on every patch backend: Root.create for the workspace-scoped
default, an O_EXCL write for the raw filesystem, and a new pinned create
operation in the sandbox mutation helper that opens the target with
O_CREAT|O_EXCL and reports a reserved exit code when it already exists.
PatchFileOps drops its separate existence check.

Resolving the host ops behind an early return removes the repeated
workspaceOnly branch inside each operation and the optional-call dance
that let a missing root silently skip a write.

* fix(agents): complete atomic apply-patch creation

* fix(agents): preserve raced create replacements

* fix(agents): handle fs-safe patch collisions

* fix(agents): publish sandbox creates atomically

* test(agents): cover exclusive create provenance rollback

* fix(agents): use typed exclusive-create signal

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-07-29 20:17:54 +08:00

2335 lines
79 KiB
TypeScript

/**
* Broad coverage for createOpenClawCodingTools.
* Verifies plugin tools, tool policy, schema cleanup, sandbox fs tools, and
* assembled tool allowlist behavior.
*/
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { replaceSessionEntry } from "../config/sessions/session-accessor.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
findUnsupportedSchemaKeywords,
GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS,
} from "../plugin-sdk/provider-tools.js";
import {
initializeGlobalHookRunner,
resetGlobalHookRunner,
} from "../plugins/hook-runner-global.js";
import { createMockPluginRegistry } from "../plugins/hooks.test-fixtures.js";
import {
clearMemoryPluginState,
registerMemoryCapability,
type MemoryFlushPlan,
} from "../plugins/memory-state.js";
import "./test-helpers/fast-bash-tools.js";
import "./test-helpers/fast-coding-tools.js";
import "./test-helpers/fast-openclaw-tools.js";
import { isPluginToolAllowed } from "../plugins/tool-grant-allowlist.js";
import { wrapToolWithBeforeToolCallHook } from "./agent-tools.before-tool-call.js";
import { createOpenClawCodingTools } from "./agent-tools.js";
import { runWithAgentRingZeroTools } from "./agent-tools.ring-zero-context.js";
import type { AuthProfileStore } from "./auth-profiles/types.js";
import { resolveConversationCapabilityProfile } from "./conversation-capability-profile.js";
import * as openClawPluginTools from "./openclaw-plugin-tools.js";
import { createOpenClawTools } from "./openclaw-tools.js";
import { expectReadWriteEditTools } from "./test-helpers/agent-tools-fs-helpers.js";
import { createAgentToolsSandboxContext } from "./test-helpers/agent-tools-sandbox-context.js";
import { stubTool } from "./test-helpers/fast-tool-stubs.js";
import { createHostSandboxFsBridge } from "./test-helpers/host-sandbox-fs-bridge.js";
import { buildEmptyExplicitToolAllowlistError } from "./tool-allowlist-guard.js";
import { DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, normalizeToolName } from "./tool-policy.js";
import { replaceWithEffectiveCronCreatorToolAllowlist } from "./tools/cron-tool.js";
import { getGatewayToolCallerIdentity } from "./tools/gateway-caller-context.js";
const tinyPngBuffer = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO2f7z8AAAAASUVORK5CYII=",
"base64",
);
const XAI_UNSUPPORTED_SCHEMA_KEYWORDS = new Set(["minContains", "maxContains"]);
function collectActionValues(schema: unknown, values: Set<string>): void {
if (!schema || typeof schema !== "object") {
return;
}
const record = schema as Record<string, unknown>;
if (typeof record.const === "string") {
values.add(record.const);
}
if (Array.isArray(record.enum)) {
for (const value of record.enum) {
if (typeof value === "string") {
values.add(value);
}
}
}
if (Array.isArray(record.anyOf)) {
for (const variant of record.anyOf) {
collectActionValues(variant, values);
}
}
}
async function writeSessionStore(
storeTemplate: string,
agentId: string,
entries: Record<string, unknown>,
) {
const storePath = storeTemplate.replaceAll("{agentId}", agentId);
for (const [sessionKey, entry] of Object.entries(entries)) {
await replaceSessionEntry({ agentId, sessionKey, storePath }, entry as SessionEntry);
}
}
function createToolsForStoredSession(storeTemplate: string, sessionKey: string) {
return createOpenClawCodingTools({
sessionKey,
config: {
session: {
store: storeTemplate,
},
agents: {
defaults: {
subagents: {
maxSpawnDepth: 2,
},
},
},
},
});
}
function expectNoSubagentControlTools(tools: ReturnType<typeof createOpenClawCodingTools>) {
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("sessions_spawn")).toBe(false);
expect(names.has("sessions_list")).toBe(false);
expect(names.has("sessions_history")).toBe(false);
expect(names.has("subagents")).toBe(false);
}
function applyRuntimeToolsAllow<T extends { name: string }>(tools: T[], toolsAllow: string[]) {
const allowSet = new Set(toolsAllow.map((name) => normalizeToolName(name)));
return tools.filter((tool) => allowSet.has(normalizeToolName(tool.name)));
}
type OpenClawCodingTool = ReturnType<typeof createOpenClawCodingTools>[number];
type OpenClawToolsOptions = NonNullable<Parameters<typeof createOpenClawTools>[0]>;
function toolNameList(tools: readonly { name: string }[]): string[] {
return tools.map((tool) => tool.name);
}
function requireTool(tools: OpenClawCodingTool[], name: string): OpenClawCodingTool {
const tool = tools.find((candidate) => candidate.name === name);
if (!tool) {
throw new Error(`expected ${name} tool`);
}
return tool;
}
function requireToolExecute(tool: OpenClawCodingTool): NonNullable<OpenClawCodingTool["execute"]> {
if (!tool.execute) {
throw new Error(`expected ${tool.name} tool execute`);
}
return tool.execute;
}
function latestCreateOpenClawToolsOptions(): OpenClawToolsOptions {
const calls = vi.mocked(createOpenClawTools).mock.calls;
const lastCall = calls.at(-1);
const options = lastCall?.[0];
if (!options) {
throw new Error("expected createOpenClawTools call");
}
return options;
}
function expectListIncludes(
list: readonly string[] | undefined,
expected: readonly string[],
): void {
if (!list) {
throw new Error("expected string list");
}
for (const value of expected) {
expect(list.includes(value)).toBe(true);
}
}
function cronCreatorToolNames(
list: OpenClawToolsOptions["cronCreatorToolAllowlist"] | undefined,
): string[] | undefined {
return list?.map((entry) => (typeof entry === "string" ? entry : entry.name));
}
describe("createOpenClawCodingTools", () => {
it("forwards the session web-search gate to core tool materialization", () => {
vi.mocked(createOpenClawTools).mockClear();
createOpenClawCodingTools({ webSearchEnabled: false });
expect(latestCreateOpenClawToolsOptions().webSearchEnabled).toBe(false);
});
it("reads node-hosted skill content through the assembled workspace-only read tool", async () => {
const locator = "node://node-1/skills/pond/SKILL.md";
const tools = createOpenClawCodingTools({
config: { tools: { fs: { workspaceOnly: true } } },
skillsSnapshot: {
prompt: "",
skills: [{ name: "pond" }],
resolvedSkills: [
{
name: "pond",
description: "Pond skill",
filePath: locator,
baseDir: "node://node-1/skills/pond",
readContent: "# Pond\nassembled-marker",
source: "openclaw-node",
sourceInfo: {
source: "openclaw-node",
path: locator,
scope: "temporary",
origin: "top-level",
},
disableModelInvocation: false,
},
],
},
});
const result = await requireTool(tools, "read").execute("node-skill-read", {
path: locator,
});
expect(JSON.stringify(result)).toContain("assembled-marker");
});
const testConfig: OpenClawConfig = {};
afterEach(() => {
clearMemoryPluginState();
resetGlobalHookRunner();
});
it("exposes only gateway config reads to owner sessions", () => {
const tools = createOpenClawCodingTools({ config: testConfig });
const gateway = requireTool(tools, "gateway");
const parameters = gateway.parameters as {
properties?: Record<string, unknown>;
};
const action = parameters.properties?.action as
| { const?: unknown; enum?: unknown[] }
| undefined;
const values = new Set<string>();
collectActionValues(action, values);
expect([...values]).toEqual(["config.get", "config.schema.lookup"]);
});
it("does not add Tool Search control tools from the shared factory by default", () => {
const tools = createOpenClawCodingTools({
config: {
tools: {
toolSearch: true,
},
},
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("tool_search_code")).toBe(false);
expect(names.has("tool_search")).toBe(false);
expect(names.has("tool_describe")).toBe(false);
expect(names.has("tool_call")).toBe(false);
});
it("passes explicit channel and requester facts to wrapped tool hooks", async () => {
const beforeToolCall = vi.fn();
initializeGlobalHookRunner(
createMockPluginRegistry([{ hookName: "before_tool_call", handler: beforeToolCall }]),
);
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hook-channel-"));
await fs.writeFile(path.join(tmpDir, "note.txt"), "hello");
const memberRoleIds = ["maintainer-role"];
const tools = createOpenClawCodingTools({
workspaceDir: tmpDir,
messageChannel: "discord",
agentAccountId: "operations",
currentChannelId: "telegram:-100123",
hookChannelId: "-100123",
memberRoleIds,
senderId: "maintainer-user",
senderIsOwner: false,
});
memberRoleIds.push("late-role");
const readTool = requireTool(tools, "read");
await requireToolExecute(readTool)("tool-hook-channel", { path: "note.txt" });
expect(beforeToolCall).toHaveBeenCalledTimes(1);
expect(beforeToolCall.mock.calls[0]?.[1]).toEqual(
expect.objectContaining({
channelId: "-100123",
requester: {
channel: "discord",
accountId: "operations",
senderId: "maintainer-user",
senderIsOwner: false,
roleIds: ["maintainer-role"],
},
}),
);
});
it("re-wraps existing before_tool_call hooks once with the current context", async () => {
const beforeToolCall = vi.fn();
initializeGlobalHookRunner(
createMockPluginRegistry([{ hookName: "before_tool_call", handler: beforeToolCall }]),
);
const execute = vi.fn().mockResolvedValue({ content: [], details: { ok: true } });
const wrapped = wrapToolWithBeforeToolCallHook(
{
name: "already_wrapped",
label: "Already wrapped",
description: "Already wrapped tool",
parameters: {},
execute,
},
{ agentId: "main", sessionId: "session-original" },
);
vi.mocked(createOpenClawTools).mockReturnValueOnce([wrapped as never]);
const tools = createOpenClawCodingTools({ agentId: "main", sessionId: "session-new" });
const tool = requireTool(tools, "already_wrapped");
await requireToolExecute(tool)("call-wrapped", {});
expect(beforeToolCall).toHaveBeenCalledTimes(1);
expect(beforeToolCall.mock.calls[0]?.[1]).toEqual(
expect.objectContaining({ agentId: "main", sessionId: "session-new" }),
);
expect(execute).toHaveBeenCalledTimes(1);
expect(tool.parameters).toEqual({ type: "object", properties: {} });
});
it("adds Tool Search control tools when explicitly requested", () => {
const tools = createOpenClawCodingTools({
includeToolSearchControls: true,
config: {
tools: {
toolSearch: true,
},
},
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("tool_search_code")).toBe(true);
expect(names.has("tool_search")).toBe(true);
expect(names.has("tool_describe")).toBe(true);
expect(names.has("tool_call")).toBe(true);
});
it("keeps Tool Search controls available under restrictive tool profiles", () => {
const tools = createOpenClawCodingTools({
includeToolSearchControls: true,
config: {
tools: {
profile: "coding",
toolSearch: true,
},
},
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("tool_search_code")).toBe(true);
expect(names.has("tool_search")).toBe(true);
expect(names.has("tool_describe")).toBe(true);
expect(names.has("tool_call")).toBe(true);
expect(names.has("message")).toBe(false);
});
it("keeps Tool Search controls available under restrictive tool allowlists", () => {
const tools = createOpenClawCodingTools({
includeToolSearchControls: true,
config: {
tools: {
allow: ["read"],
toolSearch: true,
},
},
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("read")).toBe(true);
expect(names.has("exec")).toBe(false);
expect(names.has("tool_search_code")).toBe(true);
expect(names.has("tool_search")).toBe(true);
expect(names.has("tool_describe")).toBe(true);
expect(names.has("tool_call")).toBe(true);
});
it("lets explicit deny policies remove Tool Search controls", () => {
const tools = createOpenClawCodingTools({
includeToolSearchControls: true,
config: {
tools: {
profile: "coding",
deny: ["tool_search_code"],
toolSearch: true,
},
},
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("tool_search_code")).toBe(false);
expect(names.has("read")).toBe(true);
});
it("keeps Tool Search controls when core OpenClaw tools are not materialized", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const tools = createOpenClawCodingTools({
includeCoreTools: false,
includeToolSearchControls: true,
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: true,
},
config: {
tools: {
toolSearch: true,
},
},
});
const names = new Set(tools.map((tool) => tool.name));
expect(createOpenClawToolsMock).not.toHaveBeenCalled();
expect(names.has("tool_search_code")).toBe(true);
expect(names.has("tool_search")).toBe(true);
expect(names.has("tool_describe")).toBe(true);
expect(names.has("tool_call")).toBe(true);
expect(names.has("message")).toBe(false);
expect(names.has("exec")).toBe(false);
});
it("exposes control-plane tools to configured sessions", () => {
const tools = createOpenClawCodingTools({
config: testConfig,
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("cron")).toBe(true);
expect(names.has("gateway")).toBe(true);
expect(names.has("nodes")).toBe(true);
});
it("resolves isolated cron runtime toolsAllow", () => {
const allowed = applyRuntimeToolsAllow(
createOpenClawCodingTools({
config: testConfig,
}),
["cron"],
);
expect(allowed.map((tool) => tool.name)).toEqual(["cron"]);
expect(
buildEmptyExplicitToolAllowlistError({
sources: [{ label: "runtime toolsAllow", entries: ["cron"] }],
callableToolNames: allowed.map((tool) => tool.name),
toolsEnabled: true,
}),
).toBeNull();
});
it("keeps the injected ring-zero tool under policy and rejects a same-name replacement", () => {
const injectedTool = {
...stubTool("openclaw"),
label: "OpenClaw",
description: "trusted ring-zero tool",
execute: async () => ({ content: [], details: {} }),
};
const duplicateTool = {
...stubTool("openclaw"),
label: "OpenClaw",
description: "duplicate plugin tool",
execute: async () => ({ content: [], details: {} }),
};
vi.mocked(createOpenClawTools).mockReturnValueOnce([duplicateTool]);
const tools = runWithAgentRingZeroTools([injectedTool], () =>
createOpenClawCodingTools({
config: { tools: { allow: ["read"], deny: ["openclaw"] } },
runtimeToolAllowlist: ["openclaw"],
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: true,
},
}),
);
expect(tools).toHaveLength(1);
expect(tools[0]?.name).toBe("openclaw");
expect(tools[0]?.description).toBe("trusted ring-zero tool");
});
it("uses runtime toolsAllow when materializing plugin tools", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
runtimeToolAllowlist: ["memory_search", "memory_get"],
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
const options = latestCreateOpenClawToolsOptions();
expectListIncludes(options.pluginToolAllowlist, ["memory_search", "memory_get"]);
});
it("does not inherit native-harness bridge runtime allowlists", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
runtimeToolAllowlist: ["sessions_spawn", "memory_search"],
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
expect(latestCreateOpenClawToolsOptions().pluginToolAllowlist).toEqual([
"sessions_spawn",
"memory_search",
]);
expect(latestCreateOpenClawToolsOptions().inheritedToolAllowlist).toEqual([]);
});
it("inherits embedded runtime toolsAllow when explicitly marked as parent capability", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const runtimeToolAllowlist = ["sessions_spawn", "memory_search"];
createOpenClawCodingTools({
config: testConfig,
runtimeToolAllowlist,
conversationCapabilityProfile: resolveConversationCapabilityProfile({
config: testConfig,
runtimeToolAllowlist,
inheritRuntimeToolAllowlist: true,
}),
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
const inheritedAllow = latestCreateOpenClawToolsOptions().inheritedToolAllowlist;
expectListIncludes(inheritedAllow, ["sessions_spawn"]);
expect(inheritedAllow?.includes("read")).toBe(false);
expect(inheritedAllow?.includes("exec")).toBe(false);
});
it("lets direct restricted callers inherit runtime toolsAllow into subagent spawns", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
runtimeToolAllowlist: ["sessions_spawn", "read"],
inheritRuntimeToolAllowlist: true,
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
const inheritedAllow = latestCreateOpenClawToolsOptions().inheritedToolAllowlist;
expectListIncludes(inheritedAllow, ["sessions_spawn", "read"]);
expect(inheritedAllow?.includes("exec")).toBe(false);
});
it("keeps restricted spawn inheritance in the caller-owned runtime snapshot", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const inheritedToolAllowlistRef: string[] = [];
createOpenClawCodingTools({
config: { tools: { allow: ["read", "sessions_spawn"] } },
inheritedToolAllowlistRef,
});
expect(latestCreateOpenClawToolsOptions().inheritedToolAllowlist).toBe(
inheritedToolAllowlistRef,
);
expectListIncludes(inheritedToolAllowlistRef, ["read", "sessions_spawn"]);
expect(inheritedToolAllowlistRef).not.toContain("exec");
});
it("does not snapshot additive alsoAllow policies for spawn inheritance", () => {
const inheritedToolAllowlistRef: string[] = [];
createOpenClawCodingTools({
config: { tools: { alsoAllow: ["read"], deny: ["exec"] } },
inheritedToolAllowlistRef,
});
expect(inheritedToolAllowlistRef).toEqual([]);
expect(latestCreateOpenClawToolsOptions().inheritedToolDenylist).toContain("exec");
});
it("preserves runtime-allowed message through restrictive profiles", () => {
const tools = createOpenClawCodingTools({
config: { tools: { profile: "minimal" } },
runtimeToolAllowlist: ["message"],
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(toolNameList(tools)).toContain("message");
});
it("preserves runtime-allowed message through local model lean filtering", () => {
const tools = createOpenClawCodingTools({
config: {
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
tools: { profile: "minimal" },
},
runtimeToolAllowlist: ["message"],
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(toolNameList(tools)).toContain("message");
});
it("preserves configured media tools through local model lean filtering", () => {
const tools = createOpenClawCodingTools({
config: {
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
list: [
{
id: "artist",
tools: {
alsoAllow: ["video_generate"],
byProvider: {
ollama: {
alsoAllow: ["tts"],
},
},
},
},
],
},
tools: {
alsoAllow: ["pdf"],
byProvider: {
"ollama/qwen3.5:9b": {
alsoAllow: ["image_generate"],
},
},
},
},
agentId: "artist",
modelProvider: "ollama",
modelId: "qwen3.5:9b",
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(toolNameList(tools)).toEqual(
expect.arrayContaining(["image_generate", "pdf", "tts", "video_generate"]),
);
});
it("does not treat built-in profile tools as lean-mode overrides", () => {
const tools = createOpenClawCodingTools({
config: {
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
tools: {
profile: "coding",
},
},
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(toolNameList(tools)).not.toEqual(
expect.arrayContaining(["image_generate", "video_generate"]),
);
});
it("preserves forced message through local model lean filtering without runtime allowlist", () => {
const tools = createOpenClawCodingTools({
config: {
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
tools: { profile: "minimal" },
},
forceMessageTool: true,
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(toolNameList(tools)).toContain("message");
});
it("preserves message-tool-only replies through local model lean filtering without runtime allowlist", () => {
const tools = createOpenClawCodingTools({
config: {
agents: {
defaults: {
experimental: {
localModelLean: true,
},
},
},
tools: { profile: "minimal" },
},
sourceReplyDeliveryMode: "message_tool_only",
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(toolNameList(tools)).toContain("message");
});
it("preserves runtime allowlist groups containing message through restrictive profiles", () => {
for (const runtimeToolAllowlist of [["group:messaging"], ["group:openclaw"], ["*"]]) {
const tools = createOpenClawCodingTools({
config: { tools: { profile: "minimal" } },
runtimeToolAllowlist,
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(toolNameList(tools)).toContain("message");
}
});
it("passes source reply delivery mode to OpenClaw tool construction", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
forceMessageTool: true,
sourceReplyDeliveryMode: "message_tool_only",
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
expect(latestCreateOpenClawToolsOptions().sourceReplyDeliveryMode).toBe("message_tool_only");
});
it("passes configured filesystem policy to OpenClaw tool construction", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: { tools: { fs: { workspaceOnly: true } } },
});
expect(latestCreateOpenClawToolsOptions().fsPolicy).toEqual({ workspaceOnly: true });
});
it("uses the canonical spawn workspace for follow-up task suggestions", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const sandboxDir = "/sandbox/workspace";
createOpenClawCodingTools({
sandbox: createAgentToolsSandboxContext({
workspaceDir: sandboxDir,
workspaceAccess: "ro",
fsBridge: createHostSandboxFsBridge(sandboxDir),
}),
workspaceDir: "/agent/workspace",
cwd: sandboxDir,
spawnWorkspaceDir: "/host/project",
});
expect(latestCreateOpenClawToolsOptions()).toMatchObject({
workspaceDir: "/agent/workspace",
spawnWorkspaceDir: "/host/project",
cwd: "/host/project",
});
});
it("keeps an unsandboxed task repo as the follow-up suggestion cwd", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
workspaceDir: "/agent/workspace",
cwd: "/task/repo",
spawnWorkspaceDir: "/agent/workspace",
});
expect(latestCreateOpenClawToolsOptions().cwd).toBe("/task/repo");
});
it("skips unrelated tool families when construction is planned from a narrow allowlist", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const tools = createOpenClawCodingTools({
config: testConfig,
toolConstructionPlan: {
includeBaseCodingTools: true,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: false,
},
});
const names = new Set(tools.map((tool) => tool.name));
expect(createOpenClawToolsMock).not.toHaveBeenCalled();
expect(names.has("read")).toBe(true);
expect(names.has("write")).toBe(true);
expect(names.has("edit")).toBe(true);
expect(names.has("exec")).toBe(false);
expect(names.has("process")).toBe(false);
expect(names.has("apply_patch")).toBe(false);
expect(names.has("message")).toBe(false);
});
it("passes plugin suppression into OpenClaw tool construction plans", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: false,
},
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
expect(latestCreateOpenClawToolsOptions().disablePluginTools).toBe(true);
});
it("forwards trusted conversation recall to OpenClaw tool construction", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const conversationRecall = {
anchorSessionKey: "agent:main:telegram:direct:owner",
scope: "same-agent-private" as const,
corpus: "sessions" as const,
};
createOpenClawCodingTools({
config: testConfig,
conversationRecall,
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: true,
includePluginTools: true,
},
});
expect(latestCreateOpenClawToolsOptions().conversationRecall).toEqual(conversationRecall);
});
it("keeps plugin-only construction off the OpenClaw core factory", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
includeCoreTools: false,
runtimeToolAllowlist: ["memory_search"],
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: true,
},
});
expect(createOpenClawToolsMock).not.toHaveBeenCalled();
});
it("forwards prepared run facts to plugin-only tool construction", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const resolvePluginToolsSpy = vi
.spyOn(openClawPluginTools, "resolveOpenClawPluginToolsForOptions")
.mockReturnValue([]);
try {
createOpenClawCodingTools({
config: testConfig,
includeCoreTools: false,
runtimeToolAllowlist: ["memory_search"],
modelProvider: "openrouter",
modelId: "openrouter/auto",
nativeChannelId: "oc_native_chat",
clientCaps: ["inline-widgets"],
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: true,
},
});
expect(createOpenClawToolsMock).not.toHaveBeenCalled();
expect(resolvePluginToolsSpy).toHaveBeenCalledTimes(1);
const pluginToolOptions = resolvePluginToolsSpy.mock.calls[0]?.[0].options;
expect(pluginToolOptions?.modelProvider).toBe("openrouter");
expect(pluginToolOptions?.modelId).toBe("openrouter/auto");
expect(pluginToolOptions?.nativeChannelId).toBe("oc_native_chat");
expect(pluginToolOptions?.clientCaps).toEqual(["inline-widgets"]);
} finally {
resolvePluginToolsSpy.mockRestore();
}
});
it("wraps plugin-only tools with scheduled creator authority and live routing context", async () => {
let observedIdentity: unknown;
const resolvePluginToolsSpy = vi
.spyOn(openClawPluginTools, "resolveOpenClawPluginToolsForOptions")
.mockReturnValue([
{
name: "file_fetch",
label: "File fetch",
description: "Fetch a file",
parameters: { type: "object", properties: {} },
execute: async () => {
observedIdentity = getGatewayToolCallerIdentity();
return { content: [{ type: "text" as const, text: "ok" }], details: {} };
},
},
]);
try {
const tools = createOpenClawCodingTools({
config: testConfig,
agentId: "main",
sessionKey: "agent:main:telegram:direct:alice",
messageProvider: "discord-voice",
messageChannel: "discord",
messageTo: "channel:123",
agentAccountId: "work",
scheduledToolPolicy: {
version: 1,
mode: "account",
ownerSessionKey: "agent:main:discord:group:ops",
ownerAccountId: "creator",
},
messageThreadId: "42",
includeCoreTools: false,
runtimeToolAllowlist: ["file_fetch"],
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: true,
},
});
await requireTool(tools, "file_fetch").execute?.("tool-call-1", {});
expect(observedIdentity).toEqual({
agentId: "main",
sessionKey: "agent:main:telegram:direct:alice",
turnSourceChannel: "discord",
turnSourceTo: "channel:123",
turnSourceAccountId: "creator",
turnSourceThreadId: "42",
});
} finally {
resolvePluginToolsSpy.mockRestore();
}
});
it("forwards owner identity to plugin-only tool construction", () => {
const resolvePluginToolsSpy = vi
.spyOn(openClawPluginTools, "resolveOpenClawPluginToolsForOptions")
.mockReturnValue([]);
try {
createOpenClawCodingTools({
config: testConfig,
includeCoreTools: false,
runtimeToolAllowlist: ["codex_threads"],
senderIsOwner: true,
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: true,
},
});
expect(resolvePluginToolsSpy).toHaveBeenCalledTimes(1);
expect(resolvePluginToolsSpy.mock.calls[0]?.[0].options?.senderIsOwner).toBe(true);
} finally {
resolvePluginToolsSpy.mockRestore();
}
});
it("forwards the native channel id through standard tool construction", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
chatType: "group",
nativeChannelId: "oc_native_chat",
messageActionTurnCapability: "turn-capability-1",
});
expect(latestCreateOpenClawToolsOptions().nativeChannelId).toBe("oc_native_chat");
expect(latestCreateOpenClawToolsOptions().currentChatType).toBe("group");
expect(latestCreateOpenClawToolsOptions().messageActionTurnCapability).toBe(
"turn-capability-1",
);
});
it("separates scheduled Gateway authority from the live delivery account", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: testConfig,
agentAccountId: "delivery",
scheduledToolPolicy: {
version: 1,
mode: "account",
ownerSessionKey: "agent:main:discord:group:ops",
ownerAccountId: "creator",
},
});
expect(latestCreateOpenClawToolsOptions()).toMatchObject({
agentAccountId: "delivery",
gatewayCallerAccountId: "creator",
});
});
it("forwards auth profiles to plugin-only tool construction", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const resolvePluginToolsSpy = vi
.spyOn(openClawPluginTools, "resolveOpenClawPluginToolsForOptions")
.mockReturnValue([]);
const authProfileStore = {
version: 1,
order: { xai: ["xai-oauth"] },
profiles: {
"xai-oauth": {
type: "oauth",
provider: "xai",
access: "xai-oauth-access-token", // pragma: allowlist secret
refresh: "xai-oauth-refresh-token", // pragma: allowlist secret
expires: Date.now() + 60_000,
},
},
} satisfies AuthProfileStore;
try {
createOpenClawCodingTools({
config: {
auth: {
order: {
xai: ["xai-oauth"],
},
},
},
authProfileStore,
includeCoreTools: false,
runtimeToolAllowlist: ["x_search"],
toolConstructionPlan: {
includeBaseCodingTools: false,
includeShellTools: false,
includeChannelTools: false,
includeOpenClawTools: false,
includePluginTools: true,
},
});
expect(createOpenClawToolsMock).not.toHaveBeenCalled();
expect(resolvePluginToolsSpy).toHaveBeenCalledTimes(1);
const pluginToolOptions = resolvePluginToolsSpy.mock.calls[0]?.[0].options;
expect(pluginToolOptions?.authProfileStore).toBe(authProfileStore);
} finally {
resolvePluginToolsSpy.mockRestore();
}
});
it("uses tools.alsoAllow for optional plugin discovery without widening to all plugins", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: { tools: { alsoAllow: ["lobster"] } },
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
expect(latestCreateOpenClawToolsOptions().pluginToolAllowlist).toStrictEqual([
"lobster",
DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY,
]);
});
it("materializes additive runtime tools while preserving normal deny policy", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const config: OpenClawConfig = {
tools: {
profile: "coding",
deny: ["workboard_block"],
},
};
createOpenClawCodingTools({
config,
conversationCapabilityProfile: resolveConversationCapabilityProfile({
config,
runtimePluginToolGrant: {
pluginId: "workboard",
toolNames: ["workboard_heartbeat", "workboard_complete"],
},
}),
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
expect(latestCreateOpenClawToolsOptions().pluginToolAllowlist).not.toContain(
"workboard_heartbeat",
);
expect(
isPluginToolAllowed(
new Set(latestCreateOpenClawToolsOptions().pluginToolAllowlist),
"workboard",
"workboard_heartbeat",
),
).toBe(true);
expect(latestCreateOpenClawToolsOptions()).not.toHaveProperty("runtimePluginToolGrant");
expectListIncludes(latestCreateOpenClawToolsOptions().pluginToolDenylist, ["workboard_block"]);
});
it("passes explicit denylist entries to OpenClaw tool factory planning", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: { tools: { deny: ["pdf"] } },
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
expectListIncludes(latestCreateOpenClawToolsOptions().pluginToolDenylist, ["pdf"]);
});
it("passes inherited allowlist entries to OpenClaw plugin discovery", async () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const agentId = `inherited-allow-${Date.now()}-${Math.random().toString(16).slice(2)}`;
const storeTemplate = path.join(
os.tmpdir(),
`openclaw-session-store-${agentId}-{agentId}.json`,
);
await writeSessionStore(storeTemplate, agentId, {
[`agent:${agentId}:subagent:limited`]: {
sessionId: "limited-session",
updatedAt: Date.now(),
spawnDepth: 1,
subagentRole: "orchestrator",
subagentControlScope: "children",
inheritedToolAllow: ["custom_plugin_tool", "sessions_spawn"],
},
});
createOpenClawCodingTools({
sessionKey: `agent:${agentId}:subagent:limited`,
config: {
session: {
store: storeTemplate,
},
},
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
expectListIncludes(latestCreateOpenClawToolsOptions().pluginToolAllowlist, [
"custom_plugin_tool",
"sessions_spawn",
]);
});
it("passes effective allow-list-restricted tool surface to spawned sessions", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
config: { tools: { allow: ["read", "sessions_spawn"] } },
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
const inheritedAllow = latestCreateOpenClawToolsOptions().inheritedToolAllowlist;
expectListIncludes(inheritedAllow, ["read", "sessions_spawn"]);
expect(inheritedAllow?.includes("exec")).toBe(false);
expect(inheritedAllow?.includes("process")).toBe(false);
});
it("passes group-restricted tool surface to cron-created agent turns", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
sessionKey: "agent:main:whatsapp:group:restricted-room",
config: {
tools: { allow: ["read", "exec", "process", "cron"] },
channels: {
whatsapp: {
groups: {
"restricted-room": {
tools: { allow: ["read", "cron"] },
},
},
},
},
},
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
const cronAllow = latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist;
const cronAllowNames = cronCreatorToolNames(cronAllow);
expectListIncludes(cronAllowNames, ["read", "cron"]);
expect(cronAllowNames?.includes("exec")).toBe(false);
expect(cronAllowNames?.includes("process")).toBe(false);
});
it("passes the final unrestricted tool surface to cron-created agent turns", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({ config: {} });
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
const cronAllowNames = cronCreatorToolNames(
latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist,
);
expectListIncludes(cronAllowNames, ["read", "cron", "exec"]);
});
it("lets embedded attempts refresh a caller-owned cron creator tool surface", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
const cronCreatorToolAllowlistRef: NonNullable<
OpenClawToolsOptions["cronCreatorToolAllowlist"]
> = [];
createOpenClawCodingTools({
config: { tools: { allow: ["read", "cron"] } },
cronCreatorToolAllowlistRef,
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
const cronAllow = latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist;
expect(cronAllow).toBe(cronCreatorToolAllowlistRef);
expect(cronCreatorToolNames(cronAllow)).toEqual(["read", "cron"]);
replaceWithEffectiveCronCreatorToolAllowlist(cronCreatorToolAllowlistRef, [
stubTool("read"),
stubTool("cron"),
stubTool("bundle_mcp_search"),
]);
expect(cronCreatorToolNames(cronAllow)).toEqual(["read", "cron", "bundle_mcp_search"]);
});
it("passes deny-restricted tool surface to cron-created agent turns", () => {
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
createOpenClawToolsMock.mockClear();
createOpenClawCodingTools({
sessionKey: "agent:main:whatsapp:group:restricted-room",
config: {
tools: { allow: ["read", "exec", "process", "cron"] },
channels: {
whatsapp: {
groups: {
"restricted-room": {
tools: { deny: ["exec", "process"] },
},
},
},
},
},
});
expect(createOpenClawToolsMock).toHaveBeenCalledTimes(1);
const cronAllow = latestCreateOpenClawToolsOptions().cronCreatorToolAllowlist;
const cronAllowNames = cronCreatorToolNames(cronAllow);
expectListIncludes(cronAllowNames, ["read", "cron"]);
expect(cronAllowNames?.includes("exec")).toBe(false);
expect(cronAllowNames?.includes("process")).toBe(false);
});
it("records core tool-prep stages for hot-path diagnostics", () => {
const stages: string[] = [];
createOpenClawCodingTools({
config: testConfig,
recordToolPrepStage: (name) => stages.push(name),
});
expectListIncludes(stages, [
"tool-policy",
"workspace-policy",
"base-coding-tools",
"shell-tools",
"openclaw-tools:test-helper",
"openclaw-tools",
"message-provider-policy",
"model-provider-policy",
"authorization-policy",
"schema-normalization",
"tool-hooks",
"abort-wrappers",
"deferred-followup-descriptions",
]);
expect(stages.indexOf("tool-policy")).toBeLessThan(stages.indexOf("workspace-policy"));
expect(stages.indexOf("workspace-policy")).toBeLessThan(stages.indexOf("base-coding-tools"));
expect(stages.indexOf("openclaw-tools:test-helper")).toBeLessThan(
stages.indexOf("openclaw-tools"),
);
expect(stages.indexOf("schema-normalization")).toBeLessThan(stages.indexOf("tool-hooks"));
});
it("preserves action enums in normalized schemas", () => {
const defaultTools = createOpenClawCodingTools({ config: testConfig });
const toolNames = ["canvas", "nodes", "cron", "gateway", "message"];
const missingNames = toolNames.filter(
(name) => !defaultTools.some((candidate) => candidate.name === name),
);
expect(missingNames).toStrictEqual([]);
for (const name of toolNames) {
const tool = defaultTools.find((candidate) => candidate.name === name);
const parameters = tool?.parameters as {
properties?: Record<string, unknown>;
};
const action = parameters.properties?.action as
| { const?: unknown; enum?: unknown[] }
| undefined;
const values = new Set<string>();
collectActionValues(action, values);
const min = name === "gateway" ? 1 : 2;
expect(values.size).toBeGreaterThanOrEqual(min);
}
});
it("enforces apply_patch availability and canonical names across model/provider constraints", () => {
const defaultTools = createOpenClawCodingTools({ config: testConfig });
expect(toolNameList(defaultTools)).toContain("exec");
expect(toolNameList(defaultTools)).toContain("process");
expect(toolNameList(defaultTools)).toContain("apply_patch");
const openAiTools = createOpenClawCodingTools({
config: testConfig,
modelProvider: "openai",
modelId: "gpt-5.4",
});
expect(toolNameList(openAiTools)).toContain("apply_patch");
const codexTools = createOpenClawCodingTools({
config: testConfig,
modelProvider: "openai",
modelId: "gpt-5.4",
});
expect(toolNameList(codexTools)).toContain("apply_patch");
const disabledConfig: OpenClawConfig = {
tools: {
exec: {
applyPatch: { enabled: false },
},
},
};
const disabledOpenAiTools = createOpenClawCodingTools({
config: disabledConfig,
modelProvider: "openai",
modelId: "gpt-5.4",
});
expect(toolNameList(disabledOpenAiTools)).not.toContain("apply_patch");
const anthropicTools = createOpenClawCodingTools({
config: disabledConfig,
modelProvider: "anthropic",
modelId: "claude-opus-4-6",
});
expect(toolNameList(anthropicTools)).not.toContain("apply_patch");
const allowModelsConfig: OpenClawConfig = {
tools: {
exec: {
applyPatch: { allowModels: ["gpt-5.4"] },
},
},
};
const allowed = createOpenClawCodingTools({
config: allowModelsConfig,
modelProvider: "openai",
modelId: "gpt-5.4",
});
expect(toolNameList(allowed)).toContain("apply_patch");
const denied = createOpenClawCodingTools({
config: allowModelsConfig,
modelProvider: "openai",
modelId: "gpt-5.4-mini",
});
expect(toolNameList(denied)).not.toContain("apply_patch");
const oauthTools = createOpenClawCodingTools({
config: testConfig,
modelProvider: "anthropic",
modelAuthMode: "oauth",
});
const names = new Set(oauthTools.map((tool) => tool.name));
expect(names.has("exec")).toBe(true);
expect(names.has("read")).toBe(true);
expect(names.has("write")).toBe(true);
expect(names.has("edit")).toBe(true);
expect(names.has("apply_patch")).toBe(true);
});
it("provides top-level object schemas for all tools", () => {
const tools = createOpenClawCodingTools({ config: testConfig });
const offenders = tools
.map((tool) => {
const schema =
tool.parameters && typeof tool.parameters === "object"
? (tool.parameters as Record<string, unknown>)
: null;
return {
name: tool.name,
type: schema?.type,
keys: schema ? Object.keys(schema).toSorted() : null,
};
})
.filter((entry) => entry.type !== "object");
expect(offenders).toStrictEqual([]);
});
it("does not expose provider-specific message tools", () => {
const tools = createOpenClawCodingTools({ messageProvider: "discord" });
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("discord")).toBe(false);
expect(names.has("slack")).toBe(false);
expect(names.has("telegram")).toBe(false);
expect(names.has("whatsapp")).toBe(false);
});
it("separates the canonical message provider from transport tool policy", () => {
vi.mocked(createOpenClawTools).mockClear();
const tools = createOpenClawCodingTools({
config: {
tools: {
toolsBySender: {
"channel:discord:speaker-1": { deny: ["exec"] },
},
},
},
messageProvider: "discord",
toolPolicyMessageProvider: "discord-voice",
senderId: "speaker-1",
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("exec")).toBe(false);
expect(names.has("tts")).toBe(false);
expect(latestCreateOpenClawToolsOptions().agentChannel).toBe("discord");
});
it("filters session tools for sub-agent sessions by default", () => {
const tools = createOpenClawCodingTools({
sessionKey: "agent:main:subagent:test",
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("sessions_list")).toBe(false);
expect(names.has("sessions_history")).toBe(false);
expect(names.has("sessions_send")).toBe(false);
expect(names.has("sessions_spawn")).toBe(false);
expect(names.has("subagents")).toBe(false);
expect(names.has("read")).toBe(true);
expect(names.has("exec")).toBe(true);
expect(names.has("process")).toBe(true);
expect(names.has("apply_patch")).toBe(true);
});
it("uses stored spawnDepth to apply leaf tool policy for flat depth-2 session keys", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-depth-policy-"));
try {
const storeTemplate = path.join(tmpDir, "sessions-{agentId}.json");
await writeSessionStore(storeTemplate, "main", {
"agent:main:subagent:flat": {
sessionId: "session-flat-depth-2",
updatedAt: Date.now(),
spawnDepth: 2,
},
});
const tools = createToolsForStoredSession(storeTemplate, "agent:main:subagent:flat");
expectNoSubagentControlTools(tools);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it("applies subagent tool policy to ACP children spawned under a subagent envelope", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-acp-subagent-policy-"));
try {
const storeTemplate = path.join(tmpDir, "sessions-{agentId}.json");
await writeSessionStore(storeTemplate, "main", {
"agent:main:acp:child": {
sessionId: "session-acp-child",
updatedAt: Date.now(),
spawnedBy: "agent:main:subagent:parent",
spawnDepth: 2,
subagentRole: "leaf",
subagentControlScope: "none",
},
"agent:main:acp:plain": {
sessionId: "session-acp-plain",
updatedAt: Date.now(),
spawnedBy: "agent:main:main",
},
"agent:main:acp:parent": {
sessionId: "session-acp-parent",
updatedAt: Date.now(),
spawnedBy: "agent:main:subagent:parent",
},
});
await writeSessionStore(storeTemplate, "writer", {
"agent:writer:acp:child": {
sessionId: "session-acp-cross-agent-child",
updatedAt: Date.now(),
spawnedBy: "agent:main:acp:parent",
},
});
const persistedEnvelopeTools = createToolsForStoredSession(
storeTemplate,
"agent:main:acp:child",
);
expectNoSubagentControlTools(persistedEnvelopeTools);
const restrictedTools = createToolsForStoredSession(storeTemplate, "agent:main:acp:plain");
const restrictedNames = new Set(restrictedTools.map((tool) => tool.name));
expect(restrictedNames.has("sessions_spawn")).toBe(true);
expect(restrictedNames.has("subagents")).toBe(true);
const ancestryTools = createToolsForStoredSession(storeTemplate, "agent:writer:acp:child");
expectNoSubagentControlTools(ancestryTools);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it("applies leaf tool policy for cross-agent subagent sessions when spawnDepth is missing", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cross-agent-subagent-"));
try {
const storeTemplate = path.join(tmpDir, "sessions-{agentId}.json");
await writeSessionStore(storeTemplate, "main", {
"agent:main:subagent:parent": {
sessionId: "session-main-parent",
updatedAt: Date.now(),
spawnedBy: "agent:main:main",
},
});
await writeSessionStore(storeTemplate, "writer", {
"agent:writer:subagent:child": {
sessionId: "session-writer-child",
updatedAt: Date.now(),
spawnedBy: "agent:main:subagent:parent",
},
});
const tools = createToolsForStoredSession(storeTemplate, "agent:writer:subagent:child");
expectNoSubagentControlTools(tools);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it("supports allow-only sub-agent tool policy", () => {
const tools = createOpenClawCodingTools({
sessionKey: "agent:main:subagent:test",
config: {
tools: {
subagents: {
tools: {
allow: ["read"],
},
},
},
},
});
expect(tools.map((tool) => tool.name)).toEqual(["read"]);
});
it("applies tool profiles before allow/deny policies", () => {
const tools = createOpenClawCodingTools({
config: { tools: { profile: "messaging" } },
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("message")).toBe(true);
expect(names.has("sessions_send")).toBe(true);
// Messaging agents can spawn (and manage) sub-sessions since the
// visible-spawn parity change; execution tools stay coding-only.
expect(names.has("sessions_spawn")).toBe(true);
expect(names.has("exec")).toBe(false);
expect(names.has("browser")).toBe(false);
});
it("includes browser tool with full profile when browser is configured (#76507)", () => {
const tools = createOpenClawCodingTools({
config: {
tools: { profile: "full" },
browser: { enabled: true },
plugins: { entries: { browser: { enabled: true } } },
} as OpenClawConfig,
});
const names = new Set(tools.map((tool) => tool.name));
// full profile must not filter any tools — browser, canvas, etc. must be present.
expect(names.has("browser")).toBe(true);
expect(names.has("canvas")).toBe(true);
expect(names.has("exec")).toBe(true);
expect(names.has("message")).toBe(true);
});
it("includes browser tool with full profile (#76507)", () => {
const tools = createOpenClawCodingTools({
config: {
tools: { profile: "full" },
browser: { enabled: true },
plugins: { entries: { browser: { enabled: true } } },
} as OpenClawConfig,
});
const names = new Set(tools.map((tool) => tool.name));
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("nodes")).toBe(true);
});
it("includes browser tool without explicit profile (defaults to no filtering) (#76507)", () => {
const tools = createOpenClawCodingTools({
config: {
browser: { enabled: true },
plugins: { entries: { browser: { enabled: true } } },
} as OpenClawConfig,
});
const names = new Set(tools.map((tool) => tool.name));
// No profile means no profile filtering — all tools pass.
expect(names.has("browser")).toBe(true);
});
it("keeps browser out of coding-profile subagents unless profile-stage alsoAllow adds it", () => {
const baseConfig = {
browser: { enabled: true },
plugins: { entries: { browser: { enabled: true } } },
tools: { profile: "coding" },
} as OpenClawConfig;
const codingSubagent = createOpenClawCodingTools({
sessionKey: "agent:main:subagent:test",
config: baseConfig,
});
const codingNames = new Set(codingSubagent.map((tool) => tool.name));
expect(codingNames.has("browser")).toBe(false);
const subagentAllowOnly = createOpenClawCodingTools({
sessionKey: "agent:main:subagent:test",
config: {
...baseConfig,
tools: {
profile: "coding",
subagents: { tools: { allow: ["browser"] } },
},
} as OpenClawConfig,
});
expect(toolNameList(subagentAllowOnly)).not.toContain("browser");
const profileStageAlsoAllow = createOpenClawCodingTools({
sessionKey: "agent:main:subagent:test",
config: {
...baseConfig,
tools: { profile: "coding", alsoAllow: ["browser"] },
} as OpenClawConfig,
});
expect(toolNameList(profileStageAlsoAllow)).toContain("browser");
});
it("can keep message available when a cron route needs it under the coding profile", () => {
const codingTools = createOpenClawCodingTools({
config: { tools: { profile: "coding" } },
});
expect(toolNameList(codingTools)).not.toContain("message");
const cronTools = createOpenClawCodingTools({
config: { tools: { profile: "coding" } },
forceMessageTool: true,
});
expect(toolNameList(cronTools)).toContain("message");
});
it("keeps message available for message-tool-only source replies under the coding profile", () => {
const tools = createOpenClawCodingTools({
config: { tools: { profile: "coding" } },
sourceReplyDeliveryMode: "message_tool_only",
});
expect(toolNameList(tools)).toContain("message");
});
it("keeps heartbeat response available for heartbeat runs under the coding profile", () => {
const codingTools = createOpenClawCodingTools({
config: { tools: { profile: "coding" } },
trigger: "heartbeat",
enableHeartbeatTool: true,
forceHeartbeatTool: true,
});
expect(toolNameList(codingTools)).toContain("heartbeat_respond");
});
it("enables heartbeat response when visible replies are message-tool-only", () => {
const tools = createOpenClawCodingTools({
config: {
messages: { visibleReplies: "message_tool" },
tools: { profile: "coding" },
} as OpenClawConfig,
trigger: "heartbeat",
});
expect(toolNameList(tools)).toContain("heartbeat_respond");
});
it("keeps skill_workshop available under the coding profile", () => {
const tools = createOpenClawCodingTools({
config: { tools: { profile: "coding" } },
});
expect(toolNameList(tools)).toContain("skill_workshop");
});
it("can keep message available when a cron route needs it under a provider coding profile", () => {
const providerProfileTools = createOpenClawCodingTools({
config: { tools: { byProvider: { openai: { profile: "coding" } } } },
modelProvider: "openai",
modelId: "gpt-5.4",
});
expect(toolNameList(providerProfileTools)).not.toContain("message");
const cronTools = createOpenClawCodingTools({
config: { tools: { byProvider: { openai: { profile: "coding" } } } },
modelProvider: "openai",
modelId: "gpt-5.4",
forceMessageTool: true,
});
expect(toolNameList(cronTools)).toContain("message");
});
it("expands group shorthands in global tool policy", () => {
const tools = createOpenClawCodingTools({
config: { tools: { allow: ["group:fs"] } },
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("read")).toBe(true);
expect(names.has("write")).toBe(true);
expect(names.has("edit")).toBe(true);
expect(names.has("exec")).toBe(false);
expect(names.has("browser")).toBe(false);
});
it("expands group shorthands in global tool deny policy", () => {
const tools = createOpenClawCodingTools({
config: { tools: { deny: ["group:fs"] } },
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("read")).toBe(false);
expect(names.has("write")).toBe(false);
expect(names.has("edit")).toBe(false);
expect(names.has("exec")).toBe(true);
});
it("lets agent profiles override global profiles", () => {
const tools = createOpenClawCodingTools({
sessionKey: "agent:work:main",
config: {
tools: { profile: "coding" },
agents: {
list: [{ id: "work", tools: { profile: "messaging" } }],
},
},
});
const names = new Set(tools.map((tool) => tool.name));
expect(names.has("message")).toBe(true);
expect(names.has("exec")).toBe(false);
expect(names.has("read")).toBe(false);
});
it("removes unsupported JSON Schema keywords for Cloud Code Assist API compatibility", () => {
const googleTools = createOpenClawCodingTools({
modelProvider: "google",
});
for (const tool of googleTools) {
const violations = findUnsupportedSchemaKeywords(
tool.parameters,
`${tool.name}.parameters`,
GEMINI_UNSUPPORTED_SCHEMA_KEYWORDS,
);
expect(violations).toStrictEqual([]);
}
});
it("applies xai model compat for direct Grok tool cleanup", () => {
const xaiTools = createOpenClawCodingTools({
modelProvider: "xai",
modelCompat: {
toolSchemaProfile: "xai",
unsupportedToolSchemaKeywords: Array.from(XAI_UNSUPPORTED_SCHEMA_KEYWORDS),
toolCallArgumentsEncoding: "html-entities",
},
});
expect(toolNameList(xaiTools)).not.toContain("web_search");
for (const tool of xaiTools) {
const violations = findUnsupportedSchemaKeywords(
tool.parameters,
`${tool.name}.parameters`,
XAI_UNSUPPORTED_SCHEMA_KEYWORDS,
);
expect(violations).toStrictEqual([]);
}
});
it("returns image-aware read metadata for images and text-only blocks for text files", async () => {
const defaultTools = createOpenClawCodingTools();
const readTool = requireTool(defaultTools, "read");
const readExecute = requireToolExecute(readTool);
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-read-"));
try {
const imagePath = path.join(tmpDir, "sample.png");
await fs.writeFile(imagePath, tinyPngBuffer);
const imageResult = await readExecute("tool-1", {
path: imagePath,
});
const imageBlocks = imageResult?.content?.filter((block) => block.type === "image") as
| Array<{ mimeType?: string }>
| undefined;
const imageTextBlocks = imageResult?.content?.filter((block) => block.type === "text") as
| Array<{ text?: string }>
| undefined;
const imageText = imageTextBlocks?.map((block) => block.text ?? "").join("\n") ?? "";
expect(imageText).toContain("Read image file [image/png]");
if ((imageBlocks?.length ?? 0) > 0) {
expect(imageBlocks?.every((block) => block.mimeType === "image/png")).toBe(true);
} else {
expect(imageText).toContain("[Image omitted:");
}
const textPath = path.join(tmpDir, "sample.txt");
const contents = "Hello from openclaw read tool.";
await fs.writeFile(textPath, contents, "utf8");
const textResult = await readExecute("tool-2", {
path: textPath,
});
expect(textResult?.content).toEqual([{ type: "text", text: contents }]);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it("filters tools by sandbox policy", () => {
const sandboxDir = path.join(os.tmpdir(), "openclaw-sandbox");
const sandbox = createAgentToolsSandboxContext({
workspaceDir: sandboxDir,
agentWorkspaceDir: path.join(os.tmpdir(), "openclaw-workspace"),
workspaceAccess: "none" as const,
fsBridge: createHostSandboxFsBridge(sandboxDir),
tools: {
allow: ["bash"],
deny: ["browser"],
},
});
const tools = createOpenClawCodingTools({ sandbox });
expect(toolNameList(tools)).toContain("exec");
expect(toolNameList(tools)).not.toContain("read");
expect(toolNameList(tools)).not.toContain("browser");
});
it("hard-disables write/edit when sandbox workspaceAccess is ro", () => {
const sandboxDir = path.join(os.tmpdir(), "openclaw-sandbox");
const sandbox = createAgentToolsSandboxContext({
workspaceDir: sandboxDir,
agentWorkspaceDir: path.join(os.tmpdir(), "openclaw-workspace"),
workspaceAccess: "ro" as const,
fsBridge: createHostSandboxFsBridge(sandboxDir),
tools: {
allow: ["read", "write", "edit"],
deny: [],
},
});
const tools = createOpenClawCodingTools({ sandbox });
expect(toolNameList(tools)).toContain("read");
expect(toolNameList(tools)).not.toContain("write");
expect(toolNameList(tools)).not.toContain("edit");
});
it("accepts canonical parameters for read/write/edit", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-canonical-"));
try {
const tools = createOpenClawCodingTools({ workspaceDir: tmpDir });
const { readTool, writeTool, editTool } = expectReadWriteEditTools(tools);
const filePath = "canonical-test.txt";
await writeTool?.execute("tool-canonical-1", {
path: filePath,
content: "hello world",
});
await editTool?.execute("tool-canonical-2", {
path: filePath,
edits: [{ oldText: "world", newText: "universe" }],
});
const result = await readTool?.execute("tool-canonical-3", {
path: filePath,
});
const textBlocks = result?.content?.filter((block) => block.type === "text") as
| Array<{ text?: string }>
| undefined;
const combinedText = textBlocks?.map((block) => block.text ?? "").join("\n");
expect(combinedText).toContain("hello universe");
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it("roots memory flush append-only writes in the workspace when cwd differs", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-workspace-"));
const taskCwd = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-cwd-"));
const memoryRelativePath = "memory/2026-03-24.md";
const workspaceMemoryFile = path.join(workspaceDir, memoryRelativePath);
const taskMemoryFile = path.join(taskCwd, memoryRelativePath);
try {
await fs.mkdir(path.dirname(workspaceMemoryFile), { recursive: true });
await fs.writeFile(workspaceMemoryFile, "seed", "utf8");
const tools = createOpenClawCodingTools({
workspaceDir,
cwd: taskCwd,
trigger: "memory",
memoryFlushWritePath: memoryRelativePath,
});
const writeExecute = requireToolExecute(requireTool(tools, "write"));
await writeExecute("tool-memory-flush-workspace", {
path: memoryRelativePath,
content: "new durable note",
});
await expect(fs.readFile(workspaceMemoryFile, "utf8")).resolves.toBe(
"seed\nnew durable note",
);
await expect(fs.stat(taskMemoryFile)).rejects.toThrow();
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
await fs.rm(taskCwd, { recursive: true, force: true });
}
});
it("records ordinary write, edit, and apply_patch memory provenance from turn taint", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-write-taint-"));
const rollback = vi.fn(async () => {});
const recordWriteProvenance = vi.fn<NonNullable<MemoryFlushPlan["recordWriteProvenance"]>>(
async () => rollback,
);
registerMemoryCapability("memory-core", {
flushPlanResolver: () => ({
softThresholdTokens: 1,
forceFlushTranscriptBytes: 1,
reserveTokensFloor: 1,
prompt: "flush",
systemPrompt: "flush",
relativePath: "memory/2026-07-29.md",
recordWriteProvenance,
}),
});
let tainted = false;
try {
const tools = createOpenClawCodingTools({
workspaceDir,
config: { tools: { fs: { workspaceOnly: true } } },
senderIsOwner: true,
isTurnTainted: () => tainted,
});
const write = requireToolExecute(requireTool(tools, "write"));
const edit = requireToolExecute(requireTool(tools, "edit"));
const applyPatch = requireToolExecute(requireTool(tools, "apply_patch"));
await write("write-memory", {
path: "memory/2026-07-29.md",
content: "owner-requested note\n",
});
tainted = true;
await edit("edit-memory", {
path: "memory/2026-07-29.md",
edits: [{ oldText: "note", newText: "network-derived note" }],
});
await applyPatch("patch-memory", {
input: [
"*** Begin Patch",
"*** Add File: memory/project.md",
"+network project note",
"*** End Patch",
].join("\n"),
});
expect(recordWriteProvenance.mock.calls.map(([entry]) => entry.originClass)).toEqual([
"agent",
"untrusted",
"untrusted",
]);
expect(recordWriteProvenance).toHaveBeenLastCalledWith(
expect.objectContaining({
relativePath: "memory/project.md",
contentBefore: "",
contentAfter: "network project note\n",
}),
);
await expect(
applyPatch("patch-existing-memory", {
input: [
"*** Begin Patch",
"*** Add File: memory/project.md",
"+replacement",
"*** End Patch",
].join("\n"),
}),
).rejects.toThrow(/file already exists/i);
expect(rollback).toHaveBeenCalledOnce();
await expect(fs.readFile(path.join(workspaceDir, "memory/project.md"), "utf8")).resolves.toBe(
"network project note\n",
);
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("records agent provenance after an untainted same-turn delete and recreate", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-recreate-"));
let recordedOrigin: "agent" | "untrusted" | undefined;
registerMemoryCapability("memory-core", {
flushPlanResolver: () => ({
softThresholdTokens: 1,
forceFlushTranscriptBytes: 1,
reserveTokensFloor: 1,
prompt: "flush",
systemPrompt: "flush",
relativePath: "memory/recreated.md",
recordWriteProvenance: async (entry) => {
recordedOrigin = entry.originClass;
},
clearWriteProvenance: async () => {
recordedOrigin = undefined;
},
}),
});
try {
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
await fs.writeFile(path.join(workspaceDir, "memory/recreated.md"), "old\n", "utf8");
const applyPatch = requireToolExecute(
requireTool(
createOpenClawCodingTools({
workspaceDir,
senderIsOwner: true,
isTurnTainted: () => false,
}),
"apply_patch",
),
);
await applyPatch("delete-memory", {
input: "*** Begin Patch\n*** Delete File: memory/recreated.md\n*** End Patch",
});
await applyPatch("recreate-memory", {
input: "*** Begin Patch\n*** Add File: memory/recreated.md\n+recreated\n*** End Patch",
});
expect(recordedOrigin).toBe("agent");
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("orders parallel apply_patch delete cleanup before a tainted recreate", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-race-"));
let recordedOrigin: "agent" | "untrusted" | undefined;
let releaseCleanup!: () => void;
let signalCleanupStarted!: () => void;
const cleanupRelease = new Promise<void>((resolve) => {
releaseCleanup = resolve;
});
const cleanupStarted = new Promise<void>((resolve) => {
signalCleanupStarted = resolve;
});
const recordWriteProvenance = vi.fn<NonNullable<MemoryFlushPlan["recordWriteProvenance"]>>(
async (entry) => {
recordedOrigin = entry.originClass;
},
);
registerMemoryCapability("memory-core", {
flushPlanResolver: () => ({
softThresholdTokens: 1,
forceFlushTranscriptBytes: 1,
reserveTokensFloor: 1,
prompt: "flush",
systemPrompt: "flush",
relativePath: "memory/raced.md",
recordWriteProvenance,
clearWriteProvenance: async () => {
signalCleanupStarted();
await cleanupRelease;
recordedOrigin = undefined;
},
}),
});
try {
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
await fs.writeFile(path.join(workspaceDir, "memory/raced.md"), "old\n", "utf8");
const applyPatch = requireToolExecute(
requireTool(
createOpenClawCodingTools({
workspaceDir,
senderIsOwner: true,
isTurnTainted: () => true,
}),
"apply_patch",
),
);
const deleting = applyPatch("delete-raced-memory", {
input: "*** Begin Patch\n*** Delete File: memory/raced.md\n*** End Patch",
});
await cleanupStarted;
const recreating = applyPatch("recreate-raced-memory", {
input: "*** Begin Patch\n*** Add File: memory/raced.md\n+network note\n*** End Patch",
});
await Promise.resolve();
expect(recordWriteProvenance).not.toHaveBeenCalled();
releaseCleanup();
await Promise.all([deleting, recreating]);
expect(recordedOrigin).toBe("untrusted");
expect(recordWriteProvenance).toHaveBeenCalledOnce();
} finally {
releaseCleanup();
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("records sandbox-backed memory writes before mutation", async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-memory-sandbox-taint-"));
const recordWriteProvenance = vi.fn<NonNullable<MemoryFlushPlan["recordWriteProvenance"]>>(
async () => {},
);
registerMemoryCapability("memory-core", {
flushPlanResolver: () => ({
softThresholdTokens: 1,
forceFlushTranscriptBytes: 1,
reserveTokensFloor: 1,
prompt: "flush",
systemPrompt: "flush",
relativePath: "memory/2026-07-29.md",
recordWriteProvenance,
}),
});
try {
const sandbox = createAgentToolsSandboxContext({
workspaceDir,
fsBridge: createHostSandboxFsBridge(workspaceDir),
workspaceAccess: "rw",
});
const tools = createOpenClawCodingTools({
workspaceDir,
sandbox,
senderIsOwner: true,
isTurnTainted: () => true,
});
await requireToolExecute(requireTool(tools, "write"))("sandbox-memory", {
path: "memory/2026-07-29.md",
content: "sandbox network note\n",
});
expect(recordWriteProvenance).toHaveBeenCalledWith(
expect.objectContaining({
relativePath: "memory/2026-07-29.md",
originClass: "untrusted",
contentBefore: "",
contentAfter: "sandbox network note\n",
}),
);
} finally {
await fs.rm(workspaceDir, { recursive: true, force: true });
}
});
it("rejects legacy alias parameters", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-legacy-alias-"));
try {
const tools = createOpenClawCodingTools({ workspaceDir: tmpDir });
const { readTool, writeTool, editTool } = expectReadWriteEditTools(tools);
await expect(
writeTool?.execute("tool-legacy-write", {
file: "legacy.txt",
content: "hello old value",
}),
).rejects.toThrow(/Missing required parameter: path/);
await expect(
editTool?.execute("tool-legacy-edit", {
filePath: "legacy.txt",
old_text: "old",
newString: "new",
}),
).rejects.toThrow(/Missing required parameters: path, edits/);
await expect(
readTool?.execute("tool-legacy-read", {
file_path: "legacy.txt",
}),
).rejects.toThrow(/Missing required parameter: path/);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it("rejects structured content blocks for write", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-structured-write-"));
try {
const tools = createOpenClawCodingTools({ workspaceDir: tmpDir });
const writeTool = requireTool(tools, "write");
const writeExecute = requireToolExecute(writeTool);
await expect(
writeExecute("tool-structured-write", {
path: "structured-write.js",
content: [
{ type: "text", text: "const path = require('path');\n" },
{ type: "input_text", text: "const root = path.join(process.env.HOME, 'clawd');\n" },
],
}),
).rejects.toThrow(/Missing required parameter: content/);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
it("rejects structured edit payloads", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-structured-edit-"));
try {
const filePath = path.join(tmpDir, "structured-edit.js");
await fs.writeFile(filePath, "const value = 'old';\n", "utf8");
const tools = createOpenClawCodingTools({ workspaceDir: tmpDir });
const editTool = requireTool(tools, "edit");
const editExecute = requireToolExecute(editTool);
await expect(
editExecute("tool-structured-edit", {
path: "structured-edit.js",
edits: [
{
oldText: [{ type: "text", text: "old" }],
newText: [{ kind: "text", value: "new" }],
},
],
}),
).rejects.toThrow(/Missing required parameter: edits/);
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
});
});
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */