fix(tools): honor tools.fs.workspaceOnly=false for host write/edit (#28822)

Merged via squash.

Prepared head SHA: 83d432961d
Co-authored-by: lailoo <20536249+lailoo@users.noreply.github.com>
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
This commit is contained in:
大猫子
2026-02-28 07:53:20 +08:00
committed by GitHub
parent ad804b0356
commit 1725839720
4 changed files with 250 additions and 8 deletions

View File

@@ -667,16 +667,16 @@ export function createSandboxedEditTool(params: SandboxToolParams) {
return wrapToolParamNormalization(base, CLAUDE_PARAM_GROUPS.edit);
}
export function createHostWorkspaceWriteTool(root: string) {
export function createHostWorkspaceWriteTool(root: string, options?: { workspaceOnly?: boolean }) {
const base = createWriteTool(root, {
operations: createHostWriteOperations(root),
operations: createHostWriteOperations(root, options),
}) as unknown as AnyAgentTool;
return wrapToolParamNormalization(base, CLAUDE_PARAM_GROUPS.write);
}
export function createHostWorkspaceEditTool(root: string) {
export function createHostWorkspaceEditTool(root: string, options?: { workspaceOnly?: boolean }) {
const base = createEditTool(root, {
operations: createHostEditOperations(root),
operations: createHostEditOperations(root, options),
}) as unknown as AnyAgentTool;
return wrapToolParamNormalization(base, CLAUDE_PARAM_GROUPS.edit);
}
@@ -757,7 +757,26 @@ function createSandboxEditOperations(params: SandboxToolParams) {
} as const;
}
function createHostWriteOperations(root: string) {
function createHostWriteOperations(root: string, options?: { workspaceOnly?: boolean }) {
const workspaceOnly = options?.workspaceOnly !== false;
if (!workspaceOnly) {
// When workspaceOnly is false, allow writes anywhere on the host
return {
mkdir: async (dir: string) => {
const resolved = path.resolve(dir);
await fs.mkdir(resolved, { recursive: true });
},
writeFile: async (absolutePath: string, content: string) => {
const resolved = path.resolve(absolutePath);
const dir = path.dirname(resolved);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(resolved, content, "utf-8");
},
} as const;
}
// When workspaceOnly is true (default), enforce workspace boundary
return {
mkdir: async (dir: string) => {
const relative = toRelativePathInRoot(root, dir, { allowRoot: true });
@@ -777,7 +796,30 @@ function createHostWriteOperations(root: string) {
} as const;
}
function createHostEditOperations(root: string) {
function createHostEditOperations(root: string, options?: { workspaceOnly?: boolean }) {
const workspaceOnly = options?.workspaceOnly !== false;
if (!workspaceOnly) {
// When workspaceOnly is false, allow edits anywhere on the host
return {
readFile: async (absolutePath: string) => {
const resolved = path.resolve(absolutePath);
return await fs.readFile(resolved);
},
writeFile: async (absolutePath: string, content: string) => {
const resolved = path.resolve(absolutePath);
const dir = path.dirname(resolved);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(resolved, content, "utf-8");
},
access: async (absolutePath: string) => {
const resolved = path.resolve(absolutePath);
await fs.access(resolved);
},
} as const;
}
// When workspaceOnly is true (default), enforce workspace boundary
return {
readFile: async (absolutePath: string) => {
const relative = toRelativePathInRoot(root, absolutePath);