Skip to content

Commit f1d75d3

Browse files
committed
fix(agent): resolve workdir from session only
1 parent 4440113 commit f1d75d3

File tree

3 files changed

+86
-89
lines changed

3 files changed

+86
-89
lines changed

src/main/presenter/agentPresenter/acp/agentBashHandler.ts

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { spawn, type ChildProcess } from 'child_process'
2-
import fs from 'fs/promises'
32
import path from 'path'
43
import os from 'os'
54
import { z } from 'zod'
@@ -22,7 +21,6 @@ const COMMAND_KILL_GRACE_MS = 5000
2221
const ExecuteCommandArgsSchema = z.object({
2322
command: z.string().min(1),
2423
timeout: z.number().min(100).optional(),
25-
workdir: z.string().optional(),
2624
description: z.string().min(5).max(100)
2725
})
2826

@@ -51,7 +49,7 @@ export class AgentBashHandler {
5149
throw new Error(`Invalid arguments: ${parsed.error}`)
5250
}
5351

54-
const { command, timeout, workdir } = parsed.data
52+
const { command, timeout } = parsed.data
5553
if (this.commandPermissionHandler) {
5654
const permissionCheck = this.commandPermissionHandler.checkPermission(
5755
options.conversationId,
@@ -74,7 +72,7 @@ export class AgentBashHandler {
7472
}
7573
}
7674

77-
const cwd = workdir ? await this.validatePath(workdir) : this.allowedDirectories[0]
75+
const cwd = this.allowedDirectories[0]
7876
const startedAt = Date.now()
7977
const snippetId = options.snippetId ?? nanoid()
8078

@@ -178,50 +176,6 @@ export class AgentBashHandler {
178176
return filepath
179177
}
180178

181-
private isPathAllowed(candidatePath: string): boolean {
182-
return this.allowedDirectories.some((dir) => {
183-
if (candidatePath === dir) return true
184-
const dirWithSeparator = dir.endsWith(path.sep) ? dir : `${dir}${path.sep}`
185-
return candidatePath.startsWith(dirWithSeparator)
186-
})
187-
}
188-
189-
private async validatePath(requestedPath: string): Promise<string> {
190-
const expandedPath = this.expandHome(requestedPath)
191-
const absolute = path.isAbsolute(expandedPath)
192-
? path.resolve(expandedPath)
193-
: path.resolve(process.cwd(), expandedPath)
194-
const normalizedRequested = this.normalizePath(absolute)
195-
const isAllowed = this.isPathAllowed(normalizedRequested)
196-
if (!isAllowed) {
197-
throw new Error(
198-
`Access denied - path outside allowed directories: ${absolute} not in ${this.allowedDirectories.join(', ')}`
199-
)
200-
}
201-
try {
202-
const realPath = await fs.realpath(absolute)
203-
const normalizedReal = this.normalizePath(realPath)
204-
const isRealPathAllowed = this.isPathAllowed(normalizedReal)
205-
if (!isRealPathAllowed) {
206-
throw new Error('Access denied - symlink target outside allowed directories')
207-
}
208-
return realPath
209-
} catch {
210-
const parentDir = path.dirname(absolute)
211-
try {
212-
const realParentPath = await fs.realpath(parentDir)
213-
const normalizedParent = this.normalizePath(realParentPath)
214-
const isParentAllowed = this.isPathAllowed(normalizedParent)
215-
if (!isParentAllowed) {
216-
throw new Error('Access denied - parent directory outside allowed directories')
217-
}
218-
return absolute
219-
} catch {
220-
throw new Error(`Parent directory does not exist: ${parentDir}`)
221-
}
222-
}
223-
}
224-
225179
private async runShellProcess(
226180
command: string,
227181
cwd: string,

src/main/presenter/agentPresenter/acp/agentFileSystemHandler.ts

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,11 @@ export class AgentFileSystemHandler {
164164
return filepath
165165
}
166166

167-
private async validatePath(requestedPath: string): Promise<string> {
167+
private async validatePath(requestedPath: string, baseDirectory?: string): Promise<string> {
168168
const expandedPath = this.expandHome(requestedPath)
169169
const absolute = path.isAbsolute(expandedPath)
170170
? path.resolve(expandedPath)
171-
: path.resolve(process.cwd(), expandedPath)
171+
: path.resolve(baseDirectory ?? this.allowedDirectories[0], expandedPath)
172172
const normalizedRequested = this.normalizePath(absolute)
173173
const isAllowed = this.isPathAllowed(normalizedRequested)
174174
if (!isAllowed) {
@@ -631,15 +631,15 @@ export class AgentFileSystemHandler {
631631
}
632632
}
633633

634-
async readFile(args: unknown): Promise<string> {
634+
async readFile(args: unknown, baseDirectory?: string): Promise<string> {
635635
const parsed = ReadFileArgsSchema.safeParse(args)
636636
if (!parsed.success) {
637637
throw new Error(`Invalid arguments: ${parsed.error}`)
638638
}
639639
const results = await Promise.all(
640640
parsed.data.paths.map(async (filePath: string) => {
641641
try {
642-
const validPath = await this.validatePath(filePath)
642+
const validPath = await this.validatePath(filePath, baseDirectory)
643643
const content = await fs.readFile(validPath, 'utf-8')
644644
return `${filePath}:\n${content}\n`
645645
} catch (error) {
@@ -651,22 +651,22 @@ export class AgentFileSystemHandler {
651651
return results.join('\n---\n')
652652
}
653653

654-
async writeFile(args: unknown): Promise<string> {
654+
async writeFile(args: unknown, baseDirectory?: string): Promise<string> {
655655
const parsed = WriteFileArgsSchema.safeParse(args)
656656
if (!parsed.success) {
657657
throw new Error(`Invalid arguments: ${parsed.error}`)
658658
}
659-
const validPath = await this.validatePath(parsed.data.path)
659+
const validPath = await this.validatePath(parsed.data.path, baseDirectory)
660660
await fs.writeFile(validPath, parsed.data.content, 'utf-8')
661661
return `Successfully wrote to ${parsed.data.path}`
662662
}
663663

664-
async listDirectory(args: unknown): Promise<string> {
664+
async listDirectory(args: unknown, baseDirectory?: string): Promise<string> {
665665
const parsed = ListDirectoryArgsSchema.safeParse(args)
666666
if (!parsed.success) {
667667
throw new Error(`Invalid arguments: ${parsed.error}`)
668668
}
669-
const validPath = await this.validatePath(parsed.data.path)
669+
const validPath = await this.validatePath(parsed.data.path, baseDirectory)
670670
const entries = await fs.readdir(validPath, { withFileTypes: true })
671671
const formatted = entries
672672
.map((entry) => {
@@ -677,26 +677,27 @@ export class AgentFileSystemHandler {
677677
return `Directory listing for ${parsed.data.path}:\n\n${formatted}`
678678
}
679679

680-
async createDirectory(args: unknown): Promise<string> {
680+
async createDirectory(args: unknown, baseDirectory?: string): Promise<string> {
681681
const parsed = CreateDirectoryArgsSchema.safeParse(args)
682682
if (!parsed.success) {
683683
throw new Error(`Invalid arguments: ${parsed.error}`)
684684
}
685-
const validPath = await this.validatePath(parsed.data.path)
685+
const validPath = await this.validatePath(parsed.data.path, baseDirectory)
686686
await fs.mkdir(validPath, { recursive: true })
687687
return `Successfully created directory ${parsed.data.path}`
688688
}
689689

690-
async moveFiles(args: unknown): Promise<string> {
690+
async moveFiles(args: unknown, baseDirectory?: string): Promise<string> {
691691
const parsed = MoveFilesArgsSchema.safeParse(args)
692692
if (!parsed.success) {
693693
throw new Error(`Invalid arguments: ${parsed.error}`)
694694
}
695695
const results = await Promise.all(
696696
parsed.data.sources.map(async (source) => {
697-
const validSourcePath = await this.validatePath(source)
697+
const validSourcePath = await this.validatePath(source, baseDirectory)
698698
const validDestPath = await this.validatePath(
699-
path.join(parsed.data.destination, path.basename(source))
699+
path.join(parsed.data.destination, path.basename(source)),
700+
baseDirectory
700701
)
701702
try {
702703
await fs.rename(validSourcePath, validDestPath)
@@ -709,12 +710,12 @@ export class AgentFileSystemHandler {
709710
return results.join('\n')
710711
}
711712

712-
async editText(args: unknown): Promise<string> {
713+
async editText(args: unknown, baseDirectory?: string): Promise<string> {
713714
const parsed = EditTextArgsSchema.safeParse(args)
714715
if (!parsed.success) {
715716
throw new Error(`Invalid arguments: ${parsed.error}`)
716717
}
717-
const validPath = await this.validatePath(parsed.data.path)
718+
const validPath = await this.validatePath(parsed.data.path, baseDirectory)
718719
const content = await fs.readFile(validPath, 'utf-8')
719720
let modifiedContent = content
720721

@@ -747,13 +748,13 @@ export class AgentFileSystemHandler {
747748
return diff
748749
}
749750

750-
async grepSearch(args: unknown): Promise<string> {
751+
async grepSearch(args: unknown, baseDirectory?: string): Promise<string> {
751752
const parsed = GrepSearchArgsSchema.safeParse(args)
752753
if (!parsed.success) {
753754
throw new Error(`Invalid arguments: ${parsed.error}`)
754755
}
755756

756-
const validPath = await this.validatePath(parsed.data.path)
757+
const validPath = await this.validatePath(parsed.data.path, baseDirectory)
757758
const result = await this.runGrepSearch(validPath, parsed.data.pattern, {
758759
filePattern: parsed.data.filePattern,
759760
recursive: parsed.data.recursive,
@@ -791,13 +792,13 @@ export class AgentFileSystemHandler {
791792
return `Found ${result.totalMatches} matches in ${result.files.length} files:\n\n${formattedResults}`
792793
}
793794

794-
async textReplace(args: unknown): Promise<string> {
795+
async textReplace(args: unknown, baseDirectory?: string): Promise<string> {
795796
const parsed = TextReplaceArgsSchema.safeParse(args)
796797
if (!parsed.success) {
797798
throw new Error(`Invalid arguments: ${parsed.error}`)
798799
}
799800

800-
const validPath = await this.validatePath(parsed.data.path)
801+
const validPath = await this.validatePath(parsed.data.path, baseDirectory)
801802
const result = await this.replaceTextInFile(
802803
validPath,
803804
parsed.data.pattern,
@@ -812,14 +813,14 @@ export class AgentFileSystemHandler {
812813
return result.success ? result.diff || '' : result.error || 'Text replacement failed'
813814
}
814815

815-
async directoryTree(args: unknown): Promise<string> {
816+
async directoryTree(args: unknown, baseDirectory?: string): Promise<string> {
816817
const parsed = DirectoryTreeArgsSchema.safeParse(args)
817818
if (!parsed.success) {
818819
throw new Error(`Invalid arguments: ${parsed.error}`)
819820
}
820821

821822
const buildTree = async (currentPath: string): Promise<TreeEntry[]> => {
822-
const validPath = await this.validatePath(currentPath)
823+
const validPath = await this.validatePath(currentPath, baseDirectory)
823824
const entries = await fs.readdir(validPath, { withFileTypes: true })
824825
const result: TreeEntry[] = []
825826

@@ -844,20 +845,20 @@ export class AgentFileSystemHandler {
844845
return JSON.stringify(treeData, null, 2)
845846
}
846847

847-
async getFileInfo(args: unknown): Promise<string> {
848+
async getFileInfo(args: unknown, baseDirectory?: string): Promise<string> {
848849
const parsed = GetFileInfoArgsSchema.safeParse(args)
849850
if (!parsed.success) {
850851
throw new Error(`Invalid arguments: ${parsed.error}`)
851852
}
852853

853-
const validPath = await this.validatePath(parsed.data.path)
854+
const validPath = await this.validatePath(parsed.data.path, baseDirectory)
854855
const info = await this.getFileStats(validPath)
855856
return Object.entries(info)
856857
.map(([key, value]) => `${key}: ${value}`)
857858
.join('\n')
858859
}
859860

860-
async globSearch(args: unknown): Promise<string> {
861+
async globSearch(args: unknown, baseDirectory?: string): Promise<string> {
861862
const parsed = GlobSearchArgsSchema.safeParse(args)
862863
if (!parsed.success) {
863864
throw new Error(`Invalid arguments: ${parsed.error}`)
@@ -867,7 +868,9 @@ export class AgentFileSystemHandler {
867868
validateGlobPattern(pattern)
868869

869870
// Determine root directory
870-
const searchRoot = root ? await this.validatePath(root) : this.allowedDirectories[0]
871+
const searchRoot = root
872+
? await this.validatePath(root, baseDirectory)
873+
: await this.validatePath(baseDirectory ?? this.allowedDirectories[0])
871874

872875
// Default exclusions
873876
const defaultExclusions = [

src/main/presenter/agentPresenter/acp/agentToolManager.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import fs from 'fs'
66
import path from 'path'
77
import { app } from 'electron'
88
import logger from '@shared/logger'
9+
import { presenter } from '@/presenter'
910
import { AgentFileSystemHandler } from './agentFileSystemHandler'
1011
import { AgentBashHandler } from './agentBashHandler'
1112

@@ -155,11 +156,6 @@ export class AgentToolManager {
155156
.max(600000)
156157
.optional()
157158
.describe('Optional timeout in milliseconds'),
158-
workdir: z
159-
.string()
160-
.min(1)
161-
.optional()
162-
.describe('Working directory (defaults to workspace root); prefer this over using cd'),
163159
description: z.string().min(5).max(100).describe('Brief description of what the command does')
164160
})
165161
}
@@ -255,6 +251,35 @@ export class AgentToolManager {
255251
throw new Error(`Unknown Agent tool: ${toolName}`)
256252
}
257253

254+
private async getWorkdirForConversation(conversationId: string): Promise<string | null> {
255+
try {
256+
const session = await presenter?.sessionManager?.getSession(conversationId)
257+
if (!session?.resolved) {
258+
return null
259+
}
260+
261+
const resolved = session.resolved
262+
263+
if (resolved.chatMode === 'acp agent') {
264+
const modelId = resolved.modelId
265+
const map = resolved.acpWorkdirMap
266+
return modelId && map ? (map[modelId] ?? null) : null
267+
}
268+
269+
if (resolved.chatMode === 'agent') {
270+
return resolved.agentWorkspacePath ?? null
271+
}
272+
273+
return null
274+
} catch (error) {
275+
logger.warn('[AgentToolManager] Failed to get workdir for conversation:', {
276+
conversationId,
277+
error
278+
})
279+
return null
280+
}
281+
}
282+
258283
private getFileSystemToolDefinitions(): MCPToolDefinition[] {
259284
const schemas = this.fileSystemSchemas
260285
return [
@@ -508,31 +533,46 @@ export class AgentToolManager {
508533
}
509534

510535
const parsedArgs = validationResult.data
536+
let dynamicWorkdir: string | null = null
537+
if (conversationId) {
538+
try {
539+
dynamicWorkdir = await this.getWorkdirForConversation(conversationId)
540+
} catch (error) {
541+
logger.warn('[AgentToolManager] Failed to get workdir for conversation:', {
542+
conversationId,
543+
error
544+
})
545+
}
546+
}
547+
548+
const baseDirectory = dynamicWorkdir ?? undefined
511549

512550
try {
513551
switch (toolName) {
514552
case 'read_file':
515-
return { content: await this.fileSystemHandler.readFile(parsedArgs) }
553+
return { content: await this.fileSystemHandler.readFile(parsedArgs, baseDirectory) }
516554
case 'write_file':
517-
return { content: await this.fileSystemHandler.writeFile(parsedArgs) }
555+
return { content: await this.fileSystemHandler.writeFile(parsedArgs, baseDirectory) }
518556
case 'list_directory':
519-
return { content: await this.fileSystemHandler.listDirectory(parsedArgs) }
557+
return { content: await this.fileSystemHandler.listDirectory(parsedArgs, baseDirectory) }
520558
case 'create_directory':
521-
return { content: await this.fileSystemHandler.createDirectory(parsedArgs) }
559+
return {
560+
content: await this.fileSystemHandler.createDirectory(parsedArgs, baseDirectory)
561+
}
522562
case 'move_files':
523-
return { content: await this.fileSystemHandler.moveFiles(parsedArgs) }
563+
return { content: await this.fileSystemHandler.moveFiles(parsedArgs, baseDirectory) }
524564
case 'edit_text':
525-
return { content: await this.fileSystemHandler.editText(parsedArgs) }
565+
return { content: await this.fileSystemHandler.editText(parsedArgs, baseDirectory) }
526566
case 'glob_search':
527-
return { content: await this.fileSystemHandler.globSearch(parsedArgs) }
567+
return { content: await this.fileSystemHandler.globSearch(parsedArgs, baseDirectory) }
528568
case 'directory_tree':
529-
return { content: await this.fileSystemHandler.directoryTree(parsedArgs) }
569+
return { content: await this.fileSystemHandler.directoryTree(parsedArgs, baseDirectory) }
530570
case 'get_file_info':
531-
return { content: await this.fileSystemHandler.getFileInfo(parsedArgs) }
571+
return { content: await this.fileSystemHandler.getFileInfo(parsedArgs, baseDirectory) }
532572
case 'grep_search':
533-
return { content: await this.fileSystemHandler.grepSearch(parsedArgs) }
573+
return { content: await this.fileSystemHandler.grepSearch(parsedArgs, baseDirectory) }
534574
case 'text_replace':
535-
return { content: await this.fileSystemHandler.textReplace(parsedArgs) }
575+
return { content: await this.fileSystemHandler.textReplace(parsedArgs, baseDirectory) }
536576
case 'execute_command':
537577
if (!this.bashHandler) {
538578
throw new Error('Bash handler not initialized for execute_command tool')

0 commit comments

Comments
 (0)