mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(native): consolidate observed address validation
This commit is contained in:
@@ -498,7 +498,7 @@ function hexBytes(bytes) {
|
||||
function assertProfile(profile) {
|
||||
assert.equal(
|
||||
profile.parameters.load_image_evidence,
|
||||
"independent-mz-mapping-relocations-v1",
|
||||
"independent-mz-mapping-relocations-v2",
|
||||
);
|
||||
assert.equal(
|
||||
profile.parameters.function_body_evidence,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** Observed hexadecimal coordinate; preserve padding and encoded space spelling. */
|
||||
export const nativeAddressObservationSchema = z
|
||||
.string()
|
||||
.regex(/^(?:(?:[A-Za-z0-9._~\x2d]|%[0-9A-F]{2})+:)?0x[0-9a-f]+$/u);
|
||||
|
||||
/** Canonical hexadecimal coordinate, retaining its optional address space. */
|
||||
export const nativeAddressSchema = z
|
||||
.string()
|
||||
|
||||
@@ -2,11 +2,11 @@ import { z } from "zod";
|
||||
import { nativeMetadataRecoverySummarySchema } from "./nativeMetadataRecovery.js";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
import { binaryArchitectureSchema } from "../rawBinary.js";
|
||||
import { nativeAddressObservationSchema } from "./nativeAddress.js";
|
||||
|
||||
const integer = z.int().min(0).max(Number.MAX_SAFE_INTEGER);
|
||||
const digest = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
const hexBytes = z.string().regex(/^(?:[a-f0-9]{2})*$/u);
|
||||
const address = z.string().min(1);
|
||||
|
||||
/** Measured provider image, without exporting the complete executable bytes. */
|
||||
export const nativeLoadImageObservationSchema = z.strictObject({
|
||||
@@ -14,7 +14,7 @@ export const nativeLoadImageObservationSchema = z.strictObject({
|
||||
executable_format: z.string().min(1),
|
||||
language_id: z.string().min(1),
|
||||
compiler_spec_id: z.string().min(1),
|
||||
image_base: address,
|
||||
image_base: nativeAddressObservationSchema,
|
||||
default_address_space: z.string().min(1),
|
||||
source_files: z.array(
|
||||
z.strictObject({
|
||||
@@ -27,8 +27,8 @@ export const nativeLoadImageObservationSchema = z.strictObject({
|
||||
mappings: z.array(
|
||||
z.strictObject({
|
||||
block: z.string(),
|
||||
start: address,
|
||||
end: address,
|
||||
start: nativeAddressObservationSchema,
|
||||
end: nativeAddressObservationSchema,
|
||||
address_space: z.string().min(1),
|
||||
initialized: z.boolean(),
|
||||
loaded: z.boolean(),
|
||||
@@ -41,7 +41,7 @@ export const nativeLoadImageObservationSchema = z.strictObject({
|
||||
),
|
||||
relocations: z.array(
|
||||
z.strictObject({
|
||||
address,
|
||||
address: nativeAddressObservationSchema,
|
||||
segment: integer.max(0xffff).nullable(),
|
||||
offset: integer.max(0xffff).nullable(),
|
||||
status: z.string().min(1),
|
||||
@@ -53,10 +53,10 @@ export const nativeLoadImageObservationSchema = z.strictObject({
|
||||
memory_bytes_hex: hexBytes.nullable(),
|
||||
}),
|
||||
),
|
||||
entry_points: z.array(address),
|
||||
entry_points: z.array(nativeAddressObservationSchema),
|
||||
entry_context: z.array(
|
||||
z.strictObject({
|
||||
address,
|
||||
address: nativeAddressObservationSchema,
|
||||
registers: z.array(
|
||||
z.strictObject({
|
||||
name: z.string().min(1),
|
||||
@@ -77,7 +77,7 @@ export const nativeLoadImageCheckSchema = z.strictObject({
|
||||
expected: jsonValueSchema,
|
||||
observed: jsonValueSchema,
|
||||
file_offset: integer.nullable(),
|
||||
address: address.nullable(),
|
||||
address: nativeAddressObservationSchema.nullable(),
|
||||
});
|
||||
|
||||
/** Read-only native image verification; verified applies only to declared coverage. */
|
||||
@@ -99,7 +99,7 @@ export const nativeLoadImageSchema = z.union([
|
||||
entry: z.strictObject({
|
||||
relative_segment: integer.max(0xffff),
|
||||
offset: integer.max(0xffff),
|
||||
linear_address: address,
|
||||
linear_address: nativeAddressObservationSchema,
|
||||
}),
|
||||
observations: nativeLoadImageObservationSchema,
|
||||
checks: z.array(nativeLoadImageCheckSchema),
|
||||
@@ -112,10 +112,10 @@ export const nativeLoadImageSchema = z.union([
|
||||
architecture: binaryArchitectureSchema,
|
||||
language_id: z.string().min(1),
|
||||
compiler_spec_id: z.string().min(1),
|
||||
base_address: address,
|
||||
base_address: nativeAddressObservationSchema,
|
||||
file_bytes: integer.min(1),
|
||||
entry: z.strictObject({
|
||||
address: address,
|
||||
address: nativeAddressObservationSchema,
|
||||
file_offset: z.literal(0),
|
||||
}),
|
||||
observations: nativeLoadImageObservationSchema,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { nativeAddressObservationSchema } from "../domain/native/nativeAddress.js";
|
||||
import { procedureInfoSchema } from "../domain/native/procedureInfo.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
@@ -38,7 +39,6 @@ import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"
|
||||
import {
|
||||
ghidraIdentifierSchema,
|
||||
ghidraInputAddressSchema,
|
||||
ghidraCanonicalAddressSchema,
|
||||
ghidraFunctionClassificationSchema as classification,
|
||||
ghidraFunctionBodySchema as ghidraFunctionBody,
|
||||
ghidraProcedureIdentitySchema as procedureIdentity,
|
||||
@@ -146,8 +146,8 @@ const referenceKind = availableReferenceKindSchema.safeExtend({
|
||||
});
|
||||
const referenceEdge = z
|
||||
.object({
|
||||
source_address: ghidraCanonicalAddressSchema,
|
||||
target_address: ghidraCanonicalAddressSchema,
|
||||
source_address: nativeAddressObservationSchema,
|
||||
target_address: nativeAddressObservationSchema,
|
||||
source_procedure: procedureIdentity.nullable(),
|
||||
target_procedure: procedureIdentity.nullable(),
|
||||
kind: referenceKind,
|
||||
@@ -161,7 +161,7 @@ const procedureReferences = z
|
||||
reference_kinds_available: z.boolean(),
|
||||
unresolved_calls: z.array(
|
||||
z.strictObject({
|
||||
address: ghidraCanonicalAddressSchema,
|
||||
address: nativeAddressObservationSchema,
|
||||
reason: z.string(),
|
||||
}),
|
||||
),
|
||||
@@ -170,7 +170,7 @@ const procedureReferences = z
|
||||
const procedureInfo = procedureInfoSchema
|
||||
.safeExtend({
|
||||
name: z.string().min(1),
|
||||
entrypoint: ghidraCanonicalAddressSchema,
|
||||
entrypoint: nativeAddressObservationSchema,
|
||||
length: z.int().min(0),
|
||||
locals: z.array(localVariable),
|
||||
classification,
|
||||
@@ -207,7 +207,7 @@ const ghidraNativeApiBoundary = nativeApiBoundarySchema.superRefine(
|
||||
]);
|
||||
if (
|
||||
addresses.some(
|
||||
(address) => !ghidraCanonicalAddressSchema.safeParse(address).success,
|
||||
(address) => !nativeAddressObservationSchema.safeParse(address).success,
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
@@ -233,7 +233,7 @@ const ghidraNativeValueFlow = nativeValueFlowSchema.superRefine(
|
||||
]);
|
||||
if (
|
||||
addresses.some(
|
||||
(address) => !ghidraCanonicalAddressSchema.safeParse(address).success,
|
||||
(address) => !nativeAddressObservationSchema.safeParse(address).success,
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
@@ -322,7 +322,7 @@ const ghidraFunctionDossier = functionDossierSchema
|
||||
];
|
||||
if (
|
||||
addresses.some(
|
||||
(address) => !ghidraCanonicalAddressSchema.safeParse(address).success,
|
||||
(address) => !nativeAddressObservationSchema.safeParse(address).success,
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
@@ -382,8 +382,8 @@ const resultSchemas = {
|
||||
}),
|
||||
analyze_function: ghidraFunctionDossier,
|
||||
procedure_assembly: z.string(),
|
||||
procedure_callees: z.array(ghidraCanonicalAddressSchema),
|
||||
procedure_callers: z.array(ghidraCanonicalAddressSchema),
|
||||
procedure_callees: z.array(nativeAddressObservationSchema),
|
||||
procedure_callers: z.array(nativeAddressObservationSchema),
|
||||
procedure_info: procedureInfo,
|
||||
procedure_pseudo_code: z.string().nullable(),
|
||||
read_function_instructions: ghidraFunctionInstructionWindow,
|
||||
@@ -391,7 +391,7 @@ const resultSchemas = {
|
||||
inspect_native_data_type: nativeDataTypeSchema,
|
||||
resolve_native_call_targets: nativeCallTargetsSchema,
|
||||
procedure_references: procedureReferences,
|
||||
xrefs: z.array(ghidraCanonicalAddressSchema),
|
||||
xrefs: z.array(nativeAddressObservationSchema),
|
||||
} satisfies Readonly<Record<GhidraFunctionOperation, z.ZodType>>;
|
||||
|
||||
const functionBoundaryObservationOperations: ReadonlySet<GhidraFunctionOperation> =
|
||||
@@ -414,8 +414,8 @@ const functionObservationEnvelope = z.strictObject({
|
||||
selection: z
|
||||
.strictObject({
|
||||
file_offset: z.int().min(0).max(Number.MAX_SAFE_INTEGER),
|
||||
mapped_address: ghidraCanonicalAddressSchema,
|
||||
procedure_address: ghidraCanonicalAddressSchema,
|
||||
mapped_address: nativeAddressObservationSchema,
|
||||
procedure_address: nativeAddressObservationSchema,
|
||||
})
|
||||
.exactOptional(),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { nativeAddressObservationSchema } from "../domain/native/nativeAddress.js";
|
||||
import { nativeByteReadSchema } from "../domain/native/nativeByteRead.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
@@ -147,10 +148,6 @@ export const parseGhidraInventoryInput = (
|
||||
return ok(parsed.data);
|
||||
};
|
||||
|
||||
export const ghidraCanonicalAddressSchema = z
|
||||
.string()
|
||||
.regex(/^(?:0x[0-9a-f]+|(?:[A-Za-z0-9._~-]|%[0-9A-F]{2})+:0x[0-9a-f]+)$/u);
|
||||
const canonicalAddress = ghidraCanonicalAddressSchema;
|
||||
const symbolFacts = z
|
||||
.object({
|
||||
primary: z.boolean(),
|
||||
@@ -164,7 +161,7 @@ const procedureFacts = z
|
||||
.object({
|
||||
external: z.boolean(),
|
||||
thunk: z.boolean(),
|
||||
thunk_target: canonicalAddress.nullable(),
|
||||
thunk_target: nativeAddressObservationSchema.nullable(),
|
||||
})
|
||||
.strict();
|
||||
/** Ghidra FunctionManager classification, independent of body ownership inference. */
|
||||
@@ -179,8 +176,8 @@ export const ghidraFunctionBodySchema = functionBodySchema.superRefine(
|
||||
body.provenance !== "ghidra-function-body-address-set" ||
|
||||
body.ranges.some(
|
||||
({ start, end }) =>
|
||||
!ghidraCanonicalAddressSchema.safeParse(start).success ||
|
||||
!ghidraCanonicalAddressSchema.safeParse(end).success,
|
||||
!nativeAddressObservationSchema.safeParse(start).success ||
|
||||
!nativeAddressObservationSchema.safeParse(end).success,
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
@@ -194,7 +191,7 @@ export const ghidraFunctionBodySchema = functionBodySchema.superRefine(
|
||||
/** Strict provider identity shared by inventory and function-analysis results. */
|
||||
export const ghidraProcedureIdentitySchema = z
|
||||
.object({
|
||||
address: ghidraCanonicalAddressSchema,
|
||||
address: nativeAddressObservationSchema,
|
||||
name: z.string().min(1),
|
||||
classification: ghidraFunctionClassificationSchema,
|
||||
body: ghidraFunctionBodySchema,
|
||||
@@ -221,7 +218,7 @@ const stringFacts = z
|
||||
})
|
||||
.strict();
|
||||
const baseItem = {
|
||||
address: canonicalAddress,
|
||||
address: nativeAddressObservationSchema,
|
||||
value: z.string(),
|
||||
};
|
||||
const symbolItem = z.object({ ...baseItem, symbol: symbolFacts }).strict();
|
||||
@@ -240,15 +237,15 @@ const availablePermissions = z
|
||||
const memoryRegion = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
start: canonicalAddress,
|
||||
end: canonicalAddress,
|
||||
start: nativeAddressObservationSchema,
|
||||
end: nativeAddressObservationSchema,
|
||||
readable: z.boolean(),
|
||||
writable: z.boolean(),
|
||||
executable: z.boolean(),
|
||||
permissions: availablePermissions,
|
||||
provenance: z.literal("ghidra-memory-block"),
|
||||
address_space: z.string().min(1),
|
||||
image_base: canonicalAddress,
|
||||
image_base: nativeAddressObservationSchema,
|
||||
initialized: z.boolean(),
|
||||
overlay: z.boolean(),
|
||||
})
|
||||
@@ -259,7 +256,7 @@ const segment = memoryRegion
|
||||
const containingProcedure = z.discriminatedUnion("found", [
|
||||
z
|
||||
.object({
|
||||
query_address: canonicalAddress,
|
||||
query_address: nativeAddressObservationSchema,
|
||||
found: z.literal(true),
|
||||
procedure: ghidraProcedureIdentitySchema,
|
||||
})
|
||||
@@ -285,7 +282,7 @@ const containingProcedure = z.discriminatedUnion("found", [
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
query_address: canonicalAddress,
|
||||
query_address: nativeAddressObservationSchema,
|
||||
found: z.literal(false),
|
||||
procedure: z.null(),
|
||||
reason: z.enum(["outside_segments", "not_in_procedure"]),
|
||||
@@ -314,7 +311,11 @@ const resultSchemas = {
|
||||
...value.searched.skipped_not_loaded,
|
||||
].flatMap((block) => [block.start, block.end]),
|
||||
];
|
||||
if (addresses.some((value) => !canonicalAddress.safeParse(value).success))
|
||||
if (
|
||||
addresses.some(
|
||||
(value) => !nativeAddressObservationSchema.safeParse(value).success,
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Byte search addresses must be canonical",
|
||||
@@ -322,10 +323,10 @@ const resultSchemas = {
|
||||
}),
|
||||
inspect_native_load_image: nativeLoadImageObservationSchema,
|
||||
read_bytes: nativeByteReadSchema
|
||||
.safeExtend({ address: canonicalAddress })
|
||||
.safeExtend({ address: nativeAddressObservationSchema })
|
||||
.strict(),
|
||||
address_to_file_offset: z.strictObject({
|
||||
address: canonicalAddress,
|
||||
address: nativeAddressObservationSchema,
|
||||
file_offset: z.int().min(0).max(Number.MAX_SAFE_INTEGER),
|
||||
}),
|
||||
address_name: z.string().nullable(),
|
||||
@@ -334,7 +335,7 @@ const resultSchemas = {
|
||||
list_procedures: z.array(procedureItem),
|
||||
list_segments: z.array(segment),
|
||||
list_strings: z.array(stringItem),
|
||||
procedure_address: canonicalAddress,
|
||||
procedure_address: nativeAddressObservationSchema,
|
||||
resolve_containing_procedure: containingProcedure,
|
||||
search_procedures: z.array(searchItem),
|
||||
search_strings: z.array(searchItem),
|
||||
|
||||
@@ -4,8 +4,13 @@ import {
|
||||
compareDosFixture,
|
||||
fixtureDosLoadImage,
|
||||
} from "./GhidraLoadImage.fixture.js";
|
||||
import { nativeLoadImageObservationSchema } from "../domain/native/nativeLoadImage.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import {
|
||||
nativeLoadImageObservationSchema,
|
||||
nativeLoadImageSchema,
|
||||
} from "../domain/native/nativeLoadImage.js";
|
||||
import type { NativeLoadImageObservation } from "../domain/native/nativeLoadImage.js";
|
||||
import { parseGhidraInventoryResult } from "./GhidraInventoryValues.js";
|
||||
import { prepareGhidraDosLoadImage } from "./GhidraLoadImageValues.js";
|
||||
|
||||
it("does not interpret an ambiguous Windows-header candidate as a DOS image", () => {
|
||||
@@ -229,6 +234,103 @@ const damage: ReadonlyArray<
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
const malformedCoordinates: ReadonlyArray<
|
||||
readonly [string, (observation: NativeLoadImageObservation) => void]
|
||||
> = [
|
||||
["image base", (value) => (value.image_base = "not-an-address")],
|
||||
[
|
||||
"mapping start",
|
||||
(value) => {
|
||||
const mapping = value.mappings[0];
|
||||
if (mapping) mapping.start = "not-an-address";
|
||||
},
|
||||
],
|
||||
[
|
||||
"mapping end",
|
||||
(value) => {
|
||||
const mapping = value.mappings[0];
|
||||
if (mapping) mapping.end = "not-an-address";
|
||||
},
|
||||
],
|
||||
[
|
||||
"relocation address",
|
||||
(value) => {
|
||||
const relocation = value.relocations[0];
|
||||
if (relocation) relocation.address = "not-an-address";
|
||||
},
|
||||
],
|
||||
["entry point", (value) => (value.entry_points[0] = "not-an-address")],
|
||||
[
|
||||
"entry context address",
|
||||
(value) => {
|
||||
value.entry_context.push({ address: "not-an-address", registers: [] });
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
it.each(malformedCoordinates)(
|
||||
"rejects a malformed nonempty %s at both native load-image boundaries",
|
||||
(_name, mutate) => {
|
||||
const observation = structuredClone(fixtureDosLoadImage().observation);
|
||||
mutate(observation);
|
||||
expect({
|
||||
shared: nativeLoadImageObservationSchema.safeParse(observation).success,
|
||||
provider: parseGhidraInventoryResult(
|
||||
"inspect_native_load_image",
|
||||
jsonValueSchema.parse(observation),
|
||||
).ok,
|
||||
}).toEqual({ shared: false, provider: false });
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves zero-padded, qualified, and large observation addresses", () => {
|
||||
const observation = structuredClone(fixtureDosLoadImage().observation);
|
||||
observation.image_base = "0x0000";
|
||||
const mapping = observation.mappings[0];
|
||||
const relocation = observation.relocations[0];
|
||||
if (!mapping || !relocation) throw new Error("Incomplete load-image fixture");
|
||||
mapping.start = "ram%2Fbank:0x0000000000000001";
|
||||
mapping.end = "ram%2Fbank:0x10000000000000001";
|
||||
relocation.address = "0x10000000000000009";
|
||||
observation.entry_points[0] = "ram:0x10000000000000001";
|
||||
const parsed = parseGhidraInventoryResult(
|
||||
"inspect_native_load_image",
|
||||
jsonValueSchema.parse(observation),
|
||||
);
|
||||
expect(parsed.ok).toBe(true);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
expect(parsed.value.value).toEqual(observation);
|
||||
});
|
||||
|
||||
it("rejects malformed coordinates in completed load-image checks and entries", () => {
|
||||
const fixture = fixtureDosLoadImage();
|
||||
const compared = compareDosFixture(
|
||||
fixture.bytes,
|
||||
fixture.sha256,
|
||||
fixture.observation,
|
||||
);
|
||||
if (!compared.ok || compared.value.status === "unsupported")
|
||||
throw new Error("Expected a completed DOS load-image result");
|
||||
const malformedResults = [
|
||||
{
|
||||
...compared.value,
|
||||
entry: { ...compared.value.entry, linear_address: "not-an-address" },
|
||||
},
|
||||
{
|
||||
...compared.value,
|
||||
checks: compared.value.checks.map((check, index) =>
|
||||
index === 0 ? { ...check, address: "not-an-address" } : check,
|
||||
),
|
||||
},
|
||||
];
|
||||
expect(
|
||||
malformedResults.map(
|
||||
(result) => nativeLoadImageSchema.safeParse(result).success,
|
||||
),
|
||||
).toEqual([false, false]);
|
||||
});
|
||||
|
||||
describe("independent DOS MZ import verification", () => {
|
||||
it("verifies every initialized source byte while retaining overlay and BSS scope", () => {
|
||||
const fixture = fixtureDosLoadImage();
|
||||
|
||||
@@ -1478,10 +1478,13 @@ describe("Ghidra measured raw load-image projection", () => {
|
||||
"mapping-mismatch",
|
||||
"changed-snapshot",
|
||||
"cancelled",
|
||||
"malformed-coordinate",
|
||||
] as const)(
|
||||
"streams raw snapshot identity and preserves %s outcomes",
|
||||
async (state) => {
|
||||
const fixture = fixtureRawBinaryLoadImage();
|
||||
if (state === "malformed-coordinate")
|
||||
fixture.observation.image_base = "not-an-address";
|
||||
if (state === "mapping-mismatch") {
|
||||
const mapping = fixture.observation.mappings[0];
|
||||
if (mapping === undefined)
|
||||
@@ -1542,7 +1545,9 @@ describe("Ghidra measured raw load-image projection", () => {
|
||||
{},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(signals).toEqual([controller.signal]);
|
||||
expect(signals).toEqual(
|
||||
state === "malformed-coordinate" ? [] : [controller.signal],
|
||||
);
|
||||
if (state === "verified" || state === "mapping-mismatch") {
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.result).toMatchObject({
|
||||
@@ -1567,7 +1572,9 @@ describe("Ghidra measured raw load-image projection", () => {
|
||||
_tag:
|
||||
state === "cancelled"
|
||||
? "AnalysisCancelledError"
|
||||
: "ProviderAdapterError",
|
||||
: state === "malformed-coordinate"
|
||||
? "AnalysisOutputError"
|
||||
: "ProviderAdapterError",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
managedNativeVerificationResultSchema,
|
||||
} from "../../../src/domain/managed/managedNativeVerificationSchemas.js";
|
||||
import { nativeUiResultSchema } from "../../../src/domain/native/nativeUiObservation.js";
|
||||
import { nativeLoadImageSchema } from "../../../src/domain/native/nativeLoadImage.js";
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import type { JsonValue } from "../../../src/domain/jsonValue.js";
|
||||
import { mcpTest } from "../../support/mcp/mcpFixture.js";
|
||||
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
|
||||
|
||||
import { verifyManagedNativeBoundaries } from "../../../src/domain/managed/managedNativeVerification.js";
|
||||
@@ -186,3 +190,80 @@ it("advertises semantic output invariants and keeps producers aligned with Zod",
|
||||
await Promise.allSettled([client.close(), server.close()]);
|
||||
}
|
||||
});
|
||||
|
||||
mcpTest(
|
||||
"advertises and enforces the observed load-image coordinate grammar",
|
||||
async ({ mcp }) => {
|
||||
const contract = toolContract("inspect_native_load_image");
|
||||
const server = new McpServer({
|
||||
name: "load-image-coordinates",
|
||||
version: "1",
|
||||
});
|
||||
const image = {
|
||||
status: "unsupported",
|
||||
reason: "No format-specific comparison was performed",
|
||||
observations: {
|
||||
executable_format: "ELF",
|
||||
language_id: "AARCH64:LE:64:v8A",
|
||||
compiler_spec_id: "default",
|
||||
image_base: "ram%2Fbank:0x0000",
|
||||
default_address_space: "ram",
|
||||
source_files: [],
|
||||
mappings: [],
|
||||
relocations: [],
|
||||
entry_points: ["0x10000000000000001"],
|
||||
entry_context: [],
|
||||
},
|
||||
limitations: [],
|
||||
};
|
||||
const evidence = (result: JsonValue) =>
|
||||
createEvidence(
|
||||
undefined,
|
||||
{ id: "ghidra", name: "Controlled bridge result", version: "12.1.4" },
|
||||
{ operation: contract.name, parameters: {}, result },
|
||||
);
|
||||
let reply = evidence(image);
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
async () => ({
|
||||
content: [],
|
||||
structuredContent: reply,
|
||||
}),
|
||||
);
|
||||
const client = await mcp.connect(server);
|
||||
const advertised = (await client.listTools()).tools.find(
|
||||
({ name }) => name === contract.name,
|
||||
);
|
||||
if (advertised?.outputSchema === undefined)
|
||||
throw new Error("Load-image output schema missing");
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const outputSchema = z
|
||||
.record(z.string(), z.unknown())
|
||||
.parse(advertised.outputSchema);
|
||||
expect(ajv.validateSchema(outputSchema)).toBe(true);
|
||||
const validate = ajv.compile(outputSchema);
|
||||
expect(nativeLoadImageSchema.parse(image)).toEqual(image);
|
||||
expect(validate(reply)).toBe(true);
|
||||
const accepted = await client.callTool({
|
||||
name: contract.name,
|
||||
arguments: {},
|
||||
});
|
||||
expect(accepted.isError).not.toBe(true);
|
||||
expect(accepted.structuredContent).toEqual(reply);
|
||||
|
||||
const malformed = {
|
||||
...image,
|
||||
observations: { ...image.observations, image_base: "not-an-address" },
|
||||
};
|
||||
reply = evidence(malformed);
|
||||
expect(nativeLoadImageSchema.safeParse(malformed).success).toBe(false);
|
||||
expect(validate(reply)).toBe(false);
|
||||
const rejected = await client.callTool({
|
||||
name: contract.name,
|
||||
arguments: {},
|
||||
});
|
||||
expect(rejected.isError).toBe(true);
|
||||
expect(rejected.structuredContent).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user