From 34d00fd323c793278c6ed126308c58e2fc7efd54 Mon Sep 17 00:00:00 2001
From: morluto <76467478+morluto@users.noreply.github.com>
Date: Tue, 29 Sep 2026 12:41:28 +0800
Subject: [PATCH] refactor(mcp): simplify workflow and observation scope
---
docs/electron-observation.md | 16 +++-
docs/javascript-runtime-observation.md | 6 ++
skills/reverse-engineer-anything/SKILL.md | 8 +-
.../references/native-and-artifacts.md | 21 +++--
src/contracts/electronToolContracts.ts | 13 +--
...vascriptRuntimeObservationToolContracts.ts | 6 +-
src/contracts/promptContracts.test.ts | 11 ++-
src/contracts/promptContracts.ts | 17 +---
src/domain/electronObservation.ts | 8 +-
src/domain/javascriptRuntimeObservation.ts | 17 ++--
src/server/createServer.ts | 12 ++-
src/server/registerElectronTools.ts | 85 ++++++++++++++++---
...gisterJavaScriptRuntimeObservationTools.ts | 77 +++++++++++++++--
tests/boundary/mcp/electronMcp.test.ts | 13 ++-
.../javascriptRuntimeObservationMcp.test.ts | 5 +-
tests/boundary/mcp/serverWorkflowMcp.test.ts | 5 +-
16 files changed, 239 insertions(+), 81 deletions(-)
diff --git a/docs/electron-observation.md b/docs/electron-observation.md
index 0009eea0b7..902e8f98ee 100644
--- a/docs/electron-observation.md
+++ b/docs/electron-observation.md
@@ -28,6 +28,12 @@ Endpoints accept only explicit-port loopback HTTP URLs. Roots are canonicalized
inline array. CDP response-size limits remain internal and oversized discovery
responses fail explicitly.
+For the MCP follow-up call, pass the discovery Evidence ID and selected target
+ID to `inspect_electron_page`. REA reuses the endpoint and authorized roots from
+that retained discovery Evidence, checks the selected ID against its target
+list, and reapplies permission policy. The agent does not repeat the roots. The
+CLI continues to accept its endpoint and roots directly.
+
```bash
rea list-electron-targets http://127.0.0.1:9223 --json
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
@@ -93,16 +99,18 @@ The CLI and MCP surfaces accept the same schema. For a JSON request file:
rea capture-electron-scenario scenario.json --json
```
-The result records bounded action status and targets, correlated app/window/WebContents,
+The result records action status and targets, correlated app/window/WebContents,
preload, session, navigation, shell, permission, popup, download, protocol,
native-addon, process, and IPC timeline events. IPC channels are capped at
1,024 characters and argument-shape metadata at 32 entries; values are never
-retained. Renderer crash/restart and deep-link actions are synthetic, bounded
+retained. Renderer crash/restart and deep-link actions are synthetic
scenario controls; their attempted and observed outcomes remain in the
timeline. The active hook blocks and records external shell/navigation,
permission, download, popup, updater, and OS-integration effects. The timeline
-is explicitly partial when attachment starts after application activity or when
-the event budget is exhausted.
+is explicitly partial when attachment starts after application activity. The
+owned experiment has an internal 60-second deadline and a 5-second per-action
+timeout so hung work reaches process cleanup; these are lifecycle timeouts, not
+caller-selected capture limits.
Active Electron Evidence can also be supplied to
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md). That
diff --git a/docs/javascript-runtime-observation.md b/docs/javascript-runtime-observation.md
index 4288ac842b..3512b4dcd2 100644
--- a/docs/javascript-runtime-observation.md
+++ b/docs/javascript-runtime-observation.md
@@ -35,6 +35,12 @@ Excluded locations are counted but never retained.
array. The Inspector discovery response-size ceiling remains internal; an
oversized response fails explicitly.
+For the MCP follow-up call, pass the discovery Evidence ID and selected target
+ID to `observe_javascript_runtime`. REA reuses the endpoint and file/origin
+scope from that retained discovery Evidence, checks the selected ID against its
+target list, and reapplies permission policy. The agent does not repeat roots or
+origins. The CLI continues to accept its endpoint and scope arguments directly.
+
## Passive protocol boundary
The provider sends exactly two protocol commands:
diff --git a/skills/reverse-engineer-anything/SKILL.md b/skills/reverse-engineer-anything/SKILL.md
index 55b2e63961..15b0eda1ab 100644
--- a/skills/reverse-engineer-anything/SKILL.md
+++ b/skills/reverse-engineer-anything/SKILL.md
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, an
metadata:
version: "23"
tool_count: 111
- catalog_digest: "8e57d3a38cf7e8873ccf94037adb5f59cf1eceba8dc18bd20c95baff19689ebf"
+ catalog_digest: "fcd5f0637e95ab25da4598c2e6c596cebedbc039efe43521fff6ef982e4a6b2a"
---
# REA
@@ -23,12 +23,14 @@ Choose the first tool from the target the user supplied. Do not call
- ASAR or extracted JavaScript/Electron tree:
`analyze_javascript_application`.
- Archive, application package, ZIP/APK/IPA/MSIX/AppX, or DMG:
- `open_binary` with the supplied local path, then `inspect_artifact`.
+ `open_binary` with the supplied local path; use `inspect_artifact` when its
+ graph and findings help answer the question.
- Managed PE/CLI assembly: `inspect_managed_artifact`.
- User-owned browser page already open: `list_browser_targets`.
- User-owned Electron runtime already open: `list_electron_targets`.
- Native executable, library, or analysis database: `open_binary`, then
- `binary_overview`.
+ use focused analysis tools directly; call `binary_overview` when metadata or
+ inventory context is useful.
If the app is missing, ask which app to inspect. Resolve a human-readable app
name to one clear installed artifact when possible; ask only when matches are
diff --git a/skills/reverse-engineer-anything/references/native-and-artifacts.md b/skills/reverse-engineer-anything/references/native-and-artifacts.md
index bcac20b712..9fe0afdcb9 100644
--- a/skills/reverse-engineer-anything/references/native-and-artifacts.md
+++ b/skills/reverse-engineer-anything/references/native-and-artifacts.md
@@ -2,8 +2,9 @@
## Native targets
-After `open_binary`, use `binary_overview` once and narrow around the requested
-feature. Prefer literal search, names, decompilation, callers, callees, and
+After `open_binary`, use focused search, procedure, or function tools directly.
+Use `binary_overview` when target metadata or inventory context helps answer the
+question. Prefer literal search, names, decompilation, callers, callees, and
cross-references. Addresses and recovered pseudocode are analysis observations,
not original source. Provider unavailability and unsupported metadata remain
unknown rather than false.
@@ -23,14 +24,16 @@ parser and must not become an implicit setup dependency.
## Packages and extraction
-Call `inspect_artifact` before extraction for application bundles, archives,
-ZIP/APK/IPA/MSIX/AppX, ASAR, or DMG inputs. It returns the complete artifact
-graph inline. Cite graph manifest IDs.
+Use `inspect_artifact` for application bundles, archives, ZIP/APK/IPA/MSIX/AppX,
+ASAR, or DMG inputs when the artifact graph and findings help answer the
+question. It returns the complete artifact graph inline. Cite graph manifest
+IDs when using them.
-`extract_artifact` takes no arguments and materializes all regular files into a
-fresh temporary directory chosen by REA. Symlinks and encrypted entries are
-inventory facts, not extractable files. The operation still requires the
-filesystem-write permission grant; callers do not choose the destination.
+Use `extract_artifact` when materialized files are needed. It takes no arguments
+and materializes all regular files into a fresh temporary directory chosen by
+REA. Symlinks and encrypted entries are inventory facts, not extractable files.
+The operation still requires the filesystem-write permission grant; callers do
+not choose the destination.
Native DMG traversal is macOS-only, read-only, and requires both operator policy
and `native_mount_approved: true`; without both, retain the root-hash-only result.
diff --git a/src/contracts/electronToolContracts.ts b/src/contracts/electronToolContracts.ts
index 7f89e8d34e..4a8fa120ec 100644
--- a/src/contracts/electronToolContracts.ts
+++ b/src/contracts/electronToolContracts.ts
@@ -38,14 +38,6 @@ const activeExample = {
application_root: "/Applications/Example.app/Contents/Resources",
args: [],
actions: [{ step_id: "exercise-ipc", kind: "click", selector: "#run" }],
- limits: {
- max_duration_ms: 60_000,
- action_timeout_ms: 5_000,
- max_actions: 20,
- max_ipc_events: 2_000,
- max_processes: 32,
- max_windows: 32,
- },
};
/** Root-confined Electron file-page discovery and inspection contracts. */
@@ -72,7 +64,7 @@ export const ELECTRON_TOOL_CONTRACTS = [
name: "inspect_electron_page",
...toolContractMetadata("inspect_electron_page"),
description:
- "Passively inspect one approved Electron file page through CDP. Returns root-confined frames, DOM structure, resource paths, and scripts without evaluating renderer JavaScript or invoking Electron APIs; script contents require separate approval.",
+ "Passively inspect one Electron file page using its list_electron_targets Evidence and returned target ID. Pass the discovery Evidence ID to reuse its endpoint and authorized file roots; this avoids repeating scope and cannot widen permission. Returns frames, DOM structure, resource paths, and scripts without evaluating renderer JavaScript or invoking Electron APIs.",
kind: "electron-provider",
inputSchema: inspectElectronPageToolInputSchema,
outputSchema: inspectionOutputSchema,
@@ -80,8 +72,7 @@ export const ELECTRON_TOOL_CONTRACTS = [
{
title: "Inspect an approved Electron page",
input: {
- cdp_endpoint: endpoint,
- allowed_file_roots: [root],
+ discovery_evidence_id: "EVIDENCE_ID_FROM_LIST_ELECTRON_TARGETS",
target_id: "TARGET_ID_FROM_LIST_ELECTRON_TARGETS",
},
},
diff --git a/src/contracts/javascriptRuntimeObservationToolContracts.ts b/src/contracts/javascriptRuntimeObservationToolContracts.ts
index e7fbb5bf2d..da8387eae3 100644
--- a/src/contracts/javascriptRuntimeObservationToolContracts.ts
+++ b/src/contracts/javascriptRuntimeObservationToolContracts.ts
@@ -36,7 +36,7 @@ export const JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS = [
name: "observe_javascript_runtime",
...toolContractMetadata("observe_javascript_runtime"),
description:
- "Attach passively to one exact approved Node/Electron V8 Inspector target and capture bounded Debugger.scriptParsed plus Runtime execution-context events. REA sends only Runtime.enable and Debugger.enable: it never evaluates, pauses, resumes, reads source, or instruments the target. require/import edges, EventEmitter activity, and Electron IPC remain explicit unknowns; reconcile the returned Evidence with static Application Graph Evidence using reconcile_javascript_runtime.",
+ "Attach passively to one Node/Electron V8 Inspector target using its list_javascript_runtime_targets Evidence and returned target ID. Pass the discovery Evidence ID to reuse its endpoint and authorized file/origin scope; this avoids repeating scope and cannot widen permission. Captures Debugger.scriptParsed and Runtime execution-context events. REA never evaluates, pauses, resumes, reads source, or instruments the target; require/import edges, EventEmitter activity, and Electron IPC remain explicit unknowns. Reconcile the result with static Application Graph Evidence using reconcile_javascript_runtime.",
kind: "runtime-provider",
inputSchema: observeJavaScriptRuntimeToolInputSchema,
outputSchema: evidenceResultOf(javascriptRuntimeObservationSchema),
@@ -44,8 +44,8 @@ export const JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS = [
{
title: "Observe one approved Node runtime",
input: {
- inspector_endpoint: endpoint,
- allowed_file_roots: [root],
+ discovery_evidence_id:
+ "EVIDENCE_ID_FROM_LIST_JAVASCRIPT_RUNTIME_TARGETS",
target_id: "TARGET_ID_FROM_LIST_JAVASCRIPT_RUNTIME_TARGETS",
},
},
diff --git a/src/contracts/promptContracts.test.ts b/src/contracts/promptContracts.test.ts
index 181c120929..ac3131b57d 100644
--- a/src/contracts/promptContracts.test.ts
+++ b/src/contracts/promptContracts.test.ts
@@ -84,15 +84,14 @@ describe("guided prompt contracts", () => {
).toBeLessThan(
rendered.get("audit_residual_unknowns")?.indexOf("`update_unknown`") ?? 0,
);
+ expect(rendered.get("prepare_bounded_process_capture")).not.toContain(
+ "`binary_session`",
+ );
expect(
rendered
.get("prepare_bounded_process_capture")
- ?.indexOf("`binary_session`"),
- ).toBeLessThan(
- rendered
- .get("prepare_bounded_process_capture")
- ?.indexOf("`capture_process_scenario`") ?? 0,
- );
+ ?.indexOf("`capture_process_scenario`"),
+ ).toBeGreaterThanOrEqual(0);
expect(
rendered
.get("compare_application_versions")
diff --git a/src/contracts/promptContracts.ts b/src/contracts/promptContracts.ts
index 048c5beeec..a0d2bbabfa 100644
--- a/src/contracts/promptContracts.ts
+++ b/src/contracts/promptContracts.ts
@@ -116,18 +116,14 @@ export const PROMPT_CONTRACTS = [
},
steps: [
{
- tools: [
- "binary_session",
- "open_binary",
- "analyze_javascript_application",
- ],
+ tools: ["open_binary", "analyze_javascript_application"],
instruction:
"For an approved JavaScript/Electron artifact, reconstruct its application graph without execution when useful; open target_path only when no matching target is active.",
},
{
- tools: ["list_documents", "set_current_document", "binary_overview"],
+ tools: ["list_documents", "set_current_document"],
instruction:
- "Select an explicitly discovered document, then establish bounded binary, language, segment, and procedure context.",
+ "Select a document only when needed for the requested analysis.",
},
{
tools: ["search_strings", "search_procedures", "procedure_address"],
@@ -294,7 +290,7 @@ export const PROMPT_CONTRACTS = [
},
steps: [
{
- tools: ["binary_session", "open_binary", "binary_overview"],
+ tools: ["open_binary"],
instruction:
"Confirm the target only when needed; open or analyze it if it is not already active and the request requires it.",
},
@@ -395,11 +391,6 @@ export const PROMPT_CONTRACTS = [
),
},
steps: [
- {
- tools: ["binary_session"],
- instruction:
- "Inspect capture capability, declared effects, and limits before proposing execution. An unavailable capability is not permission to widen policy.",
- },
{
tools: ["get_evidence_bundle"],
instruction:
diff --git a/src/domain/electronObservation.ts b/src/domain/electronObservation.ts
index 7e867a1562..6b810de39f 100644
--- a/src/domain/electronObservation.ts
+++ b/src/domain/electronObservation.ts
@@ -66,11 +66,15 @@ export const inspectElectronPageInputSchema = z
}));
export const inspectElectronPageToolInputSchema = z.union([
z.strictObject({
- ...inspectElectronPageFacts,
+ discovery_evidence_id: z.string().min(1),
+ target_id: z.string().trim().min(1).max(256),
+ observation_ms: z.number().int().min(0).max(10_000).default(100),
include_script_sources: z.literal(false).default(false),
}),
z.strictObject({
- ...inspectElectronPageFacts,
+ discovery_evidence_id: z.string().min(1),
+ target_id: z.string().trim().min(1).max(256),
+ observation_ms: z.number().int().min(0).max(10_000).default(100),
include_script_sources: z.literal(true),
}),
]);
diff --git a/src/domain/javascriptRuntimeObservation.ts b/src/domain/javascriptRuntimeObservation.ts
index 0e514ad449..f37ac209a9 100644
--- a/src/domain/javascriptRuntimeObservation.ts
+++ b/src/domain/javascriptRuntimeObservation.ts
@@ -82,18 +82,23 @@ export const DEFAULT_JAVASCRIPT_RUNTIME_OBSERVATION_LIMITS = {
};
/** Input for one bounded, attach-only V8 Inspector observation. */
-export const observeJavaScriptRuntimeToolInputSchema = z
+export const observeJavaScriptRuntimeToolInputSchema = z.strictObject({
+ discovery_evidence_id: z.string().min(1),
+ target_id: z.string().trim().min(1).max(256),
+ runtime_kind: javascriptRuntimeKindSchema.optional(),
+ observation_ms: z.number().int().min(0).max(10_000).default(100),
+});
+
+/** Input after discovery Evidence supplies the authorized target scope. */
+export const observeJavaScriptRuntimeInputSchema = z
.strictObject({
...runtimeScope,
target_id: z.string().trim().min(1).max(256),
runtime_kind: javascriptRuntimeKindSchema.optional(),
observation_ms: z.number().int().min(0).max(10_000).default(100),
})
- .superRefine(requireRuntimeScope);
-
-/** Internal parsed input with REA's fixed collection bounds applied. */
-export const observeJavaScriptRuntimeInputSchema =
- observeJavaScriptRuntimeToolInputSchema.transform((input) => ({
+ .superRefine(requireRuntimeScope)
+ .transform((input) => ({
...input,
limits: DEFAULT_JAVASCRIPT_RUNTIME_OBSERVATION_LIMITS,
}));
diff --git a/src/server/createServer.ts b/src/server/createServer.ts
index e726b852b3..ddb4a0ef92 100644
--- a/src/server/createServer.ts
+++ b/src/server/createServer.ts
@@ -55,10 +55,10 @@ import {
} from "./sessionToolPolicies.js";
const TARGET_FREE_INSTRUCTIONS =
- "Start with binary_session to see the active target and available providers. For an archive or application package, call open_binary(path), then inspect_artifact for the complete graph and findings; use extract_artifact to materialize its regular contents when needed. For standalone JavaScript or ASAR analysis, use analyze_javascript_application. For managed PE/CLI files, use inspect_managed_artifact. For native binaries, open the target and start with binary_overview. For a live page or runtime, discover targets with list_browser_targets, list_electron_targets, or list_javascript_runtime_targets before inspection; scenario capture tools are for controlled interactions.";
+ "Choose tools from the target and question. For an archive or application package, open_binary(path) binds the active target; use inspect_artifact when its graph and findings help, and extract_artifact when materialized files are needed. For standalone JavaScript or ASAR analysis, use analyze_javascript_application. For managed PE/CLI files, use inspect_managed_artifact. For native binaries, open the target and use focused search, procedure, or function tools directly; binary_overview is available when metadata or inventory context is useful. For a live page or runtime, discover targets with list_browser_targets, list_electron_targets, or list_javascript_runtime_targets when needed; scenario capture tools are for controlled interactions.";
const ACTIVE_TARGET_INSTRUCTIONS =
- "REA analyzes the active reverse-engineering target. For native binaries, call binary_overview for target metadata and inventory counts, then search_procedures or list_procedures to choose a function and analyze_function with its returned name or address for a complete dossier. Use inspect_native_api when you need to reconstruct a native API boundary, and focused procedure tools when you need only one facet such as assembly, callers, or references.";
+ "REA analyzes the active reverse-engineering target. For native binaries, use search_procedures or list_procedures to choose a function and analyze_function with its returned name or address for a complete dossier. Call binary_overview when target metadata or inventory counts are useful. Use inspect_native_api when you need to reconstruct a native API boundary, and focused procedure tools when you need only one facet such as assembly, callers, or references.";
export interface CreateServerOptions {
readonly logger?: Logger;
@@ -336,10 +336,18 @@ const registerObservationTools = ({
...common,
electron: options.electronObservation,
electronActive: options.electronActiveObservation,
+ evidenceLookup:
+ session === undefined
+ ? undefined
+ : (evidenceId) => session.evidenceById(evidenceId),
});
registerJavaScriptRuntimeObservationTools(server, {
...common,
runtime: options.javascriptRuntimeObservation,
+ evidenceLookup:
+ session === undefined
+ ? undefined
+ : (evidenceId) => session.evidenceById(evidenceId),
});
registerApplicationTools(server, {
logger,
diff --git a/src/server/registerElectronTools.ts b/src/server/registerElectronTools.ts
index 259e8c74a5..58361d5d6f 100644
--- a/src/server/registerElectronTools.ts
+++ b/src/server/registerElectronTools.ts
@@ -15,9 +15,16 @@ import type { ProgressReporter } from "../application/ProgressReporter.js";
import { ELECTRON_TOOL_CONTRACTS } from "../contracts/electronToolContracts.js";
import type { ToolContract } from "../contracts/toolContracts.js";
import type { AnalysisError } from "../domain/errors.js";
+import { AnalysisInputError } from "../domain/errors.js";
+import { resolveEvidenceReferences } from "../application/EvidenceReferenceResolver.js";
import type { Evidence } from "../domain/evidence.js";
-import type { Result } from "../domain/result.js";
-import { inspectElectronPageInputSchema } from "../domain/electronObservation.js";
+import { err, type Result } from "../domain/result.js";
+import {
+ electronTargetListSchema,
+ inspectElectronPageInputSchema,
+ inspectElectronPageToolInputSchema,
+ listElectronTargetsInputSchema,
+} from "../domain/electronObservation.js";
import type { Logger } from "../logger.js";
import { mcpProgressReporter } from "./mcpProgress.js";
import { logToolExecution } from "./toolLogging.js";
@@ -30,6 +37,9 @@ interface ElectronToolRegistration {
readonly electronActive: ElectronActiveObservationPort | undefined;
readonly permissionAuthority: PermissionAuthority | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
+ readonly evidenceLookup:
+ | ((evidenceId: string) => Evidence | undefined)
+ | undefined;
}
interface ElectronToolContext {
@@ -78,16 +88,61 @@ export const registerElectronTools = (
options,
inspectContract,
{ input, context },
- (parsed, { signal, progress }) =>
- inspectElectronPage(
+ async (parsed, { signal, progress }) => {
+ const request = inspectElectronPageToolInputSchema.parse(parsed);
+ const source = resolveEvidenceReferences(
+ options.evidenceLookup,
+ [request.discovery_evidence_id],
+ [
+ {
+ operation: "list_electron_targets",
+ predicate: "rea.electron-target-list/v1",
+ },
+ ],
+ );
+ if (!source.ok) return err(source.error);
+ const discovery = source.value[0];
+ if (discovery === undefined) return err(invalidDiscoveryEvidence());
+ const scope = listElectronTargetsInputSchema.safeParse(
+ discovery.parameters,
+ );
+ const listed = electronTargetListSchema.safeParse(
+ discovery.normalized_result,
+ );
+ if (!scope.success || !listed.success)
+ return err(invalidDiscoveryEvidence());
+ if (
+ !listed.data.targets.some(
+ ({ target_id }) => target_id === request.target_id,
+ )
+ )
+ return {
+ ok: false,
+ error: new AnalysisInputError(
+ "inspect_electron_page",
+ undefined,
+ [
+ {
+ path: ["target_id"],
+ reason: "invalid_value",
+ message:
+ "Choose a target returned by the referenced discovery Evidence",
+ },
+ ],
+ ),
+ };
+ return inspectElectronPage(
options.electron,
options.permissionAuthority,
- inspectElectronPageInputSchema.parse(parsed),
- {
- signal,
- progress,
- },
- ),
+ inspectElectronPageInputSchema.parse({
+ ...scope.data,
+ target_id: request.target_id,
+ observation_ms: request.observation_ms,
+ include_script_sources: request.include_script_sources,
+ }),
+ { signal, progress },
+ );
+ },
),
);
server.registerTool(
@@ -133,6 +188,16 @@ export const registerElectronTools = (
);
};
+const invalidDiscoveryEvidence = () =>
+ new AnalysisInputError("inspect_electron_page", undefined, [
+ {
+ path: ["discovery_evidence_id"],
+ reason: "invalid_value",
+ message:
+ "The referenced Evidence is not a usable Electron target discovery",
+ },
+ ]);
+
const runElectronTool = async (
options: ElectronToolRegistration,
contract: ToolContract,
diff --git a/src/server/registerJavaScriptRuntimeObservationTools.ts b/src/server/registerJavaScriptRuntimeObservationTools.ts
index 81ddf7df33..7b036552ce 100644
--- a/src/server/registerJavaScriptRuntimeObservationTools.ts
+++ b/src/server/registerJavaScriptRuntimeObservationTools.ts
@@ -7,12 +7,19 @@ import {
observeJavaScriptRuntime,
} from "../application/JavaScriptRuntimeObservationService.js";
import type { PermissionAuthority } from "../application/PermissionAuthority.js";
+import { resolveEvidenceReferences } from "../application/EvidenceReferenceResolver.js";
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "../contracts/javascriptRuntimeObservationToolContracts.js";
import type { ToolContract } from "../contracts/toolContracts.js";
import type { AnalysisError } from "../domain/errors.js";
+import { AnalysisInputError } from "../domain/errors.js";
import type { Evidence } from "../domain/evidence.js";
-import type { Result } from "../domain/result.js";
-import { observeJavaScriptRuntimeInputSchema } from "../domain/javascriptRuntimeObservation.js";
+import { err, type Result } from "../domain/result.js";
+import {
+ javascriptRuntimeTargetListSchema,
+ listJavaScriptRuntimeTargetsInputSchema,
+ observeJavaScriptRuntimeInputSchema,
+ observeJavaScriptRuntimeToolInputSchema,
+} from "../domain/javascriptRuntimeObservation.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
@@ -23,6 +30,9 @@ interface RuntimeToolRegistration {
readonly runtime: JavaScriptRuntimeObservationPort | undefined;
readonly permissionAuthority: PermissionAuthority | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
+ readonly evidenceLookup:
+ | ((evidenceId: string) => Evidence | undefined)
+ | undefined;
}
/** Register passive Inspector tools even when policy keeps them unavailable. */
@@ -57,17 +67,72 @@ export const registerJavaScriptRuntimeObservationTools = (
options,
observeContract,
{ input, context },
- (parsed, signal) =>
- observeJavaScriptRuntime(
+ async (parsed, signal) => {
+ const request = observeJavaScriptRuntimeToolInputSchema.parse(parsed);
+ const source = resolveEvidenceReferences(
+ options.evidenceLookup,
+ [request.discovery_evidence_id],
+ [
+ {
+ operation: "list_javascript_runtime_targets",
+ predicate: "rea.javascript-runtime-target-list/v1",
+ },
+ ],
+ );
+ if (!source.ok) return err(source.error);
+ const discovery = source.value[0];
+ if (discovery === undefined) return err(invalidDiscoveryEvidence());
+ const scope = listJavaScriptRuntimeTargetsInputSchema.safeParse(
+ discovery.parameters,
+ );
+ const listed = javascriptRuntimeTargetListSchema.safeParse(
+ discovery.normalized_result,
+ );
+ if (!scope.success || !listed.success)
+ return err(invalidDiscoveryEvidence());
+ if (
+ !listed.data.targets.some(
+ ({ target_id }) => target_id === request.target_id,
+ )
+ )
+ return err(
+ new AnalysisInputError("observe_javascript_runtime", undefined, [
+ {
+ path: ["target_id"],
+ reason: "invalid_value",
+ message:
+ "Choose a target returned by the referenced discovery Evidence",
+ },
+ ]),
+ );
+ return observeJavaScriptRuntime(
options.runtime,
options.permissionAuthority,
- observeJavaScriptRuntimeInputSchema.parse(parsed),
+ observeJavaScriptRuntimeInputSchema.parse({
+ ...scope.data,
+ target_id: request.target_id,
+ ...(request.runtime_kind === undefined
+ ? {}
+ : { runtime_kind: request.runtime_kind }),
+ observation_ms: request.observation_ms,
+ }),
{ signal },
- ),
+ );
+ },
),
);
};
+const invalidDiscoveryEvidence = () =>
+ new AnalysisInputError("observe_javascript_runtime", undefined, [
+ {
+ path: ["discovery_evidence_id"],
+ reason: "invalid_value",
+ message:
+ "The referenced Evidence is not a usable Inspector target discovery",
+ },
+ ]);
+
const runRuntimeTool = async (
options: RuntimeToolRegistration,
contract: ToolContract,
diff --git a/tests/boundary/mcp/electronMcp.test.ts b/tests/boundary/mcp/electronMcp.test.ts
index afddfc78f7..8b9cd43526 100644
--- a/tests/boundary/mcp/electronMcp.test.ts
+++ b/tests/boundary/mcp/electronMcp.test.ts
@@ -96,11 +96,11 @@ it("exposes root-confined Electron discovery and inspection as Evidence", async
targets: [{ target_id: "electron-page" }],
},
});
+ const discoveryEvidenceId = evidenceIdFrom(listed.structuredContent);
const inspected = await client.callTool({
name: "inspect_electron_page",
arguments: {
- cdp_endpoint: browser.endpoint,
- allowed_file_roots: [root],
+ discovery_evidence_id: discoveryEvidenceId,
target_id: "electron-page",
observation_ms: 0,
include_script_sources: true,
@@ -369,3 +369,12 @@ const evidenceFor = (session: BinarySession, value: unknown) => {
if (evidence === undefined) throw new TypeError("Missing session Evidence");
return evidence;
};
+
+const evidenceIdFrom = (value: unknown): string => {
+ if (typeof value !== "object" || value === null)
+ throw new TypeError("Missing structured result");
+ const evidenceId = Reflect.get(value, "evidence_id");
+ if (typeof evidenceId !== "string")
+ throw new TypeError("Missing Evidence ID");
+ return evidenceId;
+};
diff --git a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts
index a7e3742a0d..4a25bb4f04 100644
--- a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts
+++ b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts
@@ -81,13 +81,12 @@ describe("JavaScript runtime observation MCP tools", () => {
targets: [{ target_id: inspector.targetId }],
},
});
+ const discoveryEvidenceId = evidenceIdFrom(listed.structuredContent);
const observedRuntime = await client.callTool({
name: "observe_javascript_runtime",
arguments: {
- inspector_endpoint: inspector.endpoint,
- allowed_file_roots: [root],
- allowed_origins: [],
+ discovery_evidence_id: discoveryEvidenceId,
target_id: inspector.targetId,
runtime_kind: "node",
observation_ms: 10,
diff --git a/tests/boundary/mcp/serverWorkflowMcp.test.ts b/tests/boundary/mcp/serverWorkflowMcp.test.ts
index 48b2afb158..6394d17283 100644
--- a/tests/boundary/mcp/serverWorkflowMcp.test.ts
+++ b/tests/boundary/mcp/serverWorkflowMcp.test.ts
@@ -49,7 +49,10 @@ it("advertises the native function discovery-to-dossier workflow", async () => {
});
expect(client.getInstructions()).toContain(
- "call binary_overview for target metadata and inventory counts, then search_procedures or list_procedures to choose a function",
+ "use search_procedures or list_procedures to choose a function",
+ );
+ expect(client.getInstructions()).toContain(
+ "Call binary_overview when target metadata or inventory counts are useful",
);
const tools = new Map(