mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 14:45:46 +00:00
fix: default and gate apply_patch like write
This commit is contained in:
@@ -36,8 +36,9 @@ The tool accepts a single `input` string that wraps one or more file operations:
|
||||
- `tools.exec.applyPatch.workspaceOnly` defaults to `true` (workspace-contained). Set it to `false` only if you intentionally want `apply_patch` to write/delete outside the workspace directory.
|
||||
- Use `*** Move to:` within an `*** Update File:` hunk to rename files.
|
||||
- `*** End of File` marks an EOF-only insert when needed.
|
||||
- Experimental and disabled by default. Enable with `tools.exec.applyPatch.enabled`.
|
||||
- OpenAI-only (including OpenAI Codex). Optionally gate by model via
|
||||
- Available by default for OpenAI and OpenAI Codex models. Set
|
||||
`tools.exec.applyPatch.enabled: false` to disable it.
|
||||
- Optionally gate by model via
|
||||
`tools.exec.applyPatch.allowModels`.
|
||||
- Config is only under `tools.exec`.
|
||||
|
||||
|
||||
@@ -184,16 +184,17 @@ Paste (bracketed by default):
|
||||
{ "tool": "process", "action": "paste", "sessionId": "<id>", "text": "line1\nline2\n" }
|
||||
```
|
||||
|
||||
## apply_patch (experimental)
|
||||
## apply_patch
|
||||
|
||||
`apply_patch` is a subtool of `exec` for structured multi-file edits.
|
||||
Enable it explicitly:
|
||||
It is enabled by default for OpenAI and OpenAI Codex models. Use config only
|
||||
when you want to disable it or restrict it to specific models:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
exec: {
|
||||
applyPatch: { enabled: true, workspaceOnly: true, allowModels: ["gpt-5.2"] },
|
||||
applyPatch: { workspaceOnly: true, allowModels: ["gpt-5.2"] },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -202,6 +203,7 @@ Enable it explicitly:
|
||||
Notes:
|
||||
|
||||
- Only available for OpenAI/OpenAI Codex models.
|
||||
- Tool policy still applies; `allow: ["exec"]` implicitly allows `apply_patch`.
|
||||
- Tool policy still applies; `allow: ["write"]` implicitly allows `apply_patch`.
|
||||
- Config lives under `tools.exec.applyPatch`.
|
||||
- `tools.exec.applyPatch.enabled` defaults to `true`; set it to `false` to disable the tool for OpenAI models.
|
||||
- `tools.exec.applyPatch.workspaceOnly` defaults to `true` (workspace-contained). Set it to `false` only if you intentionally want `apply_patch` to write/delete outside the workspace directory.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"] },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -214,9 +214,7 @@ describe("FS tools with workspaceOnly=false", () => {
|
||||
config: {
|
||||
tools: {
|
||||
exec: {
|
||||
applyPatch: {
|
||||
enabled: true,
|
||||
},
|
||||
applyPatch: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12358,7 +12358,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA = {
|
||||
},
|
||||
"tools.exec.applyPatch.enabled": {
|
||||
label: "Enable apply_patch",
|
||||
help: "Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
|
||||
help: "Enable or disable apply_patch for OpenAI and OpenAI Codex models when allowed by tool policy (default: true).",
|
||||
tags: ["tools"],
|
||||
},
|
||||
"tools.exec.applyPatch.workspaceOnly": {
|
||||
|
||||
@@ -537,7 +537,7 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"diagnostics.cacheTrace.includePrompt": "Include prompt text in trace output (default: true).",
|
||||
"diagnostics.cacheTrace.includeSystem": "Include system prompt in trace output (default: true).",
|
||||
"tools.exec.applyPatch.enabled":
|
||||
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
|
||||
"Enable or disable apply_patch for OpenAI and OpenAI Codex models when allowed by tool policy (default: true).",
|
||||
"tools.exec.applyPatch.workspaceOnly":
|
||||
"Restrict apply_patch paths to the workspace directory (default: true). Set false to allow writing outside the workspace (dangerous).",
|
||||
"tools.exec.applyPatch.allowModels":
|
||||
|
||||
@@ -262,9 +262,9 @@ export type ExecToolConfig = {
|
||||
* Default false to reduce context noise.
|
||||
*/
|
||||
notifyOnExitEmptySuccess?: boolean;
|
||||
/** apply_patch subtool configuration (experimental). */
|
||||
/** apply_patch subtool configuration. */
|
||||
applyPatch?: {
|
||||
/** Enable apply_patch for OpenAI models (default: false). */
|
||||
/** Enable apply_patch for OpenAI models (default: true; set false to disable). */
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Restrict apply_patch paths to the workspace directory.
|
||||
|
||||
Reference in New Issue
Block a user