Merge pull request #278 from morluto/agent/managed-app-manifest-graph
test(dotnet): verify managed app graph manifests
This commit is contained in:
commit
5be405169e
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue