* 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>
6.6 KiB
.NET single-file bundles
Use rea inspect-artifact /absolute/path/host --json to inventory an ordinary
.NET single-file host and rea extract-artifact /absolute/path/host --json to
materialize its members into a fresh, REA-owned output directory. With a host
selected through open_binary, the corresponding MCP tools are
inspect_artifact and extract_artifact. No .NET installation is needed for
inspection or extraction.
For a compact inventory, call inspect_artifact with detail: "summary",
then use inspect_analysis_view with the returned parent_evidence_id.
Inventory occurrence pages accept logical_paths, occurrence_ids, selected
fields, offset, and limit; node pages accept batches of artifact_ids
to retrieve expanded-byte digests. Include format_metadata and
source_location when member declarations and stored extents matter. Identity
fields survive projection, and the complete inspection Evidence stays retained.
REA detects the apphost marker and its preceding header offset from PE, ELF, and thin Mach-O bytes, regardless of filename. An unbundled apphost's zero offset remains an ordinary native artifact. Universal Mach-O slice inventory keeps its existing behavior; bundled contents inside slices are not expanded. When a native file has a recognized PyInstaller CArchive, apphost-marker scanning stops before its payload. A .NET helper stored inside that archive does not replace the outer container's format; an outer .NET host can still contain a PyInstaller helper as a member.
Supported layout
| Header | Producer generation | Member encoding |
|---|---|---|
| 1.0 | .NET Core 3.x | Uncompressed |
| 2.0 | .NET 5 | Uncompressed |
| 6.0 | .NET 6 and later using this layout | Uncompressed or raw DEFLATE |
Versions, minor versions, and flags outside this set are refused explicitly; they do not silently become an ordinary PE or a successful empty inventory. Flags 0 and the v2+ compatibility flag 1 are admitted. Unknown file-type bytes are retained with a limitation, rather than treated as executable authority. Encrypted/protected wrappers and recovering their payloads are outside this reader's scope.
The host keeps its whole-file SHA-256. Each embedded file gets its expanded-byte
SHA-256, path, declared size, compressed size, and format_metadata declaration
(major_version, minor_version, file_type). Its source_location describes
the stored host extent, including the compressed length when applicable.
The root manifest's root_format is dotnet-bundle; the root content node
still records its native PE/ELF/Mach-O format. A file-type declaration or .dll
suffix does not establish a valid assembly. Inspect an extracted component
explicitly with the managed tools to establish PE/CLI facts.
Admission and containment
Metadata is range-read with a 64 KiB cache. Policy budgets are 16 MiB of manifest metadata, 100,000 files, 64 KiB per UTF-8 string, 512 MiB of stored or expanded bytes per file, 256 path parts per member, 250,000 distinct in-memory path-tree nodes, and 2 GiB of aggregate expanded bytes. The node budget counts shared directory prefixes only once. These are REA resource policies, not limits imposed by Microsoft's format. Marker scanning streams 64 KiB chunks and reuses completed probe facts during extraction; ordinary native files without a marker incur one extra read pass before host hashing.
Extraction hashes the whole host again through the same descriptor used to read members and compares it with the inventory's root digest before creating an output directory. This adds a full-host read pass to extraction, including when the embedded members themselves are small.
Malformed/truncated headers, unsafe or conflicting paths, overlapping member ranges, members crossing the manifest, contradictory configuration extents, invalid UTF-8, corrupt/trailing compressed data, and incorrect expanded sizes fail before output publication. Source descriptor/path identity is checked during reading; extraction verifies the host digest and member bytes against the inventory. Existing output roots are never overwritten. Cancellation and failures release owned streams/descriptors through the shared extraction rollback scope.
Reproduce producer fixtures
The source-owned generator uses the installed .NET 8 SDK's Microsoft
HostModel producer to emit x64 v1, v2, plain v6, and compressed v6 bundles
for Windows, Linux, or macOS. It does not run generated hosts or download runtime packs. Supply an
SDK apphost template from
packs/Microsoft.NETCore.App.Host.<rid>/<version>/runtimes/<rid>/native/apphost
(apphost.exe on Windows). Pass windows (the default), linux, or osx
as the optional third generator argument to match that template.
dotnet build scripts/fixtures/dotnet-bundle/BundleFixture.csproj -c Release --configfile scripts/fixtures/dotnet-bundle/NuGet.Config -o /absolute/path/producer -p:BaseIntermediateOutputPath=/absolute/path/producer-obj/
dotnet /absolute/path/producer/BundleFixture.dll /absolute/path/apphost.exe /absolute/path/absent-bundles-directory
rea inspect-artifact /absolute/path/absent-bundles-directory/net8-compressed/Probe.exe --json
rea extract-artifact /absolute/path/absent-bundles-directory/net8-compressed/Probe.exe --json
The generator writes oracle.json beside each host. An independent C#
BinaryReader/DeflateStream pass records version, member types and stored
extents, then compares every expanded member digest with its source file.
Compare those records with inspection Evidence and the extracted bytes.
This verifies Microsoft's compression writer separately from synthetic parser
fixtures; it is not evidence that the bundled host executes on a target runtime.
Deterministic boundary and public-entrypoint coverage:
npm run test:focused -- tests/boundary/filesystem/dotnetBundleArtifacts.test.ts tests/acceptance/dotnetBundleJourney.test.ts
Layout references: Microsoft's manifest writer, file entry writer, apphost marker handling, and compression producer.