mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
fix(mcp): isolate optional observation adapter failures (#838)
This commit is contained in:
+3
-1
@@ -51,7 +51,8 @@
|
||||
"**/hopper/**",
|
||||
"**/ida/**",
|
||||
"**/android/**",
|
||||
"**/firmware/**"
|
||||
"**/firmware/**",
|
||||
"**/browser/*Provider.*"
|
||||
],
|
||||
"message": "Provider imports belong behind typed ports or in production composition."
|
||||
}
|
||||
@@ -111,6 +112,7 @@
|
||||
"**/ida/**",
|
||||
"**/android/**",
|
||||
"**/firmware/**",
|
||||
"**/browser/*Provider.*",
|
||||
"**/composition/**"
|
||||
],
|
||||
"message": "Application workflows use typed ports; production construction belongs in composition."
|
||||
|
||||
@@ -93,3 +93,27 @@ providers. Composition may import their implementations. Binary composition is
|
||||
still at its existing entrypoint while its direct-analysis callers are investigated.
|
||||
This concrete pilot does not introduce a shared provider interface or module
|
||||
manifest.
|
||||
|
||||
## Independent optional observation adapters
|
||||
|
||||
Browser observation, browser scenarios, Electron observation, Electron scenarios
|
||||
and V8 Inspector observation now have separate factories in `src/composition/`.
|
||||
CLI commands reuse them; MCP startup dynamically imports each factory independently.
|
||||
`OptionalObservationProviders` describes these five existing typed ports only. It
|
||||
is provisional startup wiring, not an extensible plugin manifest or a universal
|
||||
provider interface.
|
||||
|
||||
A failed import or constructor retains the successful peers. The complete tool
|
||||
catalog remains advertised. Session availability reports the failed adapter and
|
||||
its actual reason, and affected handlers return the existing capability-unavailable
|
||||
error. Explicit loading failures take precedence over configured availability.
|
||||
Passive Electron and active Electron failures are independent; static JavaScript
|
||||
analysis and runtime reconciliation remain usable without either adapter. CLI
|
||||
Electron commands also load only their selected runtime factory at execution.
|
||||
|
||||
Source identities are available without importing the optional implementations.
|
||||
Registration acquires no engines, probes no endpoints, changes no permissions and
|
||||
installs nothing. Core transport failures and shutdown still use their existing
|
||||
lifecycle. Fault-injection coverage uses the production loading/startup path and
|
||||
actual MCP SDK calls; recording ports establish composition behavior, while the
|
||||
real Inspector lane separately establishes runtime behavior.
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import type {
|
||||
OptionalProviderLoadFailures,
|
||||
OptionalProviderLoadFailure,
|
||||
} from "./OptionalObservationProviders.js";
|
||||
|
||||
import {
|
||||
providerCapability,
|
||||
@@ -28,6 +32,7 @@ type ToolAvailabilityReason = "available" | ToolUnavailabilityReason;
|
||||
type ProviderDescriptor = ProviderCapability;
|
||||
export type AvailabilityPolicy = {
|
||||
readonly processCaptureEnabled: boolean;
|
||||
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
|
||||
readonly firmwareInspectionEnabled?: boolean;
|
||||
readonly firmwareExtractionEnabled?: boolean;
|
||||
readonly androidAnalysisEnabled?: boolean;
|
||||
@@ -186,6 +191,12 @@ const availabilityFor = (context: AvailabilityContext): Availability => {
|
||||
return navigationContextAvailability(context.descriptors);
|
||||
const javascriptApplication = javascriptApplicationAvailability(context);
|
||||
if (javascriptApplication !== null) return javascriptApplication;
|
||||
const failure = optionalProviderFailureFor(context);
|
||||
if (failure !== undefined)
|
||||
return {
|
||||
reason: "provider_unavailable",
|
||||
remediation: `Adapter ${failure.providerId} could not load: ${failure.reason}. Repair the adapter installation and restart REA; other capabilities remain available.`,
|
||||
};
|
||||
const workflowAvailability = workflowAvailabilityFor(context);
|
||||
if (workflowAvailability !== null) return workflowAvailability;
|
||||
const targetDecision = targetAvailability(context);
|
||||
@@ -193,6 +204,23 @@ const availabilityFor = (context: AvailabilityContext): Availability => {
|
||||
return providerAvailability(context);
|
||||
};
|
||||
|
||||
const optionalProviderFailureFor = ({
|
||||
name,
|
||||
kind,
|
||||
policy,
|
||||
}: AvailabilityContext): OptionalProviderLoadFailure | undefined => {
|
||||
const failures = policy.optionalProviderLoadFailures;
|
||||
if (name === "capture_browser_scenario")
|
||||
return failures?.browserScenarioCapture;
|
||||
if (kind === "browser-provider") return failures?.browserObservation;
|
||||
if (name === "capture_electron_scenario")
|
||||
return failures?.electronActiveObservation;
|
||||
if (kind === "electron-provider") return failures?.electronObservation;
|
||||
if (kind === "runtime-provider")
|
||||
return failures?.javascriptRuntimeObservation;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const javascriptApplicationAvailability = ({
|
||||
name,
|
||||
}: AvailabilityContext): Availability | null => {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { BrowserObservationPort } from "./BrowserObservationPort.js";
|
||||
import type { BrowserScenarioCapturePort } from "./BrowserScenarioCapturePort.js";
|
||||
import type { ElectronObservationPort } from "./ElectronObservationPort.js";
|
||||
import type { ElectronActiveObservationPort } from "./ElectronActiveObservationPort.js";
|
||||
import type { JavaScriptRuntimeObservationPort } from "./JavaScriptRuntimeObservationPort.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
|
||||
/** Existing observation ports loaded independently at MCP startup. */
|
||||
export interface OptionalObservationProviders {
|
||||
readonly browserObservation?: BrowserObservationPort;
|
||||
readonly browserScenarioCapture?: BrowserScenarioCapturePort;
|
||||
readonly electronObservation?: ElectronObservationPort;
|
||||
readonly electronActiveObservation?: ElectronActiveObservationPort;
|
||||
readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort;
|
||||
}
|
||||
|
||||
/** A module failed before its port could be constructed; no engine was opened. */
|
||||
export interface OptionalProviderLoadFailure {
|
||||
readonly providerId: string;
|
||||
readonly reason: string;
|
||||
}
|
||||
|
||||
export type OptionalProviderLoadFailures = Readonly<
|
||||
Partial<
|
||||
Record<keyof OptionalObservationProviders, OptionalProviderLoadFailure>
|
||||
>
|
||||
>;
|
||||
|
||||
/** Successful ports and failed peers from one independent loading attempt. */
|
||||
export interface OptionalProviderLoadResult extends OptionalObservationProviders {
|
||||
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
|
||||
}
|
||||
|
||||
/** Preserve the module's failure reason through the existing tool error algebra. */
|
||||
export const optionalProviderUnavailable = (
|
||||
failure: OptionalProviderLoadFailure,
|
||||
operation: string,
|
||||
): AnalysisCapabilityUnavailableError =>
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
failure.providerId,
|
||||
operation,
|
||||
`Adapter could not load: ${failure.reason}`,
|
||||
);
|
||||
@@ -64,11 +64,8 @@ import { captureCdpScreenshot } from "./CdpScreenshot.js";
|
||||
import { comparePngScreenshots } from "./PngVisualDiff.js";
|
||||
|
||||
/** Public identity committed by passive browser observations. */
|
||||
export const CDP_BROWSER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "rea-cdp-browser",
|
||||
name: "REA Chrome DevTools Protocol observation provider",
|
||||
version: "2",
|
||||
});
|
||||
export { CDP_BROWSER_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
import { CDP_BROWSER_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
const IDENTITY = CDP_BROWSER_PROVIDER_IDENTITY;
|
||||
const CLEANUP_DOMAINS = ["Network", "Debugger", "Runtime", "Page"] as const;
|
||||
|
||||
|
||||
@@ -30,11 +30,8 @@ import { inspectCdpElectronPage } from "./CdpElectronInspection.js";
|
||||
import { authorizedElectronFile } from "./ElectronFileScope.js";
|
||||
|
||||
/** Public identity committed by passive Electron observations. */
|
||||
export const CDP_ELECTRON_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "rea-cdp-electron",
|
||||
name: "REA Electron file-page CDP observation provider",
|
||||
version: "1",
|
||||
});
|
||||
export { CDP_ELECTRON_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
import { CDP_ELECTRON_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
const IDENTITY = CDP_ELECTRON_PROVIDER_IDENTITY;
|
||||
|
||||
/** Passive Electron provider for local file pages exposed by loopback CDP. */
|
||||
|
||||
@@ -41,12 +41,8 @@ const lazyPlaywrightFactory: BrowserScenarioSessionFactory = {
|
||||
},
|
||||
};
|
||||
|
||||
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY: ProviderIdentity =
|
||||
Object.freeze({
|
||||
id: "rea-playwright-browser-scenario",
|
||||
name: "REA Playwright browser scenario capture provider",
|
||||
version: "1",
|
||||
});
|
||||
export { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
import { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
|
||||
type SnapshotKind = BrowserScenario["capture"]["after_each_step"][number];
|
||||
const SNAPSHOT_KINDS = [
|
||||
|
||||
@@ -42,12 +42,8 @@ const OPERATION = "capture_electron_scenario" as const;
|
||||
const STARTUP_TIMEOUT_MS = 60_000;
|
||||
|
||||
/** Public identity for provider-owned Electron runtime experiments. */
|
||||
export const PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY: ProviderIdentity =
|
||||
Object.freeze({
|
||||
id: "rea-playwright-electron-active",
|
||||
name: "REA Playwright active Electron observation provider",
|
||||
version: "1",
|
||||
});
|
||||
export { PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
import { PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
|
||||
const hookPath = fileURLToPath(
|
||||
new URL("../../scripts/electron-active-hook.cjs", import.meta.url),
|
||||
|
||||
@@ -39,11 +39,8 @@ import {
|
||||
} from "./V8InspectorEndpoint.js";
|
||||
|
||||
/** Public identity committed by passive V8 Inspector observations. */
|
||||
export const V8_INSPECTOR_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "rea-v8-inspector",
|
||||
name: "REA passive Node/Electron V8 Inspector provider",
|
||||
version: "1",
|
||||
});
|
||||
export { V8_INSPECTOR_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "./providerIdentities.js";
|
||||
|
||||
export interface ScriptDraft {
|
||||
readonly rawUrl: string;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ProviderIdentity } from "../application/AnalysisProvider.js";
|
||||
|
||||
/** Identity available without loading the CdpBrowserProvider implementation. */
|
||||
export const CDP_BROWSER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "rea-cdp-browser",
|
||||
name: "REA Chrome DevTools Protocol observation provider",
|
||||
version: "2",
|
||||
});
|
||||
|
||||
/** Identity available without loading the PlaywrightBrowserScenarioProvider implementation. */
|
||||
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY: ProviderIdentity =
|
||||
Object.freeze({
|
||||
id: "rea-playwright-browser-scenario",
|
||||
name: "REA Playwright browser scenario capture provider",
|
||||
version: "1",
|
||||
});
|
||||
|
||||
/** Identity available without loading the CdpElectronProvider implementation. */
|
||||
export const CDP_ELECTRON_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "rea-cdp-electron",
|
||||
name: "REA Electron file-page CDP observation provider",
|
||||
version: "1",
|
||||
});
|
||||
|
||||
/** Identity available without loading the PlaywrightElectronActiveProvider implementation. */
|
||||
export const PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY: ProviderIdentity =
|
||||
Object.freeze({
|
||||
id: "rea-playwright-electron-active",
|
||||
name: "REA Playwright active Electron observation provider",
|
||||
version: "1",
|
||||
});
|
||||
|
||||
/** Identity available without loading the V8InspectorProvider implementation. */
|
||||
export const V8_INSPECTOR_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "rea-v8-inspector",
|
||||
name: "REA passive Node/Electron V8 Inspector provider",
|
||||
version: "1",
|
||||
});
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
} from "../application/ElectronObservationService.js";
|
||||
import { captureElectronScenario } from "../application/ElectronActiveObservationService.js";
|
||||
import { reconcileJavaScriptRuntimeEvidence } from "../application/JavaScriptRuntimeReconciliationService.js";
|
||||
import { CdpElectronProvider } from "../browser/CdpElectronProvider.js";
|
||||
import { PlaywrightElectronActiveProvider } from "../browser/PlaywrightElectronActiveProvider.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import {
|
||||
inspectElectronPageInputSchema,
|
||||
@@ -61,9 +59,10 @@ const registerElectronActiveCommand = (
|
||||
input.value,
|
||||
);
|
||||
if (!parsed.success) return inputError("capture_electron_scenario");
|
||||
const context = await electronContext();
|
||||
const { createElectronScenarioProvider } =
|
||||
await import("../composition/electronScenario.js");
|
||||
const result = await captureElectronScenario(
|
||||
context.activeProvider,
|
||||
createElectronScenarioProvider(),
|
||||
parsed.data,
|
||||
);
|
||||
return result.ok ? result.value : cliError(result.error);
|
||||
@@ -181,16 +180,12 @@ const registerJavaScriptApplicationCommand = (
|
||||
});
|
||||
};
|
||||
|
||||
const electronContext = async () => {
|
||||
return {
|
||||
ok: true as const,
|
||||
provider: new CdpElectronProvider(),
|
||||
activeProvider: new PlaywrightElectronActiveProvider(),
|
||||
};
|
||||
const electronObservationContext = async () => {
|
||||
const { createElectronObservationProvider } =
|
||||
await import("../composition/electronObservation.js");
|
||||
return { provider: createElectronObservationProvider() };
|
||||
};
|
||||
|
||||
const electronObservationContext = electronContext;
|
||||
|
||||
const inputError = (operation: string): JsonValue =>
|
||||
cliError(new AnalysisInputError(operation));
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { CdpBrowserProvider } from "./browser/CdpBrowserProvider.js";
|
||||
import { createBrowserObservationProvider } from "./composition/browserObservation.js";
|
||||
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
|
||||
import type { JsonValue } from "./domain/jsonValue.js";
|
||||
|
||||
/** Build the passive browser provider; scope is supplied on each request. */
|
||||
export const browserContext = () => ({ provider: new CdpBrowserProvider() });
|
||||
export const browserContext = () => ({
|
||||
provider: createBrowserObservationProvider(),
|
||||
});
|
||||
|
||||
/** Project a browser operation failure consistently across CLI command groups. */
|
||||
export const browserCliError = (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Cli } from "incur";
|
||||
import { z } from "zod";
|
||||
|
||||
import { captureBrowserScenario } from "./application/BrowserScenarioCaptureService.js";
|
||||
import { PlaywrightBrowserScenarioProvider } from "./browser/PlaywrightBrowserScenarioProvider.js";
|
||||
import { createBrowserScenarioProvider } from "./composition/browserScenario.js";
|
||||
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
||||
import { parseCliJsonInput } from "./cliJsonInput.js";
|
||||
import { logCliCommand } from "./cliLogging.js";
|
||||
@@ -35,7 +35,7 @@ export const registerBrowserScenarioCommands = (
|
||||
if (!scenario.success)
|
||||
return cliError(new AnalysisInputError(OPERATION));
|
||||
const result = await captureBrowserScenario(
|
||||
new PlaywrightBrowserScenarioProvider(),
|
||||
createBrowserScenarioProvider(),
|
||||
scenario.data,
|
||||
);
|
||||
return result.ok ? result.value : cliError(result.error);
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
listJavaScriptRuntimeTargets,
|
||||
observeJavaScriptRuntime,
|
||||
} from "./application/JavaScriptRuntimeObservationService.js";
|
||||
import { V8InspectorProvider } from "./browser/V8InspectorProvider.js";
|
||||
import { createJavaScriptRuntimeObservationProvider } from "./composition/javascriptRuntimeObservation.js";
|
||||
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
||||
import { logCliCommand } from "./cliLogging.js";
|
||||
import {
|
||||
@@ -89,7 +89,9 @@ export const registerJavaScriptRuntimeObservationCommands = (
|
||||
});
|
||||
};
|
||||
|
||||
const runtimeContext = () => ({ provider: new V8InspectorProvider() });
|
||||
const runtimeContext = () => ({
|
||||
provider: createJavaScriptRuntimeObservationProvider(),
|
||||
});
|
||||
|
||||
const inputError = (operation: string): JsonValue =>
|
||||
cliError(new AnalysisInputError(operation));
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { BrowserObservationPort } from "../application/BrowserObservationPort.js";
|
||||
import { CdpBrowserProvider } from "../browser/CdpBrowserProvider.js";
|
||||
|
||||
/** Construct a fresh provider without opening a target or acquiring an engine. */
|
||||
export const createBrowserObservationProvider = (): BrowserObservationPort =>
|
||||
new CdpBrowserProvider();
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
|
||||
import { PlaywrightBrowserScenarioProvider } from "../browser/PlaywrightBrowserScenarioProvider.js";
|
||||
|
||||
/** Construct a fresh provider without opening a target or acquiring an engine. */
|
||||
export const createBrowserScenarioProvider = (): BrowserScenarioCapturePort =>
|
||||
new PlaywrightBrowserScenarioProvider();
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { ElectronObservationPort } from "../application/ElectronObservationPort.js";
|
||||
import { CdpElectronProvider } from "../browser/CdpElectronProvider.js";
|
||||
|
||||
/** Construct a fresh provider without opening a target or acquiring an engine. */
|
||||
export const createElectronObservationProvider = (): ElectronObservationPort =>
|
||||
new CdpElectronProvider();
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { ElectronActiveObservationPort } from "../application/ElectronActiveObservationPort.js";
|
||||
import { PlaywrightElectronActiveProvider } from "../browser/PlaywrightElectronActiveProvider.js";
|
||||
|
||||
/** Construct a fresh provider without opening a target or acquiring an engine. */
|
||||
export const createElectronScenarioProvider =
|
||||
(): ElectronActiveObservationPort => new PlaywrightElectronActiveProvider();
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { JavaScriptRuntimeObservationPort } from "../application/JavaScriptRuntimeObservationPort.js";
|
||||
import { V8InspectorProvider } from "../browser/V8InspectorProvider.js";
|
||||
|
||||
/** Construct a fresh provider without opening a target or acquiring an engine. */
|
||||
export const createJavaScriptRuntimeObservationProvider =
|
||||
(): JavaScriptRuntimeObservationPort => new V8InspectorProvider();
|
||||
@@ -0,0 +1,97 @@
|
||||
import type {
|
||||
OptionalObservationProviders,
|
||||
OptionalProviderLoadFailure,
|
||||
OptionalProviderLoadResult,
|
||||
} from "../application/OptionalObservationProviders.js";
|
||||
import {
|
||||
CDP_BROWSER_PROVIDER_IDENTITY,
|
||||
PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
|
||||
CDP_ELECTRON_PROVIDER_IDENTITY,
|
||||
PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
|
||||
V8_INSPECTOR_PROVIDER_IDENTITY,
|
||||
} from "../browser/providerIdentities.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
/** Precisely typed startup factories for the five existing observation ports. */
|
||||
export type OptionalObservationFactories = {
|
||||
readonly [Port in keyof OptionalObservationProviders]-?: () => Promise<
|
||||
NonNullable<OptionalObservationProviders[Port]>
|
||||
>;
|
||||
};
|
||||
|
||||
const productionFactories: OptionalObservationFactories = {
|
||||
browserObservation: async () =>
|
||||
(
|
||||
await import("./browserObservation.js")
|
||||
).createBrowserObservationProvider(),
|
||||
browserScenarioCapture: async () =>
|
||||
(await import("./browserScenario.js")).createBrowserScenarioProvider(),
|
||||
electronObservation: async () =>
|
||||
(
|
||||
await import("./electronObservation.js")
|
||||
).createElectronObservationProvider(),
|
||||
electronActiveObservation: async () =>
|
||||
(await import("./electronScenario.js")).createElectronScenarioProvider(),
|
||||
javascriptRuntimeObservation: async () =>
|
||||
(
|
||||
await import("./javascriptRuntimeObservation.js")
|
||||
).createJavaScriptRuntimeObservationProvider(),
|
||||
};
|
||||
|
||||
const loadPort = async <Port>(
|
||||
factory: () => Promise<Port>,
|
||||
providerId: string,
|
||||
): Promise<Result<Port, OptionalProviderLoadFailure>> => {
|
||||
try {
|
||||
return ok(await factory());
|
||||
} catch (cause: unknown) {
|
||||
return err({
|
||||
providerId,
|
||||
reason: cause instanceof Error ? cause.message : String(cause),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** Load each adapter independently, retaining healthy peers and exact failures. */
|
||||
export const loadOptionalObservationProviders = async (
|
||||
factories: OptionalObservationFactories = productionFactories,
|
||||
): Promise<OptionalProviderLoadResult> => {
|
||||
const [browser, scenario, electron, electronActive, runtime] =
|
||||
await Promise.all([
|
||||
loadPort(factories.browserObservation, CDP_BROWSER_PROVIDER_IDENTITY.id),
|
||||
loadPort(
|
||||
factories.browserScenarioCapture,
|
||||
PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY.id,
|
||||
),
|
||||
loadPort(
|
||||
factories.electronObservation,
|
||||
CDP_ELECTRON_PROVIDER_IDENTITY.id,
|
||||
),
|
||||
loadPort(
|
||||
factories.electronActiveObservation,
|
||||
PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY.id,
|
||||
),
|
||||
loadPort(
|
||||
factories.javascriptRuntimeObservation,
|
||||
V8_INSPECTOR_PROVIDER_IDENTITY.id,
|
||||
),
|
||||
]);
|
||||
return {
|
||||
...(browser.ok ? { browserObservation: browser.value } : {}),
|
||||
...(scenario.ok ? { browserScenarioCapture: scenario.value } : {}),
|
||||
...(electron.ok ? { electronObservation: electron.value } : {}),
|
||||
...(electronActive.ok
|
||||
? { electronActiveObservation: electronActive.value }
|
||||
: {}),
|
||||
...(runtime.ok ? { javascriptRuntimeObservation: runtime.value } : {}),
|
||||
optionalProviderLoadFailures: {
|
||||
...(browser.ok ? {} : { browserObservation: browser.error }),
|
||||
...(scenario.ok ? {} : { browserScenarioCapture: scenario.error }),
|
||||
...(electron.ok ? {} : { electronObservation: electron.error }),
|
||||
...(electronActive.ok
|
||||
? {}
|
||||
: { electronActiveObservation: electronActive.error }),
|
||||
...(runtime.ok ? {} : { javascriptRuntimeObservation: runtime.error }),
|
||||
},
|
||||
};
|
||||
};
|
||||
+2
-23
@@ -14,6 +14,7 @@ import { SERVER_START_FAILED } from "./main/messages.js";
|
||||
import { createRuntimeState } from "./main/state.js";
|
||||
import { openInitialTarget } from "./main/startup.js";
|
||||
import { startMcpTransport } from "./main/transport.js";
|
||||
import { loadOptionalObservationProviders } from "./composition/optionalObservationProviders.js";
|
||||
import { registerConfigReload } from "./main/reload.js";
|
||||
import { createShutdown } from "./main/shutdown.js";
|
||||
|
||||
@@ -70,29 +71,7 @@ export const run = async (
|
||||
logger,
|
||||
serverLogger,
|
||||
loadOptionalProviders:
|
||||
dependencies.loadOptionalProviders ??
|
||||
(async () => {
|
||||
const [
|
||||
{ CdpBrowserProvider },
|
||||
{ PlaywrightBrowserScenarioProvider },
|
||||
{ CdpElectronProvider },
|
||||
{ PlaywrightElectronActiveProvider },
|
||||
{ V8InspectorProvider },
|
||||
] = await Promise.all([
|
||||
import("./browser/CdpBrowserProvider.js"),
|
||||
import("./browser/PlaywrightBrowserScenarioProvider.js"),
|
||||
import("./browser/CdpElectronProvider.js"),
|
||||
import("./browser/PlaywrightElectronActiveProvider.js"),
|
||||
import("./browser/V8InspectorProvider.js"),
|
||||
]);
|
||||
return {
|
||||
browserObservation: new CdpBrowserProvider(),
|
||||
browserScenarioCapture: new PlaywrightBrowserScenarioProvider(),
|
||||
electronObservation: new CdpElectronProvider(),
|
||||
electronActiveObservation: new PlaywrightElectronActiveProvider(),
|
||||
javascriptRuntimeObservation: new V8InspectorProvider(),
|
||||
};
|
||||
}),
|
||||
dependencies.loadOptionalProviders ?? loadOptionalObservationProviders,
|
||||
});
|
||||
if (!transport.ok) return 1;
|
||||
const unregisterReload = registerConfigReload({
|
||||
|
||||
+11
-12
@@ -2,25 +2,16 @@ import type { StdioServerHandle } from "@modelcontextprotocol/server/stdio";
|
||||
|
||||
import type { BinarySession } from "../application/BinarySession.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import {
|
||||
createServer,
|
||||
type CreateServerOptions,
|
||||
} from "../server/createServer.js";
|
||||
import { createServer } from "../server/createServer.js";
|
||||
import type { RuntimeDependencies } from "./types.js";
|
||||
import type { OptionalProviderLoadResult } from "../application/OptionalObservationProviders.js";
|
||||
import {
|
||||
MCP_CONNECTION_LOST,
|
||||
MCP_CONNECTION_START_FAILED,
|
||||
} from "./messages.js";
|
||||
|
||||
/** Optional adapters whose absence must not prevent the core MCP server. */
|
||||
export type OptionalProviders = Pick<
|
||||
CreateServerOptions,
|
||||
| "browserObservation"
|
||||
| "browserScenarioCapture"
|
||||
| "electronObservation"
|
||||
| "electronActiveObservation"
|
||||
| "javascriptRuntimeObservation"
|
||||
>;
|
||||
export type OptionalProviders = OptionalProviderLoadResult;
|
||||
|
||||
interface ServerContext {
|
||||
readonly logger: Logger;
|
||||
@@ -51,6 +42,14 @@ export const startMcpTransport = async (
|
||||
"Optional MCP providers could not load; affected tools remain unavailable",
|
||||
);
|
||||
}
|
||||
for (const failure of Object.values(
|
||||
optionalProviders.optionalProviderLoadFailures ?? {},
|
||||
)) {
|
||||
serverLogger.warn(
|
||||
{ providerId: failure.providerId, error: failure.reason },
|
||||
"Optional MCP adapter could not load; its peers remain available",
|
||||
);
|
||||
}
|
||||
let handle: StdioServerHandle;
|
||||
try {
|
||||
handle = dependencies.serve(
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioC
|
||||
import type { ElectronActiveObservationPort } from "../application/ElectronActiveObservationPort.js";
|
||||
import type { ElectronObservationPort } from "../application/ElectronObservationPort.js";
|
||||
import type { JavaScriptRuntimeObservationPort } from "../application/JavaScriptRuntimeObservationPort.js";
|
||||
import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js";
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { silentLogger, type Logger } from "../logger.js";
|
||||
import { registerApplicationTools } from "./registerApplicationTools.js";
|
||||
@@ -51,6 +52,7 @@ export interface CreateServerOptions {
|
||||
readonly electronActiveObservation?: ElectronActiveObservationPort;
|
||||
readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort;
|
||||
readonly availabilityPolicy?: () => SessionAvailability;
|
||||
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
|
||||
}
|
||||
|
||||
const installSessionToolAvailability = (
|
||||
@@ -60,6 +62,7 @@ const installSessionToolAvailability = (
|
||||
) => {
|
||||
if (session === undefined) return undefined;
|
||||
const policy = sessionAvailabilityPolicy(options.availabilityPolicy, {
|
||||
optionalProviderLoadFailures: options.optionalProviderLoadFailures,
|
||||
optionalFeatures: {
|
||||
firmwareInspectionEnabled:
|
||||
options.firmwareAnalysis !== undefined ||
|
||||
@@ -259,19 +262,27 @@ const registerObservationTools = ({
|
||||
registerBrowserTools(server, {
|
||||
...common,
|
||||
browser: options.browserObservation,
|
||||
loadFailure: options.optionalProviderLoadFailures?.browserObservation,
|
||||
});
|
||||
registerBrowserScenarioTool(server, {
|
||||
...common,
|
||||
provider: options.browserScenarioCapture,
|
||||
loadFailure: options.optionalProviderLoadFailures?.browserScenarioCapture,
|
||||
});
|
||||
registerElectronTools(server, {
|
||||
...common,
|
||||
electron: options.electronObservation,
|
||||
electronActive: options.electronActiveObservation,
|
||||
observationLoadFailure:
|
||||
options.optionalProviderLoadFailures?.electronObservation,
|
||||
activeLoadFailure:
|
||||
options.optionalProviderLoadFailures?.electronActiveObservation,
|
||||
});
|
||||
registerJavaScriptRuntimeObservationTools(server, {
|
||||
...common,
|
||||
runtime: options.javascriptRuntimeObservation,
|
||||
loadFailure:
|
||||
options.optionalProviderLoadFailures?.javascriptRuntimeObservation,
|
||||
});
|
||||
registerApplicationTools(server, {
|
||||
evidenceById,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
optionalProviderUnavailable,
|
||||
type OptionalProviderLoadFailure,
|
||||
} from "../application/OptionalObservationProviders.js";
|
||||
import { err } from "../domain/result.js";
|
||||
import type { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
@@ -12,6 +17,7 @@ import { toCallToolResult } from "./toolResult.js";
|
||||
|
||||
interface BrowserScenarioToolRegistration {
|
||||
readonly logger: Logger;
|
||||
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
|
||||
readonly provider: BrowserScenarioCapturePort | undefined;
|
||||
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
|
||||
}
|
||||
@@ -27,6 +33,11 @@ export const registerBrowserScenarioTool = (
|
||||
toolRegistrationOptions(contract),
|
||||
async (input, context) => {
|
||||
const scenario = browserScenarioSchema.parse(input);
|
||||
if (options.loadFailure !== undefined)
|
||||
return toCallToolResult(
|
||||
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
|
||||
contract,
|
||||
);
|
||||
const result = await logToolExecution(options.logger, contract.name, () =>
|
||||
captureBrowserScenario(options.provider, scenario, {
|
||||
signal: context.mcpReq.signal,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
optionalProviderUnavailable,
|
||||
type OptionalProviderLoadFailure,
|
||||
} from "../application/OptionalObservationProviders.js";
|
||||
import { err } from "../domain/result.js";
|
||||
import type { McpServer, ServerContext } from "@modelcontextprotocol/server";
|
||||
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
@@ -28,6 +33,7 @@ import { toCallToolResult } from "./toolResult.js";
|
||||
|
||||
interface BrowserToolRegistration {
|
||||
readonly logger: Logger;
|
||||
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
|
||||
readonly browser: BrowserObservationPort | undefined;
|
||||
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
|
||||
}
|
||||
@@ -182,6 +188,11 @@ const runBrowserTool = async <Input>(
|
||||
) => Promise<Result<Evidence, AnalysisError>>,
|
||||
) => {
|
||||
const { input, context } = request;
|
||||
if (options.loadFailure !== undefined)
|
||||
return toCallToolResult(
|
||||
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
|
||||
contract,
|
||||
);
|
||||
const result = await logToolExecution(options.logger, contract.name, () =>
|
||||
execute(input, {
|
||||
signal: context.mcpReq.signal,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
optionalProviderUnavailable,
|
||||
type OptionalProviderLoadFailure,
|
||||
} from "../application/OptionalObservationProviders.js";
|
||||
import { err } from "../domain/result.js";
|
||||
import type { McpServer, ServerContext } from "@modelcontextprotocol/server";
|
||||
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
@@ -25,6 +30,8 @@ import { toCallToolResult } from "./toolResult.js";
|
||||
|
||||
interface ElectronToolRegistration {
|
||||
readonly logger: Logger;
|
||||
readonly observationLoadFailure?: OptionalProviderLoadFailure | undefined;
|
||||
readonly activeLoadFailure?: OptionalProviderLoadFailure | undefined;
|
||||
readonly electron: ElectronObservationPort | undefined;
|
||||
readonly electronActive: ElectronActiveObservationPort | undefined;
|
||||
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
|
||||
@@ -134,6 +141,18 @@ const runElectronTool = async <Input>(
|
||||
) => Promise<Result<Evidence, AnalysisError>>,
|
||||
) => {
|
||||
const { input, context } = request;
|
||||
const failure =
|
||||
contract.name === "capture_electron_scenario"
|
||||
? options.activeLoadFailure
|
||||
: contract.name === "list_electron_targets" ||
|
||||
contract.name === "inspect_electron_page"
|
||||
? options.observationLoadFailure
|
||||
: undefined;
|
||||
if (failure !== undefined)
|
||||
return toCallToolResult(
|
||||
err(optionalProviderUnavailable(failure, contract.name)),
|
||||
contract,
|
||||
);
|
||||
const result = await logToolExecution(options.logger, contract.name, () =>
|
||||
execute(input, {
|
||||
signal: context.mcpReq.signal,
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
optionalProviderUnavailable,
|
||||
type OptionalProviderLoadFailure,
|
||||
} from "../application/OptionalObservationProviders.js";
|
||||
import { err } from "../domain/result.js";
|
||||
import type { McpServer, ServerContext } from "@modelcontextprotocol/server";
|
||||
|
||||
import type { BinarySessionPort } from "../application/BinarySession.js";
|
||||
@@ -19,6 +24,7 @@ import { toCallToolResult } from "./toolResult.js";
|
||||
|
||||
interface RuntimeToolRegistration {
|
||||
readonly logger: Logger;
|
||||
readonly loadFailure?: OptionalProviderLoadFailure | undefined;
|
||||
readonly runtime: JavaScriptRuntimeObservationPort | undefined;
|
||||
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
|
||||
}
|
||||
@@ -68,6 +74,11 @@ const runRuntimeTool = async <Input>(
|
||||
) => Promise<Result<Evidence, AnalysisError>>,
|
||||
) => {
|
||||
const { input, context } = request;
|
||||
if (options.loadFailure !== undefined)
|
||||
return toCallToolResult(
|
||||
err(optionalProviderUnavailable(options.loadFailure, contract.name)),
|
||||
contract,
|
||||
);
|
||||
const result = await logToolExecution(options.logger, contract.name, () =>
|
||||
execute(input, context.mcpReq.signal),
|
||||
);
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import type { AvailabilityPolicy } from "../application/CapabilityInventory.js";
|
||||
import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js";
|
||||
import { platform } from "node:process";
|
||||
|
||||
export type SessionAvailability = AvailabilityPolicy;
|
||||
|
||||
export interface SessionAvailabilityDefaults {
|
||||
readonly optionalProviderLoadFailures?:
|
||||
| OptionalProviderLoadFailures
|
||||
| undefined;
|
||||
readonly optionalFeatures?: Pick<
|
||||
SessionAvailability,
|
||||
| "browserObservationEnabled"
|
||||
@@ -21,24 +25,34 @@ export interface SessionAvailabilityDefaults {
|
||||
export const sessionAvailabilityPolicy = (
|
||||
configured: (() => SessionAvailability) | undefined,
|
||||
defaults: SessionAvailabilityDefaults,
|
||||
): (() => SessionAvailability) =>
|
||||
configured ??
|
||||
(() => ({
|
||||
processCaptureEnabled: platform !== "win32",
|
||||
firmwareInspectionEnabled:
|
||||
defaults.optionalFeatures?.firmwareInspectionEnabled ?? false,
|
||||
firmwareExtractionEnabled:
|
||||
defaults.optionalFeatures?.firmwareExtractionEnabled ?? false,
|
||||
androidAnalysisEnabled:
|
||||
defaults.optionalFeatures?.androidAnalysisEnabled ?? false,
|
||||
browserObservationEnabled:
|
||||
defaults.optionalFeatures?.browserObservationEnabled ?? false,
|
||||
browserScenarioEnabled:
|
||||
defaults.optionalFeatures?.browserScenarioEnabled ?? false,
|
||||
electronObservationEnabled:
|
||||
defaults.optionalFeatures?.electronObservationEnabled ?? false,
|
||||
electronAutomationEnabled:
|
||||
defaults.optionalFeatures?.electronAutomationEnabled ?? false,
|
||||
v8InspectorObservationEnabled:
|
||||
defaults.optionalFeatures?.v8InspectorObservationEnabled ?? false,
|
||||
}));
|
||||
): (() => SessionAvailability) => {
|
||||
const policy =
|
||||
configured ??
|
||||
(() => ({
|
||||
processCaptureEnabled: platform !== "win32",
|
||||
firmwareInspectionEnabled:
|
||||
defaults.optionalFeatures?.firmwareInspectionEnabled ?? false,
|
||||
firmwareExtractionEnabled:
|
||||
defaults.optionalFeatures?.firmwareExtractionEnabled ?? false,
|
||||
androidAnalysisEnabled:
|
||||
defaults.optionalFeatures?.androidAnalysisEnabled ?? false,
|
||||
browserObservationEnabled:
|
||||
defaults.optionalFeatures?.browserObservationEnabled ?? false,
|
||||
browserScenarioEnabled:
|
||||
defaults.optionalFeatures?.browserScenarioEnabled ?? false,
|
||||
electronObservationEnabled:
|
||||
defaults.optionalFeatures?.electronObservationEnabled ?? false,
|
||||
electronAutomationEnabled:
|
||||
defaults.optionalFeatures?.electronAutomationEnabled ?? false,
|
||||
v8InspectorObservationEnabled:
|
||||
defaults.optionalFeatures?.v8InspectorObservationEnabled ?? false,
|
||||
}));
|
||||
return () => ({
|
||||
...policy(),
|
||||
...(defaults.optionalProviderLoadFailures === undefined
|
||||
? {}
|
||||
: {
|
||||
optionalProviderLoadFailures: defaults.optionalProviderLoadFailures,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
import { afterEach, expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { BinarySessionPort } from "../../../src/application/BinarySessionPort.js";
|
||||
import {
|
||||
loadOptionalObservationProviders,
|
||||
type OptionalObservationFactories,
|
||||
} from "../../../src/composition/optionalObservationProviders.js";
|
||||
import {
|
||||
TOOL_CONTRACTS,
|
||||
toolContract,
|
||||
} from "../../../src/contracts/toolContracts.js";
|
||||
import { run } from "../../../src/main.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { optionalObservationFactories } from "../../fixtures/optionalObservationProviders.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { mcpTest } from "../../support/mcp/mcpFixture.js";
|
||||
|
||||
const failedAdapters = [
|
||||
["browserObservation", "list_browser_targets", "rea-cdp-browser"],
|
||||
[
|
||||
"browserScenarioCapture",
|
||||
"capture_browser_scenario",
|
||||
"rea-playwright-browser-scenario",
|
||||
],
|
||||
["electronObservation", "list_electron_targets", "rea-cdp-electron"],
|
||||
[
|
||||
"electronActiveObservation",
|
||||
"capture_electron_scenario",
|
||||
"rea-playwright-electron-active",
|
||||
],
|
||||
[
|
||||
"javascriptRuntimeObservation",
|
||||
"list_javascript_runtime_targets",
|
||||
"rea-v8-inspector",
|
||||
],
|
||||
] as const;
|
||||
|
||||
const resources: Array<() => Promise<void>> = [];
|
||||
afterEach(async () => {
|
||||
for (const close of resources.splice(0).reverse()) await close();
|
||||
});
|
||||
|
||||
const boot = async (factories: OptionalObservationFactories) => {
|
||||
const loaded = await loadOptionalObservationProviders(factories);
|
||||
let server: McpServer | undefined;
|
||||
let session: BinarySessionPort | undefined;
|
||||
let shutdown: (() => void) | undefined;
|
||||
let resolveClose: (() => void) | undefined;
|
||||
const closed = new Promise<void>((resolve) => {
|
||||
resolveClose = resolve;
|
||||
});
|
||||
const output: string[] = [];
|
||||
const exitCodes: number[] = [];
|
||||
expect(
|
||||
await run({
|
||||
env: {},
|
||||
loadOptionalProviders: async () => loaded,
|
||||
createServer: (analysis, selectedSession, options) => {
|
||||
session = selectedSession;
|
||||
// A configured policy cannot hide an actual adapter loading failure.
|
||||
return createServer(analysis, selectedSession, {
|
||||
...options,
|
||||
availabilityPolicy: () => ({
|
||||
processCaptureEnabled: true,
|
||||
browserObservationEnabled: true,
|
||||
browserScenarioEnabled: true,
|
||||
electronObservationEnabled: true,
|
||||
electronAutomationEnabled: true,
|
||||
v8InspectorObservationEnabled: true,
|
||||
}),
|
||||
});
|
||||
},
|
||||
serve: (factory) => {
|
||||
const created = factory({ era: "legacy" });
|
||||
if (!(created instanceof McpServer))
|
||||
throw new Error("Expected the production synchronous MCP factory");
|
||||
server = created;
|
||||
return {
|
||||
close: async () => {
|
||||
await server?.close();
|
||||
resolveClose?.();
|
||||
},
|
||||
};
|
||||
},
|
||||
registerShutdown: (handler) => {
|
||||
shutdown = handler;
|
||||
return () => {};
|
||||
},
|
||||
writeStderr: (message) => output.push(message),
|
||||
setExitCode: (code) => exitCodes.push(code),
|
||||
}),
|
||||
).toBe(0);
|
||||
expect(output).toEqual([]);
|
||||
if (server === undefined || session === undefined || shutdown === undefined)
|
||||
throw new Error("Runtime did not construct the MCP server and session");
|
||||
const close = shutdown;
|
||||
resources.push(async () => {
|
||||
close();
|
||||
await closed;
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
expect(exitCodes).toEqual([]);
|
||||
});
|
||||
return { server, session, loaded };
|
||||
};
|
||||
|
||||
mcpTest.for(failedAdapters)(
|
||||
"isolates %s and retains successful peers",
|
||||
async ([port, operation, providerId], { mcp }) => {
|
||||
const calls: string[] = [];
|
||||
const failure = `${port} synthetic import failure`;
|
||||
const factories = optionalObservationFactories(calls);
|
||||
const connected = await boot({
|
||||
...factories,
|
||||
[port]: async () => {
|
||||
throw new Error(failure);
|
||||
},
|
||||
});
|
||||
expect(connected.loaded.optionalProviderLoadFailures).toEqual({
|
||||
[port]: { providerId, reason: failure },
|
||||
});
|
||||
for (const [peer] of failedAdapters)
|
||||
expect(connected.loaded[peer] !== undefined).toBe(peer !== port);
|
||||
const client = await mcp.connect(connected.server);
|
||||
const names = (await client.listTools()).tools.map(({ name }) => name);
|
||||
expect(new Set(names)).toEqual(
|
||||
new Set(TOOL_CONTRACTS.map(({ name }) => name)),
|
||||
);
|
||||
expect(names).toHaveLength(TOOL_CONTRACTS.length);
|
||||
const example = toolContract(operation).examples[0];
|
||||
if (example === undefined)
|
||||
throw new Error(`Missing example for ${operation}`);
|
||||
const unavailable = await client.callTool({
|
||||
name: operation,
|
||||
arguments: example.input,
|
||||
});
|
||||
expect(unavailable.isError).toBe(true);
|
||||
expect(unavailable.structuredContent).toMatchObject({
|
||||
error: {
|
||||
details: {
|
||||
provider_id: providerId,
|
||||
operation,
|
||||
reason: `Adapter could not load: ${failure}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
const status = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
expect(status.structuredContent).toMatchObject({
|
||||
result: {
|
||||
tool_availability: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: operation,
|
||||
available: false,
|
||||
reason: "provider_unavailable",
|
||||
remediation: expect.stringContaining(failure),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "analyze_javascript_application",
|
||||
available: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "reconcile_javascript_runtime",
|
||||
available: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name:
|
||||
port === "electronActiveObservation"
|
||||
? "list_electron_targets"
|
||||
: "capture_electron_scenario",
|
||||
available: true,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
const healthy =
|
||||
port === "javascriptRuntimeObservation"
|
||||
? "list_browser_targets"
|
||||
: "list_javascript_runtime_targets";
|
||||
const listed = await client.callTool({
|
||||
name: healthy,
|
||||
arguments:
|
||||
healthy === "list_browser_targets"
|
||||
? { cdp_endpoint: "http://127.0.0.1:9222" }
|
||||
: { inspector_endpoint: "http://127.0.0.1:9222" },
|
||||
});
|
||||
expect(listed.isError).not.toBe(true);
|
||||
const evidenceId = z
|
||||
.object({ evidence: z.object({ evidence_id: z.string() }) })
|
||||
.parse(listed.structuredContent).evidence.evidence_id;
|
||||
expect(connected.session.evidenceById(evidenceId)?.operation).toBe(healthy);
|
||||
expect(calls).toEqual([
|
||||
port === "javascriptRuntimeObservation" ? "browser" : "runtime",
|
||||
]);
|
||||
const reconciled = await client.callTool({
|
||||
name: "reconcile_javascript_runtime",
|
||||
arguments: toolContract("reconcile_javascript_runtime").examples[0]
|
||||
?.input,
|
||||
});
|
||||
expect(reconciled.isError).not.toBe(true);
|
||||
if (
|
||||
port === "electronObservation" ||
|
||||
port === "electronActiveObservation"
|
||||
) {
|
||||
const directory = await createTestTempDirectory("rea-optional-static-");
|
||||
await writeFile(
|
||||
join(directory, "package.json"),
|
||||
JSON.stringify({ name: "optional-isolation", main: "index.js" }),
|
||||
);
|
||||
await writeFile(join(directory, "index.js"), "exports.answer = 42;\n");
|
||||
const analyzed = await client.callTool({
|
||||
name: "analyze_javascript_application",
|
||||
arguments: { input_path: directory, format: "directory" },
|
||||
});
|
||||
expect(analyzed.isError).not.toBe(true);
|
||||
expect(analyzed.structuredContent).toMatchObject({
|
||||
evidence: { operation: "analyze_javascript_application" },
|
||||
});
|
||||
}
|
||||
const malformed = await client.callTool({
|
||||
name: operation,
|
||||
arguments: { unexpected: true },
|
||||
});
|
||||
expect(malformed.isError).toBe(true);
|
||||
expect(JSON.stringify(malformed)).not.toContain(failure);
|
||||
},
|
||||
);
|
||||
|
||||
mcpTest(
|
||||
"starts with every optional adapter failed and creates fresh connection state",
|
||||
async ({ mcp }) => {
|
||||
const failed = async (): Promise<never> => {
|
||||
throw new Error("all optional adapters unavailable");
|
||||
};
|
||||
const first = await boot({
|
||||
browserObservation: failed,
|
||||
browserScenarioCapture: failed,
|
||||
electronObservation: failed,
|
||||
electronActiveObservation: failed,
|
||||
javascriptRuntimeObservation: failed,
|
||||
});
|
||||
const client = await mcp.connect(first.server);
|
||||
expect((await client.listTools()).tools).toHaveLength(
|
||||
TOOL_CONTRACTS.length,
|
||||
);
|
||||
const status = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
expect(status.isError).not.toBe(true);
|
||||
const second = await boot(optionalObservationFactories([]));
|
||||
expect(second.loaded.optionalProviderLoadFailures).toEqual({});
|
||||
expect(second.session.exportEvidenceBundle().records).toEqual([]);
|
||||
expect(second.loaded.browserObservation).toBeDefined();
|
||||
},
|
||||
);
|
||||
@@ -16,6 +16,9 @@ describe("incremental module import boundaries", () => {
|
||||
["src/application/probe.ts", "../android/JadxProvider.js", false],
|
||||
["src/application/probe.ts", "../composition/android.js", false],
|
||||
["src/server/probe.ts", "../firmware/FirmwareProvider.js", false],
|
||||
["src/server/probe.ts", "../browser/CdpBrowserProvider.js", false],
|
||||
["src/application/probe.ts", "../browser/V8InspectorProvider.js", false],
|
||||
["src/composition/probe.ts", "../browser/CdpBrowserProvider.js", true],
|
||||
["src/domain/probe.ts", "./result.js", true],
|
||||
["src/contracts/probe.ts", "../domain/result.js", true],
|
||||
["src/application/probe.ts", "../domain/result.js", true],
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import type { BrowserObservationPort } from "../../src/application/BrowserObservationPort.js";
|
||||
import type { JavaScriptRuntimeObservationPort } from "../../src/application/JavaScriptRuntimeObservationPort.js";
|
||||
import {
|
||||
CDP_BROWSER_PROVIDER_IDENTITY,
|
||||
V8_INSPECTOR_PROVIDER_IDENTITY,
|
||||
} from "../../src/browser/providerIdentities.js";
|
||||
import { createBrowserScenarioProvider } from "../../src/composition/browserScenario.js";
|
||||
import { createElectronObservationProvider } from "../../src/composition/electronObservation.js";
|
||||
import { createElectronScenarioProvider } from "../../src/composition/electronScenario.js";
|
||||
import type { OptionalObservationFactories } from "../../src/composition/optionalObservationProviders.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../src/domain/analysisErrorCore.js";
|
||||
import { err, ok } from "../../src/domain/result.js";
|
||||
|
||||
const unselected = async () =>
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"recording-fixture",
|
||||
"unselected",
|
||||
"This fixture implements target listing only",
|
||||
),
|
||||
);
|
||||
|
||||
/** Recording ports exercise application composition without claiming real engines. */
|
||||
export const optionalObservationFactories = (
|
||||
calls: string[],
|
||||
): OptionalObservationFactories => {
|
||||
const browser: BrowserObservationPort = {
|
||||
identity: () => CDP_BROWSER_PROVIDER_IDENTITY,
|
||||
listTargets: async () => {
|
||||
calls.push("browser");
|
||||
return ok({
|
||||
browser: {
|
||||
product: "Recording browser",
|
||||
protocol_version: "1.3",
|
||||
revision: "fixture",
|
||||
user_agent: "fixture",
|
||||
js_version: "fixture",
|
||||
},
|
||||
targets: [],
|
||||
excluded: { disallowed_origin: 0, unsupported_url: 0, non_page: 0 },
|
||||
limitations: ["Recording port; no real browser was connected."],
|
||||
});
|
||||
},
|
||||
inspectPage: unselected,
|
||||
analyzeBundle: unselected,
|
||||
observeSession: unselected,
|
||||
discoverWebMcpTools: unselected,
|
||||
compareCaptures: unselected,
|
||||
captureScreenshot: unselected,
|
||||
compareScreenshots: unselected,
|
||||
};
|
||||
const runtime: JavaScriptRuntimeObservationPort = {
|
||||
identity: () => V8_INSPECTOR_PROVIDER_IDENTITY,
|
||||
listTargets: async () => {
|
||||
calls.push("runtime");
|
||||
return ok({
|
||||
runtime: {
|
||||
product: "Recording Node.js",
|
||||
protocol_version: "1.0",
|
||||
v8_version: null,
|
||||
},
|
||||
targets: [],
|
||||
excluded: { unsupported_location: 0, unconnectable: 0 },
|
||||
limitations: ["Recording port; no real Inspector was connected."],
|
||||
});
|
||||
},
|
||||
observe: unselected,
|
||||
};
|
||||
return {
|
||||
browserObservation: async () => browser,
|
||||
browserScenarioCapture: async () => createBrowserScenarioProvider(),
|
||||
electronObservation: async () => createElectronObservationProvider(),
|
||||
electronActiveObservation: async () => createElectronScenarioProvider(),
|
||||
javascriptRuntimeObservation: async () => runtime,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user