From 472e30e9bc97918f6d47fcda23be7c721469932c Mon Sep 17 00:00:00 2001 From: mawkone Date: Tue, 9 Jun 2026 16:25:51 -0700 Subject: [PATCH] chore(telemetry): replace fragile regex path normalization with bulletproof path.posix.resolve --- vibn-frontend/app/api/mcp/route.ts | 54 ++++++++++++++++++------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/vibn-frontend/app/api/mcp/route.ts b/vibn-frontend/app/api/mcp/route.ts index 421535c..56e93e4 100644 --- a/vibn-frontend/app/api/mcp/route.ts +++ b/vibn-frontend/app/api/mcp/route.ts @@ -4494,6 +4494,19 @@ async function toolShellExec( ); } + const rawCwd = typeof params.cwd === "string" ? params.cwd : "."; + const cwd = normalizeFsPath(rawCwd, project.slug); + if (cwd instanceof NextResponse) return cwd; + + let finalCommand = command; + // Soft command rewrite for obvious no-op bad pathing patterns + if (project.slug) { + finalCommand = finalCommand.replace( + new RegExp(`cd\\s+(?:/workspace/)?${project.slug}\\s*&&\\s*`), + "", + ); + } + // Lazy-provision: if there's no dev container yet, create one before // running the command. The first call is ~10-15s; subsequent calls // skip this branch entirely. @@ -4507,8 +4520,8 @@ async function toolShellExec( try { const result = await execInDevContainer({ projectId: project.id, - command, - cwd: typeof params.cwd === "string" ? params.cwd : "/workspace", + command: finalCommand, + cwd, timeoutMs: Number.isFinite(Number(params.timeoutMs)) ? Number(params.timeoutMs) : Number.isFinite(Number(params.timeout_ms)) @@ -4558,56 +4571,53 @@ function shq(s: string): string { } function normalizeFsPath( - p: string, + input: string, projectSlug?: string, ): string | NextResponse { - if (!p || typeof p !== "string") { + if (!input || typeof input !== "string") { return NextResponse.json( { error: 'Param "path" is required' }, { status: 400 }, ); } - // 1. Strip any leading /workspace/ - let normalized = p.replace(/^\/workspace\//, ""); + let p = input.trim(); - // 2. Strip any leading ./ - if (normalized.startsWith("./")) { - normalized = normalized.slice(2); + // Remove wrapper prefixes the model commonly hallucinates. + p = p.replace(/^\/workspace\/?/, ""); + p = p.replace(/^\.\//, ""); + + if (projectSlug && p.startsWith(`${projectSlug}/`)) { + p = p.slice(projectSlug.length + 1); } - // 3. Strip project slug prefix if the model hallucinates it - if (projectSlug && normalized.startsWith(`${projectSlug}/`)) { - normalized = normalized.slice(projectSlug.length + 1); - } - - const projectRoot = FS_ROOT; - const abs = `${projectRoot}/${normalized}`.replace(/\/+/g, "/"); - const norm = abs.replace(/\/[^/]+\/\.\.(?=\/|$)/g, "").replace(/\/+/g, "/"); + const resolved = path.posix.resolve("/workspace", p); // When projectSlug is set, REJECT paths outside the project root. + // (We use startWith("/workspace/") to ensure it doesn't match "/workspace-other") if (projectSlug) { - if (!norm.startsWith(projectRoot) && norm !== projectRoot) { + if (resolved !== "/workspace" && !resolved.startsWith("/workspace/")) { return NextResponse.json( { ok: false, - error: `PATH_OUTSIDE_PROJECT: path "${p}" resolves to "${norm}" which is outside the active project at "${projectRoot}".`, + error: `PATH_OUTSIDE_PROJECT: path "${input}" resolves to "${resolved}" which is outside the active project root at "/workspace".`, }, { status: 400 }, ); } } else { // Workspace-level fallback (legacy behaviour) - if (!norm.startsWith(FS_ROOT) && norm !== FS_ROOT) { + if (resolved !== "/workspace" && !resolved.startsWith("/workspace/")) { return NextResponse.json( { - error: `Path "${p}" is outside ${FS_ROOT}; use shell.exec for system paths.`, + error: `Path "${input}" is outside /workspace; use shell.exec for system paths.`, }, { status: 400 }, ); } } - return norm; + + return resolved; } async function runFsCmd(