* feat(artifacts): inventory and extract .NET single-file bundles * fix(artifacts): retain bundle source I/O failure reasons * docs(artifacts): record Linux bundle regression verification * fix(artifacts): preserve bundle source failures and cancellation * docs(artifacts): record synchronized bundle verification * fix(artifacts): bind bundle extraction to inventoried host * docs(artifacts): record bundle verification outcome * fix(artifacts): bound bundle paths and suggest extraction * fix(artifacts): preserve nested executable container boundaries * test(artifacts): verify projected .NET members and simplify extraction admission * fix(artifacts): explain fixed bundle limits at their producing boundary --------- Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
54 KiB
Managed-code analysis
REA inspects .NET PE/CLI artifacts without loading or executing their code. It can identify an assembly, inspect metadata and CIL, compare members across builds, and connect declared native calls to supplied native-analysis evidence. CIL is Common Intermediate Language, the instruction format used by managed assemblies.
The seven shipped tools are:
| Task | MCP tool |
|---|---|
| Identify an artifact and its managed deployment form | inspect_managed_artifact |
| Inspect types, signatures, method bodies, calls, and field access | inspect_managed_members |
| List declared native calls and native implementation indicators | inspect_managed_native_boundaries |
| Compare members across builds and map build-local tokens | compare_managed_members |
| Check native call declarations against supplied export or function evidence | verify_managed_native_boundaries |
| Import decompiled code against verified static member identities | import_managed_reconstruction |
| Add managed findings to an application graph | project_managed_application_graph |
Each has a matching CLI command: replace underscores with hyphens and prefix
the name with rea, for example rea inspect-managed-artifact.
Native-body bridge mapping and managed runtime execution are outside the
current tool set.
The canonical tool inventory is the build-generated catalog.
Inspect an assembly
rea inspect-managed-artifact /absolute/path/Assembly.dll --json
rea inspect-managed-members /absolute/path/Assembly.dll --json
Use the artifact result to identify the assembly and its metadata coverage.
Member results retain build-local tokens, signatures, CIL bodies, and decoded
relationships. Use an exact reported full_name with --type when a focused
inspection is needed; see selected member inspection.
Compare separate builds with 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.
ECMA-335 permits empty metadata streams to be omitted. An absent #Blob
stream therefore does not prevent module or assembly identity from being read
when all blob indexes are zero; a nonzero reference to that missing heap is
reported as a missing-stream issue.
Use normalized_result.parent_evidence_id to page original rows:
{
"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.
Member summary and retained member rows
Complete inspect_managed_members delivery always includes the assembly-wide
member_refs and exported_types, which type_names does not narrow, so even
one selected or unmatched type can return megabytes. In MCP, select
{"path":"/absolute/path/Assembly.dll","type_names":["Namespace.Type"],"detail":"summary"}
to keep the complete source in the session and receive identity, metadata
status, the selection with its unmatched names, coverage/issues, the separate
user-string coverage, limitations and per-collection counts. Then page original
rows with view kind managed-members:
{
"source": {
"kind": "retained-evidence",
"evidence_id": "<parent_evidence_id>"
},
"view": {
"kind": "managed-members",
"collection": "call_edges",
"offset": 0,
"limit": 64
}
}
Collections are types, exported_types, fields, methods, member_refs,
call_edges, field_accesses and user_strings (the literals). Each page names
its collection, retains original rows, order, tokens, signatures and body facts,
and carries the source selection and coverage; user_strings pages carry the
literal pool's own coverage. Older Evidence that never examined exported types
or user strings reports a null count and refuses those pages rather than
returning an empty one.
To find specific retained members without returning their IL anchors, select exact names and defining types, then project columns:
{
"source": {
"kind": "retained-evidence",
"evidence_id": "<parent_evidence_id>"
},
"view": {
"kind": "managed-members",
"collection": "methods",
"names": ["ParseNumber", "ParseReadNumber"],
"declaring_types": ["Newtonsoft.Json.JsonTextReader"],
"fields": ["name", "declaring_type", "signature"],
"offset": 0,
"limit": 64
}
}
Filters are exact and case-sensitive; lists are alternatives and filters
intersect. An empty list matches nothing. Names select name in types,
exported types, fields, methods and member references; in call edges they select
target_name, and in field accesses field_name. Declaring-type filters select
declaring_type in fields/methods, parent_name in member references,
target_declaring_type in call edges and field_declaring_type in field accesses.
Types, exported types and user strings do not accept a declaring-type filter;
user strings do not accept a name filter. No display strings are reparsed.
fields accepts only columns of that collection. Omitting it preserves full
rows, including method bodies. Selecting body explicitly returns the full
recorded body and anchors; selecting no columns ([]) returns identity fields.
Projected or filtered rows add their original source_index; tokens and source
byte offsets remain, with defining/parent tokens or IL-site endpoints where
applicable. The page retains module/MVID, artifact digest, parent Evidence,
producer selection and source coverage. Full facts remain in the parent.
Paging offsets and totals count known matches in original order. source_count
counts the producer-returned collection; unresolved_filter_rows counts rows
that could match but lack a requested name/type. Those candidates remain unknown
and are excluded from the matched total. Exhausting the known matches does not
prove absence among unresolved or unexamined source rows.
CLI summary and saved member rows
The CLI accepts the same complete and summary detail choices. Omitted or
--detail complete returns complete Evidence. To receive a summary, choose a
new Evidence bundle destination so its original complete parent remains
available after the command exits:
rea inspect-managed-members /absolute/path/Assembly.dll --type Namespace.Type \
--detail summary --evidence-bundle /absolute/path/evidence/members.json --json
--evidence-bundle writes the complete original Evidence in REA's canonical
bundle format. Its parent directory must exist, and it never replaces an
existing path. Relative paths resolve against the command's working directory.
The option also works with complete delivery when a saved bundle is useful.
Summary without a destination is rejected before analysis. A publication failure
returns a typed error with the completed Evidence under
details.partial_observation; it does not report successful summary delivery.
Cancellation before publication prevents the new output. Once atomic
publication has started, it can complete despite cancellation; a cancelled
delivery then retains the committed path and byte count under
details.diagnostics.evidence_bundle.
Copy normalized_result.parent_evidence_id from the summary into the
source.evidence_id field of the managed-members request above, then save that
small request as members-page.json and run:
rea inspect-analysis-view /absolute/path/evidence/members-page.json \
--evidence-bundle /absolute/path/evidence/members.json --json
The view command authenticates the selected bundle and resolves that exact
parent ID. It does not reopen the assembly, so the original target may have
been moved or removed. Subsequent calls can select another collection or use
the returned coverage.next_offset to continue the same collection. An inline
source already includes its parent and cannot also select --evidence-bundle.
Summary changes delivery, while type selection still controls decoding and the same inspection resource limits apply. Each separate CLI page reads and authenticates the saved bundle; the full source is retained on disk.
Shipped scope
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading
an assembly. It reports observed implementation markers and unavailable facts;
it does not itself unpack .NET single-file hosts or infer a NativeAOT identity
from an ordinary native PE. It does not decode IL2CPP; use
IL2CPP metadata for the image, type and method names in a
Unity global-metadata.dat. Inputs without admitted CLI metadata do not
become managed assemblies through naming or routing guesses.
Inspect separately obtained components explicitly and preserve their identities.
For ordinary .NET single-file hosts, use the existing artifact tools to inventory and extract bundle members, then inspect the extracted assembly explicitly with the managed tools.
A valid implementation marker establishes a candidate native boundary, not recovered native semantics or runtime behavior.
An assembly targeting .NET Standard
declares API compatibility across multiple .NET implementations. REA preserves
that target in target_frameworks and leaves runtime_family as unknown
unless independent markers identify a runtime family, such as a ReadyToRun
signature.
When ReadyToRun is observed, runtime_family remains modern-dotnet even if
Unity or target-framework hints are also present; the separate hint evidence
is retained in the classification evidence list.
Resource admission
The static provider admits regular input files up to 128 MiB before retaining their bytes. Reads remain bounded if a file grows after admission. Metadata, CIL decoding, and projected inspection records have a separate conservative 64 MiB representation budget; shared heap strings are cached, but repeated output occurrences still consume that budget. These are inspection resource policies, not PE format limits or a hard bound on total V8 heap use.
Path-based member comparison uses the same per-input snapshot policy and inspects its two selected paths sequentially, retaining each inspection rather than both full byte snapshots. The limit therefore remains a per-file resource admission rule, not an aggregate comparison limit. Comparison Evidence keeps the caller-selected paths as its parameters and artifact locations.
Input overflow returns a resource constraint. Decode or projection overflow
reports resource-limit with unavailable or partial coverage; omitted facts do
not become empty observations. Ambiguous PE section mappings are rejected
rather than interpreted according to section order.
Minimal Edit-and-Continue metadata marked by #JTD is not a standalone
assembly inventory. The pinned .NET 9 reader
uses different reference widths for this mode, and delta handles require the
baseline and prior generations. The released
metadata serializer
emits this marker with #-; the
PE writer
can embed that metadata root. REA reports unsupported-metadata at the marker
header before interpreting table rows, preserving the selected path, artifact
digest, parsed metadata version and stream names, and admitted PE/CLI facts.
Metadata status is partial; member and native
declaration inventories are unavailable. Empty collections do not establish
absence, and a requested type name is not declared unmatched. Ordinary
standalone #~ and #- metadata keep their existing interpretation. This does
not add raw .dmeta input, baseline attachment or delta reconstruction.
Regression coverage uses authored metadata in a generated PE shell; it does
not establish naturally emitted application PEs, CLR loadability or a real
Edit-and-Continue execution.
Selected member inspection
Member inspection admits type names, tokens, list ranges/counts, nesting facts,
module identity and their output representation before decoding members. If
later decoding or projection reaches the 64 MiB representation budget, the
result retains this type inventory with coverage.state: "partial" and the
original resource-limit coordinate. It does not return partially admitted
member collections: empty fields, methods, references and edges are unexamined,
not proof of absence; exported types, user strings and signature-resolution
attestation remain null. Summary and retained pages keep this partial coverage.
The capacity limit is unchanged. If metadata or the type inventory itself cannot
be admitted, the result remains unavailable.
Use the returned types[].full_name values to select declaring types before
their signatures and CIL bodies are decoded:
rea inspect-managed-members /absolute/path/Assembly.dll --type Example.Parser
MCP uses inspect_managed_members with
{"path":"/absolute/path/Assembly.dll","type_names":["Example.Parser"]}.
Repeat --type or supply multiple unique type_names for several types.
Names match the reported full_name exactly. Top-level names require that
namespace-qualified spelling. Nested declarations also accept their bare
name, including when their own namespace is nonempty; this can select several
types with that name. Selection uses the parsed TypeDef facts and comparison
uses the returned declaring types, rather than reconstructing scope from the
caller's spelling. Each type's enclosing_type_token records its enclosing
TypeDef. nesting_read distinguishes complete source rows (true), incomplete
current rows (false), and historical Evidence (null). A null enclosing token
alone does not establish that a type is top-level.
Each selected type also retains name_sha256, a framed digest of its namespace,
name and enclosing names. It uses the same bounded, cached owner as signature
type operands and remains available when selection omits the enclosing types.
It adds one fixed-size digest per returned type rather than repeated name paths.
The result preserves the requested names in selection.type_names and reports
unmatched names separately. If metadata names are unreadable, limitations retain
that uncertainty. With no selection, selection is null and whole-assembly
inspection keeps its existing resource policy. Selection restricts types,
fields, methods, call edges, field accesses and user strings; member references
remain an assembly-wide inventory, so output size is not guaranteed to be small.
Coverage describes the selected members. Comparisons leave members outside the
opposite observation's selection unknown, and application graphs report their
selected scope as partial. Neither makes an absence claim about the whole
assembly. Older member Evidence without selection remains readable. The
comparison resolves a member's declaring TypeDef token in its own source, then
checks that nominal name_sha256 against the opposite selected type set.
Pairing, absence and aggregate selected-scope agreement share these retained
facts, including enclosing names, rather than requested spellings or display
labels. Equivalent requests can cover the same types, while the same display
name can select different nested owners. Such disjoint selected owners cannot
establish a member pair or added/removed status even when both inventories are
complete. Missing nominal identities cannot establish selected membership,
including historical selected Evidence that retains only display names;
the explicitly limited historical whole-inventory comparison remains available.
Incomplete selected inventories or missing identities leave aggregate agreement
unknown and coverage partial; known observed member pairs remain available.
Complete empty selected inventories agree even if their unmatched request names
differ. Whole-assembly and selected
inventories remain distinct scopes, and each source retains its exact request.
The method_rows interval records the examined zero-based MethodDef offset, row
count and full table total before type selection. A complete observation can
cover only that interval. Comparison keeps omissions from a method page unknown:
build-local row positions cannot prove member absence across builds. Historical
Evidence without this interval also cannot establish method absence; observed
pairs and field inventory comparisons remain available.
For MCP callers, request detail: "summary" and page the types collection
using the returned parent Evidence ID. Select one or more returned full names
in a subsequent inspect_managed_members request. Selection is a recovery path,
not a guarantee: a single type or the assembly-wide member references can still
exhaust the representation budget. A successful command exit means the Evidence
was returned; inspect coverage.state and coverage.issues before treating any
collection as complete.
User strings
ldstr anchors keep their build-local 0x70 tokens. user_strings.literals
lists each distinct token loaded by an inspected method body once, in token
order, with its #US file offset and decoded value. Values keep their stored
UTF-16 code units, including unpaired surrogates. Token 0x70000000 names the
heap's empty entry at offset 0 and reads as ""; any other item there is
malformed. The terminal flag is checked against ECMA-335 II.24.2.4, and the
length prefix against the width II.23.2 assigns. A difference is reported with
its byte offset as a user-string issue while preserving the readable literal.
Toolchain conventions can differ; this conformance observation does not
establish runtime rejection. A missing #US stream is one stream-level issue;
a malformed item is reported against its token. User strings carry their own
coverage: any of these makes user_strings.state partial and lists the issue
in user_strings.issues, while the member coverage and metadata.status are
unaffected, and readable literals remain. The state is also partial when an
inspected method's CIL was not fully decoded (a malformed exception section
alone does not count) or a method that may belong to the inspected types was
not inspected (its row could not be read, or its type's name could not be read
under a type_names selection), because ldstr instructions there were not
observed. Normalized CIL digests do not include decoded values.
compare_managed_members does not decode or compare literals: an unchanged
method does not establish unchanged strings, and the comparison says so in its
limitations. Older member Evidence without user_strings reads as null: not
examined.
Edge target declaring types
member_refs[].parent_name, call_edges[].target_declaring_type, and
field_accesses[].field_declaring_type name the type that declares each
target. Referenced spec signatures validate encoded type coordinates against
the metadata tables, including nested type arguments and custom modifiers.
TypeRef resolution scopes are checked transitively with cycle detection;
readable names survive malformed scopes as partial evidence.
TypeDef and TypeRef names use the namespace-qualified full_name
form, and nested types keep bare names. A generic-instantiation TypeSpec
parent names its generic type, such as System.Collections.Generic.List`1.
Other well-formed TypeSpec forms and ModuleRef or MethodDef parents stay
null. A method-spec call edge reports the name and declaring type of the
generic method it instantiates. A field access is named only by a field or a
field-signature MemberRef. TypeSpec and MethodSpec blobs are decoded with the
member-signature grammar, and a MethodSpec must instantiate a generic method
with as many type arguments as it declares. Malformed or unsupported rows and
blobs, and arity mismatches, make coverage partial. A readable MethodSpec
method column that names a method, or a TypeRef whose resolution scope is
invalid, keeps its name.
Older member Evidence without these fields reads them as null: unknown.
Analysis objective
Start by identifying the artifact with inspect_managed_artifact, then inspect
its types and CIL with inspect_managed_members. Use member comparison
for another build, and managed/native boundary rules
when matching declared native calls to supplied native Evidence. Static inspection
does not load or execute the assembly. Reconstructed structure and unresolved
native/runtime questions retain their own authority and coverage.
When the selected source changes during acquisition or no longer matches its
admitted digest, inspection and comparison report retryable artifact_changed;
any completed hash retains the observed digest and byte count. Retry against a
stable source. This differs from corrupted retained Evidence, which remains an
Evidence-integrity failure. Malformed selected PE input has the same input-format
classification in inspection and comparison. Its issue identifies the selected
filename and argument: path for inspection, or leftPath / rightPath for CLI
comparison. A failed right-side comparison retains the completed left observation.
Member comparison
Call edges and field accesses retain il_offset as their instruction site within
the caller's build-local method token and artifact identity. Their
target_identity_sha256 combines the existing enclosing-type nominal digest,
member name and resolved decoded signature. Comparison joins edges to their
instruction sites rather than array positions and compares these identities;
display labels alone cannot establish equal targets. Same-named nested types
under different enclosing types and different overload signatures stay distinct.
This is nominal metadata identity, not assembly resolution or runtime binding.
Historical edges without a site or identity, unresolved/cyclic type scopes,
TypeSpec owners and unresolved MethodSpec instantiations retain unknown target
equivalence. Valid MethodSpecs combine the base member identity with the
resolved instantiation signature, preserving generic argument distinctions.
Names and semantic labels do not establish durable member identity. Comparison preserves observed differences, structural inferences, and unknowns separately.
Cross-version matching first pairs members of the same declared type and name.
A current declared type uses its observed name_sha256, so same-named nested
types in different enclosing types stay distinct even in selected inventories.
Malformed, missing, contradictory, cyclic or over-deep enclosing identity remains unknown;
the comparison preserves display names without using them to repair that gap.
Historical observations retain an explicitly limited bare-name mode when either
side predates the identity facts. A known current identity gap still remains
unknown beside a historical observation. Matching then pairs
methods by exact CIL/signature identity, then by signature, then by
structural body shape; fields by signature. Exact CIL/signature identity
requires equal raw CIL digests and decoded fingerprints. Equal decoded
fingerprints alone cannot establish an exact match: they omit switch targets
and collapse distinct NaN encodings. Only then may a remap pair a
renamed or moved member across declared types or names: a method by exact
CIL/signature identity or structural body shape, a field by exact decoded
signature. A remap never takes a member whose own declared type and name remain
unpaired on the other side, so a member is never taken from its own changed
counterpart. A member whose declared identity is unresolved could belong to any type. It
never makes a declared-identity pair, and any remap group it joins stays
ambiguous. Unresolved same-named counterparts prevent definitive added/removed
claims, while observed names and tokens remain available in the result.
The exact-signature key uses names only as part of that
full tuple; names alone never select a pair. Duplicate tuples remain ambiguous
rather than being paired by token order. Every round compares signatures by
resolved_sha256: the raw signature with each TypeDef or TypeRef operand
replaced by a digest of that type's namespace and name, hashed as separate
values so A + B.C differs from A.B + C, after its enclosing types for a
nested type (Outer/Inner). The defining assembly is not part of the name:
a TypeDef and a TypeRef with the same namespace and name compare as the
same nominal name, and a TypeRef's ResolutionScope assembly is ignored, so a type
forwarded from one assembly to another still pairs. Two different types with
the same full name in different assemblies therefore have equal name digests.
Comparison reports this scope limitation inline; it does not establish CLR
type identity or runtime binding equivalence.
Nesting deeper than 64 levels is not named, and such a signature compares by
its raw digest. Comparison keys keep resolved and raw digests apart, so a raw
digest never pairs with an equal resolved one.
Raw signature bytes name types by metadata row, and rows are build-local, so
the same signature usually has different raw bytes in two builds. A signature
that names no types resolves to its raw digest. When an operand cannot be
resolved, such as a TypeSpec, or when REA could not parse the signature, the
raw digest stands in. Missing or unreadable TypeDef and TypeRef operands are
reported as coverage issues. Invalid NestedClass rows retain their failure
reasons and withhold TypeDef signature identities; type names and nesting are
parsed once by their shared owner. Each current inspection records
signature_resolution: "type-names-v2", even when none of its operands can be
resolved. Version 2 also appends readable names to signature operands, for
example class:7772 (AnalysisJob). Coded indexes remain build-local references;
names are display text and do not change raw or resolved signature digests.
Nested names use Outer/Inner. Generic instantiations keep their decoded
arguments, for example class:9 (System.Collec