Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion core/tools/definitions/ls.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Tool } from "../..";

import { ToolPolicy } from "@continuedev/terminal-security";
import { resolveInputPath } from "../../util/pathResolver";
import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";
import { evaluateFileAccessPolicy } from "../policies/fileAccess";

export const lsTool: Tool = {
type: "function",
Expand All @@ -20,7 +23,7 @@ export const lsTool: Tool = {
dirPath: {
type: "string",
description:
"The directory path relative to the root of the project. Use forward slash paths like '/'. rather than e.g. '.'",
"The directory path. Can be relative to project root, absolute path, tilde path (~/...), or file:// URI. Use forward slash paths",
},
recursive: {
type: "boolean",
Expand All @@ -39,4 +42,26 @@ export const lsTool: Tool = {
],
},
toolCallIcon: "FolderIcon",
preprocessArgs: async (args, { ide }) => {
const dirPath = args.dirPath as string;

// Default to current directory if no path provided
const pathToResolve = dirPath || ".";
const resolvedPath = await resolveInputPath(ide, pathToResolve);

// Store the resolved path info in args for policy evaluation
return {
...args,
_resolvedPath: resolvedPath,
};
},
evaluateToolCallPolicy: (
basePolicy: ToolPolicy,
parsedArgs: Record<string, unknown>,
): ToolPolicy => {
const resolvedPath = parsedArgs._resolvedPath as any;
if (!resolvedPath) return basePolicy;

return evaluateFileAccessPolicy(basePolicy, resolvedPath.isWithinWorkspace);
},
};
27 changes: 26 additions & 1 deletion core/tools/definitions/readFile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ToolPolicy } from "@continuedev/terminal-security";
import { Tool } from "../..";
import { ResolvedPath, resolveInputPath } from "../../util/pathResolver";
import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";
import { evaluateFileAccessPolicy } from "../policies/fileAccess";

export const readFileTool: Tool = {
type: "function",
Expand All @@ -21,7 +24,7 @@ export const readFileTool: Tool = {
filepath: {
type: "string",
description:
"The path of the file to read, relative to the root of the workspace (NOT uri or absolute path)",
"The path of the file to read. Can be a relative path (from workspace root), absolute path, tilde path (~/...), or file:// URI",
},
},
},
Expand All @@ -32,4 +35,26 @@ export const readFileTool: Tool = {
},
defaultToolPolicy: "allowedWithoutPermission",
toolCallIcon: "DocumentIcon",
preprocessArgs: async (args, { ide }) => {
const filepath = args.filepath as string;
const resolvedPath = await resolveInputPath(ide, filepath);

// Store the resolved path info in args for policy evaluation
return {
...args,
_resolvedPath: resolvedPath,
};
},
evaluateToolCallPolicy: (
basePolicy: ToolPolicy,
parsedArgs: Record<string, unknown>,
): ToolPolicy => {
const resolvedPath = parsedArgs._resolvedPath as
| ResolvedPath
| null
| undefined;
if (!resolvedPath) return basePolicy;

return evaluateFileAccessPolicy(basePolicy, resolvedPath.isWithinWorkspace);
},
};
22 changes: 22 additions & 0 deletions core/tools/definitions/readFileRange.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ToolPolicy } from "@continuedev/terminal-security";
import { Tool } from "../..";
import { ResolvedPath, resolveInputPath } from "../../util/pathResolver";
import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";
import { evaluateFileAccessPolicy } from "../policies/fileAccess";

export const readFileRangeTool: Tool = {
type: "function",
Expand Down Expand Up @@ -49,4 +52,23 @@ export const readFileRangeTool: Tool = {
},
defaultToolPolicy: "allowedWithoutPermission",
toolCallIcon: "DocumentIcon",
preprocessArgs: async (args, { ide }) => {
const filepath = args.filepath as string;
const resolvedPath = await resolveInputPath(ide, filepath);

// Store the resolved path info in args for policy evaluation
return {
...args,
_resolvedPath: resolvedPath,
};
},
evaluateToolCallPolicy: (
basePolicy: ToolPolicy,
parsedArgs: Record<string, unknown>,
): ToolPolicy => {
const resolvedPath = parsedArgs._resolvedPath as ResolvedPath | null | undefined;
if (!resolvedPath) return basePolicy;

return evaluateFileAccessPolicy(basePolicy, resolvedPath.isWithinWorkspace);
},
};
22 changes: 22 additions & 0 deletions core/tools/definitions/viewSubdirectory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { ToolPolicy } from "@continuedev/terminal-security";
import { Tool } from "../..";
import { resolveInputPath } from "../../util/pathResolver";
import { BUILT_IN_GROUP_NAME, BuiltInToolNames } from "../builtIn";
import { evaluateFileAccessPolicy } from "../policies/fileAccess";

export const viewSubdirectoryTool: Tool = {
type: "function",
Expand Down Expand Up @@ -31,4 +34,23 @@ export const viewSubdirectoryTool: Tool = {
},
defaultToolPolicy: "allowedWithPermission",
toolCallIcon: "FolderOpenIcon",
preprocessArgs: async (args, { ide }) => {
const directoryPath = args.directory_path as string;
const resolvedPath = await resolveInputPath(ide, directoryPath);

// Store the resolved path info in args for policy evaluation
return {
...args,
_resolvedPath: resolvedPath,
};
},
evaluateToolCallPolicy: (
basePolicy: ToolPolicy,
parsedArgs: Record<string, unknown>,
): ToolPolicy => {
const resolvedPath = parsedArgs._resolvedPath as any;
if (!resolvedPath) return basePolicy;

return evaluateFileAccessPolicy(basePolicy, resolvedPath.isWithinWorkspace);
},
};
17 changes: 9 additions & 8 deletions core/tools/implementations/lsTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import ignore from "ignore";

import { ToolImpl } from ".";
import { walkDir } from "../../indexing/walkDir";
import { resolveRelativePathInDir } from "../../util/ideUtils";
import { resolveInputPath } from "../../util/pathResolver";
import { ContinueError, ContinueErrorReason } from "../../util/errors";

export function resolveLsToolDirPath(dirPath: string | undefined) {
if (!dirPath || dirPath === ".") {
return "/";
}
if (dirPath.startsWith(".")) {
// Don't strip leading slash from absolute paths - let the resolver handle it
if (dirPath.startsWith(".") && !dirPath.startsWith("./")) {
return dirPath.slice(1);
}
return dirPath.replace(/\\/g, "/");
Expand All @@ -19,15 +20,15 @@ const MAX_LS_TOOL_LINES = 200;

export const lsToolImpl: ToolImpl = async (args, extras) => {
const dirPath = resolveLsToolDirPath(args?.dirPath);
const uri = await resolveRelativePathInDir(dirPath, extras.ide);
if (!uri) {
const resolvedPath = await resolveInputPath(extras.ide, dirPath);
if (!resolvedPath) {
throw new ContinueError(
ContinueErrorReason.DirectoryNotFound,
`Directory ${args.dirPath} not found. Make sure to use forward-slash paths`,
`Directory ${args.dirPath} not found or is not accessible. You can use absolute paths, relative paths, or paths starting with ~`,
);
}

const entries = await walkDir(uri, extras.ide, {
const entries = await walkDir(resolvedPath.uri, extras.ide, {
returnRelativeUrisPaths: true,
include: "both",
recursive: args?.recursive ?? false,
Expand All @@ -39,12 +40,12 @@ export const lsToolImpl: ToolImpl = async (args, extras) => {
let content =
lines.length > 0
? lines.join("\n")
: `No files/folders found in ${dirPath}`;
: `No files/folders found in ${resolvedPath.displayPath}`;

const contextItems = [
{
name: "File/folder list",
description: `Files/folders in ${dirPath}`,
description: `Files/folders in ${resolvedPath.displayPath}`,
content,
},
];
Expand Down
24 changes: 14 additions & 10 deletions core/tools/implementations/readFile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { resolveRelativePathInDir } from "../../util/ideUtils";
import { resolveInputPath } from "../../util/pathResolver";
import { getUriPathBasename } from "../../util/uri";

import { ToolImpl } from ".";
Expand All @@ -9,31 +9,35 @@ import { ContinueError, ContinueErrorReason } from "../../util/errors";

export const readFileImpl: ToolImpl = async (args, extras) => {
const filepath = getStringArg(args, "filepath");
throwIfFileIsSecurityConcern(filepath);

const firstUriMatch = await resolveRelativePathInDir(filepath, extras.ide);
if (!firstUriMatch) {
// Resolve the path first to get the actual path for security check
const resolvedPath = await resolveInputPath(extras.ide, filepath);
if (!resolvedPath) {
throw new ContinueError(
ContinueErrorReason.FileNotFound,
`File "${filepath}" does not exist. You might want to check the path and try again.`,
`File "${filepath}" does not exist or is not accessible. You might want to check the path and try again.`,
);
}
const content = await extras.ide.readFile(firstUriMatch);

// Security check on the resolved display path
throwIfFileIsSecurityConcern(resolvedPath.displayPath);

const content = await extras.ide.readFile(resolvedPath.uri);

await throwIfFileExceedsHalfOfContext(
filepath,
resolvedPath.displayPath,
content,
extras.config.selectedModelByRole.chat,
);

return [
{
name: getUriPathBasename(firstUriMatch),
description: filepath,
name: getUriPathBasename(resolvedPath.uri),
description: resolvedPath.displayPath,
content,
uri: {
type: "file",
value: firstUriMatch,
value: resolvedPath.uri,
},
},
];
Expand Down
23 changes: 14 additions & 9 deletions core/tools/implementations/readFileRange.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { resolveRelativePathInDir } from "../../util/ideUtils";
import { resolveInputPath } from "../../util/pathResolver";
import { getUriPathBasename } from "../../util/uri";

import { ToolImpl } from ".";
import { throwIfFileIsSecurityConcern } from "../../indexing/ignore";
import { getNumberArg, getStringArg } from "../parseArgs";
import { throwIfFileExceedsHalfOfContext } from "./readFileLimit";
import { ContinueError, ContinueErrorReason } from "../../util/errors";
Expand Down Expand Up @@ -31,16 +32,20 @@ export const readFileRangeImpl: ToolImpl = async (args, extras) => {
);
}

const firstUriMatch = await resolveRelativePathInDir(filepath, extras.ide);
if (!firstUriMatch) {
// Resolve the path first to get the actual path for security check
const resolvedPath = await resolveInputPath(extras.ide, filepath);
if (!resolvedPath) {
throw new ContinueError(
ContinueErrorReason.FileNotFound,
`File "${filepath}" does not exist. You might want to check the path and try again.`,
`File "${filepath}" does not exist or is not accessible. You might want to check the path and try again.`,
);
}

// Security check on the resolved display path
throwIfFileIsSecurityConcern(resolvedPath.displayPath);

// Use the IDE's readRangeInFile method with 0-based range (IDE expects 0-based internally)
const content = await extras.ide.readRangeInFile(firstUriMatch, {
const content = await extras.ide.readRangeInFile(resolvedPath.uri, {
start: {
line: startLine - 1, // Convert from 1-based to 0-based
character: 0,
Expand All @@ -52,21 +57,21 @@ export const readFileRangeImpl: ToolImpl = async (args, extras) => {
});

await throwIfFileExceedsHalfOfContext(
filepath,
resolvedPath.displayPath,
content,
extras.config.selectedModelByRole.chat,
);

const rangeDescription = `${filepath} (lines ${startLine}-${endLine})`;
const rangeDescription = `${resolvedPath.displayPath} (lines ${startLine}-${endLine})`;

return [
{
name: getUriPathBasename(firstUriMatch),
name: getUriPathBasename(resolvedPath.uri),
description: rangeDescription,
content,
uri: {
type: "file",
value: firstUriMatch,
value: resolvedPath.uri,
},
},
];
Expand Down
12 changes: 6 additions & 6 deletions core/tools/implementations/viewSubdirectory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import generateRepoMap from "../../util/generateRepoMap";
import { resolveRelativePathInDir } from "../../util/ideUtils";
import { resolveInputPath } from "../../util/pathResolver";

import { ToolImpl } from ".";
import { ContinueError, ContinueErrorReason } from "../../util/errors";
Expand All @@ -8,25 +8,25 @@ import { getStringArg } from "../parseArgs";
export const viewSubdirectoryImpl: ToolImpl = async (args: any, extras) => {
const directory_path = getStringArg(args, "directory_path");

const uri = await resolveRelativePathInDir(directory_path, extras.ide);
const resolvedPath = await resolveInputPath(extras.ide, directory_path);

if (!uri) {
if (!resolvedPath) {
throw new ContinueError(
ContinueErrorReason.DirectoryNotFound,
`Directory path "${directory_path}" does not exist.`,
`Directory path "${directory_path}" does not exist or is not accessible.`,
);
}

const repoMap = await generateRepoMap(extras.llm, extras.ide, {
dirUris: [uri],
dirUris: [resolvedPath.uri],
outputRelativeUriPaths: true,
includeSignatures: false,
});

return [
{
name: "Repo map",
description: `Map of ${directory_path}`,
description: `Map of ${resolvedPath.displayPath}`,
content: repoMap,
},
];
Expand Down
26 changes: 26 additions & 0 deletions core/tools/policies/fileAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ToolPolicy } from "@continuedev/terminal-security";

/**
* Evaluates file access policy based on whether the file is within workspace boundaries
*
* @param basePolicy - The base policy from tool definition or user settings
* @param isWithinWorkspace - Whether the file/directory is within workspace
* @returns The evaluated policy - more restrictive for files outside workspace
*/
export function evaluateFileAccessPolicy(
basePolicy: ToolPolicy,
isWithinWorkspace: boolean
): ToolPolicy {
// If tool is disabled, keep it disabled
if (basePolicy === "disabled") {
return "disabled";
}

// Files within workspace use the base policy (typically "allowedWithoutPermission")
if (isWithinWorkspace) {
return basePolicy;
}

// Files outside workspace always require permission for security
return "allowedWithPermission";
}
Loading
Loading