From 00a6524b9a36a36fe1144c78e0f807bf2114b396 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 9 Nov 2026 06:03:48 +0800 Subject: [PATCH] fix(managed): preserve native export coverage during verification (#9472) --- .../managed/managedNativeVerificationMatch.ts | 30 +++++++++- .../managedNativeVerificationSource.ts | 9 +++ .../managedWorkflowUnknownIdentityMcp.test.ts | 57 +++++++++++++++++++ 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/domain/managed/managedNativeVerificationMatch.ts b/src/domain/managed/managedNativeVerificationMatch.ts index c73783cb25..9b7fc672b2 100644 --- a/src/domain/managed/managedNativeVerificationMatch.ts +++ b/src/domain/managed/managedNativeVerificationMatch.ts @@ -20,12 +20,16 @@ export const collectNativeSymbols = ( readonly symbols: readonly NativeSymbol[]; readonly accepted: number; readonly unsupported: number; + readonly partialExports: boolean; + readonly exportLimitations: readonly string[]; readonly moduleFormatsByEvidenceId: ReadonlyMap; } => { const symbols: NativeSymbol[] = []; const moduleFormatsByEvidenceId = new Map(); let accepted = 0; let unsupported = 0; + let partialExports = false; + const exportLimitations: string[] = []; for (const observation of observations) { moduleFormatsByEvidenceId.set( observation.evidence.evidence_id, @@ -36,9 +40,23 @@ export const collectNativeSymbols = ( } else { accepted += 1; symbols.push(...observation.symbols); + if (observation.exportCoverage !== undefined) { + if (!observation.exportCoverage.exhaustive) partialExports = true; + for (const limitation of observation.exportCoverage.limitations) + exportLimitations.push( + `Native export Evidence ${observation.evidence.evidence_id}: ${limitation}`, + ); + } } } - return { symbols, accepted, unsupported, moduleFormatsByEvidenceId }; + return { + symbols, + accepted, + unsupported, + partialExports, + exportLimitations, + moduleFormatsByEvidenceId, + }; }; /** P/Invoke resolves a Mach-O export through dlsym, which names it without the symbol-table prefix. */ @@ -516,6 +534,8 @@ interface VerificationResultInput { readonly symbols: readonly NativeSymbol[]; readonly accepted: number; readonly unsupported: number; + readonly partialExports: boolean; + readonly exportLimitations: readonly string[]; }; readonly pinvokeImports: readonly PinvokeVerification[]; readonly nativeObservationCount: number; @@ -540,7 +560,9 @@ export const buildVerificationResult = ({ ).length; const coverage: ManagedNativeVerificationResult["coverage"] = { status: - managed.coverage.state === "complete" && native.unsupported === 0 + managed.coverage.state === "complete" && + native.unsupported === 0 && + !native.partialExports ? "complete-within-inputs" : "partial", }; @@ -594,6 +616,10 @@ export const buildVerificationResult = ({ : [ "Some native Evidence operations were unsupported by this workflow.", ]), + ...(native.partialExports + ? ["Some native export inventories are not exhaustive."] + : []), + ...native.exportLimitations, ], }; }; diff --git a/src/domain/managed/managedNativeVerificationSource.ts b/src/domain/managed/managedNativeVerificationSource.ts index 58e8a0bbe3..5164e5945a 100644 --- a/src/domain/managed/managedNativeVerificationSource.ts +++ b/src/domain/managed/managedNativeVerificationSource.ts @@ -34,6 +34,11 @@ export interface ManagedNativeObservationFacts { readonly evidence: Evidence; readonly symbols: readonly NativeSymbol[]; readonly supported: boolean; + /** Coverage of an export inventory; function observations are targeted. */ + readonly exportCoverage?: { + readonly exhaustive: boolean; + readonly limitations: readonly string[]; + }; } const inputFailure = ( @@ -62,6 +67,10 @@ const readNativeObservation = ( return ok({ evidence, supported: true, + exportCoverage: { + exhaustive: payload.value.exports.exhaustive, + limitations: payload.value.exports.limitations, + }, symbols: payload.value.exports.items.map((symbol) => ({ evidence_id: evidence.evidence_id, operation: evidence.operation, diff --git a/tests/boundary/mcp/managedWorkflowUnknownIdentityMcp.test.ts b/tests/boundary/mcp/managedWorkflowUnknownIdentityMcp.test.ts index 6bf579b9c8..9e1d2cd3d8 100644 --- a/tests/boundary/mcp/managedWorkflowUnknownIdentityMcp.test.ts +++ b/tests/boundary/mcp/managedWorkflowUnknownIdentityMcp.test.ts @@ -176,6 +176,63 @@ it("records a residual question for partial verification with zero item counts", expect(harness.session.listUnknowns()).toHaveLength(1); }); +it("retains partial native export coverage after an observed match", async () => { + const harness = await createApplicationMcpHarness({ + session: createTestBinarySession(() => { + throw new Error("No provider needed"); + }), + }); + onTestFinished(harness.close); + const original = MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations[0]; + if (original === undefined || original.subject === null) + throw new Error("Managed verification example is missing native Evidence"); + const report = inspectMachoSchema.parse(original.normalized_result); + const native = createEvidence( + { + path: original.subject.local_path!, + sha256: original.subject.digest.sha256, + format: original.subject.format, + }, + original.provider, + { + operation: "inspect_macho", + parameters: original.parameters, + result: { + ...report, + exports: { + ...report.exports, + exhaustive: false, + limitations: ["Export inventory may be incomplete."], + }, + }, + rawResult: null, + }, + ); + const response = await harness.client.callTool({ + name: "verify_managed_native_boundaries", + arguments: { + managed_boundaries: + MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries, + native_observations: [native], + }, + }); + expect(response.isError, JSON.stringify(response.content)).not.toBe(true); + const evidence = parseEvidence(response.structuredContent); + const result = managedNativeVerificationResultSchema.parse( + evidence.normalized_result, + ); + expect(result.summary).toMatchObject({ + verified: 1, + unresolved: 0, + contradicted: 0, + }); + expect(result.coverage.status).toBe("partial"); + expect(result.limitations).toContainEqual( + expect.stringContaining("Export inventory may be incomplete."), + ); + expect(harness.session.listUnknowns()).toHaveLength(1); +}); + it("records distinct unresolved managed boundary checks and makes exact replay idempotent", async () => { const harness = await createApplicationMcpHarness({ session: createTestBinarySession(() => {