diff --git a/src/server/registerManagedWorkflowTools/evidence.ts b/src/server/registerManagedWorkflowTools/evidence.ts index 7b6e3fd784..ea266428fa 100644 --- a/src/server/registerManagedWorkflowTools/evidence.ts +++ b/src/server/registerManagedWorkflowTools/evidence.ts @@ -4,20 +4,25 @@ import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js"; import type { Result } from "../../domain/result.js"; import { err, ok } from "../../domain/result.js"; +/** + * Authenticate caller-supplied records. When `expectedOperations` is omitted, + * the shared workflow owns which authentic operations it can use. + */ const parseExpectedEvidence = ( rawEvidence: readonly Evidence[], - expectedOperations: readonly string[], + expectedOperations?: readonly string[], ): Result => { try { const evidence = rawEvidence.map(parseEvidence); - for (const record of evidence) - if ( - !expectedOperations.includes(record.operation) || - record.predicate_type !== "rea.analysis" - ) - throw new TypeError( - `Expected ${expectedOperations.join(" or ")} Evidence, received ${record.operation}`, - ); + if (expectedOperations !== undefined) + for (const record of evidence) + if ( + !expectedOperations.includes(record.operation) || + record.predicate_type !== "rea.analysis" + ) + throw new TypeError( + `Expected ${expectedOperations.join(" or ")} Evidence, received ${record.operation}`, + ); return ok(evidence); } catch (cause: unknown) { return err( @@ -43,11 +48,14 @@ export const resolveManagedBoundaryEvidence = ( ): Result => parseExpectedEvidence([evidence], ["inspect_managed_native_boundaries"]); +/** + * Authenticate native observations without an operation allowlist: the domain + * counts authentic operations it cannot read symbols from as unsupported. + */ export const resolveNativeEvidence = ( evidence: readonly Evidence[], -): Result => { - return parseExpectedEvidence(evidence, ["inspect_macho", "analyze_function"]); -}; +): Result => + parseExpectedEvidence(evidence); export const sourceEvidence = (input: { readonly managed_artifact?: Evidence | undefined; diff --git a/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts b/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts new file mode 100644 index 0000000000..c3e7dc6f2a --- /dev/null +++ b/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts @@ -0,0 +1,116 @@ +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { afterAll, beforeAll, expect } from "vitest"; + +import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; +import { managedNativeVerificationResultSchema } from "../../../src/domain/managed/managedNativeVerificationSchemas.js"; +import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +const client = new Client({ name: "managed-native-adapters", version: "1" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [resolve("scripts/rea.mjs"), "mcp"], + cwd: process.cwd(), + env: { PATH: process.env.PATH ?? "" }, + stderr: "pipe", +}); +beforeAll(async () => { + await client.connect(transport); + transport.stderr?.on("data", () => undefined); +}); +afterAll(async () => { + await Promise.all([client.close(), transport.close()]); +}); + +/** Authentic native Evidence whose operation the workflow cannot read symbols from. */ +const signatureEvidence = () => + createEvidence( + { + path: "/usr/bin/true", + sha256: "a".repeat(64), + format: "mach-o", + architecture: "arm64", + }, + { id: "native", name: "Native", version: "simulated" }, + { + operation: "inspect_signature", + parameters: {}, + result: { signed: true }, + limitations: ["No native provider executed."], + }, + ); + +cliTest( + "counts authentic unsupported native Evidence identically through MCP and CLI", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-managed-native-adapters-"); + const path = join(root, "fixture.dll"); + await writeFile(path, buildManagedPeFixture({ pinvoke: {} })); + const inspected = await client.callTool({ + name: "inspect_managed_native_boundaries", + arguments: { path }, + }); + expect(inspected.isError).not.toBe(true); + const managed = parseEvidence(inspected.structuredContent); + const native = signatureEvidence(); + const input = { + managed_boundaries: managed, + native_observations: [native], + }; + + const mcp = await client.callTool({ + name: "verify_managed_native_boundaries", + arguments: input, + }); + expect(mcp.isError, JSON.stringify(mcp.content)).not.toBe(true); + const mcpResult = managedNativeVerificationResultSchema.parse( + parseEvidence(mcp.structuredContent).normalized_result, + ); + + const request = join(root, "verify.json"); + await writeFile(request, JSON.stringify(input)); + const cliRun = await cli.run({ + arguments: ["verify-managed-native-boundaries", request, "--json"], + }); + expect(cliRun.exitCode, cliRun.stderr).toBe(0); + const cliResult = managedNativeVerificationResultSchema.parse( + parseEvidence(cliRun.json).normalized_result, + ); + + expect(mcpResult).toEqual(cliResult); + expect(mcpResult).toMatchObject({ + native_observations: { + total: 1, + accepted: 0, + unsupported: 1, + symbols: 0, + }, + summary: { unresolved: 1 }, + pinvoke_imports: [ + expect.objectContaining({ basis: "unsupported-native-evidence" }), + ], + coverage: { status: "partial" }, + evidence_links: [managed.evidence_id, native.evidence_id], + }); + expect(mcpResult.limitations).toContain( + "Some native Evidence operations were unsupported by this workflow.", + ); + + const tampered = await client.callTool({ + name: "verify_managed_native_boundaries", + arguments: { + ...input, + native_observations: [{ ...native, operation: "inspect_macho" }], + }, + }); + expect(parseMcpToolError(tampered)).toMatchObject({ + error: { code: "evidence_integrity_mismatch" }, + }); + }, + 60_000, +);