mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 11:25:39 +00:00
Own CDP transports from admission through observed close, retain failed owners for retry, and keep active peers separate from pending cleanup. Preserve primary errors and completed facts through final cancellation. Forward passive Electron CLI cancellation and dispose browser, runtime and scenario owners from CLI and MCP shutdown. Refs #4255, #4256, #4257
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import { isAbsolute, resolve, sep } from "node:path";
|
|
|
|
import { Cli, z } from "incur";
|
|
|
|
import { logCliCommand } from "../cliLogging.js";
|
|
import {
|
|
inspectElectronPageInputSchema,
|
|
listElectronTargetsInputSchema,
|
|
} from "../domain/javascript/electronObservation.js";
|
|
import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js";
|
|
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
import type { Logger } from "pino";
|
|
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
|
import { createElectronFuseService } from "../composition/electronFuses.js";
|
|
import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js";
|
|
import {
|
|
electronPageInspectionOptions,
|
|
javascriptApplicationOptions,
|
|
} from "../cliObservationOptions.js";
|
|
import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnalysis.js";
|
|
import { withCommandCancellation } from "./commandCancellation.js";
|
|
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
|
|
|
/** Register CLI equivalents of the Electron MCP tools. */
|
|
export const registerElectronCommands = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
): void => {
|
|
registerElectronObservationCommands(cli, logger);
|
|
registerElectronFuseCommand(cli, logger);
|
|
registerElectronActiveCommand(cli, logger, environment);
|
|
registerJavaScriptApplicationCommand(cli, logger);
|
|
registerJavaScriptRuntimeReconciliationCommand(cli, logger);
|
|
};
|
|
|
|
const registerElectronFuseCommand = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
const service = createElectronFuseService();
|
|
cli.command(CLI_COMMANDS.inspectElectronFuses, {
|
|
description:
|
|
"Inspect packaged Electron fuse bytes without launching the application",
|
|
args: z.object({
|
|
path: z
|
|
.string()
|
|
.describe(
|
|
"Local Electron .app, Electron Framework binary, PE executable, or ELF executable",
|
|
),
|
|
}),
|
|
run: ({ args }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.inspectElectronFuses, async () => {
|
|
const result = await service.inspect(
|
|
{ path: absoluteElectronFusePath(args.path) },
|
|
{ signal },
|
|
);
|
|
return result.ok ? result.value : projectAnalysisError(result.error);
|
|
}),
|
|
),
|
|
});
|
|
};
|
|
|
|
/** 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,
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.captureElectronScenario, {
|
|
description: "Run one owned Electron scenario with cancellable actions",
|
|
args: z.object({
|
|
inputJson: z
|
|
.string()
|
|
.describe(
|
|
"Inline JSON or a JSON file matching capture_electron_scenario",
|
|
),
|
|
}),
|
|
run: ({ args }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.captureElectronScenario,
|
|
async () => {
|
|
const input = await parseCliJsonInput(
|
|
args.inputJson,
|
|
"capture_electron_scenario",
|
|
);
|
|
if (!input.ok) return input.error;
|
|
const parsed = electronActiveObservationInputSchema.safeParse(
|
|
resolveCliJsonPaths(input.value, [
|
|
["executable_path"],
|
|
["application_path"],
|
|
["application_root"],
|
|
]),
|
|
);
|
|
if (!parsed.success)
|
|
return inputError(
|
|
"capture_electron_scenario",
|
|
parsed.error.issues,
|
|
input.value,
|
|
);
|
|
const { captureElectronScenario } =
|
|
await import("../application/javascript/ElectronActiveObservationService.js");
|
|
const { createElectronScenarioProvider } =
|
|
await import("../composition/electronScenario.js");
|
|
const result = await captureElectronScenario(
|
|
createElectronScenarioProvider(environment),
|
|
parsed.data,
|
|
{ signal },
|
|
);
|
|
return result.ok ? result.value : cliError(result.error);
|
|
},
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerJavaScriptRuntimeReconciliationCommand = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.reconcileJavaScriptRuntime, {
|
|
description:
|
|
"Reconcile static application and passive runtime Evidence JSON",
|
|
args: z.object({
|
|
inputJson: z
|
|
.string()
|
|
.describe(
|
|
"Inline JSON or a JSON file path matching reconcile_javascript_runtime",
|
|
),
|
|
}),
|
|
run: ({ args }) =>
|
|
logCliCommand(
|
|
logger,
|
|
CLI_COMMANDS.reconcileJavaScriptRuntime,
|
|
async () => {
|
|
const input = await parseCliJsonInput(
|
|
args.inputJson,
|
|
"reconcile_javascript_runtime",
|
|
);
|
|
if (!input.ok) return input.error;
|
|
const { reconcileJavaScriptRuntimeEvidence } =
|
|
await import("../application/javascript/JavaScriptRuntimeReconciliationService.js");
|
|
const result = reconcileJavaScriptRuntimeEvidence(input.value);
|
|
return result.ok ? result.value : cliError(result.error);
|
|
},
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerElectronObservationCommands = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
registerElectronTargetList(cli, logger);
|
|
registerElectronPageInspection(cli, logger);
|
|
};
|
|
|
|
const registerElectronTargetList = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.listElectronTargets, {
|
|
description:
|
|
"List local file pages exposed by the selected Electron CDP endpoint",
|
|
args: z.object({
|
|
endpoint: z.string().describe("Literal-loopback Electron CDP endpoint"),
|
|
}),
|
|
run: ({ args }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, "list-electron-targets", async () => {
|
|
const context = await electronObservationContext();
|
|
const parsed = listElectronTargetsInputSchema.safeParse({
|
|
cdp_endpoint: args.endpoint,
|
|
});
|
|
if (!parsed.success) return inputError("list_electron_targets");
|
|
const { listElectronTargets } =
|
|
await import("../application/javascript/ElectronObservationService.js");
|
|
return await finishAnalysisCommand(
|
|
() =>
|
|
listElectronTargets(context.provider, parsed.data, { signal }),
|
|
{
|
|
close: async () => {
|
|
await context.provider.close?.();
|
|
},
|
|
},
|
|
"rea-cdp-electron",
|
|
"list_electron_targets",
|
|
undefined,
|
|
signal,
|
|
);
|
|
}),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerElectronPageInspection = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.inspectElectronPage, {
|
|
description: "Passively inspect one Electron file page",
|
|
args: z.object({
|
|
endpoint: z.string().describe("Literal-loopback Electron CDP endpoint"),
|
|
targetId: z.string().describe("Target ID from list-electron-targets"),
|
|
}),
|
|
options: electronPageInspectionOptions,
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, "inspect-electron-page", async () => {
|
|
const context = await electronObservationContext();
|
|
const parsed = inspectElectronPageInputSchema.safeParse({
|
|
cdp_endpoint: args.endpoint,
|
|
target_id: args.targetId,
|
|
observation_ms: options.observationMs,
|
|
include_script_sources: options.includeScriptSources,
|
|
});
|
|
if (!parsed.success) return inputError("inspect_electron_page");
|
|
const { inspectElectronPage } =
|
|
await import("../application/javascript/ElectronObservationService.js");
|
|
return await finishAnalysisCommand(
|
|
() =>
|
|
inspectElectronPage(context.provider, parsed.data, { signal }),
|
|
{
|
|
close: async () => {
|
|
await context.provider.close?.();
|
|
},
|
|
},
|
|
"rea-cdp-electron",
|
|
"inspect_electron_page",
|
|
undefined,
|
|
signal,
|
|
);
|
|
}),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerJavaScriptApplicationCommand = (
|
|
cli: ReturnType<typeof Cli.create>,
|
|
logger: Logger,
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.analyzeJavaScriptApplication, {
|
|
description:
|
|
"Statically reconstruct a local JavaScript/Electron application",
|
|
args: z.object({
|
|
path: z.string().describe("ASAR or extracted application path"),
|
|
}),
|
|
options: javascriptApplicationOptions,
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.analyzeJavaScriptApplication, () =>
|
|
runCliJavaScriptApplicationAnalysis(
|
|
{
|
|
input_path: args.path,
|
|
format: options.artifactFormat,
|
|
integrity_policy: options.integrityPolicy,
|
|
...(options.maxSemanticNodes === undefined
|
|
? {}
|
|
: { max_semantic_nodes: options.maxSemanticNodes }),
|
|
...(options.maxSemanticNodesPerFile === undefined
|
|
? {}
|
|
: {
|
|
max_semantic_nodes_per_file:
|
|
options.maxSemanticNodesPerFile,
|
|
}),
|
|
...(options.sourcePath === undefined
|
|
? {}
|
|
: { source_paths: options.sourcePath }),
|
|
...(options.maxHeapMb === undefined
|
|
? {}
|
|
: { max_heap_mb: options.maxHeapMb }),
|
|
...(options.analysisTimeoutMs === undefined
|
|
? {}
|
|
: { analysis_timeout_ms: options.analysisTimeoutMs }),
|
|
},
|
|
signal,
|
|
),
|
|
),
|
|
),
|
|
});
|
|
};
|
|
|
|
const electronObservationContext = async () => {
|
|
const { createElectronObservationProvider } =
|
|
await import("../composition/electronObservation.js");
|
|
return { provider: createElectronObservationProvider() };
|
|
};
|
|
|
|
const inputError = (
|
|
operation: string,
|
|
issues?: Parameters<typeof projectInputIssues>[0],
|
|
input?: unknown,
|
|
): JsonValue =>
|
|
cliError(
|
|
new AnalysisInputError(
|
|
operation,
|
|
undefined,
|
|
issues === undefined ? [] : projectInputIssues(issues, input),
|
|
),
|
|
);
|
|
|
|
const cliError = (
|
|
error: Parameters<typeof projectAnalysisError>[0],
|
|
): JsonValue => ({
|
|
error: "Electron analysis failed",
|
|
...projectAnalysisError(error),
|
|
});
|