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 <noreply@anthropic.com>
This commit is contained in:
parent
c8ef1f916c
commit
f652709cac
|
|
@ -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,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue