Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// FileSearch tool based on ListDirectory implementation
import { CommandValidation, InvokeOutput, requiresPathAcceptance, validatePath } from './toolShared'
import {
CommandValidation,
InvokeOutput,
requiresPathAcceptance,
resolveCanonicalPath,
validatePath,
} from './toolShared'
import { workspaceUtils } from '@aws/lsp-core'
import { Features } from '@aws/language-server-runtimes/server-interface/server'
import { sanitize } from '@aws/lsp-core/out/util/path'
import { DEFAULT_EXCLUDE_DIRS, DEFAULT_EXCLUDE_FILES } from '../../chat/constants'
import { CancellationToken } from '@aws/language-server-runtimes/protocol'
const Fuse = require('fuse.js')
Expand Down Expand Up @@ -56,7 +61,7 @@ export class FileSearch {
}

public async invoke(params: FileSearchParams, token?: CancellationToken): Promise<InvokeOutput> {
const path = sanitize(params.path)
const path = await resolveCanonicalPath(params.path)
try {
// Get all files and directories
const listing = await workspaceUtils.readDirectoryRecursively(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { sanitize } from '@aws/lsp-core/out/util/path'
import { CommandValidation, InvokeOutput, requiresPathAcceptance, validatePath } from './toolShared'
import {
CommandValidation,
InvokeOutput,
requiresPathAcceptance,
resolveCanonicalPath,
validatePath,
} from './toolShared'
import { Features } from '@aws/language-server-runtimes/server-interface/server'
import { FSREAD_MAX_PER_FILE, FSREAD_MAX_TOTAL } from '../constants/constants'

Expand Down Expand Up @@ -57,7 +62,7 @@ export class FsRead {
public async invoke(params: FsReadParams): Promise<InvokeOutput> {
const fileResult: FileReadResult[] = []
for (const path of params.paths) {
const sanitizedPath = sanitize(path)
const sanitizedPath = await resolveCanonicalPath(path)
const content = await this.readFile(sanitizedPath)
this.logging.info(`Read file: ${sanitizedPath}, size: ${content.length}`)
fileResult.push({ path, content, truncated: false })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { CommandValidation, ExplanatoryParams, InvokeOutput, requiresPathAcceptance } from './toolShared'
import {
CommandValidation,
ExplanatoryParams,
InvokeOutput,
requiresPathAcceptance,
resolveCanonicalPath,
} from './toolShared'
import { EmptyPathError, EmptyDiffsError, FileNotExistsError, TextNotFoundError, MultipleMatchesError } from '../errors'
import { Features } from '@aws/language-server-runtimes/server-interface/server'
import { sanitize } from '@aws/lsp-core/out/util/path'
import * as os from 'os'

interface BaseParams extends ExplanatoryParams {
Expand Down Expand Up @@ -37,15 +42,15 @@ export class FsReplace {
if (!params.diffs || params.diffs.length === 0) {
throw new EmptyDiffsError()
}
const sanitizedPath = sanitize(params.path)
const sanitizedPath = await resolveCanonicalPath(params.path)
const fileExists = await this.workspace.fs.exists(sanitizedPath)
if (!fileExists) {
throw new FileNotExistsError()
}
}

public async invoke(params: FsReplaceParams): Promise<InvokeOutput> {
const sanitizedPath = sanitize(params.path)
const sanitizedPath = await resolveCanonicalPath(params.path)

await this.handleReplace(params, sanitizedPath)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { CommandValidation, ExplanatoryParams, InvokeOutput, requiresPathAcceptance } from './toolShared'
import {
CommandValidation,
ExplanatoryParams,
InvokeOutput,
requiresPathAcceptance,
resolveCanonicalPath,
} from './toolShared'
import { EmptyPathError, MissingContentError, FileExistsWithSameContentError, EmptyAppendContentError } from '../errors'
import { Features } from '@aws/language-server-runtimes/server-interface/server'
import { sanitize } from '@aws/lsp-core/out/util/path'
import { LocalProjectContextController } from '../../../shared/localProjectContextController'
import { URI } from 'vscode-uri'

Expand Down Expand Up @@ -41,7 +46,7 @@ export class FsWrite {
if (!params.path) {
throw new EmptyPathError()
}
const sanitizedPath = sanitize(params.path)
const sanitizedPath = await resolveCanonicalPath(params.path)
switch (params.command) {
case 'create': {
if (params.fileText === undefined) {
Expand All @@ -65,7 +70,7 @@ export class FsWrite {
}

public async invoke(params: FsWriteParams): Promise<InvokeOutput> {
const sanitizedPath = sanitize(params.path)
const sanitizedPath = await resolveCanonicalPath(params.path)
let content = ''
switch (params.command) {
case 'create':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// Port from VSC: https://github.com/aws/aws-toolkit-vscode/blob/0eea1d8ca6e25243609a07dc2a2c31886b224baa/packages/core/src/codewhispererChat/tools/listDirectory.ts#L19
import { CommandValidation, InvokeOutput, requiresPathAcceptance, validatePath } from './toolShared'
import {
CommandValidation,
InvokeOutput,
requiresPathAcceptance,
resolveCanonicalPath,
validatePath,
} from './toolShared'
import { CancellationError, workspaceUtils } from '@aws/lsp-core'
import { Features } from '@aws/language-server-runtimes/server-interface/server'
import { sanitize } from '@aws/lsp-core/out/util/path'
import { DEFAULT_EXCLUDE_DIRS, DEFAULT_EXCLUDE_FILES } from '../../chat/constants'
import { CancellationToken } from '@aws/language-server-runtimes/protocol'

Expand Down Expand Up @@ -59,7 +64,7 @@ export class ListDirectory {
}

public async invoke(params: ListDirectoryParams, token?: CancellationToken): Promise<InvokeOutput> {
const path = sanitize(params.path)
const path = await resolveCanonicalPath(params.path)
try {
const result = await workspaceUtils.readDirectoryWithTreeOutput(
{ workspace: this.workspace, logging: this.logging },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Features } from '@aws/language-server-runtimes/server-interface/server'
import { workspaceUtils } from '@aws/lsp-core'
import { getWorkspaceFolderPaths } from '@aws/lsp-core/out/util/workspaceUtils'
import { sanitize } from '@aws/lsp-core/out/util/path'
import * as fs from 'fs'
import * as path from 'path'
import { CommandCategory } from './executeBash'
Expand Down Expand Up @@ -62,6 +63,17 @@ export async function resolveSymlinkAwarePath(inputPath: string): Promise<string
}
}

/**
* Resolve an input path to the single canonical on-disk location a read, write,
* or list should act on: `sanitize()` (expand `~`, make absolute) followed by
* `resolveSymlinkAwarePath()` (follow symlinks at every segment). Tools call
* this once and act on the returned value so the workspace-boundary decision
* and the operation itself cannot resolve to different targets.
*/
export async function resolveCanonicalPath(inputPath: string): Promise<string> {
return resolveSymlinkAwarePath(sanitize(inputPath))
}

/**
* Canonicalize workspace folder paths through the filesystem so boundary
* comparisons stay accurate even when a workspace lives under a symlinked
Expand Down Expand Up @@ -202,15 +214,12 @@ export async function requiresPathAcceptance(
approvedPaths?: Map<string, Set<string>>
): Promise<CommandValidation> {
try {
// Canonicalize the path in a symlink-aware way before the
// workspace-boundary check. This resolves symlinks at every segment,
// including a symlink at the leaf whose target does not exist yet
// (a "dangling" symlink). A string-only resolve, or an fs.realpath
// that silently falls back to the literal link name when the target
// is missing, would treat such a link as in-workspace based on its
// name alone even though a write or read through it would land
// outside the workspace.
const canonicalPath = await resolveSymlinkAwarePath(inputPath)
// Canonicalize in a symlink-aware way before the workspace-boundary
// check: a link whose name sits inside the workspace can point outside
// it, including when the target does not exist yet (a dangling link).
// The I/O tools resolve via the same helper and operate on that value,
// so the boundary decision and the operation share one resolved path.
const canonicalPath = await resolveCanonicalPath(inputPath)

// Then check if the path is already approved for this specific tool
if (isPathApproved(canonicalPath, toolName, approvedPaths)) {
Expand Down
Loading