fix(electron): admit provider reports and bind selected inputs (#10041)

* fix(electron): admit runtime observations before publishing evidence

* fix(electron): retain passive observation source JSON
This commit is contained in:
morluto authored and GitHub committed 2026-11-11 17:15:54 +08:00
1 parent 550b7df262
commit ecf6ac9f8d
8 files changed
+489 -97

No files matched your search

@@ -265,3 +265,144 @@ it.skipIf(process.platform === "win32")(
});
},
);
it("does not enter the active provider after caller cancellation", async () => {
const { readable } = await fixture();
const { provider, capture } = recordingProvider();
const controller = new AbortController();
const reason = { requested: "stop before capture" };
controller.abort(reason);
const result = await captureElectronScenario(
provider,
electronActiveObservationInputSchema.parse({
executable_path: process.execPath,
application_path: join(readable, "main.js"),
}),
{ signal: controller.signal },
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError", cause: reason },
});
expect(capture).not.toHaveBeenCalled();
});
it.each([false, true])(
"validates completed active output before late cancellation (malformed: %s)",
async (malformed) => {
const { readable } = await fixture();
const controller = new AbortController();
const reason = { requested: "stop during capture finalization" };
const application = join(readable, "main.js");
const report = createElectronActiveObservationFixtureResult(application);
const result = await captureElectronScenario(
{
identity: () => ({ id: "test", name: "test", version: "1" }),
capture: async () => {
controller.abort(reason);
return ok(
malformed
? { ...report, limitations: [undefined] as never }
: report,
);
},
},
electronActiveObservationInputSchema.parse({
executable_path: process.execPath,
application_path: application,
}),
{ signal: controller.signal },
);
expect(result).toMatchObject({
ok: false,
error: malformed
? { _tag: "AnalysisOutputError", partialObservation: undefined }
: {
_tag: "AnalysisCancelledError",
cause: reason,
partialObservation: {
operation: "capture_electron_scenario",
normalized_result: report,
},
},
});
},
);
it.each(["executable_path", "application_path"] as const)(
"does not attest active output for another %s",
async (field) => {
const { readable } = await fixture();
const application = join(readable, "main.js");
const report = createElectronActiveObservationFixtureResult(application);
const changed = {
...report,
application: { ...report.application, [field]: join(readable, "other") },
};
const result = await captureElectronScenario(
{
identity: () => ({ id: "test", name: "test", version: "1" }),
capture: async () => ok(changed),
},
electronActiveObservationInputSchema.parse({
executable_path: process.execPath,
application_path: application,
}),
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError", partialObservation: changed },
});
},
);
it("does not invoke hidden provider getters while validating an active capture", async () => {
const { readable } = await fixture();
const application = join(readable, "main.js");
const report = createElectronActiveObservationFixtureResult(application);
let getterCalls = 0;
Object.defineProperty(report, "windows", {
enumerable: false,
get() {
getterCalls += 1;
throw new Error("Hidden windows getter must not run");
},
});
await expect(
captureElectronScenario(
{
identity: () => ({ id: "test", name: "test", version: "1" }),
capture: async () => ok(report),
},
electronActiveObservationInputSchema.parse({
executable_path: process.execPath,
application_path: application,
}),
),
).resolves.toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
});
expect(getterCalls).toBe(0);
});
it("refuses active result extensions under its strict completed contract", async () => {
const { readable } = await fixture();
const application = join(readable, "main.js");
const report = createElectronActiveObservationFixtureResult(application);
const result = await captureElectronScenario(
{
identity: () => ({ id: "test", name: "test", version: "1" }),
capture: async () =>
ok({ ...report, provider_metadata: { source_id: "unsupported" } }),
},
electronActiveObservationInputSchema.parse({
executable_path: process.execPath,
application_path: application,
}),
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
});
});
@@ -9,15 +9,22 @@ import type {
} from "./ElectronActiveObservationPort.js";
import {
AnalysisAccessDeniedError,
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisOutputError,
type AnalysisInputIssue,
} from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Evidence } from "../../domain/evidence.js";
import type { ElectronActiveObservationInput } from "../../domain/javascript/electronActiveObservation.js";
import {
electronActiveObservationResultSchema,
type ElectronActiveObservationInput,
} from "../../domain/javascript/electronActiveObservation.js";
import { err, ok, type Result } from "../../domain/result.js";
import { createElectronActiveEvidence } from "./ElectronActiveEvidence.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
const OPERATION = "capture_electron_scenario" as const;
const PROVIDER_ID = "rea-playwright-electron-active";
@@ -186,8 +193,16 @@ export const captureElectronScenario = async (
input: ElectronActiveObservationInput,
options: ExecutionOptions = {},
): Promise<Result<Evidence, AnalysisError>> => {
if (options.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const canonical = await canonicalizeInput(input);
if (!canonical.ok) return canonical;
if (options.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
if (provider === undefined)
return err(
new AnalysisCapabilityUnavailableError(
@@ -197,15 +212,45 @@ export const captureElectronScenario = async (
),
);
const captured = await provider.capture(canonical.value, options);
return captured.ok
? {
ok: true,
value: createElectronActiveEvidence(
input,
captured.value,
provider.identity(),
canonical.value,
),
}
: captured;
if (!captured.ok) return captured;
const json = jsonValueSchema.safeParse(captured.value);
if (!json.success)
return err(
new AnalysisOutputError(
OPERATION,
"Electron provider returned non-JSON active capture data.",
{ cause: json.error },
),
);
const report = electronActiveObservationResultSchema.safeParse(json.data);
if (!report.success)
return err(
new AnalysisOutputError(
OPERATION,
"Electron provider returned a malformed active capture.",
{ cause: report.error },
),
);
if (
report.data.application.executable_path !==
canonical.value.executable_path ||
report.data.application.application_path !==
canonical.value.application_path
)
return err(
new AnalysisOutputError(
OPERATION,
"Electron provider changed the selected executable or application.",
{ partialObservation: report.data },
),
);
return completeProviderEvidence(
createElectronActiveEvidence(
input,
report.data,
provider.identity(),
canonical.value,
),
options.signal,
);
};
+14 -3
View File
@@ -1,3 +1,5 @@
import { isDeepStrictEqual } from "node:util";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { ProviderIdentity } from "../AnalysisProvider.js";
import {
createEvidence,
@@ -11,16 +13,23 @@ import type {
ListElectronTargetsInput,
} from "../../domain/javascript/electronObservation.js";
/** Own JSON source and the facts admitted by the named Electron output contract. */
export interface ElectronEvidenceReport {
readonly source: JsonValue;
readonly facts: ElectronTargetList | ElectronPageInspection;
}
type ElectronOperation = "list_electron_targets" | "inspect_electron_page";
/** Create Evidence for one Electron observation. */
export const createElectronEvidence = (
operation: ElectronOperation,
input: ListElectronTargetsInput | InspectElectronPageInput,
result: ElectronTargetList | ElectronPageInspection,
report: ElectronEvidenceReport,
provider: ProviderIdentity,
): Evidence =>
createEvidence(undefined, provider, {
): Evidence => {
const result = report.facts;
return createEvidence(undefined, provider, {
predicateType:
operation === "list_electron_targets"
? "rea.electron-target-list"
@@ -28,6 +37,7 @@ export const createElectronEvidence = (
operation,
parameters: parameters(input),
result,
rawResult: isDeepStrictEqual(report.source, result) ? null : report.source,
confidence: "observed",
authority: "external-service",
environment: {
@@ -41,6 +51,7 @@ export const createElectronEvidence = (
"Browser host platform and architecture were not observed.",
],
});
};
const parameters = (
input: ListElectronTargetsInput | InspectElectronPageInput,
@@ -274,3 +274,132 @@ it.each([
);
},
);
it.each([
{ ...inspection, target: { ...inspection.target, target_id: "other-page" } },
{
...inspection,
capture_window: { ...inspection.capture_window, observation_ms: 1 },
},
])(
"refuses to attest a different selected page or capture window",
async (report) => {
const provider = pageProvider(
new AnalysisCancelledError("inspect_electron_page"),
);
const result = await inspectElectronPage(
{ ...provider, inspectPage: async () => ok(report) },
input,
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError", partialObservation: report },
});
const failure = new AnalysisCancelledError("inspect_electron_page", {
partialObservation: {
kind: "electron-page-observation",
inspection: report,
cleanup: "transport-closed",
},
});
const partial = failure.partialObservation;
const cancelled = await inspectElectronPage(pageProvider(failure), input);
expect(cancelled).toMatchObject({ ok: false, error: failure });
expect(failure.partialObservation).toEqual(partial);
expect(failure.secondaryFailures).toMatchObject([
{ _tag: "AnalysisOutputError" },
]);
},
);
it.each(["completed", "partial"] as const)(
"does not invoke hidden provider getters in a %s page report",
async (phase) => {
const report = { ...inspection };
let getterCalls = 0;
Object.defineProperty(report, "target", {
enumerable: false,
get() {
getterCalls += 1;
throw new Error("Hidden target getter must not run");
},
});
const failure = new AnalysisCancelledError("inspect_electron_page", {
partialObservation: {
kind: "electron-page-observation",
inspection: report,
cleanup: "transport-closed",
},
});
const provider = pageProvider(failure);
const result = inspectElectronPage(
phase === "partial"
? provider
: { ...provider, inspectPage: async () => ok(report) },
input,
);
await expect(result).resolves.toMatchObject({
ok: false,
error:
phase === "partial"
? { _tag: "AnalysisCancelledError" }
: { _tag: "AnalysisOutputError" },
});
expect(getterCalls).toBe(0);
},
);
it.each(["extension", "omission"])(
"retains passive Electron source %s before named contract normalization",
async (kind) => {
const { workers: _workers, ...withoutWorkers } = inspection;
const source =
kind === "extension"
? { ...inspection, provider_metadata: { source_id: "own-source" } }
: withoutWorkers;
const provider: ElectronObservationPort = {
identity: () => identity,
listTargets: async () =>
err(new AnalysisCancelledError("list_electron_targets")),
inspectPage: async () => ok(source as typeof inspection),
};
const result = await inspectElectronPage(provider, input);
if (!result.ok) throw result.error;
expect(result.value.raw_result).toEqual(source);
expect(result.value.normalized_result).toHaveProperty("workers", []);
expect(result.value.normalized_result).not.toHaveProperty(
"provider_metadata",
);
},
);
it("retains passive source metadata when promoting a completed failure inspection", async () => {
const source = {
...inspection,
provider_metadata: { source_id: "partial-source" },
};
const failure = new AnalysisCancelledError("inspect_electron_page", {
partialObservation: {
kind: "electron-page-observation",
inspection: source,
cleanup: "transport-closed",
},
});
const result = await inspectElectronPage(pageProvider(failure), input);
if (result.ok) throw new Error("expected retained failure");
expect(parseEvidence(result.error.partialObservation).raw_result).toEqual(
source,
);
});
it("does not duplicate unchanged passive Electron source JSON", async () => {
const result = await inspectElectronPage(
{
...pageProvider(new AnalysisCancelledError("inspect_electron_page")),
inspectPage: async () => ok(inspection),
},
input,
);
if (!result.ok) throw result.error;
expect(result.value.raw_result).toBeNull();
});
@@ -14,6 +14,7 @@ import {
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import { createElectronEvidence } from "./ElectronEvidence.js";
import { jsonObjectSchema, jsonValueSchema } from "../../domain/jsonValue.js";
import {
electronPageInspectionSchema,
electronTargetListSchema,
@@ -39,7 +40,16 @@ export const listElectronTargets = async (
);
const result = await ready.value.listTargets(input, options);
if (!result.ok) return result;
const report = electronTargetListSchema.safeParse(result.value);
const json = jsonValueSchema.safeParse(result.value);
if (!json.success)
return err(
new AnalysisOutputError(
"list_electron_targets",
"Electron provider returned non-JSON target data.",
{ cause: json.error },
),
);
const report = electronTargetListSchema.safeParse(json.data);
if (!report.success)
return err(
new AnalysisOutputError(
@@ -52,7 +62,7 @@ export const listElectronTargets = async (
createElectronEvidence(
"list_electron_targets",
input,
report.data,
{ source: json.data, facts: report.data },
ready.value.identity(),
),
options.signal,
@@ -75,19 +85,28 @@ export const inspectElectronPage = async (
);
const result = await ready.value.inspectPage(input, options);
if (!result.ok) {
const partial = result.error.partialObservation;
if (
partial !== undefined &&
"kind" in partial &&
partial.kind === "electron-page-observation"
) {
const report = electronPageInspectionSchema.safeParse(partial.inspection);
const partial = jsonObjectSchema.safeParse(result.error.partialObservation);
if (partial.success && partial.data.kind === "electron-page-observation") {
const report = electronPageInspectionSchema.safeParse(
partial.data.inspection,
);
if (report.success) {
const mismatch = pageSelectionFailure(input, report.data);
if (mismatch !== undefined) {
result.error.retainSecondaryFailure(mismatch);
return err(
keepIncompleteObservation(
result.error,
"inspect_electron_page",
"Electron provider retained a non-JSON incomplete observation.",
),
);
}
const inspection = {
...report.data,
limitations: [
...report.data.limitations,
partial.cleanup === "transport-closed"
partial.data.cleanup === "transport-closed"
? "REA closed its CDP transport; this does not imply target shutdown."
: "CDP transport cleanup remains incomplete.",
],
@@ -95,7 +114,10 @@ export const inspectElectronPage = async (
result.error.partialObservation = createElectronEvidence(
"inspect_electron_page",
input,
inspection,
{
source: jsonValueSchema.parse(partial.data.inspection),
facts: inspection,
},
ready.value.identity(),
);
return result;
@@ -109,7 +131,16 @@ export const inspectElectronPage = async (
),
);
}
const report = electronPageInspectionSchema.safeParse(result.value);
const json = jsonValueSchema.safeParse(result.value);
if (!json.success)
return err(
new AnalysisOutputError(
"inspect_electron_page",
"Electron provider returned non-JSON page data.",
{ cause: json.error },
),
);
const report = electronPageInspectionSchema.safeParse(json.data);
if (!report.success)
return err(
new AnalysisOutputError(
@@ -118,17 +149,33 @@ export const inspectElectronPage = async (
{ cause: report.error },
),
);
const mismatch = pageSelectionFailure(input, report.data);
if (mismatch !== undefined)
return err(mismatch.retainPartialObservation(report.data));
return completeProviderEvidence(
createElectronEvidence(
"inspect_electron_page",
input,
report.data,
{ source: json.data, facts: report.data },
ready.value.identity(),
),
options.signal,
);
};
/** Retained facts must describe the page and window the caller selected. */
const pageSelectionFailure = (
input: InspectElectronPageInput,
report: ElectronPageInspection,
): AnalysisOutputError | undefined =>
report.target.target_id !== input.target_id ||
report.capture_window.observation_ms !== input.observation_ms
? new AnalysisOutputError(
"inspect_electron_page",
"Electron provider changed the selected page or observation window.",
)
: undefined;
const requireProvider = (
provider: ElectronObservationPort | undefined,
operation: "list_electron_targets" | "inspect_electron_page",
+10 -2
View File
@@ -436,7 +436,10 @@ import type {
import type { FileOffsetPartialObservation } from "./native/fileOffsetPartialObservation.js";
import type { NativeCallPartialObservation } from "./native/nativeCallPartialObservation.js";
import type { HopperCallerReconciliationPartial } from "./native/hopperCallerReconciliation.js";
import type { ElectronActivePartialObservation } from "./javascript/electronActiveObservation.js";
import type {
ElectronActiveObservationResult,
ElectronActivePartialObservation,
} from "./javascript/electronActiveObservation.js";
import type { JavaScriptRuntimeObservation } from "./javascript/javascriptRuntimeObservation.js";
import type {
ArtifactInventoryPartialObservation,
@@ -446,7 +449,10 @@ import type { WebScriptExportPartialObservation } from "./webScriptExport.js";
import type { JavaScriptRecoveryPartialObservation } from "./javascript/javascriptRecovery.js";
import type { BrowserScenarioPartialObservation } from "./browserScenarioCapture.js";
import type { Evidence } from "./evidence.js";
import type { ElectronPagePartialObservation } from "./javascript/electronObservation.js";
import type {
ElectronPageInspection,
ElectronPagePartialObservation,
} from "./javascript/electronObservation.js";
import type { AndroidPartialObservation } from "./android/androidPartialObservation.js";
import type { FirmwarePartialObservation } from "./firmware/firmwareAnalysis.js";
import type {
@@ -516,6 +522,7 @@ export type AnalysisPartialObservation =
| HopperCallerReconciliationPartial
| FileOffsetPartialObservation
| ElectronActivePartialObservation
| ElectronActiveObservationResult
| BrowserScenarioPartialObservation
| WebPageInspection
| WebBundleAnalysis
@@ -541,6 +548,7 @@ export type AnalysisPartialObservation =
| InspectSignature
| InspectMacho
| ElectronPagePartialObservation
| ElectronPageInspection
| AdbActivityStartPartialObservation
| AdbAcquiredFilePartialObservation
| AdbPackagePullPartialObservation
+13 -5
View File
@@ -118,7 +118,7 @@ it("records completed Electron facts after final detach as server Evidence", asy
file_path: "/tmp/index.html",
attached: true,
},
...emptyElectronPageCoverage(1),
...emptyElectronPageCoverage(0),
};
const provider: ElectronObservationPort = {
identity: () => ({
@@ -849,10 +849,18 @@ it.skipIf(process.platform === "win32")(
);
await symlink("application/child", join(root, "link"));
const capture = vi.fn<ElectronActiveObservationPort["capture"]>(
async (input) =>
resultOk(
createElectronActiveObservationFixtureResult(input.application_path),
),
async (input) => {
const report = createElectronActiveObservationFixtureResult(
input.application_path,
);
return resultOk({
...report,
application: {
...report.application,
executable_path: input.executable_path,
},
});
},
);
const client = await activePathAdmissionClient(capture);
const selected = {
+65 -62
View File
@@ -1,3 +1,5 @@
import { jsonValueSchema } from "../../src/domain/jsonValue.js";
import type { ElectronPageInspection } from "../../src/domain/javascript/electronObservation.js";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -57,71 +59,72 @@ export const electronRuntimeEvidence = (
observation_ms: 100,
include_script_sources: sourceIncluded,
});
const report = {
browser: {
product: "Electron/fixture",
protocol_version: "1.3",
revision: "fixture",
user_agent: "Electron fixture",
js_version: "13",
},
target: {
target_id: targetId,
type: "page",
title: "Fixture",
file_path: join(root, "index.html"),
attached: false,
},
capture_window: {
started_at: "2026-07-15T00:00:00.000Z",
ended_at: "2026-07-15T00:00:00.100Z",
observation_ms: 100,
},
completeness: captureCompleteness(options),
frames: [
{
frame_id: "frame-main",
parent_frame_id: null,
file_path: join(root, "index.html"),
},
],
dom: { total_nodes: 0, nodes: [] },
scripts: {
total: 1,
items: [
{
script_key: `electron_script_${"1".repeat(64)}`,
frame_id: "frame-main",
file_path: join(root, scriptFile),
cdp_hash: "fixture",
length: Buffer.byteLength(source),
is_module: true,
language: "JavaScript",
source: {
included: true as const,
artifact: createWebTextArtifact(source, "text/javascript"),
},
},
],
},
resources: [],
workers: includeWorker
? [
{
target_id: "worker-main",
type: "worker",
file_path: join(root, "worker.js"),
attached: false,
opener_target_id: targetId,
parent_frame_id: "frame-main",
},
]
: [],
limitations: ["Synthetic passive capture fixture."],
} satisfies ElectronPageInspection;
return createElectronEvidence(
"inspect_electron_page",
input,
{
browser: {
product: "Electron/fixture",
protocol_version: "1.3",
revision: "fixture",
user_agent: "Electron fixture",
js_version: "13",
},
target: {
target_id: targetId,
type: "page",
title: "Fixture",
file_path: join(root, "index.html"),
attached: false,
},
capture_window: {
started_at: "2026-07-15T00:00:00.000Z",
ended_at: "2026-07-15T00:00:00.100Z",
observation_ms: 100,
},
completeness: captureCompleteness(options),
frames: [
{
frame_id: "frame-main",
parent_frame_id: null,
file_path: join(root, "index.html"),
},
],
dom: { total_nodes: 0, nodes: [] },
scripts: {
total: 1,
items: [
{
script_key: `electron_script_${"1".repeat(64)}`,
frame_id: "frame-main",
file_path: join(root, scriptFile),
cdp_hash: "fixture",
length: Buffer.byteLength(source),
is_module: true,
language: "JavaScript",
source: {
included: true as const,
artifact: createWebTextArtifact(source, "text/javascript"),
},
},
],
},
resources: [],
workers: includeWorker
? [
{
target_id: "worker-main",
type: "worker",
file_path: join(root, "worker.js"),
attached: false,
opener_target_id: targetId,
parent_frame_id: "frame-main",
},
]
: [],
limitations: ["Synthetic passive capture fixture."],
},
{ source: jsonValueSchema.parse(report), facts: report },
{
id: "rea-cdp-electron",
name: "REA Electron file-page CDP observation provider",