fix(electron): preserve selected fuse inspection paths (#3104)

Preserve caller-selected POSIX symlink/parent paths during CLI anchoring and
Electron bundle traversal. Resolve terminal directory suffixes through checked
real directories, retaining their identities for before/after readback. Keep
the existing Windows CLI resolution, bounded scan, and output contracts.

Validation: six regressions reproduced on main 7ad75bdc8; 41 focused tests pass
with one Windows-only skip. Full static/docs checks and final main-sync fast
checks pass. Official Electron v44.7.1 release archives, extracted executable
hashes and independent fuse wires verify actual ELF/PE/Mach-O bytes. Real CLI
and stdio MCP reproduce main's wrong target and confirm corrected artifact
identity plus CLI/MCP parity; owned MCP servers exit. Equivalent direct-file
outputs remain 963-1,054 o200k tokens. Individual timing measurements and their
limits are recorded in the PR; no speedup is claimed. Native Windows/macOS REA
execution and target application execution are not claimed.

Co-authored-by: Yann Roberto <165009858+shoyann@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-14 12:49:17 +08:00
1 parent f4df6ab280
commit ec67aebed0
5 files changed
+304 -29

No files matched your search

+5
View File
@@ -20,6 +20,11 @@ requires an absolute filesystem path and preserves that exact selection:
}
```
On POSIX hosts, the CLI preserves path components so symlinks are followed
before parent (`..`) steps. Bundle selections may end in `.` or parent steps
through real directories; a terminal parent step through a directory symlink
is rejected. Windows CLI paths retain native drive and separator resolution.
Check this before `observe_javascript_runtime` with `electron-main`. When
`EnableNodeCliInspectArguments` is disabled or removed, Electron ignores
Inspector flags, and a later target listing can simply show no main-process
+12 -2
View File
@@ -1,4 +1,4 @@
import { resolve } from "node:path";
import { isAbsolute, resolve, sep } from "node:path";
import { Cli, z } from "incur";
@@ -55,7 +55,7 @@ const registerElectronFuseCommand = (
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectElectronFuses, async () => {
const result = await service.inspect(
{ path: resolve(args.path) },
{ path: absoluteElectronFusePath(args.path) },
{ signal },
);
return result.ok ? result.value : projectAnalysisError(result.error);
@@ -64,6 +64,16 @@ const registerElectronFuseCommand = (
});
};
/** Anchor relative input without changing which file symlink/.. selects. */
const absoluteElectronFusePath = (path: string): string => {
if (path === "") return path;
// Win32 resolves dot components before directory links, including drive-relative paths.
if (sep === "\\") return resolve(path);
if (isAbsolute(path)) return path;
const base = process.cwd();
return `${base}${base.endsWith(sep) ? "" : sep}${path}`;
};
const registerElectronActiveCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
+61 -15
View File
@@ -1,6 +1,6 @@
import type { BigIntStats } from "node:fs";
import { lstat, readlink } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, sep } from "node:path";
import { basename, dirname, isAbsolute, sep } from "node:path";
import { isPathWithinRoot } from "../domain/localPath.js";
@@ -56,9 +56,13 @@ export const resolveElectronFuseTarget = async (
frameworkRootOf(selectedPath) === null
? { path: selectedPath, metadata, nodes: [] }
: await resolveFrameworkSymlink(selectedPath, signal);
else if (metadata.isDirectory())
else if (metadata.isDirectory()) {
file = await resolveBundleBinary(selectedPath, signal);
else
file = {
...file,
nodes: [{ path: selectedPath, metadata }, ...file.nodes],
};
} else
throw new ElectronFusePathFailure(
"invalid_format",
`Expected a regular file or Electron app bundle: ${selectedPath}`,
@@ -94,20 +98,58 @@ const resolveBundleBinary = async (
selectedPath: string,
signal?: AbortSignal,
): Promise<ResolvedElectronFuseFile> => {
const name = basename(selectedPath);
const directory = await resolveDirectorySuffix(selectedPath, signal);
const name = basename(directory.path);
let file: ResolvedElectronFuseFile;
if (name === FRAMEWORK_NAME)
return resolveKnownBinary(selectedPath, selectedPath, signal);
if (name.endsWith(".app")) {
return resolveKnownBinary(
join(selectedPath, "Contents", "Frameworks", FRAMEWORK_NAME),
file = await resolveKnownBinary(directory.path, selectedPath, signal);
else if (name.endsWith(".app"))
file = await resolveKnownBinary(
appendPath(directory.path, "Contents", "Frameworks", FRAMEWORK_NAME),
selectedPath,
signal,
);
else
throw new ElectronFusePathFailure(
"invalid_format",
`Expected an Electron .app bundle, ${FRAMEWORK_NAME} directory, or executable file: ${selectedPath}`,
);
return { ...file, nodes: [...directory.nodes, ...file.nodes] };
};
/** Reduce directory suffixes only when doing so preserves filesystem selection. */
const resolveDirectorySuffix = async (
selectedPath: string,
signal?: AbortSignal,
): Promise<{
readonly path: string;
readonly nodes: readonly ElectronFusePathNode[];
}> => {
let path = selectedPath;
const pending: string[] = [];
const nodes: ElectronFusePathNode[] = [];
while (true) {
signal?.throwIfAborted();
const name = basename(path);
// lstat("link/") follows a link; inspect the named component itself.
if (name !== "") while (path.endsWith(sep)) path = path.slice(0, -1);
if (name === "." || name === "..") {
pending.push(name);
path = dirname(path);
continue;
}
const component = pending.pop();
if (component === undefined) return { path, nodes };
if (component === ".") continue;
const metadata = await lstat(path, { bigint: true });
if (!metadata.isDirectory() || metadata.isSymbolicLink())
throw new ElectronFusePathFailure(
"invalid_value",
`A bundle path ending in '..' requires a real directory before each parent segment: ${selectedPath}`,
);
nodes.push({ path, metadata });
path = dirname(path);
}
throw new ElectronFusePathFailure(
"invalid_format",
`Expected an Electron .app bundle, ${FRAMEWORK_NAME} directory, or executable file: ${selectedPath}`,
);
};
/** `root` is the framework directory for both an `.app` and a framework selection. */
@@ -154,7 +196,7 @@ const resolveInside = async (
last = "directory";
continue;
}
const absolute = join(root, ...resolved, segment);
const absolute = appendPath(root, ...resolved, segment);
if (!isPathWithinRoot(root, absolute)) return { kind: "escapes" };
let metadata;
try {
@@ -185,7 +227,7 @@ const resolveInside = async (
}
if (last !== "file" || fileMetadata === undefined)
return { kind: "not-file" };
const path = join(root, ...resolved);
const path = appendPath(root, ...resolved);
return isPathWithinRoot(root, path)
? { kind: "file", path, metadata: fileMetadata, nodes }
: { kind: "escapes" };
@@ -229,7 +271,7 @@ const requireRealDirectories = async (
let current = anchor;
const nodes: ElectronFusePathNode[] = [];
for (const segment of segments) {
current = join(current, segment);
current = appendPath(current, segment);
let metadata;
try {
metadata = await lstat(current, { bigint: true });
@@ -301,6 +343,10 @@ const relativeInside = (root: string, path: string): string | null => {
return value === "" ? null : value.split(sep).join("/");
};
/** Append known components without normalizing a caller-selected parent path. */
const appendPath = (parent: string, ...segments: readonly string[]): string =>
`${parent}${parent.endsWith(sep) ? "" : sep}${segments.join(sep)}`;
const isMissing = (cause: unknown): boolean =>
cause instanceof Error &&
"code" in cause &&
+17
View File
@@ -682,3 +682,20 @@ it("reports a checked version directory removed before open as changed", async (
readElectronFuseImage(located.file.path, { selection: located.file }),
).rejects.toBeInstanceOf(RegularFileChangedError);
});
it("retains the identity of a directory removed by a terminal parent segment", async () => {
const root = await createTestTempDirectory("rea-electron-parent-identity-");
const framework = join(root, "Electron Framework.framework");
const versions = join(framework, "Versions");
await mkdir(versions, { recursive: true });
await writeFile(
join(framework, "Electron Framework"),
thinFuseImage([0x31]).bytes,
);
const located = await resolveElectronFuseTarget(`${versions}/..`);
await rename(versions, `${versions}.saved`);
await mkdir(versions);
await expect(
readElectronFuseImage(located.file.path, { selection: located.file }),
).rejects.toBeInstanceOf(RegularFileChangedError);
});
+209 -12
View File
@@ -1,5 +1,5 @@
import { mkdir, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { mkdir, realpath, symlink, writeFile } from "node:fs/promises";
import { dirname, join, parse, sep } from "node:path";
import { createHash } from "node:crypto";
import { expect } from "vitest";
@@ -13,6 +13,142 @@ import {
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
// Windows resolves dot components before directory links in Win32 paths.
cliTest.skipIf(process.platform === "win32").for([
{ kind: "binary", mode: "absolute" },
{ kind: "binary", mode: "relative" },
{ kind: "bundle", mode: "absolute" },
{ kind: "bundle", mode: "relative" },
] as const)(
"preserves $mode $kind selections containing a symlink before a parent segment",
async ({ kind, mode }, { cli }) => {
const root = await createTestTempDirectory("rea-electron-fuses-cli-path-");
const framework = "Contents/Frameworks/Electron Framework.framework";
const directory =
kind === "binary"
? join(root, "Fixture.app", framework, "Versions")
: root;
const selectedTail =
kind === "binary" ? "A/Electron Framework" : "Fixture.app";
const binaryTail =
kind === "binary"
? selectedTail
: `Fixture.app/${framework}/Versions/A/Electron Framework`;
const binary = join(directory, "nested", binaryTail);
const sibling = join(directory, binaryTail);
const image = thinFuseImage([0x31, 0x30, 0x30, 0x31]);
const otherImage = thinFuseImage([0x30, 0x30, 0x30, 0x30]);
await mkdir(dirname(binary), { recursive: true });
await mkdir(dirname(sibling), { recursive: true });
await mkdir(join(directory, "nested", "B"), { recursive: true });
await writeFile(binary, image.bytes);
await writeFile(sibling, otherImage.bytes);
await symlink("nested/B", join(directory, "Current"));
const selected = `${directory}/Current/../${selectedTail}`;
expect(await realpath(selected)).toBe(
kind === "binary" ? binary : join(root, "nested", "Fixture.app"),
);
const result = await cli.run({
arguments: [
"inspect-electron-fuses",
mode === "absolute" ? selected : selected.slice(root.length + 1),
"--json",
],
cwd: root,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_electron_fuses").outputSchema.parse(
result.json,
);
const digest = createHash("sha256").update(image.bytes).digest("hex");
expect(evidence.normalized_result.artifact).toMatchObject({
selected_path: selected,
resolved_path:
kind === "binary"
? binary
: `${selected}/${framework}/Versions/A/Electron Framework`,
sha256: digest,
});
expect(
await realpath(evidence.normalized_result.artifact.resolved_path),
).toBe(binary);
expect(evidence.subject?.local_path).toBe(
evidence.normalized_result.artifact.resolved_path,
);
expect(evidence.subject?.digest.sha256).toBe(digest);
expect(evidence.normalized_result.runtime_implications).toEqual(
expect.arrayContaining([
expect.objectContaining({
subject: "electron_run_as_node",
outcome: "honored",
}),
expect.objectContaining({
subject: "node_cli_inspect_arguments",
outcome: "honored",
}),
]),
);
const control = await cli.run({
arguments: ["inspect-electron-fuses", sibling, "--json"],
});
expect(control.exitCode, control.stderr).toBe(0);
const siblingEvidence = toolContract(
"inspect_electron_fuses",
).outputSchema.parse(control.json);
expect(siblingEvidence.normalized_result.artifact.sha256).toBe(
createHash("sha256").update(otherImage.bytes).digest("hex"),
);
expect(siblingEvidence.normalized_result.runtime_implications).toEqual(
expect.arrayContaining([
expect.objectContaining({
subject: "electron_run_as_node",
outcome: "not_honored",
}),
expect.objectContaining({
subject: "node_cli_inspect_arguments",
outcome: "not_honored",
}),
]),
);
},
);
cliTest.skipIf(process.platform !== "win32")(
"retains Windows drive, root, and mixed-separator path resolution",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-fuses-win-path-");
const tail =
"Fixture.app/Contents\\Frameworks/Electron Framework.framework/Versions\\A/Electron Framework";
const binary = join(root, tail);
const image = thinFuseImage([0x31]);
await mkdir(dirname(binary), { recursive: true });
await writeFile(binary, image.bytes);
const volume = parse(root).root;
for (const input of [
binary.replaceAll("\\", "/"),
tail,
`${volume.slice(0, -1)}${tail}`,
binary.slice(volume.length - 1),
]) {
const result = await cli.run({
arguments: ["inspect-electron-fuses", input, "--json"],
cwd: root,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract(
"inspect_electron_fuses",
).outputSchema.parse(result.json);
expect(evidence.normalized_result.artifact).toMatchObject({
selected_path: binary,
resolved_path: binary,
sha256: createHash("sha256").update(image.bytes).digest("hex"),
});
}
},
);
cliTest(
"reads thin and universal Electron fuse wires through the public CLI",
async ({ cli }) => {
@@ -116,19 +252,80 @@ cliTest(
await mkdir(join(binary, ".."), { recursive: true });
await writeFile(binary, image.bytes);
await symlink("A", join(binary, "..", "..", "Current"));
const result = await cli.run({
arguments: ["inspect-electron-fuses", app, "--json"],
for (const { input, cwd, selected } of [
{ input: app, cwd: root, selected: app },
{ input: `${app}/./`, cwd: root, selected: `${app}/./` },
{ input: "Fixture.app/.", cwd: root, selected: `${app}/.` },
{ input: ".", cwd: app, selected: `${app}${sep}.` },
{
input: "..",
cwd: join(app, "Contents"),
selected: `${app}${sep}Contents${sep}..`,
},
{
input: ".././..",
cwd: join(app, "Contents", "Frameworks"),
selected: `${app}${sep}Contents${sep}Frameworks${sep}.././..`,
},
]) {
const result = await cli.run({
arguments: ["inspect-electron-fuses", input, "--json"],
cwd,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract(
"inspect_electron_fuses",
).outputSchema.parse(result.json);
expect(evidence.normalized_result.artifact).toMatchObject({
selected_path: process.platform === "win32" ? app : selected,
resolved_path: binary,
format: "mach-o",
});
expect(evidence.subject?.local_path).toBe(binary);
}
const empty = await cli.run({
arguments: ["inspect-electron-fuses", "", "--json"],
cwd: app,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_electron_fuses").outputSchema.parse(
result.json,
expect(empty.exitCode, empty.stderr).toBe(1);
expect(empty.json).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_electron_fuses" },
});
},
);
cliTest.skipIf(process.platform === "win32")(
"refuses a terminal bundle parent segment after a directory symlink",
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-electron-fuses-cli-parent-",
);
expect(evidence.normalized_result.artifact).toMatchObject({
selected_path: app,
resolved_path: binary,
format: "mach-o",
const app = join(root, "Fixture.app");
const binary = join(
app,
"Contents",
"Frameworks",
"Electron Framework.framework",
"Electron Framework",
);
await mkdir(dirname(binary), { recursive: true });
await writeFile(binary, thinFuseImage([0x31]).bytes);
await symlink("Contents", join(app, "linked"));
const result = await cli.run({
arguments: ["inspect-electron-fuses", `${app}/linked//..`, "--json"],
});
expect(result.exitCode, result.stderr).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: {
operation: "inspect_electron_fuses",
issues: [
expect.objectContaining({ path: ["path"], reason: "invalid_value" }),
],
},
});
expect(evidence.subject?.local_path).toBe(binary);
},
);