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(