fix: default and gate apply_patch like write

This commit is contained in:
Peter Steinberger
2026-03-27 01:02:20 +00:00
parent c326083ad8
commit b9c60fd37a
13 changed files with 85 additions and 44 deletions

View File

@@ -56,12 +56,9 @@ describe("Agent-specific tool filtering", () => {
try {
const cfg: OpenClawConfig = {
tools: {
allow: ["read", "exec"],
allow: ["read", "write", "exec"],
exec: {
applyPatch: {
enabled: true,
...(opts.workspaceOnly === false ? { workspaceOnly: false } : {}),
},
applyPatch: opts.workspaceOnly === false ? { workspaceOnly: false } : {},
},
},
};
@@ -188,13 +185,10 @@ describe("Agent-specific tool filtering", () => {
expect(toolNames).not.toContain("apply_patch");
});
it("should allow apply_patch when exec is allow-listed and applyPatch is enabled", () => {
it("should allow apply_patch for OpenAI models when write is allow-listed", () => {
const cfg: OpenClawConfig = {
tools: {
allow: ["read", "exec"],
exec: {
applyPatch: { enabled: true },
},
allow: ["read", "write", "exec"],
},
};
@@ -213,6 +207,30 @@ describe("Agent-specific tool filtering", () => {
expect(toolNames).toContain("apply_patch");
});
it("should allow disabling apply_patch explicitly", () => {
const cfg: OpenClawConfig = {
tools: {
allow: ["read", "write", "exec"],
exec: {
applyPatch: { enabled: false },
},
},
};
const tools = createOpenClawCodingTools({
config: cfg,
sessionKey: "agent:main:main",
workspaceDir: "/tmp/test",
agentDir: "/tmp/agent",
modelProvider: "openai",
modelId: "gpt-5.2",
});
const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("exec");
expect(toolNames).not.toContain("apply_patch");
});
it("defaults apply_patch to workspace-only (blocks traversal)", async () => {
await withApplyPatchEscapeCase({}, async ({ applyPatchTool, escapedPath, patch }) => {
await expect(applyPatchTool.execute("tc1", { input: patch })).rejects.toThrow(

View File

@@ -7,7 +7,11 @@ const defaultTools = createOpenClawCodingTools({ senderIsOwner: true });
describe("createOpenClawCodingTools", () => {
it("preserves action enums in normalized schemas", () => {
const toolNames = ["browser", "canvas", "nodes", "cron", "gateway", "message"];
const toolNames = ["canvas", "nodes", "cron", "gateway", "message"];
const missingNames = toolNames.filter(
(name) => !defaultTools.some((candidate) => candidate.name === name),
);
expect(missingNames).toEqual([]);
const collectActionValues = (schema: unknown, values: Set<string>): void => {
if (!schema || typeof schema !== "object") {
@@ -33,7 +37,6 @@ describe("createOpenClawCodingTools", () => {
for (const name of toolNames) {
const tool = defaultTools.find((candidate) => candidate.name === name);
expect(tool).toBeDefined();
const parameters = tool?.parameters as {
properties?: Record<string, unknown>;
};
@@ -56,22 +59,34 @@ describe("createOpenClawCodingTools", () => {
expect(defaultTools.some((tool) => tool.name === "process")).toBe(true);
expect(defaultTools.some((tool) => tool.name === "apply_patch")).toBe(false);
const enabledConfig: OpenClawConfig = {
tools: {
exec: {
applyPatch: { enabled: true },
},
},
};
const openAiTools = createOpenClawCodingTools({
config: enabledConfig,
modelProvider: "openai",
modelId: "gpt-5.2",
});
expect(openAiTools.some((tool) => tool.name === "apply_patch")).toBe(true);
const codexTools = createOpenClawCodingTools({
modelProvider: "openai-codex",
modelId: "gpt-5.4",
});
expect(codexTools.some((tool) => tool.name === "apply_patch")).toBe(true);
const disabledConfig: OpenClawConfig = {
tools: {
exec: {
applyPatch: { enabled: false },
},
},
};
const disabledOpenAiTools = createOpenClawCodingTools({
config: disabledConfig,
modelProvider: "openai",
modelId: "gpt-5.2",
});
expect(disabledOpenAiTools.some((tool) => tool.name === "apply_patch")).toBe(false);
const anthropicTools = createOpenClawCodingTools({
config: enabledConfig,
config: disabledConfig,
modelProvider: "anthropic",
modelId: "claude-opus-4-5",
});
@@ -80,7 +95,7 @@ describe("createOpenClawCodingTools", () => {
const allowModelsConfig: OpenClawConfig = {
tools: {
exec: {
applyPatch: { enabled: true, allowModels: ["gpt-5.2"] },
applyPatch: { allowModels: ["gpt-5.2"] },
},
},
};

View File

@@ -30,8 +30,12 @@ describe("pi-tools.policy", () => {
expect(isToolAllowedByPolicyName("web_search", { deny: ["web_*"] })).toBe(false);
});
it("keeps apply_patch when exec is allowlisted", () => {
expect(isToolAllowedByPolicyName("apply_patch", { allow: ["exec"] })).toBe(true);
it("keeps apply_patch when write is allowlisted", () => {
expect(isToolAllowedByPolicyName("apply_patch", { allow: ["write"] })).toBe(true);
});
it("blocks apply_patch when write is denylisted", () => {
expect(isToolAllowedByPolicyName("apply_patch", { deny: ["write"] })).toBe(false);
});
});

View File

@@ -91,8 +91,8 @@ describe("tools.fs.workspaceOnly", () => {
workspaceDir: sandboxRoot,
config: {
tools: {
allow: ["read", "exec"],
exec: { applyPatch: { enabled: true } },
allow: ["read", "write", "exec"],
exec: { applyPatch: {} },
},
} as OpenClawConfig,
});
@@ -113,8 +113,8 @@ describe("tools.fs.workspaceOnly", () => {
workspaceDir: sandboxRoot,
config: {
tools: {
allow: ["read", "exec"],
exec: { applyPatch: { enabled: true, workspaceOnly: false } },
allow: ["read", "write", "exec"],
exec: { applyPatch: { workspaceOnly: false } },
},
} as OpenClawConfig,
});

View File

@@ -360,7 +360,7 @@ export function createOpenClawCodingTools(options?: {
// (tools.fs.workspaceOnly is a separate umbrella flag for read/write/edit/apply_patch.)
const applyPatchWorkspaceOnly = workspaceOnly || applyPatchConfig?.workspaceOnly !== false;
const applyPatchEnabled =
!!applyPatchConfig?.enabled &&
applyPatchConfig?.enabled !== false &&
isOpenAIProvider(options?.modelProvider) &&
isApplyPatchAllowedForModel({
modelProvider: options?.modelProvider,

View File

@@ -214,9 +214,7 @@ describe("FS tools with workspaceOnly=false", () => {
config: {
tools: {
exec: {
applyPatch: {
enabled: true,
},
applyPatch: {},
},
},
},

View File

@@ -63,7 +63,7 @@ const CORE_TOOL_DEFINITIONS: CoreToolDefinition[] = [
{
id: "apply_patch",
label: "apply_patch",
description: "Patch files (OpenAI)",
description: "Patch files",
sectionId: "fs",
profiles: ["coding"],
},

View File

@@ -16,13 +16,16 @@ function makeToolPolicyMatcher(policy: SandboxToolPolicy) {
if (matchesAnyGlobPattern(normalized, deny)) {
return false;
}
if (normalized === "apply_patch" && matchesAnyGlobPattern("write", deny)) {
return false;
}
if (allow.length === 0) {
return true;
}
if (matchesAnyGlobPattern(normalized, allow)) {
return true;
}
if (normalized === "apply_patch" && matchesAnyGlobPattern("exec", allow)) {
if (normalized === "apply_patch" && matchesAnyGlobPattern("write", allow)) {
return true;
}
return false;