feat(managed): add retained metadata overview and pages (#2413)
Add caller-selected managed metadata summaries and original collection pages while preserving complete CLI/MCP defaults and previous-runtime Evidence. Real SDK replies shrink substantially; actual Codex token/latency results remain mixed and are documented without a token-saving claim. Validated focused SDK/domain tests, full static/docs checks, real two-assembly repeated workflows and portable old Evidence, real SQLite WASM, and all exact-head CI checks. Raw experimental artifacts remain private. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
f43fc02475
commit
82cfcee19f
|
|
@ -135,6 +135,18 @@ assembly. Keep managed/native boundaries and unavailable reconstruction facts
|
|||
explicit. A bring-your-own reconstruction oracle is separate from the canonical
|
||||
parser and must not become an implicit setup dependency.
|
||||
|
||||
For identity/classification questions, `inspect_managed_artifact` can explicitly
|
||||
select `detail: "summary"` on the first MCP call. It retains complete metadata
|
||||
and returns a linked overview with full classification/evidence, nullable
|
||||
module/MVID, frameworks, metadata coverage/issues/limitations and returned-row
|
||||
counts. Default complete requests remain unchanged. Read original rows with
|
||||
`inspect_analysis_view`, source `normalized_result.parent_evidence_id`, and
|
||||
`view: {kind: "managed", collection: "attributes", offset: 0, limit: 16}`.
|
||||
Collections are `references`, `resources` and `attributes`. Paging does not
|
||||
repeat parsing. Returned-row exhaustion
|
||||
does not prove complete metadata or absence under partial/unavailable coverage;
|
||||
preserve build-local tokens and their artifact/MVID scope.
|
||||
|
||||
### .NET NativeAOT
|
||||
|
||||
NativeAOT binaries contain native machine code, so ordinary CIL decompilers
|
||||
|
|
|
|||
|
|
@ -40,6 +40,47 @@ inspection is needed; see [selected member inspection](#selected-member-inspecti
|
|||
Compare separate builds with [member comparison](#member-comparison), preserving
|
||||
their paths, digests, and selection scope.
|
||||
|
||||
### Metadata overview and retained rows
|
||||
|
||||
The existing CLI and omitted/complete MCP detail return complete metadata
|
||||
inventories. For identity and classification questions, select
|
||||
`inspect_managed_artifact` with
|
||||
`{"path":"/absolute/path/Assembly.dll","detail":"summary"}` on the first call.
|
||||
This keeps the complete source in the session and returns a distinct linked
|
||||
`inspect_analysis_view` overview with complete PE/CLI, classification and its
|
||||
evidence, metadata status, module/MVID, assembly, frameworks, returned-row counts,
|
||||
coverage/issues and limitations. No assembly is loaded or executed.
|
||||
|
||||
Use `normalized_result.parent_evidence_id` to page original rows:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "<parent_evidence_id>"
|
||||
},
|
||||
"view": {
|
||||
"kind": "managed",
|
||||
"collection": "attributes",
|
||||
"offset": 0,
|
||||
"limit": 16
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Collections are `references`, `resources` and `attributes`; rows retain their
|
||||
original values, nulls, order, metadata tokens and offsets. Pages retain source
|
||||
metadata status and coverage. Exhaustion covers the returned parent collection,
|
||||
not all possible metadata, and empty rows under partial/unavailable metadata do
|
||||
not establish absence. Tokens are build-local and require the recorded artifact
|
||||
SHA-256 and known module MVID for cross-observation matching.
|
||||
|
||||
Retained views do not repeat the parser or require the source file to remain
|
||||
available. Portable views use the authenticated original Evidence inline,
|
||||
including the existing `inspect-analysis-view` CLI. Session record lifetime and
|
||||
export/import semantics remain unchanged. A server without Evidence retention
|
||||
rejects selected summary before analysis; complete delivery remains available.
|
||||
|
||||
## Shipped scope
|
||||
|
||||
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ Follow-up: [EVM descriptor lifecycle acceptance](evm-lifecycle.md) verifies actu
|
|||
worker replies, late SDK cancellation, permission-induced cleanup failures,
|
||||
healthy WASM peers and old CLI parity with explicit runtime and platform scope.
|
||||
|
||||
Follow-up: [managed artifact delivery and retained views](managed-artifact-summary.md)
|
||||
records two shipped assembly checks, previous-runtime portable Evidence, and four
|
||||
actual Codex trials. Selected replies shrink substantially; observed model input
|
||||
tokens increase and latency is mixed, so model-token savings are not claimed.
|
||||
|
||||
The 12 detailed checkpoint receipts were archived byte-for-byte before removal
|
||||
from the final PR tree. The archive `review-receipts-00d51e0fc-20261012` is in the
|
||||
private experiment lab outside the checkout; its `archive-index.json` records
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const sameSettings = [
|
|||
"retained_verification",
|
||||
"native_question",
|
||||
"javascript_question",
|
||||
"managed_question",
|
||||
"provider_settings_sha256",
|
||||
"provider_search_path_sha256",
|
||||
"provider_environment_policy_sha256",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,234 @@
|
|||
# Managed artifact delivery and retained views
|
||||
|
||||
Continues [#2016](https://github.com/morluto/rea/issues/2016) after
|
||||
[#2404](https://github.com/morluto/rea/pull/2404) merged. This checkpoint pins
|
||||
the complete managed workflow before implementing another optional view.
|
||||
The baseline below precedes implementation; the follow-up adds explicit summary
|
||||
delivery and portable collection pages while preserving complete defaults.
|
||||
|
||||
## Frozen control and analyst question
|
||||
|
||||
Control source is merged main `ae8051302b5834322fa00cfa31c968ae74ab6ed9`,
|
||||
Node 24.18.0, matching locked dependencies and actual compiled stdio MCP.
|
||||
The frozen `dist/`/`bridge/` file-table SHA-256 is
|
||||
`7a4cec0fa4f7e929356b50a6b13472f80bd6f526221e427125370cca3ef98d00`.
|
||||
Measurement source SHA-256 is
|
||||
`a95d8d8e9f72c0b2b885c61907ed13054ebf7d1642f6f13b0126163840b1221a`.
|
||||
|
||||
Use the digest-pinned shipped ShareX DLL from [the corpus](corpus.json),
|
||||
2,162,688 bytes, SHA-256
|
||||
`191a191be964e434daf0f9d76d1e0d8ddce90d7e248cba2a6b5ac9577f288d53`.
|
||||
The [existing model question](questions/managed.txt) requests exact identity,
|
||||
complete managed classification and its evidence, frameworks and every declared
|
||||
native ModuleRef/PInvoke boundary with coverage and limitations. It explicitly
|
||||
excludes unrelated resource/custom-attribute inventories. Analysis reads PE/CLI
|
||||
metadata without loading or executing the assembly.
|
||||
|
||||
The current complete result has exactly the same normalized facts as the earlier
|
||||
real managed checkpoint linked from [the EVM descriptor study](evm-descriptor.md).
|
||||
This comparison does not equate fresh Evidence IDs, provider versions or whole
|
||||
catalogs. Original target bytes remain unchanged; SDK-converted named schemas
|
||||
and actual replies validate in the public measurement workflow.
|
||||
|
||||
## Observed data costs
|
||||
|
||||
| Public operation | SDK result JSON UTF-8 bytes | Tool milliseconds |
|
||||
| ----------------------------------- | --------------------------: | ----------------: |
|
||||
| `inspect_managed_artifact` | 1,270,839 | 450.306 |
|
||||
| `inspect_managed_native_boundaries` | 27,913 | 142.663 |
|
||||
|
||||
The complete workflow, including connection/discovery and both calls, takes
|
||||
3.721 seconds. The full discovered catalog is 2,482,204 compact JSON bytes;
|
||||
its input definitions are 527,569 bytes. These costs include the current main
|
||||
changes and are not silently substituted for an older catalog baseline.
|
||||
|
||||
The artifact inspection's normalized payload alone is 607,605 compact JSON UTF-8
|
||||
bytes, measured with actual Node `JSON.stringify`:
|
||||
|
||||
| Returned collection | Rows | Compact JSON UTF-8 bytes |
|
||||
| ------------------- | ----: | -----------------------: |
|
||||
| References | 38 | 12,153 |
|
||||
| Resources | 36 | 10,357 |
|
||||
| Custom attributes | 1,945 | 582,586 |
|
||||
|
||||
Custom attributes dominate the payload. The remaining identity, PE/CLI,
|
||||
classification, metadata, framework and coverage facts are comparatively small.
|
||||
`raw_result` is null. SDK result size includes both structured and text
|
||||
representations; it is not the exact context a model receives or a token count.
|
||||
|
||||
The serial owned scope takes 4.852 seconds including the driver, samples
|
||||
520,110,080 peak family bytes and records 4.699 last-observed CPU seconds.
|
||||
One CPU, 4 GiB hard memory, unchanged 3 GiB early-stop, no swap and 192 tasks
|
||||
remain in effect. No guard abort occurs. Final cgroup counters disappear and
|
||||
remain unknown; sampled zero events are not final no-OOM proof.
|
||||
|
||||
## Bounded candidate and acceptance
|
||||
|
||||
Evaluate an explicit overview on the first artifact inspection, retaining the
|
||||
unchanged complete source and returning a distinct linked view. Keep complete
|
||||
identity/classification, metadata status, nullable module/MVID, assembly,
|
||||
frameworks, original coverage/issues/limitations and returned-collection counts
|
||||
inline. Typed pages can expose original references/resources/attributes with
|
||||
their nulls, token coordinates, offsets, order and values. Returned-array
|
||||
coverage must remain distinct from whole-metadata completeness.
|
||||
|
||||
Reuse the existing managed schemas, retained Evidence application workflow and
|
||||
named registration hooks. Keep omitted/complete requests, other managed tools,
|
||||
selected target/provider behavior and original export/import identities usable.
|
||||
Do not strengthen historical admission to require a non-null subject or
|
||||
reinterpret foreign recorded paths through this host's filesystem. Delivery
|
||||
selection must not be passed to the PE/CLI parser as an analysis option.
|
||||
|
||||
At the baseline checkpoint the candidate was not implemented or measured. Compare the
|
||||
same completed analyst question, actual discovery/results, full default
|
||||
compatibility and retained paging without producer reruns. Follow with other
|
||||
shipped managed inputs, partial/unavailable metadata and real Codex trials with
|
||||
strict factual/reference checks. Preserve failed or truncated transcripts.
|
||||
Count total/cached/uncached input, output, calls/retries, whole-task time, CPU
|
||||
and family memory before recommending a token or latency improvement. A smaller
|
||||
reply alone is insufficient.
|
||||
|
||||
## Reproduce the control
|
||||
|
||||
Build and freeze the explicit source revision before editing production code.
|
||||
Supply the original shipped DLL and an explicit string-valued provider dictionary
|
||||
in a fresh private receipt directory:
|
||||
|
||||
```bash
|
||||
python3 experiments/analysis-efficiency/resource-run.py -- \
|
||||
node experiments/analysis-efficiency/measure.mjs \
|
||||
--runtime-root /absolute/control-ae805 \
|
||||
--revision ae8051302b5834322fa00cfa31c968ae74ab6ed9 \
|
||||
--artifact sharex.dll --path /absolute/ShareX.dll \
|
||||
--providers /absolute/explicit-providers.json --schemas selected \
|
||||
--output /absolute/new-control-receipts
|
||||
```
|
||||
|
||||
Raw replies, runtime copies, fingerprints and detailed resource receipts stay
|
||||
outside Git. No new provider, plugin SDK, database cache or target execution is
|
||||
introduced by this baseline.
|
||||
|
||||
## Implemented candidate and real SDK observations
|
||||
|
||||
Candidate `8bc1601ca51e417ca472d90ea0367e9418a1a0f2` adds
|
||||
`inspect_managed_artifact({path, detail: "summary"})` and managed collection pages
|
||||
through the existing `inspect_analysis_view` contract. The complete parser result
|
||||
remains retained. Omitted detail and explicit `complete` keep complete output;
|
||||
other managed tools do not acquire this selector. CLI inspection stays complete.
|
||||
The frozen candidate runtime file-table SHA-256 is
|
||||
`9e7380f537301bf0cfe2926f5619011af848af7c220750e75189498e65d825e1`.
|
||||
The revised measurement driver SHA-256 is
|
||||
`8c22f59e931fdc347dea1204ea0faf91330578db684357261ec6f03bd0efa85e`.
|
||||
|
||||
Use `--managed-question identity` on **both** sides, with `--delivery complete`
|
||||
for the control and `--delivery summary` for the candidate. The original default
|
||||
`inventory` recipe remains unchanged. Both identity recipes make the same two
|
||||
public calls, validate their advertised SDK schemas, and compare complete
|
||||
identity/classification, frameworks, coverage and native-boundary facts.
|
||||
|
||||
| Artifact | Complete metadata bytes | Summary metadata bytes | Complete two-call bytes | Summary two-call bytes |
|
||||
| --------------------- | ----------------------: | ---------------------: | ----------------------: | ---------------------: |
|
||||
| ShareX.dll | 1,270,839 | 10,549 | 1,298,752 | 38,462 |
|
||||
| ShareX.HelpersLib.dll | 1,296,839 | 10,937 | 1,586,970 | 301,068 |
|
||||
|
||||
The helper assembly is a second shipped member of the same pinned ShareX release,
|
||||
1,076,224 bytes, SHA-256
|
||||
`e91bd1ec904e2bb8a1add4bb6187443b878bf33d24fe9e27f0b799bfee9de33d`.
|
||||
It is not an independent application or a new runtime/provider lane. Its native
|
||||
boundary report remains comparatively large and unchanged. Metadata reply bytes
|
||||
fall by about 99.17% and 99.16%; two-call totals fall by about 97.04% and 81.03%.
|
||||
The first committed-candidate observations take 3.699 and 3.838 seconds, versus
|
||||
3.809 and 3.925 seconds for controls. These timings include discovery and do not
|
||||
establish a general latency improvement. The full candidate catalog grows from
|
||||
2,482,204 to 2,497,635 bytes; input definitions grow from 527,569 to 528,862.
|
||||
|
||||
An earlier dirty prototype produced the same runtime file-table hash and a
|
||||
separate provenance receipt. Its observations remain separate; a clean baseline
|
||||
commit is never presented as the source of uncommitted production changes.
|
||||
|
||||
## Actual Codex findings: token benefit is not established
|
||||
|
||||
Four sequential trials use actual Codex CLI 0.161.0, requested model
|
||||
`gpt-5.6-terra`, high reasoning, the unchanged question above and the same two
|
||||
enabled tools. The requested alias does not prove a resolved server model ID.
|
||||
Order is control, candidate, candidate, control. Both candidate trials independently
|
||||
select summary; all trials preserve dependency order, make two successful calls,
|
||||
leave the artifact unchanged and remove temporary profiles.
|
||||
|
||||
| Trial | Input tokens | Cached input | Uncached input | Output tokens | Wall seconds |
|
||||
| ----------- | -----------: | -----------: | -------------: | ------------: | -----------: |
|
||||
| Control 1 | 54,821 | 29,184 | 25,637 | 1,782 | 63.207 |
|
||||
| Candidate 1 | 77,594 | 50,432 | 27,162 | 1,583 | 46.685 |
|
||||
| Candidate 2 | 82,426 | 49,408 | 33,018 | 2,120 | 67.379 |
|
||||
| Control 2 | 55,296 | 29,184 | 26,112 | 2,361 | 62.159 |
|
||||
|
||||
Uncached input is reported input minus cached input. **Candidate input and
|
||||
uncached input are higher in both comparisons.** Latency is mixed. This feature
|
||||
demonstrates smaller selected delivery, not measured model-token savings or a
|
||||
stable whole-task speedup. Event result bytes are also not a measurement of the
|
||||
exact context presented to the model. No hidden truncation/cache mechanism is
|
||||
inferred from these aggregates. Keep further model-context investigation separate
|
||||
from this additive view implementation.
|
||||
|
||||
The question prescribes a `coverage` field without prescribing its nested labels.
|
||||
The grader now accepts only explicit equivalent artifact/native scope names;
|
||||
conflicting duplicate scopes still fail. Original failure receipts remain intact.
|
||||
Control 1 and both candidates pass the selected factual checks after that
|
||||
correction. Control 2 omits `classification.container` and does not report
|
||||
separate artifact/native coverage in the expected representation; it fails these
|
||||
strict selected checks. The prompt did not specify every nested key, so this is
|
||||
also an answer-representation limitation. It is not counted as a fully acceptable
|
||||
control answer or repaired by assuming ambiguous flat coverage. Original
|
||||
limitations were additionally reviewed against producer replies.
|
||||
|
||||
All four reports still fail the **complete** selected gate because final physical
|
||||
resource counters are unavailable. The factual subset is reported separately;
|
||||
it does not turn unknown final resource counters into a pass. Trial guards remain
|
||||
unchanged, no guard abort is observed, and sampled peaks stay below the 3 GiB
|
||||
early-stop threshold. A sampled zero OOM count is not final no-OOM proof.
|
||||
|
||||
## Previous-runtime Evidence compatibility
|
||||
|
||||
Complete actual producer Evidence from control `ae8051302b5834322fa00cfa31c968ae74ab6ed9`
|
||||
for both shipped assemblies is passed, unchanged, through the candidate's public
|
||||
SDK inline view. Each artifact makes 14 view calls: a summary, all original
|
||||
reference/resource/attribute pages at a caller-selected 256 rows, and exhausted
|
||||
pages for each collection. Summary facts, every row and order, source coverage,
|
||||
parent ID, links and target digest match; the original Evidence stays unchanged.
|
||||
ShareX has 38/36/1,945 reported rows and HelpersLib has 49/17/1,988. No producer
|
||||
operation or target mutation is requested by this portable workflow.
|
||||
|
||||
The private verification driver SHA-256 is
|
||||
`7bb1f24b488beda012b1e78132de29b868fe1ea91a5e1499d7f271801fa40380`;
|
||||
the receipt scope takes 5.860 seconds, samples 642,244,608 peak family bytes and
|
||||
5.619 last-observed CPU seconds, with no guard abort. Final counters remain unknown.
|
||||
These are actual previous-runtime records, unlike the separate synthetic
|
||||
null-subject compatibility test. They do not prove every historical release.
|
||||
|
||||
Focused SDK/domain checks cover first-call summary, complete defaults, selected
|
||||
target reuse, paging after the source disappears, malformed metadata, historical
|
||||
null subjects, digest mismatch, portable projection and unavailable retention
|
||||
before file access. The exact candidate CI passes all checks after only the
|
||||
browser cleanup and Apple `hdiutil` jobs are retried on the same source revision;
|
||||
their original failures remain recorded in the private team handoff.
|
||||
|
||||
## Committed-runtime repetition and complete defaults
|
||||
|
||||
A second SDK pair reverses the order (candidate before control) for both
|
||||
assemblies. Result byte totals repeat exactly. ShareX workflow times are
|
||||
3.644 versus 3.687 seconds; HelpersLib times are 3.714 versus 3.859 seconds.
|
||||
Both pairs preserve selected settings/facts and have no tool errors. Two pairs
|
||||
per assembly, sharing one application and host, remain a narrow descriptive
|
||||
sample rather than a general performance claim.
|
||||
|
||||
Actual omitted-detail calls on the candidate additionally reproduce **all**
|
||||
normalized metadata inventories and native-boundary facts from both controls,
|
||||
not just the identity recipe's selected subset. Their canonical complete
|
||||
Evidence IDs also match. This verifies the recorded old request and complete
|
||||
result path alongside the explicit new delivery option.
|
||||
|
||||
The changed shared projector also handles an actual shipped SQLite WASM run
|
||||
through the candidate's SDK summary workflow, with real configured WABT:
|
||||
two calls, 18,687 result JSON bytes, 5.614 seconds and no tool failure. This
|
||||
checks that neighboring dispatch path; it does not establish a new WASM
|
||||
performance improvement or additional operating-system/provider coverage.
|
||||
|
|
@ -26,6 +26,7 @@ const { values } = parseArgs({
|
|||
"verify-retained": { type: "boolean", default: false },
|
||||
"native-question": { type: "string", default: "entry" },
|
||||
"javascript-question": { type: "string", default: "inventory" },
|
||||
"managed-question": { type: "string", default: "inventory" },
|
||||
schemas: { type: "string", default: "default" },
|
||||
},
|
||||
});
|
||||
|
|
@ -42,6 +43,12 @@ if (
|
|||
) ||
|
||||
!["inventory", "selected"].includes(values["javascript-question"]) ||
|
||||
!["default", "selected"].includes(values.schemas) ||
|
||||
!["inventory", "identity"].includes(values["managed-question"]) ||
|
||||
(!["sharex.dll", "sharex.helpers.dll"].includes(values.artifact) &&
|
||||
values["managed-question"] !== "inventory") ||
|
||||
(["sharex.dll", "sharex.helpers.dll"].includes(values.artifact) &&
|
||||
values.delivery === "summary" &&
|
||||
values["managed-question"] !== "identity") ||
|
||||
(values["verify-retained"] &&
|
||||
!["weth.hex", "uniswap-v3.factory.hex", "uniswap-v3.pool.hex"].includes(
|
||||
values.artifact,
|
||||
|
|
@ -243,15 +250,55 @@ const targetSha = (value) => value.subject?.digest.sha256;
|
|||
const inventoryPaths = ["package.json", "main.js", "preload.js"];
|
||||
const unique = (values) => [...new Set(values)].sort();
|
||||
const inspectManagedTarget = async () => {
|
||||
const info = await call("inspect_managed_artifact", { path: values.path });
|
||||
const arguments_ = {
|
||||
path: values.path,
|
||||
...(values.delivery === "summary" ? { detail: "summary" } : {}),
|
||||
};
|
||||
const definition = catalogDefinitions.get("inspect_managed_artifact");
|
||||
if (definition === undefined)
|
||||
throw new Error("Managed inspection is not advertised");
|
||||
const accepted = new Ajv2020({
|
||||
strict: false,
|
||||
validateFormats: false,
|
||||
}).compile(definition.inputSchema)(arguments_);
|
||||
if (!accepted)
|
||||
throw new Error(
|
||||
"Selected managed delivery is not advertised by this runtime",
|
||||
);
|
||||
const info = await call("inspect_managed_artifact", arguments_);
|
||||
const boundaries = await call("inspect_managed_native_boundaries", {
|
||||
path: values.path,
|
||||
});
|
||||
const received = normalized(info);
|
||||
const report = received.kind === "summary" ? received.summary : received;
|
||||
let artifact = report;
|
||||
if (values["managed-question"] === "identity") {
|
||||
const {
|
||||
references,
|
||||
resources,
|
||||
attributes,
|
||||
reported_counts,
|
||||
identity_scope: _scope,
|
||||
...identity
|
||||
} = report;
|
||||
artifact = {
|
||||
...identity,
|
||||
reported_counts: reported_counts ?? {
|
||||
references: references.length,
|
||||
resources: resources.length,
|
||||
attributes: attributes.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
artifact_sha256: targetSha(info),
|
||||
artifact: normalized(info),
|
||||
artifact,
|
||||
boundaries: normalized(boundaries),
|
||||
evidence_ids: [info.evidence_id, boundaries.evidence_id],
|
||||
evidence_ids: [
|
||||
info.evidence_id,
|
||||
...(received.kind === "summary" ? [received.parent_evidence_id] : []),
|
||||
boundaries.evidence_id,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -766,6 +813,11 @@ const report = {
|
|||
: null,
|
||||
javascript_question:
|
||||
values.artifact === "obsidian.asar" ? values["javascript-question"] : null,
|
||||
managed_question: ["sharex.dll", "sharex.helpers.dll"].includes(
|
||||
values.artifact,
|
||||
)
|
||||
? values["managed-question"]
|
||||
: null,
|
||||
provider_settings_sha256: sha(JSON.stringify(selectedProviders)),
|
||||
provider_search_path_sha256: sha(
|
||||
selectedProviders.PATH ?? process.env.PATH ?? "",
|
||||
|
|
|
|||
|
|
@ -39,6 +39,19 @@ def decode(result):
|
|||
return None
|
||||
|
||||
|
||||
def named_scope(value, names):
|
||||
"""Accept explicit equivalent scope labels; contradictory duplicates stay a failure."""
|
||||
if not isinstance(value, dict):
|
||||
return None
|
||||
selected = {name: value[name] for name in names if name in value}
|
||||
values = list(selected.values())
|
||||
if not values:
|
||||
return None
|
||||
if all(entry == values[0] for entry in values):
|
||||
return values[0]
|
||||
return {'conflicting_reported_scopes': selected}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--domain', choices=['wasm', 'evm', 'android', 'managed', 'managed-nonempty', 'firmware', 'inventory', 'javascript', 'native', 'browser'], required=True)
|
||||
|
|
@ -120,8 +133,8 @@ def main():
|
|||
check(key, answer.get(key), n[key])
|
||||
for key in ['module_refs', 'pinvoke_imports']:
|
||||
check(key, answer.get('native_boundaries', {}).get(key), reference['boundaries'][key])
|
||||
check('artifact_coverage', answer.get('coverage', {}).get('artifact'), n['coverage'])
|
||||
check('native_boundaries_coverage', answer.get('coverage', {}).get('native_boundaries'), reference['boundaries']['coverage'])
|
||||
check('artifact_coverage', named_scope(answer.get('coverage'), ['artifact', 'managed_artifact', 'managed_metadata']), n['coverage'])
|
||||
check('native_boundaries_coverage', named_scope(answer.get('coverage'), ['native_boundaries', 'native_boundary_declarations']), reference['boundaries']['coverage'])
|
||||
elif domain == 'managed-nonempty':
|
||||
n = reference['boundaries']
|
||||
check('module_mvid', answer.get('module_mvid'), n['module']['mvid'])
|
||||
|
|
@ -280,6 +293,7 @@ def main():
|
|||
status = str(event['status'])
|
||||
statuses[status] = statuses.get(status, 0) + 1
|
||||
check('network_response_statuses', network['observed_response_statuses'], statuses)
|
||||
factual_checks = dict(checks)
|
||||
scopes = [json.loads(line) for line in args.resource_log.read_text().splitlines() if line.startswith('{')]
|
||||
scope = next(value for value in reversed(scopes) if 'resource_scope' in value)
|
||||
checks['no_resource_abort'] = scope['abort'] is None
|
||||
|
|
@ -298,6 +312,7 @@ def main():
|
|||
report = {
|
||||
'review_source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||
'domain': domain, 'checks': checks, 'all_selected_checks_pass': all(checks.values()),
|
||||
'factual_checks': factual_checks, 'all_factual_checks_pass': all(factual_checks.values()),
|
||||
'differences': differences, 'tool_errors': tool_errors, 'summary': summary,
|
||||
'fingerprints': {key: value for key, value in settings.items() if 'sha256' in key or key in ['runtime_revision', 'enabled_tools', 'authority']},
|
||||
'answer_file_sha256': hashlib.sha256((args.trial / 'answer.txt').read_bytes()).hexdigest(),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
|
|||
...toolContractMetadata("inspect_analysis_view"),
|
||||
kind: "application",
|
||||
description:
|
||||
'Inspect selected views of completed binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_evm_interface or inspect_web_network_capture Evidence without repeating analysis. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. For layout, JavaScript, native, inventory, WASM and EVM sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions; occurrence logical_paths matches exact recorded strings (omitted means all available, [] means none). source_complete distinguishes complete inventories from partial source records; page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Native views select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.',
|
||||
'Inspect selected views of completed binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_evm_interface, inspect_managed_artifact or inspect_web_network_capture Evidence without repeating analysis. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. For a retained record use source: {kind: "retained-evidence", evidence_id: "<parent_evidence_id>"}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: <complete Evidence>}. For layout, JavaScript, native, inventory, WASM, EVM and managed artifact sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions; occurrence logical_paths matches exact recorded strings (omitted means all available, [] means none). source_complete distinguishes complete inventories from partial source records; page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Managed artifact summaries preserve complete identity/classification, metadata status, module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed pages use kind managed and collection references/resources/attributes, retaining source coverage and build-local token scope; page exhaustion does not establish complete metadata. Native views select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.',
|
||||
inputSchema: inspectAnalysisViewInputSchema,
|
||||
outputSchema: evidenceResultOf(analysisViewResultSchema),
|
||||
examples: [
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { managedOutputSchemas } from "../toolOutputSchemaGroups.js";
|
|||
import { toolContractMetadata } from "../toolEffects.js";
|
||||
import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js";
|
||||
import { localPathStringSchema } from "../../domain/localPath.js";
|
||||
import { analysisDetailSchema } from "../analysisDetail.js";
|
||||
|
||||
/** Exact caller boundary for execution-free PE/CLI triage and identity. */
|
||||
export const managedTargetInputSchema = z.strictObject({
|
||||
|
|
@ -20,6 +21,11 @@ export const managedTargetInputSchema = z.strictObject({
|
|||
.optional(),
|
||||
});
|
||||
|
||||
/** Delivery selection belongs only to artifact inspection, not member/native queries. */
|
||||
export const managedArtifactInputSchema = managedTargetInputSchema.extend({
|
||||
detail: analysisDetailSchema.unwrap().optional(),
|
||||
});
|
||||
|
||||
/** Member inspection may select declaring types before members are decoded. */
|
||||
export const managedMembersInputSchema = managedTargetInputSchema.extend({
|
||||
type_names: z
|
||||
|
|
@ -61,15 +67,20 @@ export const MANAGED_TOOL_CONTRACTS = [
|
|||
name: "inspect_managed_artifact",
|
||||
...toolContractMetadata("inspect_managed_artifact"),
|
||||
description:
|
||||
"Open and classify an explicit managed PE/CLI path, or inspect the active managed target, then inventory exact assembly/module identity, target framework evidence, references, resources, and custom attributes without loading or executing target code. Returns complete inline inventories when metadata is admitted and explicit partial or malformed coverage otherwise.",
|
||||
'Inspect an explicit PE/CLI path, or the selected managed target, without loading or executing target code. Omitted or complete detail returns complete metadata inventories with original coverage. Select detail: "summary" on the first call for complete identity, PE/CLI, classification with its evidence, module/MVID, assembly, frameworks, returned-collection counts, coverage/issues and limitations. It retains complete Evidence and returns a distinct linked inspect_analysis_view summary. Page original references/resources/attributes with view kind managed and normalized_result.parent_evidence_id. Counts cover producer-returned rows; partial, unavailable or malformed metadata remains explicit and tokens remain build-local.',
|
||||
kind: "managed-provider",
|
||||
inputSchema: managedTargetInputSchema,
|
||||
inputSchema: managedArtifactInputSchema,
|
||||
outputSchema,
|
||||
examples: [
|
||||
{
|
||||
title: "Example inspect managed artifact request",
|
||||
input: { path: "/tmp/Example.dll" },
|
||||
},
|
||||
{
|
||||
title:
|
||||
"Inspect identity and coverage without unrelated metadata inventories",
|
||||
input: { path: "/tmp/Example.dll", detail: "summary" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { z } from "zod";
|
||||
import { artifactInventorySummaryViewResultSchema } from "../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
artifactInventorySummaryViewResultSchema,
|
||||
managedArtifactSummaryViewResultSchema,
|
||||
} from "../domain/analysisView/analysisView.js";
|
||||
import { evidenceBundleSummarySchema } from "../domain/evidenceRecordSummary.js";
|
||||
import { BINARY_ARCHITECTURES } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
|
|
@ -402,7 +405,12 @@ export const artifactOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
|||
|
||||
/** Exact Evidence schema for execution-free managed static analysis. */
|
||||
export const managedOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
|
||||
inspect_managed_artifact: evidenceResultOf(managedArtifactInspectionSchema),
|
||||
inspect_managed_artifact: evidenceResultOf(
|
||||
z.union([
|
||||
managedArtifactInspectionSchema,
|
||||
managedArtifactSummaryViewResultSchema,
|
||||
]),
|
||||
),
|
||||
inspect_managed_members: evidenceResultOf(managedMemberInspectionSchema),
|
||||
inspect_managed_native_boundaries: evidenceResultOf(
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
|
|
|
|||
|
|
@ -733,6 +733,14 @@ it("does not match unavailable layout names using a display placeholder", () =>
|
|||
});
|
||||
|
||||
it("rejects unsupported parent operations and incompatible collections", () => {
|
||||
expect(
|
||||
projectAnalysisView(layoutParent(), {
|
||||
kind: "managed",
|
||||
collection: "attributes",
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
}),
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
|
||||
const unsupported = projectAnalysisView(
|
||||
{
|
||||
evidenceId: `ev_${"d".repeat(64)}`,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,13 @@ import {
|
|||
evmSummarySchema,
|
||||
} from "./evmInterfaceView.js";
|
||||
import { projectArtifactInventoryView } from "./artifactInventoryView.js";
|
||||
import {
|
||||
projectManagedArtifactView,
|
||||
managedViewRequestSchema,
|
||||
managedViewItemSchema,
|
||||
managedSummarySchema,
|
||||
managedViewContextSchema,
|
||||
} from "./managedArtifactView.js";
|
||||
import {
|
||||
projectWasmArtifactView,
|
||||
wasmViewRequestSchema,
|
||||
|
|
@ -161,6 +168,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
|
|||
inventoryViewSchema,
|
||||
wasmViewRequestSchema,
|
||||
evmViewRequestSchema,
|
||||
managedViewRequestSchema,
|
||||
apiSurfaceViewSchema,
|
||||
]);
|
||||
|
||||
|
|
@ -274,6 +282,7 @@ const viewResultBase = {
|
|||
"inventory_artifact",
|
||||
"inspect_wasm_artifact",
|
||||
"inspect_evm_interface",
|
||||
"inspect_managed_artifact",
|
||||
]),
|
||||
parent_digest: digestSchema,
|
||||
view_digest: digestSchema,
|
||||
|
|
@ -324,6 +333,13 @@ export const evmInterfaceSummaryViewResultSchema = z.strictObject({
|
|||
summary: evmSummarySchema,
|
||||
});
|
||||
|
||||
/** Exact metadata overview for the managed artifact's selected delivery. */
|
||||
export const managedArtifactSummaryViewResultSchema = z.strictObject({
|
||||
...summaryViewResultBase,
|
||||
parent_operation: z.literal("inspect_managed_artifact"),
|
||||
summary: managedSummarySchema,
|
||||
});
|
||||
|
||||
/** Projected facts for one selected view of retained analysis Evidence. */
|
||||
export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
z.strictObject({
|
||||
|
|
@ -345,6 +361,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
|||
inventorySummarySchema,
|
||||
wasmSummarySchema,
|
||||
evmSummarySchema,
|
||||
managedSummarySchema,
|
||||
]),
|
||||
}),
|
||||
z.strictObject({
|
||||
|
|
@ -385,6 +402,14 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
|||
...viewResultBase,
|
||||
parent_operation: z.literal("inspect_evm_interface"),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("managed"),
|
||||
view: managedViewRequestSchema,
|
||||
item: managedViewItemSchema,
|
||||
...viewResultBase,
|
||||
...managedViewContextSchema.shape,
|
||||
parent_operation: z.literal("inspect_managed_artifact"),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("inventory"),
|
||||
view: inventoryViewSchema,
|
||||
|
|
@ -451,7 +476,11 @@ export type UnsignedAnalysisView =
|
|||
"view_digest"
|
||||
>
|
||||
| Omit<Extract<AnalysisViewResult, { readonly kind: "wasm" }>, "view_digest">
|
||||
| Omit<Extract<AnalysisViewResult, { readonly kind: "evm" }>, "view_digest">;
|
||||
| Omit<Extract<AnalysisViewResult, { readonly kind: "evm" }>, "view_digest">
|
||||
| Omit<
|
||||
Extract<AnalysisViewResult, { readonly kind: "managed" }>,
|
||||
"view_digest"
|
||||
>;
|
||||
|
||||
/** Attach the content digest that must change when projected bytes change. */
|
||||
export const sealAnalysisView = (
|
||||
|
|
@ -496,7 +525,7 @@ const unsupportedParent = (
|
|||
new AnalysisUnsupportedTargetError(
|
||||
INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
parentArtifactPath(parent),
|
||||
`Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, or web network capture Evidence.`,
|
||||
`Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, EVM, managed artifact, or web network capture Evidence.`,
|
||||
);
|
||||
|
||||
const parentArtifactPath = (parent: AnalysisViewParent): string => {
|
||||
|
|
@ -520,6 +549,15 @@ const parentArtifactPath = (parent: AnalysisViewParent): string => {
|
|||
return parent.evidenceId;
|
||||
};
|
||||
|
||||
/** Named source owners for views that need no extra decoder or accumulator. */
|
||||
const retainedViewProjectors = new Map<string, typeof projectWasmArtifactView>([
|
||||
["inspect_wasm_artifact", projectWasmArtifactView],
|
||||
["inspect_evm_interface", projectEvmInterfaceView],
|
||||
["inspect_managed_artifact", projectManagedArtifactView],
|
||||
["inspect_artifact", projectArtifactInventoryView],
|
||||
["inventory_artifact", projectArtifactInventoryView],
|
||||
]);
|
||||
|
||||
/** Project one selected view from authenticated parent analysis Evidence. */
|
||||
export const projectAnalysisView = (
|
||||
parent: AnalysisViewParent,
|
||||
|
|
@ -591,19 +629,9 @@ export const projectAnalysisView = (
|
|||
const projected = projectNativeFunctionView(parent, native.data, view);
|
||||
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
|
||||
}
|
||||
if (parent.operation === "inspect_wasm_artifact") {
|
||||
const projected = projectWasmArtifactView(parent, view);
|
||||
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
|
||||
}
|
||||
if (parent.operation === "inspect_evm_interface") {
|
||||
const projected = projectEvmInterfaceView(parent, view);
|
||||
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
|
||||
}
|
||||
if (
|
||||
parent.operation === "inspect_artifact" ||
|
||||
parent.operation === "inventory_artifact"
|
||||
) {
|
||||
const projected = projectArtifactInventoryView(parent, view);
|
||||
const project = retainedViewProjectors.get(parent.operation);
|
||||
if (project !== undefined) {
|
||||
const projected = project(parent, view);
|
||||
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
|
||||
}
|
||||
return err(unsupportedParent(parent));
|
||||
|
|
|
|||
|
|
@ -218,6 +218,12 @@ export const projectBinaryLayoutView = (
|
|||
...shared,
|
||||
});
|
||||
}
|
||||
if (view.kind !== "page")
|
||||
return err(
|
||||
layoutIncompatible(
|
||||
"Layout views require summary, facet, item or page selections.",
|
||||
),
|
||||
);
|
||||
if (view.collection === "modules")
|
||||
return err(
|
||||
layoutIncompatible(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
import { z } from "zod";
|
||||
import type { AnalysisError } from "../analysisErrorBase.js";
|
||||
import { uniqueSorted } from "../canonicalOrdering.js";
|
||||
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
|
||||
import {
|
||||
managedArtifactInspectionSchema,
|
||||
managedMemberInspectionSchema,
|
||||
} from "../managed/managedArtifact.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
import {
|
||||
analysisViewInputError,
|
||||
type AnalysisViewParent,
|
||||
type AnalysisViewRequest,
|
||||
type UnsignedAnalysisView,
|
||||
} from "./analysisView.js";
|
||||
import {
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
|
||||
/** Original metadata rows; offsets count entries in the returned parent collection. */
|
||||
export const managedViewRequestSchema = z.strictObject({
|
||||
kind: z.literal("managed"),
|
||||
collection: z.enum(["references", "resources", "attributes"]),
|
||||
offset: z.number().int().nonnegative().default(0),
|
||||
limit: z.number().int().positive().default(64),
|
||||
});
|
||||
|
||||
export const managedViewItemSchema = z.union([
|
||||
managedArtifactInspectionSchema.shape.references,
|
||||
managedArtifactInspectionSchema.shape.resources,
|
||||
managedArtifactInspectionSchema.shape.attributes,
|
||||
]);
|
||||
|
||||
/** Keep complete classification and coverage; unrequested inventories remain retained. */
|
||||
export const managedSummarySchema = managedArtifactInspectionSchema
|
||||
.omit({ references: true, resources: true, attributes: true })
|
||||
.extend({
|
||||
reported_counts: z.strictObject({
|
||||
references: z.number().int().nonnegative(),
|
||||
resources: z.number().int().nonnegative(),
|
||||
attributes: z.number().int().nonnegative(),
|
||||
}),
|
||||
identity_scope: managedMemberInspectionSchema.shape.identity_scope,
|
||||
});
|
||||
|
||||
export const managedViewContextSchema = z.strictObject({
|
||||
identity_scope: managedMemberInspectionSchema.shape.identity_scope,
|
||||
source_metadata_status:
|
||||
managedArtifactInspectionSchema.shape.metadata.shape.status,
|
||||
source_coverage: managedArtifactInspectionSchema.shape.coverage,
|
||||
});
|
||||
|
||||
/** Project authenticated metadata without reading a path or invoking a provider. */
|
||||
export const projectManagedArtifactView = (
|
||||
parent: AnalysisViewParent,
|
||||
view: AnalysisViewRequest,
|
||||
): Result<UnsignedAnalysisView, AnalysisError> => {
|
||||
if (view.kind !== "summary" && view.kind !== "managed")
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
{
|
||||
path: ["view"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Managed artifact inspection supports summary and managed views.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const parsed = managedArtifactInspectionSchema.safeParse(
|
||||
parent.normalizedResult,
|
||||
);
|
||||
if (!parsed.success)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(
|
||||
"inspect_analysis_view",
|
||||
parsed.error.issues.map((issue) => ({
|
||||
...issue,
|
||||
path: ["source", "normalized_result", ...issue.path],
|
||||
})),
|
||||
{ source: { normalized_result: parent.normalizedResult } },
|
||||
{ cause: parsed.error },
|
||||
),
|
||||
);
|
||||
const report = parsed.data;
|
||||
const identity_scope = {
|
||||
token_identity: "build-local" as const,
|
||||
requires_artifact_sha256: report.artifact.sha256,
|
||||
requires_mvid: report.module?.mvid ?? null,
|
||||
};
|
||||
const shared = {
|
||||
parent_evidence_id: parent.evidenceId,
|
||||
parent_operation: "inspect_managed_artifact" as const,
|
||||
parent_digest: parent.evidenceId.slice(3),
|
||||
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
|
||||
limitations: uniqueSorted([...parent.limitations, ...report.limitations]),
|
||||
unknowns: [
|
||||
"Collection counts and pages cover producer-returned rows; metadata completeness remains the original metadata status and coverage, not an absence claim about unreturned rows.",
|
||||
"Metadata tokens are build-local coordinates bound to the recorded artifact SHA-256 and module MVID; this view does not load or execute the assembly.",
|
||||
...(parent.subject == null
|
||||
? [
|
||||
"The parent has no recorded subject; its normalized artifact identity cannot be cross-checked against a subject digest.",
|
||||
]
|
||||
: []),
|
||||
...(identity_scope.requires_mvid === null
|
||||
? [
|
||||
"The module MVID is unknown; tokens cannot establish a cross-observation module match.",
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
if (view.kind === "summary") {
|
||||
const { references, resources, attributes, ...summary } = report;
|
||||
const reported_counts = {
|
||||
references: references.length,
|
||||
resources: resources.length,
|
||||
attributes: attributes.length,
|
||||
};
|
||||
const total = references.length + resources.length + attributes.length;
|
||||
return ok({
|
||||
kind: "summary",
|
||||
view,
|
||||
...shared,
|
||||
summary: { ...summary, reported_counts, identity_scope },
|
||||
coverage: completeWithinViewCoverage(total, total),
|
||||
});
|
||||
}
|
||||
const collection = report[view.collection];
|
||||
const item = collection.slice(view.offset, view.offset + view.limit);
|
||||
return ok({
|
||||
kind: "managed",
|
||||
view,
|
||||
item,
|
||||
...shared,
|
||||
identity_scope,
|
||||
source_metadata_status: report.metadata.status,
|
||||
source_coverage: report.coverage,
|
||||
coverage: pageViewCoverage(view.offset, item.length, collection.length),
|
||||
});
|
||||
};
|
||||
|
|
@ -709,7 +709,7 @@ const registerBinaryAnalysisTools = ({
|
|||
};
|
||||
registerEvidenceTools(server, NATIVE_TOOL_CONTRACTS, evidenceOptions);
|
||||
registerArtifactTools(server, { ...evidenceOptions, evidenceById });
|
||||
registerManagedTools(server, evidenceOptions);
|
||||
registerManagedTools(server, { ...evidenceOptions, evidenceById });
|
||||
if (session !== undefined)
|
||||
registerManagedWorkflowTools(server, {
|
||||
logger,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
|
||||
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
|
||||
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
|
||||
import {
|
||||
requireAnalysisViewRetention,
|
||||
summarizeRetainedAnalysis,
|
||||
} from "../application/analysisView/AnalysisViewService.js";
|
||||
|
||||
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "../contracts/managed/managedToolContracts.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import { err } from "../domain/result.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import type { Logger } from "pino";
|
||||
import { registerEvidenceTools } from "./registerEvidenceTools.js";
|
||||
import { runManagedProviderExecution } from "../composition/directAnalysis.js";
|
||||
|
|
@ -18,6 +23,7 @@ export const registerManagedTools = (
|
|||
readonly logger: Logger;
|
||||
readonly activeTarget: (() => BinaryTarget | undefined) | undefined;
|
||||
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
|
||||
readonly evidenceById?: EvidenceLookup | undefined;
|
||||
readonly withAdmittedAnalysis: import("./analysisAdmission.js").WithAdmittedAnalysis;
|
||||
},
|
||||
): void => {
|
||||
|
|
@ -28,6 +34,17 @@ export const registerManagedTools = (
|
|||
execute: async (operation, parameters, executionOptions) => {
|
||||
if (!isManagedToolName(operation))
|
||||
return analysis.execute(operation, parameters, executionOptions);
|
||||
const { detail: _detail, ...artifactParameters } = parameters;
|
||||
if (
|
||||
operation === "inspect_managed_artifact" &&
|
||||
parameters.detail === "summary"
|
||||
) {
|
||||
const ready = requireAnalysisViewRetention(operation, {
|
||||
recordEvidence: options.recordEvidence,
|
||||
evidenceById: options.evidenceById,
|
||||
});
|
||||
if (!ready.ok) return ready;
|
||||
}
|
||||
const requestedPath = parameters.path;
|
||||
const activeTarget = options.activeTarget?.();
|
||||
const path =
|
||||
|
|
@ -52,7 +69,9 @@ export const registerManagedTools = (
|
|||
path,
|
||||
operation,
|
||||
executionOptions?.signal,
|
||||
parameters,
|
||||
operation === "inspect_managed_artifact"
|
||||
? artifactParameters
|
||||
: parameters,
|
||||
);
|
||||
if (execution.ok && typeof requestedPath === "string")
|
||||
selectedManagedPath = requestedPath;
|
||||
|
|
@ -62,5 +81,13 @@ export const registerManagedTools = (
|
|||
registerEvidenceTools(server, MANAGED_TOOL_CONTRACTS, {
|
||||
...options,
|
||||
analysisForAdmission,
|
||||
projectEvidence: (operation, parameters, evidence) =>
|
||||
operation === "inspect_managed_artifact" &&
|
||||
parameters.detail === "summary"
|
||||
? summarizeRetainedAnalysis(evidence, {
|
||||
recordEvidence: options.recordEvidence,
|
||||
evidenceById: options.evidenceById,
|
||||
})
|
||||
: ok(evidence),
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,257 @@
|
|||
import { writeFile, unlink } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { composeBinarySession } from "../../../src/application/binary/BinarySessionComposition.js";
|
||||
import { AnalysisProviderRegistry } from "../../../src/application/binary/AnalysisProviderRegistry.js";
|
||||
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
|
||||
import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js";
|
||||
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import {
|
||||
managedArtifactSummaryViewResultSchema,
|
||||
analysisViewResultSchema,
|
||||
} from "../../../src/domain/analysisView/analysisView.js";
|
||||
import { managedArtifactInspectionSchema } from "../../../src/domain/managed/managedArtifact.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
|
||||
const createHarness = async () => {
|
||||
const session = composeBinarySession(new AnalysisProviderRegistry([]), [
|
||||
new ManagedStaticProvider(),
|
||||
]);
|
||||
const harness = await createApplicationMcpHarness({ session });
|
||||
onTestFinished(harness.close);
|
||||
return harness;
|
||||
};
|
||||
|
||||
it("returns a first-call overview and pages the unchanged source after its file disappears", async () => {
|
||||
const directory = await createTestTempDirectory("rea-managed-summary-");
|
||||
const path = join(directory, "fixture.dll");
|
||||
await writeFile(
|
||||
path,
|
||||
buildManagedPeFixture({
|
||||
references: ["mscorlib", "Another.Reference"],
|
||||
customAttributeRowCount: 3,
|
||||
}),
|
||||
);
|
||||
const { client, session } = await createHarness();
|
||||
const catalog = (await client.listTools()).tools;
|
||||
const definition = catalog.find((x) => x.name === "inspect_managed_artifact");
|
||||
if (definition?.outputSchema === undefined)
|
||||
throw new Error("Managed schemas are absent");
|
||||
const outputSchema: Record<string, unknown> = definition.outputSchema;
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
expect(ajv.validateSchema(definition.inputSchema)).toBe(true);
|
||||
expect(ajv.validateSchema(outputSchema)).toBe(true);
|
||||
for (const name of [
|
||||
"inspect_managed_members",
|
||||
"inspect_managed_native_boundaries",
|
||||
])
|
||||
expect(
|
||||
ajv.validate(catalog.find((x) => x.name === name)!.inputSchema, {
|
||||
path,
|
||||
detail: "summary",
|
||||
}),
|
||||
).toBe(false);
|
||||
const reply = await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: { path, detail: "summary" },
|
||||
});
|
||||
expect(reply.isError).not.toBe(true);
|
||||
expect(
|
||||
ajv.validate(outputSchema, reply.structuredContent),
|
||||
JSON.stringify(ajv.errors),
|
||||
).toBe(true);
|
||||
const receipt = parseEvidence(reply.structuredContent);
|
||||
const view = managedArtifactSummaryViewResultSchema.parse(
|
||||
receipt.normalized_result,
|
||||
);
|
||||
const parent = session.evidenceById(view.parent_evidence_id);
|
||||
if (parent === undefined)
|
||||
throw new Error("Complete managed source was not retained");
|
||||
const report = managedArtifactInspectionSchema.parse(
|
||||
parent.normalized_result,
|
||||
);
|
||||
expect(parent.operation).toBe("inspect_managed_artifact");
|
||||
expect(parent.parameters).toEqual({ path, detail: "summary" });
|
||||
expect(receipt.evidence_id).not.toBe(parent.evidence_id);
|
||||
expect(receipt.evidence_links).toEqual([parent.evidence_id]);
|
||||
const { references, resources, attributes, ...identity } = report;
|
||||
expect(view.summary).toMatchObject(identity);
|
||||
expect(view.summary.reported_counts).toEqual({
|
||||
references: references.length,
|
||||
resources: resources.length,
|
||||
attributes: attributes.length,
|
||||
});
|
||||
expect(view.summary).not.toHaveProperty("attributes");
|
||||
for (const detail of [undefined, "complete"]) {
|
||||
const complete = parseEvidence(
|
||||
(
|
||||
await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: { path, ...(detail === undefined ? {} : { detail }) },
|
||||
})
|
||||
).structuredContent,
|
||||
);
|
||||
expect(complete.operation).toBe("inspect_managed_artifact");
|
||||
expect(complete.normalized_result).toEqual(parent.normalized_result);
|
||||
}
|
||||
const reused = await client.callTool({
|
||||
name: "inspect_managed_native_boundaries",
|
||||
arguments: {},
|
||||
});
|
||||
expect(reused.isError).not.toBe(true);
|
||||
expect(parseEvidence(reused.structuredContent).subject?.local_path).toBe(
|
||||
path,
|
||||
);
|
||||
const original = JSON.stringify(parent);
|
||||
await unlink(path);
|
||||
for (const collection of ["references", "resources", "attributes"] as const) {
|
||||
const result = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
|
||||
view: { kind: "managed", collection, offset: 0, limit: 1 },
|
||||
},
|
||||
});
|
||||
expect(result.isError).not.toBe(true);
|
||||
const page = analysisViewResultSchema.parse(
|
||||
parseEvidence(result.structuredContent).normalized_result,
|
||||
);
|
||||
if (page.kind !== "managed") throw new Error("Unexpected page kind");
|
||||
expect(page.item).toEqual(report[collection].slice(0, 1));
|
||||
expect(page.identity_scope.requires_mvid).toBe(report.module?.mvid ?? null);
|
||||
expect(page.source_coverage).toEqual(report.coverage);
|
||||
expect(page.coverage.total).toBe(report[collection].length);
|
||||
}
|
||||
expect(JSON.stringify(session.evidenceById(parent.evidence_id))).toBe(
|
||||
original,
|
||||
);
|
||||
expect(
|
||||
session
|
||||
.exportEvidenceBundle()
|
||||
.records.find((x) => x.evidence_id === parent.evidence_id),
|
||||
).toEqual(parent);
|
||||
const portable = inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: parent },
|
||||
view: { kind: "summary" },
|
||||
});
|
||||
if (!portable.ok) throw portable.error;
|
||||
expect(portable.value.normalized_result).toEqual(view);
|
||||
}, 30000);
|
||||
|
||||
it("keeps malformed metadata, empty returned rows and historical null subjects explicit", async () => {
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-managed-unavailable-view-",
|
||||
);
|
||||
const path = join(directory, "malformed.dll");
|
||||
await writeFile(
|
||||
path,
|
||||
buildManagedPeFixture({ corruptMetadataSignature: true }),
|
||||
);
|
||||
const { client } = await createHarness();
|
||||
const completeReply = await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(completeReply.isError).not.toBe(true);
|
||||
const parent = parseEvidence(completeReply.structuredContent);
|
||||
const report = managedArtifactInspectionSchema.parse(
|
||||
parent.normalized_result,
|
||||
);
|
||||
expect(report.metadata.status).toBe("malformed");
|
||||
const historical = createEvidence(undefined, parent.provider, {
|
||||
operation: parent.operation,
|
||||
parameters: parent.parameters,
|
||||
result: parent.normalized_result,
|
||||
limitations: parent.limitations,
|
||||
authority: parent.authority,
|
||||
});
|
||||
const original = JSON.stringify(historical);
|
||||
for (const view of [
|
||||
{ kind: "summary" },
|
||||
{ kind: "managed", collection: "attributes", offset: 0, limit: 3 },
|
||||
]) {
|
||||
const result = inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: historical },
|
||||
view,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.subject).toBeNull();
|
||||
const projected = analysisViewResultSchema.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
expect(projected.unknowns.join(" ")).toContain("no recorded subject");
|
||||
if (projected.kind === "managed") {
|
||||
expect(projected.item).toEqual(report.attributes);
|
||||
expect(projected.source_coverage).toEqual(report.coverage);
|
||||
expect(projected.source_metadata_status).toBe("malformed");
|
||||
expect(projected.identity_scope.requires_mvid).toBeNull();
|
||||
} else if (projected.kind === "summary") {
|
||||
const summary =
|
||||
managedArtifactSummaryViewResultSchema.parse(projected).summary;
|
||||
expect(summary.classification).toEqual(report.classification);
|
||||
expect(summary.coverage).toEqual(report.coverage);
|
||||
expect(summary.metadata).toEqual(report.metadata);
|
||||
} else throw new Error("Unexpected view");
|
||||
}
|
||||
expect(JSON.stringify(historical)).toBe(original);
|
||||
const incompatible = createEvidence(
|
||||
{ path, format: "pe", sha256: "b".repeat(64) },
|
||||
parent.provider,
|
||||
{
|
||||
operation: parent.operation,
|
||||
parameters: parent.parameters,
|
||||
result: parent.normalized_result,
|
||||
},
|
||||
);
|
||||
expect(
|
||||
inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: incompatible },
|
||||
view: { kind: "summary" },
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "EvidenceIntegrityError" },
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
it("refuses selected delivery before file access when the server cannot retain Evidence", async () => {
|
||||
const directory = await createTestTempDirectory("rea-managed-fixed-view-");
|
||||
const path = join(directory, "fixture.dll");
|
||||
await writeFile(path, buildManagedPeFixture());
|
||||
const analysis = createTestBinarySession(() => {
|
||||
throw new Error("Deep provider must not start");
|
||||
});
|
||||
const server = createServer({ kind: "fixed", analysis });
|
||||
const client = new Client({ name: "managed-fixed-delivery", version: "1" });
|
||||
onTestFinished(async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
await analysis.close();
|
||||
});
|
||||
const [a, b] = InMemoryTransport.createLinkedPair();
|
||||
await server.connect(b);
|
||||
await client.connect(a);
|
||||
const rejected = await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: { path: join(directory, "absent.dll"), detail: "summary" },
|
||||
});
|
||||
expect(parseMcpToolError(rejected).error.code).toBe("capability_unavailable");
|
||||
const complete = await client.callTool({
|
||||
name: "inspect_managed_artifact",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(complete.isError).not.toBe(true);
|
||||
expect(
|
||||
toolContract("inspect_managed_artifact").outputSchema.safeParse(
|
||||
complete.structuredContent,
|
||||
).success,
|
||||
).toBe(true);
|
||||
}, 30000);
|
||||
Loading…
Reference in New Issue