From f652709cac54441f401e90b6a604997c9321d70d Mon Sep 17 00:00:00 2001 From: Shubham Ahuja <157806066+shubhamahuja9999@users.noreply.github.com> Date: Tue, 13 Oct 2026 22:35:14 +0530 Subject: [PATCH] fix(cli): keep file URLs out of operator-relative path resolution (#2882) CLI path normalization resolved every nonblank selected string against the working directory, so executable_path "file:///opt/electron" became an invented local path and failed with ENOENT for that location instead of the field's local-path requirement. Leave `file:` and `scheme://` references unchanged so the shared schema rejects them as non-local paths. One-letter prefixes stay paths, so Windows drive paths and relative names containing a colon resolve as before. Fixes #2577 Co-authored-by: Claude Opus 5.5 --- src/cliJsonInput.ts | 17 +++++++-- src/cliJsonPaths.test.ts | 34 +++++++++++++++++ .../applications/electronInvalidPaths.test.ts | 37 +++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/cliJsonInput.ts b/src/cliJsonInput.ts index 4dd38a90e..cf1b6ea72 100644 --- a/src/cliJsonInput.ts +++ b/src/cliJsonInput.ts @@ -65,12 +65,19 @@ const accessDeniedSystemCode = ( : undefined; }; +/** + * A `file:` URL or `scheme://` URL is not an operator-relative filesystem path. + * One-letter prefixes stay paths so Windows drive paths resolve normally. + */ +const URL_REFERENCE = /^(?:file:|[A-Za-z][A-Za-z0-9+.-]+:\/\/)/iu; + /** * Resolve named string fields of CLI-supplied JSON against the operator * working directory. Each entry is a key path from the document root; only - * fields holding nonblank strings are resolved, everything else passes through - * untouched so shared schemas can reject blank paths. Shared MCP contracts reject - * relative paths, so CLI workflows resolve operator-relative values before validation. + * fields holding nonblank, non-URL strings are resolved, everything else passes + * through untouched so shared schemas can reject blank paths and URLs. Shared MCP + * contracts reject relative paths, so CLI workflows resolve operator-relative + * values before validation. */ export const resolveCliJsonPaths = ( value: unknown, @@ -94,7 +101,9 @@ const resolveJsonPath = ( if (tail.length === 0) return [ key, - typeof entry === "string" && entry.trim().length > 0 + typeof entry === "string" && + entry.trim().length > 0 && + !URL_REFERENCE.test(entry) ? resolve(entry) : entry, ]; diff --git a/src/cliJsonPaths.test.ts b/src/cliJsonPaths.test.ts index 936bfacea..ed9e18642 100644 --- a/src/cliJsonPaths.test.ts +++ b/src/cliJsonPaths.test.ts @@ -56,4 +56,38 @@ describe("CLI JSON path normalization", () => { }); expect(input.browser.executable_path).toBe(" browser "); }); + + it.each([ + "file:///opt/electron", + "FILE:electron", + "https://example.test/app", + ])("keeps URL %j distinguishable from a local path", (value) => { + const input = { + executable_path: value, + application_path: resolve("/synthetic/app"), + }; + const normalized = resolveCliJsonPaths(input, paths); + expect(normalized).toEqual(input); + const parsed = electronActiveObservationInputSchema.safeParse(normalized); + expect(parsed.success).toBe(false); + if (parsed.success) throw new Error("Expected URL path rejection"); + expect(parsed.error.issues).toEqual([ + expect.objectContaining({ + path: ["executable_path"], + message: expect.stringContaining( + "must be absolute local filesystem paths", + ), + }), + ]); + }); + + it.each([ + "bin/electron", + "release:v1/electron", + "C:\\Electron\\electron.exe", + ])("keeps resolving local path %j", (value) => { + expect(resolveCliJsonPaths({ executable_path: value }, paths)).toEqual({ + executable_path: resolve(value), + }); + }); }); diff --git a/tests/acceptance/applications/electronInvalidPaths.test.ts b/tests/acceptance/applications/electronInvalidPaths.test.ts index d74c04b2b..e7d5935e2 100644 --- a/tests/acceptance/applications/electronInvalidPaths.test.ts +++ b/tests/acceptance/applications/electronInvalidPaths.test.ts @@ -67,3 +67,40 @@ cliTest( expect(session.isError, JSON.stringify(session)).not.toBe(true); }, ); + +cliTest( + "rejects a file URL path field without resolving an invented local path", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-electron-file-url-"); + const application = join(root, "main.js"); + await writeFile(application, "module.exports = {};\n"); + const response = await cli.run({ + arguments: [ + "capture-electron-scenario", + JSON.stringify({ + executable_path: "file:///opt/electron", + application_path: application, + }), + "--format", + "json", + ], + environment: { REA_LOG_LEVEL: "silent" }, + }); + expect(response.exitCode).toBe(1); + expect(response.json).toMatchObject({ + code: "invalid_request", + details: { + issues: [ + { + path: ["executable_path"], + message: expect.stringContaining( + "must be absolute local filesystem paths", + ), + }, + ], + }, + }); + expect(response.stdout).not.toContain("ENOENT"); + expect(response.stdout).not.toContain("file:/opt/electron"); + }, +);