Merge pull request #278 from morluto/agent/managed-app-manifest-graph

test(dotnet): verify managed app graph manifests
This commit is contained in:
N0zoM1z0 2026-07-16 22:56:33 +09:00 committed by GitHub
commit 5be405169e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 292 additions and 16 deletions

View File

@ -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.

View File

@ -284,7 +284,26 @@ absolute or relative to the manifest file. A compact manifest contains:
"il_size": 0,
"normalized_il_sha256": "<digest>"
}
]
],
"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": "<method name, API name, string, or digest to trace>",
"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

View File

@ -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`);