Refactor builtin file tools beyond workspace scope

Replace workspace-scoped builtin file tools with general-purpose file-* tools that accept relative, absolute, and ~/ paths. Relative paths still resolve against the configured workdir.

File operations within the workdir are auto-approved. File operations outside the workdir now emit file permission metadata and require user approval, with support for once, session, and persistent grants.

Add a shared filesystem layer for text-focused read/write/edit/list/search operations, including binary-file safeguards for text reads. parseFile and LLMParse continue to read file buffers for document/image parsing.

Update copilot prompts, background/live-note agents, knowledge workflows, and renderer labels/UI to use the new file-* tool surface and permission details.

Add package-local Vitest setup for @x/core with colocated filesystem unit tests covering path resolution, canonical permission paths, binary detection, read/write/edit behavior, glob, and grep.
This commit is contained in:
Ramnique Singh 2026-05-25 16:21:40 +05:30
parent f1d3b7b825
commit 31e35e00b8
41 changed files with 1777 additions and 615 deletions

View file

@ -5677,6 +5677,7 @@ function App() {
{rendered}
<PermissionRequest
toolCall={permRequest.toolCall}
permission={permRequest.permission}
onApprove={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve')}
onApproveSession={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'session')}
onApproveAlways={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'always')}

View file

@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
import { AlertTriangleIcon, CheckCircleIcon, CheckIcon, ChevronDownIcon, XCircleIcon, XIcon } from "lucide-react";
import type { ComponentProps } from "react";
import { ToolCallPart } from "@x/shared/dist/message.js";
import { ToolPermissionMetadata } from "@x/shared/dist/runs.js";
import z from "zod";
export type PermissionRequestProps = ComponentProps<"div"> & {
@ -22,6 +23,15 @@ export type PermissionRequestProps = ComponentProps<"div"> & {
onDeny?: () => void;
isProcessing?: boolean;
response?: 'approve' | 'deny' | null;
permission?: z.infer<typeof ToolPermissionMetadata>;
};
const fileActionLabels: Record<string, string> = {
read: "Read file",
list: "List folder",
search: "Search files",
write: "Write files",
delete: "Delete path",
};
export const PermissionRequest = ({
@ -33,14 +43,16 @@ export const PermissionRequest = ({
onDeny,
isProcessing = false,
response = null,
permission,
...props
}: PermissionRequestProps) => {
// Extract command from arguments if it's executeCommand
const command = toolCall.toolName === "executeCommand"
const command = permission?.kind === "command" || toolCall.toolName === "executeCommand"
? (typeof toolCall.arguments === "object" && toolCall.arguments !== null && "command" in toolCall.arguments
? String(toolCall.arguments.command)
: JSON.stringify(toolCall.arguments))
: null;
const filePermission = permission?.kind === "file" ? permission : null;
const isResponded = response !== null;
const isApproved = response === 'approve';
@ -113,7 +125,35 @@ export const PermissionRequest = ({
</pre>
</div>
)}
{!command && toolCall.arguments && (
{filePermission && (
<div className="rounded-md border bg-background/50 p-3 mt-3 space-y-3">
<div>
<p className="text-xs font-medium text-muted-foreground mb-1.5 uppercase tracking-wide">
Action
</p>
<p className="text-xs font-medium text-foreground">
{fileActionLabels[filePermission.operation] ?? filePermission.operation}
</p>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground mb-1.5 uppercase tracking-wide">
Path{filePermission.paths.length === 1 ? "" : "s"}
</p>
<pre className="whitespace-pre-wrap text-xs font-mono text-foreground break-all">
{filePermission.paths.join("\n")}
</pre>
</div>
<div>
<p className="text-xs font-medium text-muted-foreground mb-1.5 uppercase tracking-wide">
Approval Scope
</p>
<pre className="whitespace-pre-wrap text-xs font-mono text-foreground break-all">
{filePermission.pathPrefix}
</pre>
</div>
</div>
)}
{!command && !filePermission && toolCall.arguments && (
<div className="rounded-md border bg-background/50 p-3 mt-3">
<p className="text-xs font-medium text-muted-foreground mb-1.5 uppercase tracking-wide">
Arguments
@ -133,12 +173,12 @@ export const PermissionRequest = ({
size="sm"
onClick={onApprove}
disabled={isProcessing}
className={cn("flex-1", command && "rounded-r-none")}
className={cn("flex-1", (command || filePermission) && "rounded-r-none")}
>
<CheckIcon className="size-4" />
Approve
</Button>
{command && (
{(command || filePermission) && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button

View file

@ -479,19 +479,19 @@ export const getComposioConnectCardData = (tool: ToolCall): ComposioConnectCardD
// Human-friendly display names for builtin tools
const TOOL_DISPLAY_NAMES: Record<string, string> = {
'workspace-readFile': 'Reading file',
'workspace-writeFile': 'Writing file',
'workspace-edit': 'Editing file',
'workspace-readdir': 'Reading directory',
'workspace-exists': 'Checking path',
'workspace-stat': 'Getting file info',
'workspace-glob': 'Finding files',
'workspace-grep': 'Searching files',
'workspace-mkdir': 'Creating directory',
'workspace-rename': 'Renaming',
'workspace-copy': 'Copying file',
'workspace-remove': 'Removing',
'workspace-getRoot': 'Getting workspace root',
'file-readText': 'Reading file',
'file-writeText': 'Writing file',
'file-editText': 'Editing file',
'file-list': 'Reading directory',
'file-exists': 'Checking path',
'file-stat': 'Getting file info',
'file-glob': 'Finding files',
'file-grep': 'Searching files',
'file-mkdir': 'Creating directory',
'file-rename': 'Renaming',
'file-copy': 'Copying file',
'file-remove': 'Removing',
'file-getRoot': 'Getting file root',
'loadSkill': 'Loading skill',
'parseFile': 'Parsing file',
'LLMParse': 'Extracting content',