refactor(inputs): own absolute path validation in shared contracts

This commit is contained in:
morluto
2026-10-22 08:13:46 +08:00
parent c5c5ff45aa
commit cdd718ffb0
63 changed files with 327 additions and 408 deletions
-10
View File
@@ -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],
@@ -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(
@@ -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<null, AnalysisError> => {
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"],
@@ -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);
@@ -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" }],
},
});
},
);
@@ -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(
@@ -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);
@@ -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" }],
},
});
},
);
@@ -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(
@@ -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;
@@ -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);
@@ -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);
-12
View File
@@ -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);
@@ -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
-12
View File
@@ -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) {
@@ -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;
@@ -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;
@@ -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<AnalysisInputIssue>(({ 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;
@@ -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"] }),
]),
);
},
);
+10 -3
View File
@@ -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
+5 -2
View File
@@ -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()
+5 -2
View File
@@ -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",
),
});
+2 -2
View File
@@ -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
+5 -2
View File
@@ -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
+6 -3
View File
@@ -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",
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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(),
+5 -2
View File
@@ -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",
),
});
+12 -3
View File
@@ -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(
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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()
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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),
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+3 -13
View File
@@ -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));
};
+22 -5
View File
@@ -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,
);
},
),
);
},
);
};
+5 -2
View File
@@ -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");
+2 -2
View File
@@ -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),
);
};
+5 -2
View File
@@ -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),
);
};
+2 -2
View File
@@ -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),
);
};
+2 -2
View File
@@ -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),
);
};
+2 -2
View File
@@ -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),
);
};
+56 -68
View File
@@ -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<typeof withAdmittedAnalysis>,
contract: ReturnType<typeof toolContract<"find_changed_behavior">>,
): 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,
);
},
),
);
};
+2 -6
View File
@@ -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),
);
};
+2 -2
View File
@@ -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),
);
};
+2 -2
View File
@@ -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),
);
};
+2 -2
View File
@@ -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),
);
};
+5 -2
View File
@@ -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");
+2 -2
View File
@@ -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),
);
};
+5 -2
View File
@@ -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),
);
};
+2 -2
View File
@@ -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),
);
};
+3 -11
View File
@@ -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));
};
+2 -2
View File
@@ -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),
);
};
+17 -11
View File
@@ -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 extends ToolContract>(
contract: Contract,
@@ -32,3 +21,20 @@ export const toolRegistrationOptions = <Contract extends ToolContract>(
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 };
};
@@ -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<Record<string, unknown>>;
readonly invalid: Readonly<Record<string, unknown>>;
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 {
@@ -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);
});
+7 -1
View File
@@ -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);
});
+2 -1
View File
@@ -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",
})),
);
}
+6 -1
View File
@@ -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 () => {
@@ -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 () => {