From a5e5c68d117cdef33a47291ed4f641c9a89045f3 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <1705004398@qq.com> Date: Thu, 16 Jul 2026 21:52:21 +0800 Subject: [PATCH] test(dotnet): verify managed app graph manifests --- AGENTS.md | 2 +- docs/managed-code-analysis.md | 38 +++- scripts/verify-managed-conformance.mjs | 268 ++++++++++++++++++++++++- 3 files changed, 292 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8a80715a2..dc43d5992 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,7 @@ See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architec - `npm run verify:hopper`: build and run the real-Hopper verifier with two distinct binaries. - `npm run verify:ghidra`: build and run the real-Ghidra verifier against `GHIDRA_INSTALL_DIR` and optional `GHIDRA_TARGET_PATH`. - `npm run verify:browser`: build and run the real Chrome verifier against `REA_BROWSER_EXECUTABLE` or a platform-default Chrome-family executable. -- `npm run verify:managed`: build and run the source-owned managed PE/CLI conformance verifier for artifact triage, member inspection, managed/native boundary declarations, token drift comparison, malformed metadata, non-managed degradation, and manifest-verifier self-test; set `REA_MANAGED_APP_MANIFEST_PATH` to verify an operator-local managed app manifest. +- `npm run verify:managed`: build and run the source-owned managed PE/CLI conformance verifier for artifact triage, member inspection, managed/native boundary declarations, token drift comparison, malformed metadata, non-managed degradation, managed application-graph projection, and manifest-verifier self-test; set `REA_MANAGED_APP_MANIFEST_PATH` to verify an operator-local managed app manifest, including optional graph/trace assertions. - `npm run verify:replay`: build and run the real Linux Bubblewrap/seccomp/cgroup verifier against source-owned replay fixtures; set `REA_REPLAY_INPUT_PATH` to verify an operator-local manifest. - `npm run verify:package`: pack and test the CLI, setup transaction, skill, and canonical target-free MCP catalog in an isolated environment. - `npm run docs:generate`: generate API documentation from JSDoc comments into `docs/api/` using TypeDoc. diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index 9f378103d..7fcefcbc5 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -284,7 +284,26 @@ absolute or relative to the manifest file. A compact manifest contains: "il_size": 0, "normalized_il_sha256": "" } - ] + ], + "application_graph": { + "expected_node_kinds": [ + "artifact", + "managed-assembly", + "managed-module", + "managed-type", + "managed-method" + ], + "feature_traces": [ + { + "label": "operator-local feature label", + "method_token": "0x06000000", + "seed": "", + "match": "exact", + "case_sensitive": true, + "min_matched_seeds": 1 + } + ] + } } ``` @@ -295,12 +314,21 @@ directly by row, so selected methods do not need to appear in the first member page of a large application. `il_length` remains accepted as a legacy alias for `il_size`. +The optional `application_graph` block reuses those exact-build method +commitments. For each referenced MethodDef token, the verifier builds a bounded +managed application-graph projection from the authenticated artifact and the +single selected member page, then checks requested node kinds and feature-trace +seed matches. This proves only that the selected static facts enter REA's graph +and tracing vocabulary for that exact local build; it does not execute the +application, inspect arbitrary unlisted methods, or infer runtime behavior. + Output contains assertion status and compact identities only: file name, target SHA-256, MVID, assembly name, runtime-family, managed architecture, -method tokens, method names, signature digests, IL sizes, and normalized IL -digests. It does not print method bodies, reconstructed C#, application data, -runtime telemetry, account/service details, full filesystem inventories, or -the target's absolute path. The manifest and target remain outside git. +method tokens, method names, signature digests, IL sizes, normalized IL +digests, graph Evidence IDs, node-kind summaries, and trace seed hit counts. It +does not print method bodies, reconstructed C#, application data, runtime +telemetry, account/service details, full filesystem inventories, or the +target's absolute path. The manifest and target remain outside git. The benchmark may prove that REA reproduces selected facts for that exact local build. It cannot establish general support on its own; source-built conformance diff --git a/scripts/verify-managed-conformance.mjs b/scripts/verify-managed-conformance.mjs index b679f810b..e7b459d5e 100644 --- a/scripts/verify-managed-conformance.mjs +++ b/scripts/verify-managed-conformance.mjs @@ -66,6 +66,14 @@ const nativeBoundaryLimits = { maxHeapItemBytes: 1024 * 1024, }; +const applicationGraphLimits = { + max_types: 100, + max_methods: 100, + max_fields: 100, + max_pinvoke_imports: 100, + max_native_implementations: 100, +}; + const appManifestInspectionLimits = { ...inspectionLimits, maxMetadataBytes: 64 * 1024 * 1024, @@ -582,6 +590,24 @@ async function runManagedAppManifestSelfTest() { normalized_il_sha256: method.body.normalized_il_sha256, }, ], + application_graph: { + expected_node_kinds: [ + "artifact", + "managed-assembly", + "managed-module", + "managed-type", + "managed-method", + ], + feature_traces: [ + { + label: "trace pinned method by name", + method_token: method.token, + seed: "PinnedSemanticSlice", + match: "exact", + min_matched_seeds: 1, + }, + ], + }, }, join(workspace, "source-owned-managed-app-manifest.json"), ); @@ -589,6 +615,7 @@ async function runManagedAppManifestSelfTest() { verified: true, assertions: summary.assertions, methods: summary.methods.length, + applicationGraph: summary.application_graph ?? null, target: summary.target, }; } @@ -664,6 +691,7 @@ async function verifyManagedAppManifest(rawManifest, manifestPath) { const methods = []; let assertions = 1; + const inspectedMethods = new Map(); for (const expectedMethod of manifest.methods) { const method = inspectManagedMethodByToken(bytes, target, expectedMethod); ensure( @@ -679,6 +707,7 @@ async function verifyManagedAppManifest(rawManifest, manifestPath) { `method ${expectedMethod.token} normalized IL sha256 mismatch: expected ${expectedMethod.normalized_il_sha256}, observed ${method.body.normalized_il_sha256 ?? "null"}`, ); assertions += 4; + inspectedMethods.set(expectedMethod.token, method); methods.push({ label: expectedMethod.label ?? null, token: method.token, @@ -689,6 +718,18 @@ async function verifyManagedAppManifest(rawManifest, manifestPath) { normalized_il_sha256: method.body.normalized_il_sha256, }); } + const applicationGraph = + manifest.application_graph === undefined + ? undefined + : verifyManagedAppManifestApplicationGraph({ + graphManifest: manifest.application_graph, + bytes, + target, + artifact, + manifestMethods: manifest.methods, + inspectedMethods, + }); + if (applicationGraph !== undefined) assertions += applicationGraph.assertions; return { label: manifest.label ?? null, assertions, @@ -701,15 +742,148 @@ async function verifyManagedAppManifest(rawManifest, manifestPath) { managed_architecture: artifact.classification.managed_architecture, }, methods, + ...(applicationGraph === undefined + ? {} + : { application_graph: applicationGraph.summary }), + }; +} + +function verifyManagedAppManifestApplicationGraph({ + graphManifest, + bytes, + target, + artifact, + manifestMethods, + inspectedMethods, +}) { + const graphByToken = new Map(); + const projectionForToken = (token) => { + const existing = graphByToken.get(token); + if (existing !== undefined) return existing; + const method = inspectedMethods.get(token); + ensure( + method !== undefined, + `application graph method ${token} must also be declared in manifest.methods`, + ); + const members = inspectManagedMethodPageByToken(bytes, target, token); + const artifactEvidence = createEvidence(target, MANAGED_STATIC_PROVIDER, { + operation: "inspect_managed_artifact", + parameters: appManifestInspectionLimits, + result: artifact, + rawResult: null, + limitations: artifact.limitations, + locations: [{ kind: "artifact-path", path: target.path }], + }); + const memberEvidence = createEvidence(target, MANAGED_STATIC_PROVIDER, { + operation: "inspect_managed_members", + parameters: { + ...appManifestMemberLimits, + methodOffset: methodTokenRow(token) - 1, + }, + result: members, + rawResult: null, + limitations: members.limitations, + locations: [{ kind: "artifact-path", path: target.path }], + }); + const projected = projectManagedApplicationGraphEvidence({ + managed_artifact: artifactEvidence, + managed_members: memberEvidence, + limits: applicationGraphLimits, + }); + ensure( + projected.ok, + `application graph projection failed for method ${token}`, + ); + const projection = { + method, + evidence: projected.value, + nodeKinds: [ + ...new Set( + projected.value.normalized_result.graph.nodes.map(({ kind }) => kind), + ), + ].sort(), + summary: projected.value.normalized_result.summary, + }; + graphByToken.set(token, projection); + return projection; + }; + + const firstToken = manifestMethods[0]?.token; + ensure( + firstToken !== undefined, + "application graph verification requires at least one manifest method", + ); + const baseline = projectionForToken(firstToken); + let assertions = 1; + for (const expectedKind of graphManifest.expected_node_kinds) { + ensure( + baseline.nodeKinds.includes(expectedKind), + `application graph is missing expected node kind ${expectedKind}`, + ); + assertions += 1; + } + + const traces = []; + for (const expectedTrace of graphManifest.feature_traces) { + const projection = projectionForToken(expectedTrace.method_token); + const traced = traceApplicationFeatureEvidence({ + application: projection.evidence, + native_observations: [], + seed: { + kind: "string", + value: expectedTrace.seed, + match: expectedTrace.match, + case_sensitive: expectedTrace.case_sensitive, + }, + direction: "incoming", + limits: { + max_seed_matches: 10, + max_depth: 4, + max_nodes: 100, + max_edges: 200, + max_paths: 20, + }, + }); + ensure( + traced.ok, + `application graph trace failed for method ${expectedTrace.method_token}`, + ); + const matched = traced.value.normalized_result.summary.matched_seeds; + ensure( + matched >= expectedTrace.min_matched_seeds, + `application graph trace ${expectedTrace.seed} matched ${String(matched)} seeds, expected at least ${String(expectedTrace.min_matched_seeds)}`, + ); + assertions += 1; + traces.push({ + label: expectedTrace.label ?? null, + method_token: expectedTrace.method_token, + seed: expectedTrace.seed, + matched_seeds: matched, + trace_evidence_id: traced.value.evidence_id, + }); + } + + return { + assertions, + summary: { + projections: [...graphByToken.values()].map((projection) => ({ + method_token: projection.method.token, + graph_evidence_id: projection.evidence.evidence_id, + node_kinds: projection.nodeKinds, + summary: projection.summary, + })), + expected_node_kinds: graphManifest.expected_node_kinds, + feature_traces: traces, + }, }; } function inspectManagedMethodByToken(bytes, target, expectedMethod) { - const row = methodTokenRow(expectedMethod.token); - const members = inspectManagedMembersBytes(bytes, target, { - ...appManifestMemberLimits, - methodOffset: row - 1, - }); + const members = inspectManagedMethodPageByToken( + bytes, + target, + expectedMethod.token, + ); const method = members.methods.items.find( ({ token }) => token === expectedMethod.token, ); @@ -724,6 +898,14 @@ function inspectManagedMethodByToken(bytes, target, expectedMethod) { return method; } +function inspectManagedMethodPageByToken(bytes, target, token) { + const row = methodTokenRow(token); + return inspectManagedMembersBytes(bytes, target, { + ...appManifestMemberLimits, + methodOffset: row - 1, + }); +} + function parseManagedAppManifest(rawManifest) { const manifest = object(rawManifest, "manifest"); ensure( @@ -756,17 +938,74 @@ function parseManagedAppManifest(rawManifest) { methods: methods.map((method, index) => parseManagedAppManifestMethod(method, index), ), + application_graph: parseManagedAppManifestGraph(manifest.application_graph), + }; +} + +function parseManagedAppManifestGraph(rawGraph) { + if (rawGraph === undefined) return undefined; + const graph = object(rawGraph, "manifest.application_graph"); + const expectedKinds = + graph.expected_node_kinds === undefined + ? [] + : array( + graph.expected_node_kinds, + "manifest.application_graph.expected_node_kinds", + ).map((kind, index) => + string( + kind, + `manifest.application_graph.expected_node_kinds[${String(index)}]`, + ), + ); + const traces = + graph.feature_traces === undefined + ? [] + : array( + graph.feature_traces, + "manifest.application_graph.feature_traces", + ).map((trace, index) => parseManagedAppManifestTrace(trace, index)); + ensure( + expectedKinds.length > 0 || traces.length > 0, + "manifest.application_graph must declare expected_node_kinds or feature_traces", + ); + return { + expected_node_kinds: expectedKinds, + feature_traces: traces, + }; +} + +function parseManagedAppManifestTrace(rawTrace, index) { + const prefix = `manifest.application_graph.feature_traces[${String(index)}]`; + const trace = object(rawTrace, prefix); + const match = trace.match ?? "exact"; + ensure( + match === "exact" || match === "contains", + `${prefix}.match must be exact or contains`, + ); + const caseSensitive = trace.case_sensitive ?? true; + ensure( + typeof caseSensitive === "boolean", + `${prefix}.case_sensitive must be a boolean`, + ); + const minMatchedSeeds = trace.min_matched_seeds ?? 1; + ensure( + Number.isInteger(minMatchedSeeds) && minMatchedSeeds > 0, + `${prefix}.min_matched_seeds must be a positive integer`, + ); + return { + label: optionalString(trace.label, `${prefix}.label`), + method_token: methodToken(trace.method_token, `${prefix}.method_token`), + seed: string(trace.seed, `${prefix}.seed`), + match, + case_sensitive: caseSensitive, + min_matched_seeds: minMatchedSeeds, }; } function parseManagedAppManifestMethod(rawMethod, index) { const prefix = `manifest.methods[${String(index)}]`; const method = object(rawMethod, prefix); - const token = metadataToken(method.token, `${prefix}.token`); - ensure( - token.startsWith("0x06"), - `${prefix}.token must be a MethodDef token beginning with 0x06`, - ); + const token = methodToken(method.token, `${prefix}.token`); return { label: optionalString(method.label, `${prefix}.label`), token, @@ -853,6 +1092,15 @@ function metadataToken(value, name) { return text; } +function methodToken(value, name) { + const token = metadataToken(value, name); + ensure( + token.startsWith("0x06"), + `${name} must be a MethodDef token beginning with 0x06`, + ); + return token; +} + function methodTokenRow(token) { const row = Number.parseInt(token.slice(4), 16); ensure(row > 0, `method token ${token} has an invalid row id`);