diff --git a/CHANGELOG.md b/CHANGELOG.md index 91e733420..5d08f900d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - iMessage: fix reasoning persistence across DMs; avoid partial/duplicate replies when reasoning is enabled. (#655) — thanks @antons. - Models/Auth: allow MiniMax API configs without `models.providers.minimax.apiKey` (auth profiles / `MINIMAX_API_KEY`). (#656) — thanks @mneves75. - Agents: avoid duplicate replies when the message tool sends. (#659) — thanks @mickahouan. +- Agents/Tools: resolve workspace-relative Read/Write/Edit paths; align bash default cwd. (#642) — thanks @mukhtharcm. - iOS/Android: enable stricter concurrency/lint checks; fix Swift 6 strict concurrency issues + Android lint errors (ExifInterface, obsolete SDK check). (#662) — thanks @KristijanJovanovski. - Docs: showcase entries for ParentPay, R2 Upload, iOS TestFlight, and Oura Health. (#650) — thanks @henrino3. diff --git a/src/agents/bash-tools.ts b/src/agents/bash-tools.ts index cf9ba5055..a3d2b9f75 100644 --- a/src/agents/bash-tools.ts +++ b/src/agents/bash-tools.ts @@ -61,6 +61,7 @@ export type BashToolDefaults = { elevated?: BashElevatedDefaults; allowBackground?: boolean; scopeKey?: string; + cwd?: string; }; export type ProcessToolDefaults = { @@ -202,7 +203,8 @@ export function createBashTool( } const sandbox = elevatedRequested ? undefined : defaults?.sandbox; - const rawWorkdir = params.workdir?.trim() || process.cwd(); + const rawWorkdir = + params.workdir?.trim() || defaults?.cwd || process.cwd(); let workdir = rawWorkdir; let containerWorkdir = sandbox?.containerWorkdir; if (sandbox) { diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index d12df2b48..90afd6d0d 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -849,6 +849,7 @@ export async function compactEmbeddedPiSession(params: { agentAccountId: params.agentAccountId, sessionKey: params.sessionKey ?? params.sessionId, agentDir, + workspaceDir: effectiveWorkspace, config: params.config, abortSignal: runAbortController.signal, modelProvider: model.provider, @@ -1232,6 +1233,7 @@ export async function runEmbeddedPiAgent(params: { agentAccountId: params.agentAccountId, sessionKey: params.sessionKey ?? params.sessionId, agentDir, + workspaceDir: effectiveWorkspace, config: params.config, abortSignal: runAbortController.signal, modelProvider: model.provider, diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index 59ad6f7d0..c3c207c70 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -419,4 +419,98 @@ describe("createClawdbotCodingTools", () => { expect(violations).toEqual([]); } }); + + it("uses workspaceDir for Read tool path resolution", async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); + try { + // Create a test file in the "workspace" + const testFile = "test-workspace-file.txt"; + const testContent = "workspace path resolution test"; + await fs.writeFile(path.join(tmpDir, testFile), testContent, "utf8"); + + // Create tools with explicit workspaceDir + const tools = createClawdbotCodingTools({ workspaceDir: tmpDir }); + const readTool = tools.find((tool) => tool.name === "read"); + expect(readTool).toBeDefined(); + + // Read using relative path - should resolve against workspaceDir + const result = await readTool?.execute("tool-ws-1", { + path: testFile, + }); + + const textBlocks = result?.content?.filter( + (block) => block.type === "text", + ) as Array<{ text?: string }> | undefined; + const combinedText = textBlocks + ?.map((block) => block.text ?? "") + .join("\n"); + expect(combinedText).toContain(testContent); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); + + it("uses workspaceDir for Write tool path resolution", async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); + try { + const testFile = "test-write-file.txt"; + const testContent = "written via workspace path"; + + // Create tools with explicit workspaceDir + const tools = createClawdbotCodingTools({ workspaceDir: tmpDir }); + const writeTool = tools.find((tool) => tool.name === "write"); + expect(writeTool).toBeDefined(); + + // Write using relative path - should resolve against workspaceDir + await writeTool?.execute("tool-ws-2", { + path: testFile, + content: testContent, + }); + + // Verify file was written to workspaceDir + const written = await fs.readFile(path.join(tmpDir, testFile), "utf8"); + expect(written).toBe(testContent); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); + + it("uses workspaceDir for Edit tool path resolution", async () => { + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); + try { + const testFile = "test-edit-file.txt"; + const originalContent = "hello world"; + const expectedContent = "hello universe"; + await fs.writeFile(path.join(tmpDir, testFile), originalContent, "utf8"); + + // Create tools with explicit workspaceDir + const tools = createClawdbotCodingTools({ workspaceDir: tmpDir }); + const editTool = tools.find((tool) => tool.name === "edit"); + expect(editTool).toBeDefined(); + + // Edit using relative path - should resolve against workspaceDir + await editTool?.execute("tool-ws-3", { + path: testFile, + oldText: "world", + newText: "universe", + }); + + // Verify file was edited in workspaceDir + const edited = await fs.readFile(path.join(tmpDir, testFile), "utf8"); + expect(edited).toBe(expectedContent); + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + }); + + it("falls back to process.cwd() when workspaceDir not provided", () => { + const prevCwd = process.cwd(); + const tools = createClawdbotCodingTools(); + // Tools should be created without error + expect(tools.some((tool) => tool.name === "read")).toBe(true); + expect(tools.some((tool) => tool.name === "write")).toBe(true); + expect(tools.some((tool) => tool.name === "edit")).toBe(true); + // cwd should be unchanged + expect(process.cwd()).toBe(prevCwd); + }); }); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index ad3925b86..bfc9af9fc 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -531,6 +531,7 @@ export function createClawdbotCodingTools(options?: { sandbox?: SandboxContext | null; sessionKey?: string; agentDir?: string; + workspaceDir?: string; config?: ClawdbotConfig; abortSignal?: AbortSignal; /** @@ -571,20 +572,30 @@ export function createClawdbotCodingTools(options?: { ]); const sandboxRoot = sandbox?.workspaceDir; const allowWorkspaceWrites = sandbox?.workspaceAccess !== "ro"; + const workspaceRoot = options?.workspaceDir ?? process.cwd(); + const base = (codingTools as unknown as AnyAgentTool[]).flatMap((tool) => { if (tool.name === readTool.name) { - return sandboxRoot - ? [createSandboxedReadTool(sandboxRoot)] - : [createClawdbotReadTool(tool)]; + if (sandboxRoot) { + return [createSandboxedReadTool(sandboxRoot)]; + } + const freshReadTool = createReadTool(workspaceRoot); + return [createClawdbotReadTool(freshReadTool)]; } if (tool.name === bashToolName) return []; - if (sandboxRoot && (tool.name === "write" || tool.name === "edit")) { - return []; + if (tool.name === "write") { + if (sandboxRoot) return []; + return [createWriteTool(workspaceRoot)]; + } + if (tool.name === "edit") { + if (sandboxRoot) return []; + return [createEditTool(workspaceRoot)]; } return [tool as AnyAgentTool]; }); const bashTool = createBashTool({ ...options?.bash, + cwd: options?.workspaceDir, allowBackground, scopeKey, sandbox: sandbox