From cdd718ffb0bd0fc28b0d54f89cfae5d7661a8fd9 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:13:46 +0800 Subject: [PATCH] refactor(inputs): own absolute path validation in shared contracts --- src/application/WebModuleTraceService.ts | 10 -- src/application/WebNetworkCaptureService.ts | 24 ---- src/application/WebSourceLocationService.ts | 10 -- .../artifacts/DesktopApplicationService.ts | 11 -- .../BinaryLayoutService.test.ts | 10 +- .../binaryDiagnostics/BinaryLayoutService.ts | 12 -- .../binaryDiagnostics/PeResourcesService.ts | 12 -- .../RecordedCrashService.test.ts | 10 +- .../binaryDiagnostics/RecordedCrashService.ts | 12 -- .../captures/PacketCaptureService.ts | 12 -- .../electron/ElectronFuseService.ts | 13 -- src/application/evm/EvmInterfaceService.ts | 12 -- src/application/go/GoBinaryService.ts | 12 -- .../il2cpp/Il2cppMetadataService.ts | 14 -- src/application/rust/RustBinaryService.ts | 12 -- .../signals/SignalRecordingService.ts | 12 -- .../sqlite/SqliteDatabaseService.ts | 12 -- src/application/wasm/WasmArtifactService.ts | 26 ---- src/domain/absoluteAnalysisInputPaths.test.ts | 32 +++++ src/domain/captures/packetCapture.ts | 13 +- src/domain/desktopApplication.ts | 7 +- src/domain/electron/electronFuses.ts | 7 +- src/domain/evm/evmInterface.ts | 4 +- src/domain/go/goBinary.ts | 7 +- src/domain/il2cpp/il2cppMetadata.ts | 9 +- src/domain/native/binaryLayout.ts | 4 +- src/domain/native/peResources.ts | 4 +- src/domain/native/recordedCrash.ts | 4 +- src/domain/protobufWire.ts | 4 +- src/domain/rust/rustBinary.ts | 7 +- src/domain/signals/signalRecording.ts | 15 ++- src/domain/sqlite/sqliteDatabase.ts | 4 +- src/domain/wasm/wasmArtifact.ts | 8 +- src/domain/webModuleTrace.ts | 6 +- src/domain/webNetworkCapture.ts | 4 +- src/domain/webScriptArtifacts.ts | 4 +- src/domain/webSourceLocation.ts | 4 +- src/server/registerAnalysisViewTool.ts | 16 +-- src/server/registerBinaryDiagnosticsTools.ts | 27 +++- src/server/registerCaptureTools.ts | 7 +- src/server/registerDesktopApplicationTool.ts | 4 +- src/server/registerElectronTools.ts | 7 +- src/server/registerEvmTools.ts | 4 +- src/server/registerGoTools.ts | 4 +- src/server/registerIl2cppTools.ts | 4 +- src/server/registerInvestigationTools.ts | 124 ++++++++---------- src/server/registerPcapTools.ts | 8 +- src/server/registerPeResourcesTool.ts | 4 +- src/server/registerRecordedCrashTools.ts | 4 +- src/server/registerRustTools.ts | 4 +- src/server/registerSignalTools.ts | 7 +- src/server/registerSqliteTools.ts | 4 +- src/server/registerWasmTools.ts | 7 +- src/server/registerWebModuleTool.ts | 4 +- src/server/registerWebNetworkCaptureTool.ts | 14 +- src/server/registerWebSourceLocationTool.ts | 4 +- src/server/toolRegistrationOptions.ts | 28 ++-- .../mcp/absoluteLocalPathSchema.test.ts | 34 ++++- .../boundary/mcp/binaryDiagnosticsMcp.test.ts | 8 +- tests/boundary/mcp/recordedCrashMcp.test.ts | 8 +- tests/boundary/mcp/wasmArtifactMcp.test.ts | 3 +- tests/boundary/mcp/webModuleTraceMcp.test.ts | 7 +- .../boundary/mcp/webSourceLocationMcp.test.ts | 7 +- 63 files changed, 327 insertions(+), 408 deletions(-) create mode 100644 src/domain/absoluteAnalysisInputPaths.test.ts diff --git a/src/application/WebModuleTraceService.ts b/src/application/WebModuleTraceService.ts index c421cbe3a6..26c8f4ce5f 100644 --- a/src/application/WebModuleTraceService.ts +++ b/src/application/WebModuleTraceService.ts @@ -1,6 +1,5 @@ import { closeOwnedWebResources } from "./closeOwnedWebResources.js"; import { isDeepStrictEqual } from "node:util"; -import { isAbsolute } from "node:path"; import { z } from "zod"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { @@ -94,15 +93,6 @@ export class WebModuleTraceService { }), ); const input = parsed.data; - for (const [path, value] of [ - ["manifest_path", input.manifest_path], - ["import_map.path", input.import_map?.path], - ] as const) - if (value !== undefined && !isAbsolute(value)) - return invalid( - path.split("."), - "Expected an absolute filesystem path on this host.", - ); for (const [path, value] of [ ["importer_url", input.importer_url], ["import_map.base_url", input.import_map?.base_url], diff --git a/src/application/WebNetworkCaptureService.ts b/src/application/WebNetworkCaptureService.ts index 9bda127300..8ea9f98e4a 100644 --- a/src/application/WebNetworkCaptureService.ts +++ b/src/application/WebNetworkCaptureService.ts @@ -1,7 +1,6 @@ import { redactExplicitFailure } from "../domain/explicitSensitiveFailure.js"; import { excludeCaptureCoordinates } from "../domain/webNetworkCaptureCoordinates.js"; import { redactExplicitText } from "../domain/explicitSensitiveValues.js"; -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "./AnalysisProvider.js"; import type { WebNetworkCapturePort } from "./WebNetworkCapturePort.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; @@ -57,16 +56,6 @@ export class WebNetworkCaptureService { }), ); const input = parsed.data; - if (!isAbsolute(input.capture_path)) - return failure( - new AnalysisInputError(OPERATION, undefined, [ - { - path: ["capture_path"], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); if ( input.record_ordinals !== undefined && new Set(input.record_ordinals).size !== input.record_ordinals.length @@ -91,19 +80,6 @@ export class WebNetworkCaptureService { }, ]), ); - if ( - input.protobuf?.schema !== undefined && - !isAbsolute(input.protobuf.schema.descriptor_set_path) - ) - return failure( - new AnalysisInputError(OPERATION, undefined, [ - { - path: ["protobuf", "schema", "descriptor_set_path"], - reason: "invalid_format", - message: "Expected an absolute descriptor-set path on this host.", - }, - ]), - ); const loaded = await this.port.inspect(input, options); if (!loaded.ok) { const partial = webNetworkCaptureSchema.safeParse( diff --git a/src/application/WebSourceLocationService.ts b/src/application/WebSourceLocationService.ts index ed60a5c226..b40ad21870 100644 --- a/src/application/WebSourceLocationService.ts +++ b/src/application/WebSourceLocationService.ts @@ -1,6 +1,5 @@ import { closeOwnedWebResources } from "./closeOwnedWebResources.js"; import { createHash } from "node:crypto"; -import { isAbsolute } from "node:path"; import { z } from "zod"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { @@ -209,15 +208,6 @@ const invalid = ( const admitContext = ( input: WebSourceLocationInput, ): Result => { - for (const [path, value] of [ - ["manifest_path", input.manifest_path], - ["source_map.path", input.source_map.path], - ] as const) - if (!isAbsolute(value)) - return invalid( - path.split("."), - "Expected an absolute filesystem path on this host.", - ); if (!URL.canParse(input.source_map.url)) return invalid( ["source_map", "url"], diff --git a/src/application/artifacts/DesktopApplicationService.ts b/src/application/artifacts/DesktopApplicationService.ts index 851e00cd5f..0900949ba6 100644 --- a/src/application/artifacts/DesktopApplicationService.ts +++ b/src/application/artifacts/DesktopApplicationService.ts @@ -1,4 +1,3 @@ -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { DesktopApplicationPort } from "./DesktopApplicationPort.js"; import { ARTIFACT_GRAPH_PROVIDER } from "../InvestigationProviders.js"; @@ -41,16 +40,6 @@ export class DesktopApplicationService { cause: parsed.error, }), ); - if (!isAbsolute(parsed.data.path)) - return err( - new AnalysisInputError(operation, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute directory path on this host.", - }, - ]), - ); const result = await this.reader.inspect(parsed.data, options); if (!result.ok) return result; const report = desktopApplicationSchema.safeParse(result.value); diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts index 3ab12cfe26..bd4fd7fc52 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.test.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.test.ts @@ -25,10 +25,18 @@ it.each([ throw new Error("must not run"); }, }); - expect(await service.inspect(input)).toMatchObject({ + const result = await service.inspect(input); + expect(result).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" }, }); + if (input.path === "relative.elf") + expect(result).toMatchObject({ + ok: false, + error: { + issues: [{ path: ["path"], reason: "invalid_format" }], + }, + }); }, ); diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.ts b/src/application/binaryDiagnostics/BinaryLayoutService.ts index d3315309a6..903171394b 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.ts @@ -1,10 +1,8 @@ -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { BinaryLayoutPort } from "./BinaryLayoutPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; @@ -66,16 +64,6 @@ export class BinaryLayoutService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(OPERATION, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); const inspected = await inspect(input.data); if (!inspected.ok) return inspected; const diagnostics = binaryLayoutSchema.shape.diagnostics.safeParse( diff --git a/src/application/binaryDiagnostics/PeResourcesService.ts b/src/application/binaryDiagnostics/PeResourcesService.ts index c818ff1573..51a8a56206 100644 --- a/src/application/binaryDiagnostics/PeResourcesService.ts +++ b/src/application/binaryDiagnostics/PeResourcesService.ts @@ -1,10 +1,8 @@ -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { PeResourcesPort } from "./PeResourcesPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; @@ -47,16 +45,6 @@ export class PeResourcesService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(operation, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); const result = await this.provider.inspect(input.data, options); if (!result.ok) return result; const report = peResourcesSchema.safeParse(result.value); diff --git a/src/application/binaryDiagnostics/RecordedCrashService.test.ts b/src/application/binaryDiagnostics/RecordedCrashService.test.ts index 9c807a2822..5433257b90 100644 --- a/src/application/binaryDiagnostics/RecordedCrashService.test.ts +++ b/src/application/binaryDiagnostics/RecordedCrashService.test.ts @@ -70,10 +70,18 @@ it.each([ throw new Error("must not run"); }, }); - expect(await service.inspect(input)).toMatchObject({ + const result = await service.inspect(input); + expect(result).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" }, }); + if (input.path === "relative.elf") + expect(result).toMatchObject({ + ok: false, + error: { + issues: [{ path: ["path"], reason: "invalid_format" }], + }, + }); }, ); diff --git a/src/application/binaryDiagnostics/RecordedCrashService.ts b/src/application/binaryDiagnostics/RecordedCrashService.ts index 0aa9bbcf7a..add5700744 100644 --- a/src/application/binaryDiagnostics/RecordedCrashService.ts +++ b/src/application/binaryDiagnostics/RecordedCrashService.ts @@ -1,10 +1,8 @@ -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { RecordedCrashPort } from "./RecordedCrashPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; @@ -67,16 +65,6 @@ export class RecordedCrashService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(OPERATION, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); const inspected = await inspect(input.data); if (!inspected.ok) return inspected; const diagnostics = recordedCrashSchema.shape.diagnostics.safeParse( diff --git a/src/application/captures/PacketCaptureService.ts b/src/application/captures/PacketCaptureService.ts index 741270e936..b6435b1a7c 100644 --- a/src/application/captures/PacketCaptureService.ts +++ b/src/application/captures/PacketCaptureService.ts @@ -4,7 +4,6 @@ import { type InspectPacketFieldsInput, } from "../../domain/captures/packetFields.js"; import type { JsonValue } from "../../domain/jsonValue.js"; -import { isAbsolute } from "node:path"; import type { AnalysisExecution, ExecutionOptions, @@ -12,7 +11,6 @@ import type { import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; @@ -59,16 +57,6 @@ export class PacketCaptureService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(operation, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute local path.", - }, - ]), - ); const decoded = await this.provider.inspect(input.data, options); if (!decoded.ok) return decoded; const execution = decoded.value; diff --git a/src/application/electron/ElectronFuseService.ts b/src/application/electron/ElectronFuseService.ts index 9bbd125fb1..dbe5429203 100644 --- a/src/application/electron/ElectronFuseService.ts +++ b/src/application/electron/ElectronFuseService.ts @@ -1,11 +1,8 @@ -import { isAbsolute } from "node:path"; - import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ElectronFusePort } from "./ElectronFusePort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { @@ -45,16 +42,6 @@ export class ElectronFuseService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(OPERATION, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); const inspected = await this.provider.inspect(input.data, options); if (!inspected.ok) return inspected; const report = electronFuseReportSchema.safeParse(inspected.value); diff --git a/src/application/evm/EvmInterfaceService.ts b/src/application/evm/EvmInterfaceService.ts index 7e358ad6f3..e217e5650e 100644 --- a/src/application/evm/EvmInterfaceService.ts +++ b/src/application/evm/EvmInterfaceService.ts @@ -1,11 +1,9 @@ -import { isAbsolute } from "node:path"; import { z } from "zod"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { EvmInterfacePort } from "./EvmInterfacePort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import type { Evidence } from "../../domain/evidence.js"; @@ -47,16 +45,6 @@ export class EvmInterfaceService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(OPERATION, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); const inspected = await this.provider.inspect(input.data, options); if (!inspected.ok) return inspected; const diagnostics = diagnosticsSchema.safeParse(inspected.value.result); diff --git a/src/application/go/GoBinaryService.ts b/src/application/go/GoBinaryService.ts index 2b549a3eb5..88ea351b4a 100644 --- a/src/application/go/GoBinaryService.ts +++ b/src/application/go/GoBinaryService.ts @@ -1,10 +1,8 @@ -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { GoBinaryPort } from "./GoBinaryPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; @@ -53,16 +51,6 @@ export class GoBinaryService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(OPERATION, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); const { detail, ...providerInput } = input.data; if (detail === "summary") { const ready = requireAnalysisViewRetention(OPERATION, retention); diff --git a/src/application/il2cpp/Il2cppMetadataService.ts b/src/application/il2cpp/Il2cppMetadataService.ts index 3c98744e73..b74fd9ae58 100644 --- a/src/application/il2cpp/Il2cppMetadataService.ts +++ b/src/application/il2cpp/Il2cppMetadataService.ts @@ -5,7 +5,6 @@ import type { Il2cppSeedWriterPort, Il2cppSeedReceipt, } from "./Il2cppSeedWriterPort.js"; -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import { requireAnalysisViewRetention, @@ -111,19 +110,6 @@ export class Il2cppMetadataService { cause: input.error, }), ); - for (const field of ["path", "binary_path"] as const) { - const value = input.data[field]; - if (value !== undefined && !isAbsolute(value)) - return err( - new AnalysisInputError(OPERATION, undefined, [ - { - path: [field], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); - } if ( input.data.binary_architecture !== undefined && input.data.binary_path === undefined diff --git a/src/application/rust/RustBinaryService.ts b/src/application/rust/RustBinaryService.ts index 61e5c2d4aa..460dab04ad 100644 --- a/src/application/rust/RustBinaryService.ts +++ b/src/application/rust/RustBinaryService.ts @@ -1,10 +1,8 @@ -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { RustBinaryPort } from "./RustBinaryPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; @@ -52,16 +50,6 @@ export class RustBinaryService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(OPERATION, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); const { detail, view, ...parameters } = input.data; const compact = detail === "summary" || view !== undefined; if (compact) { diff --git a/src/application/signals/SignalRecordingService.ts b/src/application/signals/SignalRecordingService.ts index e97b8bdc33..3b433c26e2 100644 --- a/src/application/signals/SignalRecordingService.ts +++ b/src/application/signals/SignalRecordingService.ts @@ -4,7 +4,6 @@ import { type InspectSignalDecodersInput, } from "../../domain/signals/signalDecoders.js"; import type { JsonValue } from "../../domain/jsonValue.js"; -import { isAbsolute } from "node:path"; import type { AnalysisExecution, ExecutionOptions, @@ -12,7 +11,6 @@ import type { import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; @@ -59,16 +57,6 @@ export class SignalRecordingService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(operation, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute local path.", - }, - ]), - ); const decoded = await this.provider.decode(input.data, options); if (!decoded.ok) return decoded; const execution = decoded.value; diff --git a/src/application/sqlite/SqliteDatabaseService.ts b/src/application/sqlite/SqliteDatabaseService.ts index 1798fb172e..024a92c946 100644 --- a/src/application/sqlite/SqliteDatabaseService.ts +++ b/src/application/sqlite/SqliteDatabaseService.ts @@ -1,10 +1,8 @@ -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { SqliteDatabasePort } from "./SqliteDatabasePort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, } from "../../domain/analysisErrorCore.js"; import { @@ -87,16 +85,6 @@ export class SqliteDatabaseService { cause: input.error, }), ); - if (!isAbsolute(input.data.path)) - return err( - new AnalysisInputError(OPERATION, undefined, [ - { - path: ["path"], - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ]), - ); const inspected = await this.provider.inspect(input.data, options); if (!inspected.ok) return inspected; const execution = inspected.value; diff --git a/src/application/wasm/WasmArtifactService.ts b/src/application/wasm/WasmArtifactService.ts index 99d3eb9482..a77223d1f6 100644 --- a/src/application/wasm/WasmArtifactService.ts +++ b/src/application/wasm/WasmArtifactService.ts @@ -1,12 +1,9 @@ -import { isAbsolute } from "node:path"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { WasmArtifactPort } from "./WasmArtifactPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisOutputError, - type AnalysisInputIssue, } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { @@ -48,29 +45,6 @@ export class WasmArtifactService { cause: input.error, }), ); - const pathIssues = [ - { value: input.data.path, path: ["path"] }, - ...input.data.glue_paths.map((value, index) => ({ - value, - path: ["glue_paths", index], - })), - ...input.data.candidate_paths.map((value, index) => ({ - value, - path: ["candidate_paths", index], - })), - ].flatMap(({ value, path }) => - isAbsolute(value) - ? [] - : [ - { - path, - reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", - }, - ], - ); - if (pathIssues.length > 0) - return err(new AnalysisInputError(OPERATION, undefined, pathIssues)); const inspected = await this.provider.inspect(input.data, options); if (!inspected.ok) return inspected; const execution = inspected.value; diff --git a/src/domain/absoluteAnalysisInputPaths.test.ts b/src/domain/absoluteAnalysisInputPaths.test.ts new file mode 100644 index 0000000000..b4ce3eea73 --- /dev/null +++ b/src/domain/absoluteAnalysisInputPaths.test.ts @@ -0,0 +1,32 @@ +import { expect, it } from "vitest"; +import { inspectGoBinaryInputSchema } from "./go/goBinary.js"; +import { inspectRustBinaryRequestSchema } from "./rust/rustBinaryQuery.js"; +import { inspectEvmInterfaceInputSchema } from "./evm/evmInterface.js"; +import { inspectSqliteDatabaseInputSchema } from "./sqlite/sqliteDatabase.js"; +import { inspectPeResourcesInputSchema } from "./native/peResources.js"; +import { inspectElectronFusesInputSchema } from "./electron/electronFuses.js"; +import { inspectPacketCaptureInputSchema } from "./captures/packetCapture.js"; +import { decodeSignalRecordingInputSchema } from "./signals/signalRecording.js"; + +it.each([ + ["Go binary", inspectGoBinaryInputSchema], + ["Rust binary", inspectRustBinaryRequestSchema], + ["EVM interface", inspectEvmInterfaceInputSchema], + ["SQLite database", inspectSqliteDatabaseInputSchema], + ["PE resources", inspectPeResourcesInputSchema], + ["Electron fuses", inspectElectronFusesInputSchema], + ["packet capture", inspectPacketCaptureInputSchema], + ["signal recording", decodeSignalRecordingInputSchema], +] as const)( + "reports relative %s paths as a path format issue", + (_name, schema) => { + const parsed = schema.safeParse({ path: "relative/input" }); + expect(parsed.success).toBe(false); + if (!parsed.success) + expect(parsed.error.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "invalid_format", path: ["path"] }), + ]), + ); + }, +); diff --git a/src/domain/captures/packetCapture.ts b/src/domain/captures/packetCapture.ts index 3f7c0af792..2ff565b9f3 100644 --- a/src/domain/captures/packetCapture.ts +++ b/src/domain/captures/packetCapture.ts @@ -1,6 +1,9 @@ import { z } from "zod"; import { fixedTupleSchema } from "../fixedTupleSchema.js"; -import { localPathStringSchema } from "../localPath.js"; +import { + absoluteLocalPathStringSchema, + localPathStringSchema, +} from "../localPath.js"; const argument = z .string() @@ -82,10 +85,14 @@ export const packetCaptureSelectorBudgetIssue = { message: "Packet selectors exceed the 32 KiB encoded argument budget; reduce fields, decode-as rules or filter text.", }; -export const inspectPacketCaptureInputSchema = captureInputShape.refine( +const packetCaptureSelectionSchema = captureInputShape.refine( withinPacketCaptureSelectorBudget, packetCaptureSelectorBudgetIssue, ); +export const inspectPacketCaptureInputSchema = + packetCaptureSelectionSchema.safeExtend({ + path: absoluteLocalPathStringSchema.describe("Absolute local capture path"), + }); export const packetCaptureOptionsSchema = captureInputShape .omit({ path: true }) .refine(withinPacketCaptureSelectorBudget, packetCaptureSelectorBudgetIssue); @@ -106,7 +113,7 @@ export const packetCaptureSchema = z sha256: z.string().regex(/^[a-f0-9]{64}$/), bytes: z.number().int().nonnegative(), }), - selection: inspectPacketCaptureInputSchema, + selection: packetCaptureSelectionSchema, packets: z.array(packetFieldsSchema), output_window: z.strictObject({ matching_packets: z diff --git a/src/domain/desktopApplication.ts b/src/domain/desktopApplication.ts index 9091ec04d9..38f7eca71c 100644 --- a/src/domain/desktopApplication.ts +++ b/src/domain/desktopApplication.ts @@ -1,6 +1,9 @@ import { z } from "zod"; import { digestSchema, prefixedDigestSchema } from "./digests.js"; -import { localPathStringSchema } from "./localPath.js"; +import { + absoluteLocalPathStringSchema, + localPathStringSchema, +} from "./localPath.js"; import { artifactTraversalOmissionKindSchema } from "./artifactGraph.js"; export const desktopFamilySchema = z.enum([ @@ -13,7 +16,7 @@ export const desktopFamilySchema = z.enum([ "electron", ]); export const inspectDesktopApplicationInputSchema = z.strictObject({ - path: localPathStringSchema, + path: absoluteLocalPathStringSchema, max_entries: z.number().int().positive().max(100_000).default(20_000), max_probe_bytes: z .number() diff --git a/src/domain/electron/electronFuses.ts b/src/domain/electron/electronFuses.ts index ffacd44819..6755149847 100644 --- a/src/domain/electron/electronFuses.ts +++ b/src/domain/electron/electronFuses.ts @@ -1,7 +1,10 @@ import { z } from "zod"; import { digestSchema } from "../digests.js"; -import { localPathStringSchema } from "../localPath.js"; +import { + absoluteLocalPathStringSchema, + localPathStringSchema, +} from "../localPath.js"; /** ASCII sentinel that introduces an Electron fuse wire in an Electron binary. */ export const ELECTRON_FUSE_SENTINEL = "dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX"; @@ -158,7 +161,7 @@ export const electronFuseReportSchema = z }); export const inspectElectronFusesInputSchema = z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute filesystem path to a macOS .app bundle, Electron Framework binary, Windows PE executable, or Linux ELF executable", ), }); diff --git a/src/domain/evm/evmInterface.ts b/src/domain/evm/evmInterface.ts index d1c7ab16bf..01a15b15d8 100644 --- a/src/domain/evm/evmInterface.ts +++ b/src/domain/evm/evmInterface.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { localPathStringSchema } from "../localPath.js"; +import { absoluteLocalPathStringSchema } from "../localPath.js"; import { solidityMetadataSchema } from "./solidityMetadata.js"; const byteOffset = z.number().int().nonnegative(); @@ -12,7 +12,7 @@ export const MAX_EVM_TIMEOUT_MS = 2_147_483_647; /** Select the carrier representation explicitly; no RPC or deployed-contract lookup is implied. */ export const inspectEvmInterfaceInputSchema = z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute path to caller-selected local bytecode carrier", ), encoding: z diff --git a/src/domain/go/goBinary.ts b/src/domain/go/goBinary.ts index d60e9a7eeb..a14c50d840 100644 --- a/src/domain/go/goBinary.ts +++ b/src/domain/go/goBinary.ts @@ -1,7 +1,10 @@ import { z } from "zod"; import { BINARY_ARCHITECTURES } from "../binaryTargetTypes.js"; import { digestSchema } from "../digests.js"; -import { localPathStringSchema } from "../localPath.js"; +import { + absoluteLocalPathStringSchema, + localPathStringSchema, +} from "../localPath.js"; import { canonicalBase64Schema } from "../stringPatterns.js"; /** Map reader architecture names into Evidence identity; unknown names stay unknown. */ @@ -46,7 +49,7 @@ export const goModuleMetadataSchema = z.strictObject({ /** One exact local artifact, independent of any active disassembler target. */ export const inspectGoBinaryInputSchema = z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute filesystem path to the selected executable or library", ), detail: z diff --git a/src/domain/il2cpp/il2cppMetadata.ts b/src/domain/il2cpp/il2cppMetadata.ts index 0817293458..88ca5a70ce 100644 --- a/src/domain/il2cpp/il2cppMetadata.ts +++ b/src/domain/il2cpp/il2cppMetadata.ts @@ -1,6 +1,9 @@ import { z } from "zod"; import { digestSchema } from "../digests.js"; -import { localPathStringSchema } from "../localPath.js"; +import { + absoluteLocalPathStringSchema, + localPathStringSchema, +} from "../localPath.js"; import { canonicalBase64Schema } from "../stringPatterns.js"; /** Unsigned little-endian signature observed at file offset zero. */ @@ -86,7 +89,7 @@ const indexWidth = z.union([z.literal(1), z.literal(2), z.literal(4)]); /** One explicit local file; detail selects delivery, never decoding coverage. */ export const inspectIl2cppMetadataInputSchema = z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute filesystem path to a Unity IL2CPP global-metadata.dat file", ), detail: z @@ -95,7 +98,7 @@ export const inspectIl2cppMetadataInputSchema = z.strictObject({ .describe( "summary (default) retains complete Evidence and returns identity and collection counts; complete returns every image, type, method, member, parameter and string-literal row inline", ), - binary_path: localPathStringSchema + binary_path: absoluteLocalPathStringSchema .optional() .describe( "Absolute path to the caller-selected matching 64-bit IL2CPP binary (Mach-O GameAssembly.dylib, PE GameAssembly.dll or ELF libil2cpp.so); static reading, never executed", diff --git a/src/domain/native/binaryLayout.ts b/src/domain/native/binaryLayout.ts index 7d164eb6a7..84b56be5f9 100644 --- a/src/domain/native/binaryLayout.ts +++ b/src/domain/native/binaryLayout.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { canBorrowParsedJsonSnapshot } from "../immutableJson.js"; import { pwntoolsTimeoutField } from "./nativeDeadlines.js"; -import { localPathStringSchema } from "../localPath.js"; +import { absoluteLocalPathStringSchema } from "../localPath.js"; import { canonicalBase64Schema } from "../stringPatterns.js"; const unsignedHex = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/); @@ -19,7 +19,7 @@ const name = z.strictObject({ /** Explicit local object selection, independent of an active disassembler target. */ export const inspectBinaryLayoutInputSchema = z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute filesystem path to the selected binary", ), pwntools_timeout_ms: pwntoolsTimeoutField, diff --git a/src/domain/native/peResources.ts b/src/domain/native/peResources.ts index f214bc772c..91b669f18a 100644 --- a/src/domain/native/peResources.ts +++ b/src/domain/native/peResources.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { localPathStringSchema } from "../localPath.js"; +import { absoluteLocalPathStringSchema } from "../localPath.js"; const uint = z.number().int().min(0).max(0xffffffff); const range = z.strictObject({ offset: uint, bytes: uint }); @@ -21,7 +21,7 @@ export const MAX_INSPECT_PE_DIRECTORY_ENTRIES = 65536; /** Complete bounded inspection of a caller-selected PE image, without execution. */ export const inspectPeResourcesInputSchema = z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute filesystem path to a PE image", ), max_file_bytes: z diff --git a/src/domain/native/recordedCrash.ts b/src/domain/native/recordedCrash.ts index 02168bb299..8a239433a8 100644 --- a/src/domain/native/recordedCrash.ts +++ b/src/domain/native/recordedCrash.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import type { BinaryArchitecture } from "../binaryTargetTypes.js"; import { pwntoolsTimeoutField } from "./nativeDeadlines.js"; -import { localPathStringSchema } from "../localPath.js"; +import { absoluteLocalPathStringSchema } from "../localPath.js"; import { canonicalBase64Schema } from "../stringPatterns.js"; import { recordedNoteCoverageIssues } from "./recordedCrashNoteCoverage.js"; @@ -141,7 +141,7 @@ const recordedCoreRegisterSets: Record< /** Select an explicit recording and optionally request debugger-derived mapping context. */ export const inspectRecordedCrashInputSchema = z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute filesystem path to a supplied Linux ELF core", ), include_debugger_context: z diff --git a/src/domain/protobufWire.ts b/src/domain/protobufWire.ts index 67c8f86860..e8c63a91e8 100644 --- a/src/domain/protobufWire.ts +++ b/src/domain/protobufWire.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { localPathStringSchema } from "./localPath.js"; +import { absoluteLocalPathStringSchema } from "./localPath.js"; /** Optional decoding applies only to explicitly selected capture records. */ export const protobufOptionsSchema = z.strictObject({ @@ -9,7 +9,7 @@ export const protobufOptionsSchema = z.strictObject({ body_pointers: z.array(z.string()).optional(), schema: z .strictObject({ - descriptor_set_path: localPathStringSchema, + descriptor_set_path: absoluteLocalPathStringSchema, message_type: z.string().min(1), }) .optional(), diff --git a/src/domain/rust/rustBinary.ts b/src/domain/rust/rustBinary.ts index 2047b627aa..f8d0136d09 100644 --- a/src/domain/rust/rustBinary.ts +++ b/src/domain/rust/rustBinary.ts @@ -1,6 +1,9 @@ import { z } from "zod"; import { digestSchema } from "../digests.js"; -import { localPathStringSchema } from "../localPath.js"; +import { + absoluteLocalPathStringSchema, + localPathStringSchema, +} from "../localPath.js"; import { BINARY_ARCHITECTURES } from "../binaryTargetTypes.js"; /** Preserve the format owner's architecture spelling separately from Evidence vocabulary. */ @@ -68,7 +71,7 @@ export const rustcCommitSchema = z.strictObject({ /** One explicit local executable or library, independent of a disassembler target. */ export const inspectRustBinaryInputSchema = z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute filesystem path to the selected executable or library", ), }); diff --git a/src/domain/signals/signalRecording.ts b/src/domain/signals/signalRecording.ts index d73fe24b66..483421c34c 100644 --- a/src/domain/signals/signalRecording.ts +++ b/src/domain/signals/signalRecording.ts @@ -1,5 +1,8 @@ import { z } from "zod"; -import { localPathStringSchema } from "../localPath.js"; +import { + absoluteLocalPathStringSchema, + localPathStringSchema, +} from "../localPath.js"; import { windowAnnotationText } from "./annotationTextWindow.js"; // These strings are single sigrok arguments, never shell fragments. @@ -61,10 +64,16 @@ export const signalRecordingSelectorBudgetIssue = { message: "Signal selectors exceed the 32 KiB encoded argument budget; reduce decoder, format or annotation selections.", }; -export const decodeSignalRecordingInputSchema = signalInputShape.refine( +const signalRecordingSelectionSchema = signalInputShape.refine( withinSignalRecordingSelectorBudget, signalRecordingSelectorBudgetIssue, ); +export const decodeSignalRecordingInputSchema = + signalRecordingSelectionSchema.safeExtend({ + path: absoluteLocalPathStringSchema.describe( + "Absolute local recording path", + ), + }); export const signalRecordingOptionsSchema = signalInputShape .omit({ path: true }) .refine( @@ -82,7 +91,7 @@ export const signalRecordingSchema = z sha256: z.string().regex(/^[a-f0-9]{64}$/), bytes: z.number().int().nonnegative(), }), - selection: decodeSignalRecordingInputSchema, + selection: signalRecordingSelectionSchema, annotations_text: z .string() .describe( diff --git a/src/domain/sqlite/sqliteDatabase.ts b/src/domain/sqlite/sqliteDatabase.ts index 90e4a8e660..903e5f6c7b 100644 --- a/src/domain/sqlite/sqliteDatabase.ts +++ b/src/domain/sqlite/sqliteDatabase.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { localPathStringSchema } from "../localPath.js"; +import { absoluteLocalPathStringSchema } from "../localPath.js"; import type { JsonValue } from "../jsonValue.js"; /** Format identity is shared by discovery, catalog, and observed SQLite results. */ @@ -28,7 +28,7 @@ const artifactSchema = z.strictObject({ /** Inspect one frozen database and its sibling WAL, with optional ordinary-table rows. */ export const inspectSqliteDatabaseInputSchema = z .strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute path to an offline SQLite database snapshot", ), worker_timeout_ms: workerTimeout diff --git a/src/domain/wasm/wasmArtifact.ts b/src/domain/wasm/wasmArtifact.ts index 52abf38db2..968be4e19e 100644 --- a/src/domain/wasm/wasmArtifact.ts +++ b/src/domain/wasm/wasmArtifact.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { createHash } from "node:crypto"; import type { EvidenceSubjectTarget } from "../evidence.js"; -import { localPathStringSchema } from "../localPath.js"; +import { absoluteLocalPathStringSchema } from "../localPath.js"; const digest = z.string().regex(/^[a-f0-9]{64}$/); const artifact = z.strictObject({ @@ -16,11 +16,11 @@ export const DEFAULT_WASM_COMMAND_TIMEOUT_MS = 30_000; export const MAX_WASM_COMMAND_TIMEOUT_MS = 2_147_483_647; /** All artifacts are explicitly selected local files; references never trigger acquisition. */ export const inspectWasmArtifactInputSchema = z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute selected local WASM artifact path", ), - glue_paths: z.array(localPathStringSchema).max(16).default([]), - candidate_paths: z.array(localPathStringSchema).max(16).default([]), + glue_paths: z.array(absoluteLocalPathStringSchema).max(16).default([]), + candidate_paths: z.array(absoluteLocalPathStringSchema).max(16).default([]), command_timeout_ms: z .number() .int() diff --git a/src/domain/webModuleTrace.ts b/src/domain/webModuleTrace.ts index 09343ef332..d3f79b7ec6 100644 --- a/src/domain/webModuleTrace.ts +++ b/src/domain/webModuleTrace.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { jsonObjectSchema } from "./jsonValue.js"; import { digestSchema } from "./digests.js"; -import { localPathStringSchema } from "./localPath.js"; +import { absoluteLocalPathStringSchema } from "./localPath.js"; import { exportedWebScriptSchema, webScriptExportManifestSchema, @@ -10,7 +10,7 @@ import { /** Inspect one captured script's native ES module relationships. */ export const webModuleTraceInputSchema = z.strictObject({ - manifest_path: localPathStringSchema.describe( + manifest_path: absoluteLocalPathStringSchema.describe( "Absolute path to an export_web_scripts manifest", ), script_index: z @@ -27,7 +27,7 @@ export const webModuleTraceInputSchema = z.strictObject({ ), import_map: z .strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute local import-map JSON path; no fetch", ), base_url: z diff --git a/src/domain/webNetworkCapture.ts b/src/domain/webNetworkCapture.ts index 224b75b42c..8519e4b182 100644 --- a/src/domain/webNetworkCapture.ts +++ b/src/domain/webNetworkCapture.ts @@ -7,7 +7,7 @@ import { selectedBrowserDeadlineMs, } from "./browserDeadline.js"; import { jsonValueSchema } from "./jsonValue.js"; -import { localPathStringSchema } from "./localPath.js"; +import { absoluteLocalPathStringSchema } from "./localPath.js"; import { protobufDecodingSchema, protobufOptionsSchema, @@ -49,7 +49,7 @@ export const WEB_NETWORK_CAPTURE_FORMATS = [ /** Inspect an explicit retained capture without contacting its recorded destinations. */ export const inspectWebNetworkCaptureInputSchema = z.strictObject({ - capture_path: localPathStringSchema.describe( + capture_path: absoluteLocalPathStringSchema.describe( "Absolute local HAR, mitmproxy, Fiddler SAZ or aioquic qlog capture path.", ), format: z.enum(WEB_NETWORK_CAPTURE_FORMATS), diff --git a/src/domain/webScriptArtifacts.ts b/src/domain/webScriptArtifacts.ts index 24be790019..4fe3350db9 100644 --- a/src/domain/webScriptArtifacts.ts +++ b/src/domain/webScriptArtifacts.ts @@ -1,11 +1,11 @@ import { z } from "zod"; import { digestSchema } from "./digests.js"; -import { localPathStringSchema } from "./localPath.js"; +import { absoluteLocalPathStringSchema } from "./localPath.js"; import { webScriptExportManifestSchema } from "./webScriptExport.js"; /** Select one source from an existing captured-script export. */ export const selectedWebScriptInputSchema = z.strictObject({ - manifest_path: localPathStringSchema.describe( + manifest_path: absoluteLocalPathStringSchema.describe( "Absolute export_web_scripts manifest path", ), script_index: z diff --git a/src/domain/webSourceLocation.ts b/src/domain/webSourceLocation.ts index 808132932e..1e46d30f86 100644 --- a/src/domain/webSourceLocation.ts +++ b/src/domain/webSourceLocation.ts @@ -6,7 +6,7 @@ import { optionalBrowserDeadlineMs, } from "./browserDeadline.js"; import { digestSchema } from "./digests.js"; -import { localPathStringSchema } from "./localPath.js"; +import { absoluteLocalPathStringSchema } from "./localPath.js"; import { exportedWebScriptSchema } from "./webScriptExport.js"; import { capturedWebManifestIdentitySchema, @@ -24,7 +24,7 @@ export const webSourcePositionSchema = z.strictObject({ export const webSourceLocationInputSchema = selectedWebScriptInputSchema.extend( { source_map: z.strictObject({ - path: localPathStringSchema.describe( + path: absoluteLocalPathStringSchema.describe( "Absolute local source-map path; no fetch", ), url: z diff --git a/src/server/registerAnalysisViewTool.ts b/src/server/registerAnalysisViewTool.ts index 253eae45ce..67df1ba83a 100644 --- a/src/server/registerAnalysisViewTool.ts +++ b/src/server/registerAnalysisViewTool.ts @@ -9,10 +9,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; -import { - toolRegistrationOptions, - applicationValidatedInputSchema, -} from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind selected-view projection to its named contract and session Evidence owner. */ export const registerAnalysisViewTool = ( @@ -24,7 +21,7 @@ export const registerAnalysisViewTool = ( artifactResources?: ArtifactResourceScope, ): void => { const contract = toolContract("inspect_analysis_view"); - const registration = toolRegistrationOptions(contract); + const registration = applicationToolRegistrationOptions(contract); const handler = createEvidenceToolHandler( server, (input, options) => @@ -38,12 +35,5 @@ export const registerAnalysisViewTool = ( recordEvidence, withAdmittedAnalysis, ); - server.registerTool( - contract.name, - { - ...registration, - inputSchema: applicationValidatedInputSchema(registration.inputSchema), - }, - handler(contract), - ); + server.registerTool(contract.name, registration, handler(contract)); }; diff --git a/src/server/registerBinaryDiagnosticsTools.ts b/src/server/registerBinaryDiagnosticsTools.ts index 99ca2cfd4f..b786495d90 100644 --- a/src/server/registerBinaryDiagnosticsTools.ts +++ b/src/server/registerBinaryDiagnosticsTools.ts @@ -7,7 +7,9 @@ import type { EvidenceWriter } from "../application/investigation/InvestigationR import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { logToolExecution } from "./toolLogging.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; +import { err } from "../domain/result.js"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { runAdmittedToolOperation } from "./admittedToolOperation.js"; @@ -23,9 +25,23 @@ export const registerBinaryDiagnosticsTools = ( const contract = toolContract("inspect_binary_layout"); server.registerTool( contract.name, - toolRegistrationOptions(contract), - async ({ detail, ...request }, context) => - runAdmittedToolOperation( + applicationToolRegistrationOptions(contract), + async (rawInput, context) => { + const parsed = contract.inputSchema.safeParse(rawInput); + if (!parsed.success) + return server.delivery.toCallToolResult( + err( + analysisInputErrorFromIssues( + contract.name, + parsed.error.issues, + rawInput, + { cause: parsed.error }, + ), + ), + contract, + ); + const { detail, ...request } = parsed.data; + return runAdmittedToolOperation( server, withAdmittedAnalysis, contract.name, @@ -55,6 +71,7 @@ export const registerBinaryDiagnosticsTools = ( recorded, ); }, - ), + ); + }, ); }; diff --git a/src/server/registerCaptureTools.ts b/src/server/registerCaptureTools.ts index 296faba5c8..8166f37c23 100644 --- a/src/server/registerCaptureTools.ts +++ b/src/server/registerCaptureTools.ts @@ -5,7 +5,10 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { + applicationToolRegistrationOptions, + toolRegistrationOptions, +} from "./toolRegistrationOptions.js"; /** Bind the named capture contract through shared admission and retained Evidence delivery. */ export const registerCaptureTools = ( @@ -25,7 +28,7 @@ export const registerCaptureTools = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); const discovery = toolContract("inspect_packet_fields"); diff --git a/src/server/registerDesktopApplicationTool.ts b/src/server/registerDesktopApplicationTool.ts index e3bd5e3438..8edcdecc48 100644 --- a/src/server/registerDesktopApplicationTool.ts +++ b/src/server/registerDesktopApplicationTool.ts @@ -6,7 +6,7 @@ import type { EvidenceWriter } from "../application/investigation/InvestigationR import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { toolContract } from "../contracts/toolContracts.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; import { requireAnalysisViewRetention, summarizeRetainedAnalysis, @@ -49,7 +49,7 @@ export const registerDesktopApplicationTool = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerElectronTools.ts b/src/server/registerElectronTools.ts index 80f4c41ee3..f5e6778054 100644 --- a/src/server/registerElectronTools.ts +++ b/src/server/registerElectronTools.ts @@ -30,7 +30,10 @@ import type { Result } from "../domain/result.js"; import type { Logger } from "pino"; import { mcpProgressReporter } from "./mcpProgress.js"; import { logToolExecution } from "./toolLogging.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { + applicationToolRegistrationOptions, + toolRegistrationOptions, +} from "./toolRegistrationOptions.js"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { runAdmittedToolOperation } from "./admittedToolOperation.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; @@ -160,7 +163,7 @@ const registerElectronFuseTool = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerEvmTools.ts b/src/server/registerEvmTools.ts index ce155a9e5e..53a792435f 100644 --- a/src/server/registerEvmTools.ts +++ b/src/server/registerEvmTools.ts @@ -13,7 +13,7 @@ import { evmInterfaceModule } from "../composition/evm.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind the named EVM contract to shared validation and session Evidence recording. */ export const registerEvmTools = ( @@ -55,7 +55,7 @@ export const registerEvmTools = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerGoTools.ts b/src/server/registerGoTools.ts index 5c5882a44a..6307ae1bcc 100644 --- a/src/server/registerGoTools.ts +++ b/src/server/registerGoTools.ts @@ -6,7 +6,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind the exact Go contract to its shared workflow and owning session Evidence ledger. */ export const registerGoTools = ( @@ -28,7 +28,7 @@ export const registerGoTools = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerIl2cppTools.ts b/src/server/registerIl2cppTools.ts index b874785922..643d851107 100644 --- a/src/server/registerIl2cppTools.ts +++ b/src/server/registerIl2cppTools.ts @@ -6,7 +6,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind the exact IL2CPP contract to its shared workflow and owning session Evidence ledger. */ export const registerIl2cppTools = ( @@ -28,7 +28,7 @@ export const registerIl2cppTools = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerInvestigationTools.ts b/src/server/registerInvestigationTools.ts index cf02243a67..234e60e043 100644 --- a/src/server/registerInvestigationTools.ts +++ b/src/server/registerInvestigationTools.ts @@ -22,7 +22,7 @@ import { STATIC_RUNTIME_PROVIDER, } from "../application/InvestigationProviders.js"; import { - applicationValidatedInputSchema, + applicationToolRegistrationOptions, toolRegistrationOptions, } from "./toolRegistrationOptions.js"; import { withAdmittedAnalysis } from "./analysisAdmission.js"; @@ -62,74 +62,62 @@ const registerChangedBehavior = ( admission: ReturnType, contract: ReturnType>, ): void => { - const registration = toolRegistrationOptions(contract); - server.registerTool( - contract.name, - { - ...registration, - inputSchema: applicationValidatedInputSchema(registration.inputSchema), - }, - async (input, context) => - runAdmittedToolOperation( - server, - admission, - contract.name, - context.mcpReq.signal, - async () => { - const computed = await runDerivedOperation( - context, - contract.name, - () => findChangedBehavior(input), - ); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const { result, comparisonEvidenceIds } = computed.value; - const closure = evidenceClosure(session, result.evidence_links); - if (!closure.ok) - return server.delivery.toCallToolResult(closure, contract); - const links = closure.value; - const evidence = createEvidence( - undefined, - CHANGED_BEHAVIOR_PROVIDER, - { - predicateType: "rea.changed-behavior", - operation: contract.name, - parameters: { - comparison_evidence_ids: [...comparisonEvidenceIds], + const registration = applicationToolRegistrationOptions(contract); + server.registerTool(contract.name, registration, async (input, context) => + runAdmittedToolOperation( + server, + admission, + contract.name, + context.mcpReq.signal, + async () => { + const computed = await runDerivedOperation(context, contract.name, () => + findChangedBehavior(input), + ); + if (!computed.ok) + return server.delivery.toCallToolResult(computed, contract); + const { result, comparisonEvidenceIds } = computed.value; + const closure = evidenceClosure(session, result.evidence_links); + if (!closure.ok) + return server.delivery.toCallToolResult(closure, contract); + const links = closure.value; + const evidence = createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, { + predicateType: "rea.changed-behavior", + operation: contract.name, + parameters: { + comparison_evidence_ids: [...comparisonEvidenceIds], + }, + result: jsonValueSchema.parse(result), + confidence: "derived", + authority: "analyst-inference", + limitations: result.limitations, + evidenceLinks: links, + }); + const recorded = recordWorkflowEvidence( + session, + evidence, + isIncomplete(result.behavior_status), + { + question: + "Did both versions behave the same under a complete controlled replay?", + domain: "changed-behavior", + requiredAuthority: "controlled-replay", + requiredConfidence: "observed", + probes: [ + { + operation: "capture_process_scenario", + rationale: + "Capture both versions under the same bounded scenario and environment.", }, - result: jsonValueSchema.parse(result), - confidence: "derived", - authority: "analyst-inference", - limitations: result.limitations, - evidenceLinks: links, - }, - ); - const recorded = recordWorkflowEvidence( - session, - evidence, - isIncomplete(result.behavior_status), - { - question: - "Did both versions behave the same under a complete controlled replay?", - domain: "changed-behavior", - requiredAuthority: "controlled-replay", - requiredConfidence: "observed", - probes: [ - { - operation: "capture_process_scenario", - rationale: - "Capture both versions under the same bounded scenario and environment.", - }, - ], - }, - ); - return server.delivery.toEvidenceToolResult( - evidence, - contract, - recorded, - ); - }, - ), + ], + }, + ); + return server.delivery.toEvidenceToolResult( + evidence, + contract, + recorded, + ); + }, + ), ); }; diff --git a/src/server/registerPcapTools.ts b/src/server/registerPcapTools.ts index b2ebd239a2..22045362c5 100644 --- a/src/server/registerPcapTools.ts +++ b/src/server/registerPcapTools.ts @@ -10,7 +10,7 @@ import { } from "./contractToolHandler.js"; import { toolRegistrationOptions, - applicationValidatedInputSchema, + applicationToolRegistrationOptions, } from "./toolRegistrationOptions.js"; import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; import { @@ -43,7 +43,6 @@ export const registerPcapTools = ( handler(clientContract), ); const contract = toolContract("inspect_pcap_capture"); - const registration = toolRegistrationOptions(contract); const capture = createEvidenceToolHandler( server, async (rawInput, options) => { @@ -74,10 +73,7 @@ export const registerPcapTools = ( ); server.registerTool( contract.name, - { - ...registration, - inputSchema: applicationValidatedInputSchema(registration.inputSchema), - }, + applicationToolRegistrationOptions(contract), capture(contract), ); }; diff --git a/src/server/registerPeResourcesTool.ts b/src/server/registerPeResourcesTool.ts index 9958be47bf..5f1bcbbfad 100644 --- a/src/server/registerPeResourcesTool.ts +++ b/src/server/registerPeResourcesTool.ts @@ -5,7 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Register the explicit-file PE resource workflow and retain its complete Evidence. */ export const registerPeResourcesTool = ( @@ -25,7 +25,7 @@ export const registerPeResourcesTool = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerRecordedCrashTools.ts b/src/server/registerRecordedCrashTools.ts index 104c355c62..0b3d1c18ce 100644 --- a/src/server/registerRecordedCrashTools.ts +++ b/src/server/registerRecordedCrashTools.ts @@ -6,7 +6,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind recorded crash inspection to its named contract and session Evidence owner. */ export const registerRecordedCrashTools = ( @@ -26,7 +26,7 @@ export const registerRecordedCrashTools = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerRustTools.ts b/src/server/registerRustTools.ts index fbf9099caf..a4d75f2666 100644 --- a/src/server/registerRustTools.ts +++ b/src/server/registerRustTools.ts @@ -6,7 +6,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind the exact Rust contract to its shared workflow and owning session Evidence ledger. */ export const registerRustTools = ( @@ -28,7 +28,7 @@ export const registerRustTools = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerSignalTools.ts b/src/server/registerSignalTools.ts index f833ffbd56..422458db51 100644 --- a/src/server/registerSignalTools.ts +++ b/src/server/registerSignalTools.ts @@ -5,7 +5,10 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { + applicationToolRegistrationOptions, + toolRegistrationOptions, +} from "./toolRegistrationOptions.js"; /** Bind the named signal contract through shared admission and retained Evidence delivery. */ export const registerSignalTools = ( @@ -25,7 +28,7 @@ export const registerSignalTools = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); const discovery = toolContract("inspect_signal_decoders"); diff --git a/src/server/registerSqliteTools.ts b/src/server/registerSqliteTools.ts index 8d2e07d320..40a0fbb012 100644 --- a/src/server/registerSqliteTools.ts +++ b/src/server/registerSqliteTools.ts @@ -5,7 +5,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind snapshot inspection to its named contract and additive session Evidence. */ export const registerSqliteTools = ( @@ -25,7 +25,7 @@ export const registerSqliteTools = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerWasmTools.ts b/src/server/registerWasmTools.ts index a7a056cc74..c00e140d1b 100644 --- a/src/server/registerWasmTools.ts +++ b/src/server/registerWasmTools.ts @@ -14,7 +14,10 @@ import { wasmArtifactModule } from "../composition/wasm.js"; import type { Logger } from "pino"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { + applicationToolRegistrationOptions, + toolRegistrationOptions, +} from "./toolRegistrationOptions.js"; /** Bind the named WASM contract to shared validation and session Evidence recording. */ export const registerWasmTools = ( @@ -86,7 +89,7 @@ export const registerWasmTools = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerWebModuleTool.ts b/src/server/registerWebModuleTool.ts index c26a647d76..f6fc305604 100644 --- a/src/server/registerWebModuleTool.ts +++ b/src/server/registerWebModuleTool.ts @@ -6,7 +6,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind the module relationship workflow to its exact canonical contract. */ export const registerWebModuleTool = ( @@ -26,7 +26,7 @@ export const registerWebModuleTool = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerWebNetworkCaptureTool.ts b/src/server/registerWebNetworkCaptureTool.ts index 3e9759504f..b3eac73816 100644 --- a/src/server/registerWebNetworkCaptureTool.ts +++ b/src/server/registerWebNetworkCaptureTool.ts @@ -14,10 +14,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { - toolRegistrationOptions, - applicationValidatedInputSchema, -} from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind historical inspection to its named contract and caller-owned Evidence writer. */ export const registerWebNetworkCaptureTool = ( @@ -29,11 +26,10 @@ export const registerWebNetworkCaptureTool = ( evidenceById?: EvidenceLookup, ): void => { const contract = toolContract("inspect_web_network_capture"); - const registration = toolRegistrationOptions(contract); + const registration = applicationToolRegistrationOptions(contract); // Keep the exact advertised contract; the shared service validates raw input // before effects so caller-declared sensitivity also covers invalid arguments. // SDK pre-validation otherwise emits unprojected issue paths as plain text. - const inputSchema = applicationValidatedInputSchema(registration.inputSchema); const handler = createEvidenceToolHandler( server, async (rawInput, options) => { @@ -65,9 +61,5 @@ export const registerWebNetworkCaptureTool = ( recordEvidence, withAdmittedAnalysis, ); - server.registerTool( - contract.name, - { ...registration, inputSchema }, - handler(contract), - ); + server.registerTool(contract.name, registration, handler(contract)); }; diff --git a/src/server/registerWebSourceLocationTool.ts b/src/server/registerWebSourceLocationTool.ts index c295757c7f..e952c2fa8d 100644 --- a/src/server/registerWebSourceLocationTool.ts +++ b/src/server/registerWebSourceLocationTool.ts @@ -6,7 +6,7 @@ import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; /** Bind source point tracing to its exact named contract and session Evidence owner. */ export const registerWebSourceLocationTool = ( @@ -26,7 +26,7 @@ export const registerWebSourceLocationTool = ( ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/toolRegistrationOptions.ts b/src/server/toolRegistrationOptions.ts index c943be3f25..618d15f335 100644 --- a/src/server/toolRegistrationOptions.ts +++ b/src/server/toolRegistrationOptions.ts @@ -5,17 +5,6 @@ import { toolOutputSchemaWithMetadata, } from "../contracts/toolSchemaMetadata.js"; -/** Preserve advertised contracts while the application owns structured input failures. */ -export const applicationValidatedInputSchema = ( - schema: StandardSchemaWithJSON, -): StandardSchemaWithJSON => ({ - "~standard": { - ...schema["~standard"], - vendor: "rea-application", - validate: (value: unknown) => ({ value }), - }, -}); - /** Project the canonical Zod contracts directly into SDK registration. */ export const toolRegistrationOptions = ( contract: Contract, @@ -32,3 +21,20 @@ export const toolRegistrationOptions = ( outputSchema: toolOutputSchemaWithMetadata(contract), annotations: contract.annotations, }); + +/** Register a workflow that validates raw input before effects and projects its own errors. */ +export const applicationToolRegistrationOptions = < + Contract extends ToolContract, +>( + contract: Contract, +) => { + const registration = toolRegistrationOptions(contract); + const inputSchema: StandardSchemaWithJSON = { + "~standard": { + ...registration.inputSchema["~standard"], + vendor: "rea-application", + validate: (value: unknown) => ({ value }), + }, + }; + return { ...registration, inputSchema }; +}; diff --git a/tests/boundary/mcp/absoluteLocalPathSchema.test.ts b/tests/boundary/mcp/absoluteLocalPathSchema.test.ts index 58ae31f7df..536f2c3f2e 100644 --- a/tests/boundary/mcp/absoluteLocalPathSchema.test.ts +++ b/tests/boundary/mcp/absoluteLocalPathSchema.test.ts @@ -4,6 +4,7 @@ import { resolve } from "node:path"; import { expect, it } from "vitest"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createServer } from "../../../src/server/createServer.js"; import { silentLogger } from "../../../src/logger.js"; @@ -12,6 +13,7 @@ interface PathInputCase { readonly field: string; readonly valid: Readonly>; readonly invalid: Readonly>; + readonly issuePath?: readonly string[]; } const createMcp = async ( @@ -66,6 +68,30 @@ const createMcp = async ( } }; +const standaloneAnalysisCases = ( + absolute: string, + relative: string, +): readonly PathInputCase[] => + [ + { name: "inspect_go_binary", input: {} }, + { name: "inspect_rust_binary", input: {} }, + { name: "inspect_evm_interface", input: { encoding: "hex" } }, + { name: "inspect_sqlite_database", input: {} }, + { name: "inspect_pe_resources", input: {} }, + { name: "inspect_electron_fuses", input: {} }, + { name: "inspect_packet_capture", input: { fields: ["ip.addr"] } }, + { + name: "decode_signal_recording", + input: { decoders: "uart:rx=RX:baudrate=9600" }, + }, + ].map(({ name, input }) => ({ + name, + field: "path", + valid: { ...input, path: absolute }, + invalid: { ...input, path: relative }, + issuePath: ["path"], + })); + const inputCases = ( absolute: string, relative: string, @@ -158,6 +184,7 @@ const inputCases = ( ], }, ), + ...standaloneAnalysisCases(absolute, relative), ...["executable_path", "application_path", "application_root"].map( (field) => pathCase( @@ -213,13 +240,18 @@ it("rejects relative paths through in-memory MCP handlers", async () => { const firmwareCalls: string[] = []; const { client, close } = await createMcp(firmwareCalls); try { - for (const { name, field, invalid } of inputCases( + for (const { name, field, invalid, issuePath } of inputCases( resolve("rea-absolute-input"), "relative-input", )) { const result = await client.callTool({ name, arguments: invalid }); expect(result.isError, `${name}.${field}`).toBe(true); expect(JSON.stringify(result), `${name}.${field}`).toContain("absolute"); + if (issuePath !== undefined) + expect(parseMcpToolError(result).error.details).toMatchObject({ + operation: name, + issues: [{ path: issuePath, reason: "invalid_format" }], + }); } expect(firmwareCalls).toEqual([]); } finally { diff --git a/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts b/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts index 51bda12c7f..c34dd4db3d 100644 --- a/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts +++ b/tests/boundary/mcp/binaryDiagnosticsMcp.test.ts @@ -17,12 +17,16 @@ import { it("advertises exact valid schemas and records inline layout without a binary target", async () => { const value = binaryLayoutFixture(); let providerClosed = false; + let providerCalls = 0; const service = new BinaryLayoutService({ inspectAndClose(input, options) { return this.inspect(input, options); }, identity: BINARY_LAYOUT_TEST_PROVIDER, - inspect: () => Promise.resolve(ok(value)), + inspect: () => { + providerCalls++; + return Promise.resolve(ok(value)); + }, close: async () => { providerClosed = true; }, @@ -51,6 +55,7 @@ it("advertises exact valid schemas and records inline layout without a binary ta ); const { ajv, inputSchema } = schemas; expect(ajv.validate(inputSchema, { path: value.artifact.path })).toBe(true); + expect(ajv.validate(inputSchema, { path: "relative.elf" })).toBe(false); expect( ajv.validate(inputSchema, { path: value.artifact.path, @@ -97,4 +102,5 @@ it("advertises exact valid schemas and records inline layout without a binary ta }) ).isError, ).toBe(true); + expect(providerCalls).toBe(1); }); diff --git a/tests/boundary/mcp/recordedCrashMcp.test.ts b/tests/boundary/mcp/recordedCrashMcp.test.ts index 227ca5bd1d..e7b0623e7f 100644 --- a/tests/boundary/mcp/recordedCrashMcp.test.ts +++ b/tests/boundary/mcp/recordedCrashMcp.test.ts @@ -15,12 +15,16 @@ import { it("advertises exact valid schemas and records inline recorded-core evidence without a binary target", async () => { const value = recordedCrashFixture(); let providerClosed = false; + let providerCalls = 0; const service = new RecordedCrashService({ inspectAndClose(input, options) { return this.inspect(input, options); }, identity: RECORDED_CRASH_TEST_PROVIDER, - inspect: () => Promise.resolve(ok(value)), + inspect: () => { + providerCalls++; + return Promise.resolve(ok(value)); + }, close: async () => { providerClosed = true; }, @@ -54,6 +58,7 @@ it("advertises exact valid schemas and records inline recorded-core evidence wit expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); expect(ajv.validate(inputSchema, { path: value.artifact.path })).toBe(true); + expect(ajv.validate(inputSchema, { path: "relative.elf" })).toBe(false); expect( ajv.validate(inputSchema, { path: value.artifact.path, @@ -85,4 +90,5 @@ it("advertises exact valid schemas and records inline recorded-core evidence wit }) ).isError, ).toBe(true); + expect(providerCalls).toBe(1); }); diff --git a/tests/boundary/mcp/wasmArtifactMcp.test.ts b/tests/boundary/mcp/wasmArtifactMcp.test.ts index a2d394a2d7..aefbb1ddfa 100644 --- a/tests/boundary/mcp/wasmArtifactMcp.test.ts +++ b/tests/boundary/mcp/wasmArtifactMcp.test.ts @@ -374,7 +374,8 @@ it("attributes rejected WASM paths to their selected fields before provider work paths.map((path) => ({ path, reason: "invalid_format", - message: "Expected an absolute filesystem path on this host.", + expected: "regex", + message: "must be an absolute local filesystem path", })), ); } diff --git a/tests/boundary/mcp/webModuleTraceMcp.test.ts b/tests/boundary/mcp/webModuleTraceMcp.test.ts index 89923cbed6..15b2542d89 100644 --- a/tests/boundary/mcp/webModuleTraceMcp.test.ts +++ b/tests/boundary/mcp/webModuleTraceMcp.test.ts @@ -85,7 +85,12 @@ it("publishes valid SDK schemas and records the named module trace without a bin name: "trace_web_module_imports", arguments: { ...args, manifest_path: "relative.json" }, }); - expect(invalid.isError).toBe(true); + expect(parseMcpToolError(invalid).error).toMatchObject({ + code: "invalid_request", + details: { + issues: [{ path: ["manifest_path"], reason: "invalid_format" }], + }, + }); }); it("retains the module resolver cleanup owner across failed server shutdown", async () => { diff --git a/tests/boundary/mcp/webSourceLocationMcp.test.ts b/tests/boundary/mcp/webSourceLocationMcp.test.ts index 0afa5d0cbe..f85b29e662 100644 --- a/tests/boundary/mcp/webSourceLocationMcp.test.ts +++ b/tests/boundary/mcp/webSourceLocationMcp.test.ts @@ -86,7 +86,12 @@ it("publishes valid SDK schemas and records the named module trace without a bin name: "trace_web_source_location", arguments: { ...args, manifest_path: "relative.json" }, }); - expect(invalid.isError).toBe(true); + expect(parseMcpToolError(invalid).error).toMatchObject({ + code: "invalid_request", + details: { + issues: [{ path: ["manifest_path"], reason: "invalid_format" }], + }, + }); }); it("preserves missing Linux ps capability and recovery through source-map tracing", async () => {