fix(engine): shape worktree-boundary rejections as proper tool results

wrapToolsWithBoundary returned a bare {ok:false,error} object when an
agent tried to read/write/bash outside the worktree. pi-coding-agent
wraps tool returns into a toolResult message whose content field it
expects to be an array of content blocks; a bare object leaves content
undefined, which later crashes downstream with "Cannot read properties
of undefined (reading 'filter')" — the failure we've been chasing on
FN-2479 and similar.

Return { content:[{type:"text",text:...}], isError:true, ok:false, error:... }
so pi records a valid toolResult block while existing callers that
inspect .ok / .error still work.

Diagnostic evidence: transcript tail for the failing task showed three
consecutive `read` toolResults with content=array(len=0) (normalized
from undefined by our earlier guard) immediately before the assistant
message with stopReason="error".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-24 14:00:34 -07:00
parent d2da242d21
commit 097902d922

View File

@@ -828,6 +828,22 @@ function isWorktreeAllowedPath(worktreePath: string, projectRoot: string, reques
* @param projectRoot - Absolute path to the project root (if applicable)
* @returns Wrapped tools with boundary validation
*/
/**
* Build a tool result payload in the shape pi-coding-agent / pi-ai expect
* (content as an array of typed blocks, isError=true) rather than a bare
* `{ok:false,error}` object. Returning the bare object leaves the toolResult
* message with `content: undefined`, which pi's downstream handling later
* crashes on with "Cannot read properties of undefined (reading 'filter')".
*/
function boundaryRejection(message: string) {
return {
content: [{ type: "text", text: message }],
isError: true,
ok: false,
error: message,
};
}
export function wrapToolsWithBoundary(
tools: ToolDefinition[],
worktreePath: string | null,
@@ -860,22 +876,20 @@ export function wrapToolsWithBoundary(
const pathArg = params.path as string | undefined;
if (pathArg && !isWorktreeAllowedPath(worktreePath, projectRoot, pathArg)) {
const relToProject = relative(projectRoot, pathArg);
return {
ok: false,
error: `Path "${relToProject}" is outside the worktree boundary. ` +
return boundaryRejection(
`Path "${relToProject}" is outside the worktree boundary. ` +
`Coding agents can only modify files inside the current worktree. ` +
`Exception: .fusion/memory/ (project root) and .fusion/tasks/*/attachments/* are permitted for reading.`,
};
);
}
// For bash, also check the working directory if specified
const cwdArg = params.cwd as string | undefined;
if (tool.name === "bash" && cwdArg && !isWorktreeAllowedPath(worktreePath, projectRoot, cwdArg)) {
return {
ok: false,
error: `Working directory is outside the worktree boundary. ` +
return boundaryRejection(
`Working directory is outside the worktree boundary. ` +
`Commands must run inside the worktree.`,
};
);
}
// Call the original tool implementation with all arguments passed through