fix(x): blank tool paths error instead of resolving to the process cwd

tools/paths.ts' lenient expandHome passed blank input through, and
both call sites feed the result to path.resolve — so a model calling
code_agent_run with cwd: '' spawned the coding agent in the Electron
process cwd, and resolveCodeProject('') could register that cwd (a
real, existing directory) as a code project.

The 'lenient variant' turns out to be filesystem/files.ts'
expandHomePath minus exactly this guard, so this deletes tools/
paths.ts and exports the filesystem one instead of keeping a third
variant. A blank path now surfaces as a clear tool error ('Path is
required') the model can react to. Also drops the stray
resolveCodeProject comment the extraction left in paths.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-10 16:47:15 +05:30
parent 05545ba283
commit 3b6c30fac9
4 changed files with 8 additions and 23 deletions

View file

@ -73,7 +73,10 @@ function isPathInside(parent: string, child: string): boolean {
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
}
function expandHomePath(inputPath: string): string {
// Exported for tool inputs that take user/model-supplied paths (code cwd,
// bg-task projectDir): the blank guard matters there because callers feed
// the result to path.resolve, and resolve('') silently becomes process.cwd().
export function expandHomePath(inputPath: string): string {
const trimmed = inputPath.trim();
if (!trimmed) {
throw new Error('Path is required');

View file

@ -10,7 +10,7 @@ import container from "../../../di/container.js";
import { BackgroundTaskSchema, TriggersSchema } from "@x/shared/dist/background-task.js";
import * as gitService from "../../../code-mode/git/service.js";
import type { ICodeProjectsRepo } from "../../../code-mode/projects/repo.js";
import { expandHome } from "../paths.js";
import { expandHomePath } from "../../../filesystem/files.js";
// Inputs for the bg-task builtin tools. Reuse the canonical schema field
// descriptions; only `triggers` gets a tighter contextual override (the
@ -45,7 +45,7 @@ export const PatchBackgroundTaskInput = BackgroundTaskSchema.pick({
export async function resolveCodeProject(dirPath: string): Promise<
{ ok: true; projectId: string; path: string; warning?: string } | { ok: false; error: string }
> {
const abs = path.resolve(expandHome(dirPath));
const abs = path.resolve(expandHomePath(dirPath));
const projectsRepo = container.resolve<ICodeProjectsRepo>('codeProjectsRepo');
let project: Awaited<ReturnType<ICodeProjectsRepo['add']>>;
try {

View file

@ -12,7 +12,7 @@ import { ICodeModeConfigRepo } from "../../../code-mode/repo.js";
import type { ApprovalPolicy, CodeRunEvent as CodeRunEventType } from "@x/shared/dist/code-mode.js";
import type { CodeRunFeed } from "../../../code-mode/feed.js";
import type { ToolContext } from "../exec-tool.js";
import { expandHome } from "../paths.js";
import { expandHomePath } from "../../../filesystem/files.js";
import { BuiltinToolsSchema } from "../types.js";
@ -61,7 +61,7 @@ export const codeAgentRunTools: z.infer<typeof BuiltinToolsSchema> = {
// cwd argument over the session's. Expand `~` and resolve to an absolute path:
// the engine is spawned with this as the child's cwd, and `child_process.spawn`
// does NO shell tilde expansion.
const effectiveCwd = path.resolve(expandHome(ctx.codeCwd ?? cwd));
const effectiveCwd = path.resolve(expandHomePath(ctx.codeCwd ?? cwd));
// Fail loudly if the directory is missing. Otherwise the spawn below fails with
// Node's misleading "spawn <command> ENOENT" (it blames the executable, not the
// bad cwd), which reads as "the coding engine isn't installed" — see the enriched

View file

@ -1,18 +0,0 @@
// Lenient ~-expansion for user-supplied paths in tool inputs. Distinct from
// filesystem/files.ts' private expandHomePath, which throws on empty input —
// tool entries want pass-through semantics for their own validation.
import * as path from "path";
import * as os from "os";
// Turn a user-supplied directory into a registered code project id. Reuses the
// same idempotent registry the Code-section picker writes to (add() validates the
// dir exists & is a directory, and dedupes by resolved path). Returns a soft
// `warning` — not an error — when the repo isn't yet worktree-ready, so the task
// still gets created and the copilot can tell the user what to fix.
export function expandHome(p: string): string {
const t = p.trim();
if (t === '~') return os.homedir();
if (t.startsWith('~/') || t.startsWith(`~${path.sep}`)) return path.join(os.homedir(), t.slice(2));
return t;
}