From 1be9faae8e0c07a45ccaea509b51da9c42b0f18e Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Mar 2026 21:37:48 +0000 Subject: [PATCH 1/2] Fix: Fallback to workspace when exec cwd is outside allowed directories Instead of throwing an error when the LLM passes an invalid cwd (e.g. /home/user), fall back to the workspace path. This allows installed commands like pab, npm, and agent-browser to execute even when the model picks a bad working directory. File read/write/edit path validation remains strict. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/base/src/tools/exec.test.ts | 13 +++++++++++++ apps/base/src/tools/exec.ts | 14 +++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/apps/base/src/tools/exec.test.ts b/apps/base/src/tools/exec.test.ts index 73fc2337..4fc2ba08 100644 --- a/apps/base/src/tools/exec.test.ts +++ b/apps/base/src/tools/exec.test.ts @@ -110,4 +110,17 @@ describe("exec tool", () => { }) expect(result.output).toBeTruthy() }) + + it("falls back to workspace when cwd is outside allowed directories", async () => { + const result = await exec({ + command: process.execPath, + args: ["-e", 'process.stdout.write("OK")'], + env: {}, + cwd: "/home/user", + stdout: true, + stderr: false, + timeout: 2000, + }) + expect(result.output).toBe("OK") + }) }) diff --git a/apps/base/src/tools/exec.ts b/apps/base/src/tools/exec.ts index a535bfa1..d9c91ecf 100644 --- a/apps/base/src/tools/exec.ts +++ b/apps/base/src/tools/exec.ts @@ -3,7 +3,7 @@ import { promisify } from "node:util" import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { getFilteredEnv, truncateText } from "@perstack/core" import { z } from "zod/v4" -import { validatePath } from "../lib/path.js" +import { validatePath, workspacePath } from "../lib/path.js" import { successToolResult } from "../lib/tool-result.js" const execFileAsync = promisify(execFile) @@ -20,10 +20,18 @@ type ExecInput = { stderr: boolean timeout?: number } +async function resolveCwd(cwd: string): Promise { + try { + return await validatePath(cwd) + } catch { + return workspacePath + } +} + export async function exec(input: ExecInput) { - const validatedCwd = await validatePath(input.cwd) + const resolvedCwd = await resolveCwd(input.cwd) const { stdout, stderr } = await execFileAsync(input.command, input.args, { - cwd: validatedCwd, + cwd: resolvedCwd, env: getFilteredEnv(input.env), timeout: input.timeout, maxBuffer: 10 * 1024 * 1024, From bc03d640ac02500854bf62fcd192ec15f998e3bd Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 25 Mar 2026 21:42:45 +0000 Subject: [PATCH 2/2] Chore: Add changeset for @perstack/base patch bump Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/exec-cwd-fallback.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/exec-cwd-fallback.md diff --git a/.changeset/exec-cwd-fallback.md b/.changeset/exec-cwd-fallback.md new file mode 100644 index 00000000..d167e8a7 --- /dev/null +++ b/.changeset/exec-cwd-fallback.md @@ -0,0 +1,5 @@ +--- +"@perstack/base": patch +--- + +Fallback to workspace when exec cwd is outside allowed directories