chore: merge main (d8a9673f) into environment state writes

The analysis-view contract description conflicted only where this branch adds its write-plan sentence; main's GPU code-object entry is kept alongside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-21 09:00:08 -07:00
commit bec65b3ede
284 files changed
+19614 -1419

No files matched your search

@@ -181,6 +181,8 @@ explicit path or endpoint and do not need it.
absent metadata does not prove a binary is not Go, and unparsed text records
remain explicit. See the
[Go binary guide](https://github.com/morluto/rea/blob/main/docs/go-binaries.md).
- Embedded CUDA/HIP code objects: `inspect_gpu_code_objects`; inventory targets
first, then decode selected payloads and query retained GPU Evidence.
- Bundled GGUF or safetensors model declarations: `inspect_model_artifact`
with the explicit local file. Default header hashing leaves full-file identity
unknown; `hash: "full"` additionally reads all weights for SHA-256. MCP returns
+6
View File
@@ -819,6 +819,11 @@ jobs:
with:
save: "true"
- run: npm ci
- name: Typecheck with the official Windows compiler
env:
GOMAXPROCS: "2"
GOMEMLIMIT: "3GiB"
run: npm run typecheck -- --checkers 2
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rea-build-cache
@@ -849,6 +854,7 @@ jobs:
npx vitest run --coverage.enabled=false
src/process/capture/FilesystemSnapshot.test.ts
tests/process-global/filesystemSnapshotFallbackCancellation.test.ts
tests/boundary/filesystem/typescriptCompiler.test.ts
tests/boundary/filesystem/binaryTarget.test.ts
tests/boundary/filesystem/safeOutputTree.test.ts
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
+1
View File
@@ -180,6 +180,7 @@ Additional tools and host support depend on the target:
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](docs/native-investigation.md) |
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
| GPU code objects | CUDA/HIP targets, selected kernel parameters and resources, retained PTX text | Built-in static ELF/PE/fatbin/CUBIN/PTX inspection; [GPU code objects](docs/gpu-code-objects.md) |
| Model artifacts | GGUF / safetensors metadata, tensor declarations and derived totals | Built-in header inspection; optional full-file hash; [model files](docs/model-artifacts.md) |
| IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required |
| Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required |
+6 -3
View File
@@ -162,8 +162,9 @@ and during shutdown.
APKs keep other names report the `unknown` role rather than guessing.
- **Partial results keep facts.** Failed individual pulls return successful
artifacts with `coverage: "partial"` and per-file `failures`. Listings
that contain unrecognized lines report them as `unparsed_lines` and mark
partial coverage instead of dropping rows.
and package searches that contain unrecognized lines report the first ten
as `unparsed_lines`, state the exact total in a limitation when more were
seen, and mark partial coverage instead of dropping rows.
- **Mutations are explicit.** `push_adb_file`, `install_adb_package`, and
`uninstall_adb_package` declare `mutatesTarget` and never run implicitly:
pushes check for existing device files before transfer and refuse them
@@ -171,7 +172,9 @@ and during shutdown.
transfer even with `overwrite`; only a clean `test -e` exit of 1 with no
output is interpreted as absence. The check is not atomic, so a different
process can create the path before `adb push` writes it. When available, the device's
`sha256sum` verifies the transfer; installs preserve the
`sha256sum` verifies the transfer. A missing command or output without a
recognizable digest reports `device_digest_source: "unavailable"` with an
unverified-transfer limitation; installs preserve the
device's `Failure [...]` reason verbatim.
- **Producer identity stays attached.** Every result embeds the producing
adb client (`binary_path`, `path_source`, `version`, `installed_path`).
+2 -2
View File
@@ -18,7 +18,7 @@ Inspect native SAZ or HAR 1.2 offline with REA, or connect Everywhere's separate
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally selected storage key names, redacted storage-content fingerprints, or script sources.
- `analyze_web_bundle` / `rea analyze-web-bundle` parses selected script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally source-map/original-source evidence.
- `observe_web_session` / `rea observe-web-session` arms an observation window of the requested duration for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete observed `input_schema` declaration and canonical `input_schema_sha256` digest inline. Declarations are untrusted JSON data, including declared types, required fields, enums, examples and reference URLs. REA does not resolve schema references. These fields replace `input_schema_shape`. Registrations with the same name and URL in distinct observed frames retain separate identities. `tool_key` includes the observed frame ID; it is not a stable identity across navigation or browser sessions. Same-frame registration updates replace that owner only, and removal leaves other owners intact. Declared `readOnly`, `untrustedContent`, `consequential`, and `autosubmit` annotation hints are reported per registration: `true`/`false` are declared values, `null` means the declaration did not provide the hint in that capture, and a missing `consequential` key means the hint was not recorded, either in a historical capture or because the observed annotation was invalid. Consult discovery completeness and exclusions to distinguish these cases. Malformed annotation values retain the registration with incomplete coverage and an `invalid_protocol_value` exclusion. Other malformed protocol payloads can fail discovery explicitly. REA never exposes `WebMCP.invokeTool`.
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete observed `input_schema` declaration and canonical `input_schema_sha256` digest inline. Declarations are untrusted JSON data, including declared types, required fields, enums, examples and reference URLs. REA does not resolve schema references. These fields replace `input_schema_shape`. Registrations with the same name and URL in distinct observed frames retain separate identities. `tool_key` includes the observed frame ID; it is not a stable identity across navigation or browser sessions. Same-frame registration updates replace that owner only, and removal leaves other owners intact. Declared `readOnly`, `untrustedContent`, `consequential`, and `autosubmit` annotation hints are reported per registration: `true`/`false` are declared values, `null` means the declaration did not provide the hint in that capture, and a missing `consequential` key means the hint was not recorded, either in a historical capture or because the observed annotation was invalid. Consult discovery completeness and exclusions to distinguish these cases. Malformed annotation values or registration stacks retain the registration with incomplete coverage and an `invalid_protocol_value` exclusion. Other malformed protocol payloads can fail discovery explicitly. REA never exposes `WebMCP.invokeTool`.
- `compare_web_captures` / `rea compare-web-captures` compares passive captures or exact-step-aligned browser scenarios. Scenario results commit reproducible literal normalization and expose alignment failures plus artifact-level action, screenshot, DOM, accessibility, URL, history, storage, and event differences. Missing or truncated evidence is never treated as equivalence.
@@ -361,7 +361,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
WebSocket stream inventories and connection counts likewise require complete
WebSocket coverage. Redirect resource types use only shared known metadata:
an omitted type leaves equality unknown rather than proving a modification.
- Console observations with a stack source retain call type, argument types, the CDP `Runtime.Timestamp` in milliseconds since Unix epoch, and source location. An absent or invalid timestamp is `null` and makes `console_events` incomplete; an observed zero remains zero. This epoch-millisecond clock is separate from the monotonic-second timestamps in `observe-web-session`. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
- Console observations with a stack source retain call type, argument types, the CDP `Runtime.Timestamp` in milliseconds since Unix epoch, and source location. An absent or invalid timestamp is `null` and makes `console_events` incomplete; an observed zero remains zero. Argument types are listed by CDP argument position. An absent or non-array argument inventory is `null`, and a malformed argument keeps its position as `unknown`; either makes `console_events` incomplete, while an observed empty inventory remains `[]`. A malformed stack or top stack frame is not replaced by its caller: the console event is excluded, and a request initiator location or WebMCP `registration_source` is `null`; each is reported as `invalid_protocol_value`. This epoch-millisecond clock is separate from the monotonic-second timestamps in `observe-web-session`. When console text capture is selected, already-delivered primitive values are retained verbatim with their original `argument_index`; objects, getters, and remote properties are never expanded. Text capture is `unavailable` when the argument inventory is unknown.
- WebSocket observations retain direction, opcode, and payload byte length. A missing or malformed CDP opcode is returned as `null`, with opcode-dependent payload byte length and shape also `null`, and marks `websocket_frames` incomplete. Missing or wrong-typed payload data is not treated as empty; its byte length is `null`, and a text payload shape is unknown. Numeric opcode `0` and an observed empty payload remain distinct from missing data. Opcode is nullable in the public inspection schema, so clients must handle unknown protocol values. When shape capture is selected, text messages are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw payloads are never retained.
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache fingerprints hash all decoded response bytes, including bodies above 64 KiB, subject to the [64 MiB per-message CDP limit](#request-boundary). Missing or undecodable cache bodies and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
- Unavailable or malformed storage inventories mark `storage_keys` unavailable, even without fingerprints. Comparisons suppress inferred key additions and removals while preserving modifications proven by complete fingerprints on both sides. Previously exported captures are not retroactively corrected; recapture them to obtain these coverage markers.
+3 -1
View File
@@ -212,7 +212,9 @@ statements remain under the attached debugger client's control.
The result is Evidence with an initial state followed by one record per
declared action. Each step reports action status, elapsed time, sanitized URLs,
event bounds, and independently typed capture states for screenshot, DOM,
accessibility, URL, history, and storage. Console, page-error, network,
accessibility, URL, history, and storage. Actions after a failed or cancelled
action are not run and are recorded as `cancelled` with a not-executed error.
Console, page-error, network,
WebSocket, frame, worker, popup, and cancelled-download events are attributed to
steps. Retained events and requested artifacts are returned inline; omitted
events are counted by kind and identify affected network transactions.
+3 -1
View File
@@ -116,7 +116,9 @@ The CLI and MCP surfaces accept the same schema. For a JSON request file:
rea capture-electron-scenario scenario.json --json
```
The result records action status and targets, correlated app/window/WebContents,
The result records one status and target per declared action; actions after a
failed or cancelled action are not run and are recorded as `cancelled` with a
not-executed error. It also records correlated app/window/WebContents,
preload, session, navigation, shell, permission, popup, download, protocol,
native-addon, process, and IPC timeline events. IPC channels and argument-shape
metadata retain their observed lengths; payload values are never retained.
+15 -1
View File
@@ -158,7 +158,13 @@ tree semantics, vulnerability scanning, emulation or hardware runtime capture.
Supply a stable copy within the allowance. No in-place source modification.
- One queued heavy operation per provider instance; Binwalk one thread and
Unblob one worker. No automatic parallel extraction engines.
- **120-second** analysis deadline through inventory. Publication and final
- Analysis defaults to **120 seconds**, from tool resolution and input snapshot
through inspection and its final private-workspace cleanup, or extraction
through report consumption and inventory, excluding queue waiting and retained
cleanup admission. Inspection has no publication phase. Select `analysis_timeout_ms` in either MCP operation or
`--analysis-timeout-ms` in either CLI command for slower analysis. Current
results retain the actual budget; an absent historical value means unknown.
Extraction publication and its final
cleanup start a separate **120-second** budget; select
`publication_timeout_ms` in MCP or `--publication-timeout-ms` in CLI for a slow
destination. Valid budgets are integers from 1 through 2,147,483,647 ms (the
@@ -168,10 +174,18 @@ tree semantics, vulnerability scanning, emulation or hardware runtime capture.
both phases and final cleanup. Phase handoff, publication commit and final
completion also check monotonic elapsed time, so delayed timer callbacks
cannot revive an expired budget.
- Analysis timeout recovery names the selected budget and the analysis control,
not the later publication control. Keep the client call timeout above analysis,
publication and cleanup headroom; keep the input/range/question unchanged.
Resolve any reported incomplete cleanup before retrying. Raising wall time
does not raise fixed CPU, memory, report, diagnostic or input limits.
- Inherited **1 GiB per-process address-space** limit,
120 CPU seconds and zero core-dump allowance through `prlimit`.
- JSON reports: at most **8 MiB** and valid UTF-8; malformed encoding and
oversized reports fail without repairing or truncating producer data.
Exit status 0 is not an empty result: a missing, empty, malformed or
oversized report fails with `unreadable_output`, its `report_state`, the
command, exit status and captured output. REA does not retry automatically.
Diagnostic output is bounded with exact captured byte counts.
- Default staging budget: **256 MiB** and **10,000 entries**, configurable using
`max_output_bytes` / `max_output_files` (CLI `--max-output-bytes` /
+166
View File
@@ -0,0 +1,166 @@
# Static CUDA and HIP code objects
`inspect_gpu_code_objects` answers which GPU targets and kernel declarations a
local artifact carries. It reads ELF/PE sections, standalone CUDA fatbins,
CUBIN/PTX and Clang HIP bundles, including CCOB v1/v2/v3. It neither executes the
artifact nor invokes CUDA/ROCm tools. GPU execution, SASS disassembly and host
launch correlation remain outside coverage.
Start with a target inventory:
```json
{ "path": "/artifacts/library.so", "view": { "collection": "targets" } }
```
Target rows aggregate inventoried payloads and distinguish an unrequested kernel
analysis from a complete or partial one. `objects` pages expose exact object
IDs, compression declarations and encoded/header locations. Fields, exact
filters, batch IDs and stable pagination select the facts delivered to the
caller. IDs are positional within the recorded artifact SHA-256. Selecting `object_ids`
in a new inspection requires `expected_sha256` from that inventory; a changed
file is rejected before parsing selected payloads.
`target` is the inventory declaration used for selection; `payload_target` is
the independently observed inner declaration when available. Object pages keep
both values, their kind, `target_consistency` and inspection coverage even with
selected fields.
Conflicting base architectures retain both values, mark object/report/target
coverage partial and annotate the affected kernels. CUBIN flags establish a base
architecture: `base-only` explicitly leaves feature-profile agreement unknown,
including `a` variants. A standalone declaration is `single-source`, and
unexamined payloads remain `unexamined`. An exact AMDGPU target is normalized at
its note owner for comparison with a Clang bundle declaration; raw values remain
intact. Unresolved feature comparisons stay `unknown`.
`cuda_target_header` retains the OS ABI, normalized raw `e_flags` and exact
header location. CUDA OS ABI `0x33` and the observed `0x41` flags layout use
different architecture fields; they are decoded at one ABI-aware owner.
Unrecognized layouts keep the header and an unknown target instead of
inventing an SM label. Header provenance is validated against its CUBIN object.
Target filters select inventory declarations. An unselected payload's inner
target remains unexamined; an empty filtered result does not establish that
no enclosed code declares that target. Discovering every inner declaration
requires decoding those payloads.
HIP IDs with one or two separators before the architecture retain the declared
target. If a recognized AMDGPU bundle ID does not expose a target, the same
bounded note-discovery owner used for standalone AMDGPU files establishes it
before filtering. Those directory/note reads share the inspection work budget;
successive queries can select the target returned by inventory.
Decode selected targets and return a kernel page in the same call:
```json
{
"path": "/artifacts/library.so",
"analysis": "kernels",
"targets": ["sm_100"],
"view": {
"collection": "kernels",
"names": ["kernel_name"],
"fields": [
"name",
"parameter_count",
"parameter_bytes",
"parameters",
"registers",
"shared_bytes"
]
}
}
```
Only selected payloads are decoded. Other inventory entries remain present with
`not-selected` states, and overall payload coverage remains partial. An empty
kernel page proves no matching retained fact; it does not establish that an
unexamined payload contains no kernel. Missing fields remain null.
Kernel representations have different meanings:
- PTX reports parameter declarations and virtual registers. Shared bytes cover
supported static declarations inside that entry; they do not establish module
shared memory, dynamic allocation or physical device resources.
Scalar register declarations count each bare or percent-prefixed name and
each parameterized range. Unsupported declaration forms or overflowing totals
retain an unknown count and partial decoding coverage. Unsupported parameter
layouts and unknown shared-memory declarations likewise retain their known
facts, explicit limitations and partial coverage through retained pages.
- CUBIN identifies CUDA entry symbols and reads numeric `nv.info` records and
native section declarations. Global register declarations also cover compiler
versions that leave the older text-section register field empty.
- AMDGPU reads v3+ MessagePack notes. Argument counts include hidden ABI arguments
when present; SGPR/VGPR counts and fixed group/private segment sizes retain
their declared meanings. Legacy YAML metadata remains unsupported.
Each selected PTX payload becomes a retained UTF-8 text artifact with a SHA-256,
byte length and source coordinates. It is omitted from compact replies. Read it
with `inspect_analysis_view` and the parent Evidence ID returned by the compact
receipt:
```json
{
"source": { "kind": "retained-evidence", "evidence_id": "ev_..." },
"view": {
"kind": "gpu-code-objects",
"collection": "ptx",
"object_id": "...",
"offset": 0,
"limit": 4096
}
}
```
Text offsets use UTF-16 code units and must fall on Unicode scalar boundaries.
Complete Evidence retains the selected text; `export_evidence_bundle` provides
its portable source record. Historical pages never reopen the input file.
A location with `container_id: null` uses original file offsets. Other locations
use that inventoried object's decoded byte coordinates. Follow its parent,
encoded location and digest to trace decompressed metadata back to a native
section and the original file. Decoded offsets are never presented as physical
file offsets.
The CLI uses the same service and contracts:
```sh
rea inspect-gpu-code-objects /artifacts/library.so --analysis kernels \
--targets '["sm_100"]' --detail summary \
--view '{"collection":"kernels","names":["kernel_name"]}' \
--evidence_output /analysis/library-gpu.evidence.json --json
```
A full-file SHA-256 always streams every byte. Inventory then reads bounded
native directories and bundle headers; CCOB requires decompression to discover
its inner directory. Kernel analysis reads selected payloads. `bytes_read`
includes hashing and subsequent descriptor reads, so this is not a header-only
hashing workflow.
CUDA raw LZ4 blocks and Zstd, and CCOB zlib/Zstd are supported. Unknown versions
or codecs retain declarations with explicit limitations. Lengths are checked
before decoding, and CCOB's truncated MD5 is verified against decoded bytes.
Resource limits are one active inspection, 128 MiB per input/output buffer,
512 MiB of payload input, decoded output and AMD note-read work per inspection,
and 16 MiB retained facts including selected PTX. Payload work is reserved before
reading or allocating; a capacity refusal does not reread an aliased payload.
Native tables and decoded section names each have a 16 MiB structural budget;
AMDGPU ELF note discovery admits at most 8 MiB of source note bytes, with
bounded depth and container/string allocations. Standalone AMD targets are read
from those notes before target selection; selected kernel analysis reuses the
same parsed declarations. Pages have a
1 MiB response budget. Resource refusals and interrupted observations preserve
known facts and explicit uncertainty.
CUDA ELF parsing additionally shares a 262,144 record/symbol visit limit,
128 MiB of cumulative metadata section reads, and a 16 MiB serialized-size
accounting budget for temporary index facts. These limits are checked before
index growth; they are not claims about JavaScript heap size. Equivalent scoped
metadata byte ranges and repeated equal register declarations are coalesced;
conflicting values remain unknown. Parameter sections are parsed once per ELF.
Missing CUBIN symbol tables or unresolved section names produce `partial`
object, report and target coverage even when some kernel facts survive. AMD note
failures preserve earlier successfully parsed declarations and the original
failure reason. Standalone PTX recognition tries a small header first, then
continues within the existing 16 MiB text budget when leading comments or
whitespace obscure its directives; admitted full-file probe bytes are reused.
+4 -2
View File
@@ -1189,8 +1189,10 @@ truncation markers. Darwin ownership-helper compilation failures also retain
the compiler outcome, exit code or signal, and bounded stderr; a timed-out
compiler remains distinguishable from a compiler that exited with an error.
The helper compile deadline defaults to 120,000 ms; set
`REA_SWIFTC_COMPILE_TIMEOUT_MS` to an integer between 1 and 2,147,483,647 when
a busy host needs longer.
`REA_SWIFTC_COMPILE_TIMEOUT_MS` to an integer between 1 and 2,147,483,647 in
the environment that starts REA when a busy host needs longer. Restart REA to
apply a change; the next ownership-helper compile uses the new value. A
timeout names that effective deadline.
On macOS, the Java prerequisite probe waits for this independently bounded
preparation before starting its five-second command deadline. Caller cancellation
still interrupts preparation, and failed helper cleanup remains owned for retry.
+9 -1
View File
@@ -371,7 +371,15 @@ Recognized literal module-table bootstraps also support bounded lexical
substitution through module exports, import helper bodies, local wrapper calls,
returned closures, object spreads, and callback parameters. Recovered Electron
paths retain source ranges, module identities, literal channels and branch
conditions as static candidates. Unknown spreads, reassigned bindings, accessors,
conditions as static candidates. A channel the IPC call site itself
determines, such as a local constant, resolves that call's observation rather
than adding a second one. A channel or registration handler supplied through
a wrapper argument is a candidate beside the call's own unresolved
observation, because callers outside the traced paths may pass other values.
A wrapper call that is both its own entry and reached through another entry
is recorded once when both paths find the same facts; calls that supply
different handlers or channels stay distinct.
Unknown spreads, reassigned bindings, accessors,
dynamic channels and unsupported syntax do not become exact implementations.
This pass neither executes the bundle nor proves that runtime guards pass.
An ambiguous or unresolved handler retains its channel registration but does
+1 -1
View File
@@ -68,7 +68,7 @@ The background export belongs to the session. `close_binary`, or the end of a CL
The assembly label index is scanned once per export reader. Cancelling a request stops its wait without interrupting other readers; closing the session aborts and drains the shared scan before releasing its files.
Regex procedure and string search uses ECMAScript Unicode matching in a cancellable worker with a five-second matching deadline. Literal matching remains the default. Cancellation or a matching deadline leaves the session available.
Regex procedure and string search uses ECMAScript Unicode matching in a cancellable worker with a five-second matching deadline and a 128 MiB worker heap. Literal matching remains the default. Cancellation or a matching deadline leaves the session available. This release has no runtime override for that deadline or heap.
Single-function subprocess output and each cached source facet share a 16 MiB buffered-text capacity. Cached spans are checked before allocation; larger text remains in the local export and the error identifies the file and range. `analyze_function` initially returns the complete dossier, which can still be large; later retained views do not remove that first transfer. Prefer the individual text tools for a focused question.
+10 -8
View File
@@ -355,7 +355,9 @@ page size or export retained Evidence if the view is too large.
Completed `inspect_artifact` and atomic `inventory_artifact` Evidence also
support summary and inventory views. A summary reports the manifest's expected
counts separately from available record counts, observed entry kinds, inventory
Evidence IDs and inspection coverage. An inventory view selects `nodes`,
Evidence IDs, the inventory producer's traversal `source_coverage` and
inspection coverage. Inspection coverage is `partial` when that traversal
coverage is unknown. An inventory view selects `nodes`,
`occurrences`, `edges`, or `integrity_contradictions`, with `offset` and `limit`
defaulting to 0 and 64. For occurrences, optional `logical_paths` matches exact
recorded strings: omission selects all available records; `[]` selects none.
@@ -805,13 +807,13 @@ auto-analysis, bridge connection, and health readiness before returning analysis
These deadlines have different owners:
| Deadline | Owner and effect |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
| Hopper startup | REA allows 120,000 ms by default; `REA_HOPPER_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. Startup failure is reported by the first provider query. |
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid supplied values fail configuration validation. Startup failure is reported by the first provider query. |
| Darwin swiftc helper | REA allows 120,000 ms by default for ownership-helper compilation; `REA_SWIFTC_COMPILE_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. |
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
| Deadline | Owner and effect |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
| Hopper startup | REA allows 120,000 ms by default; `REA_HOPPER_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. Startup failure is reported by the first provider query. |
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid supplied values fail configuration validation. Startup failure is reported by the first provider query. |
| Darwin swiftc helper | REA allows 120,000 ms by default for ownership-helper compilation; `REA_SWIFTC_COMPILE_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. Set it in the environment that starts REA and restart REA; the next ownership-helper compile uses the new value. |
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
For [Codex and Kimi registrations](client-configuration.md), setup supplies
missing tool deadlines from the known provider allowance plus 120 seconds of
+11 -4
View File
@@ -202,8 +202,10 @@ Function dossiers retain this same string evidence. Native call edges retain
Hopper's partial `CallReference` classification and exact endpoints; detailed
reference flags remain unavailable rather than being invented.
Regex searches use ECMAScript Unicode syntax in a cancellable worker with a
five-second matching deadline. Deadline or cancellation stops matching while
leaving the Hopper API available. Literal mode retains Unicode casefold matching.
five-second matching deadline and a 128 MiB worker heap. Deadline or
cancellation stops matching while leaving the Hopper API available. This
release has no runtime override for that deadline or heap. Literal mode
retains Unicode casefold matching.
## Bounded static traces
@@ -302,8 +304,13 @@ Binary plist inspection reads the original bytes through an owned file descripto
so conversion cannot erase control characters, fractional dates, negative zero,
or the high word of a 128-bit integer. It accepts at most 64 MiB of input and
reserves an estimated 64 MiB decode budget before parsing; this estimate is not
a hard process-memory bound. Binary-source provenance records the `file`
classification without claiming a `plutil` conversion.
a hard process-memory bound. A refused input reports non-retryable
`resource_constraint` with `reported_limits.maximum_input_bytes`,
`maximum_decoded_bytes` or, for nesting deeper than 128 levels,
`maximum_nesting_depth`. These are fixed allowances: worker limits and host
memory do not raise them, and the unchanged request fails the same way.
Binary-source provenance records the `file` classification without claiming a
`plutil` conversion.
A plist explicitly selected through the path input or as the active plist target
reports an unreadable or malformed selection as `invalid_request`. A missing or
+9
View File
@@ -545,6 +545,15 @@ missing or relative SDK/compiler path fails with an actionable prerequisite
error before compilation. An incompatible installation reports the compiler's
bounded diagnostics; REA does not repair or replace the toolchain.
The compile deadline is `REA_SWIFTC_COMPILE_TIMEOUT_MS`. An omitted value is
120,000 ms, and accepted values are integers from 1 to 2,147,483,647. REA reads
the variable from the environment that starts the process and keeps that
deadline for every helper compile in that process. Restart REA to apply a
change; the next ownership-helper compile uses the new value. A timeout names
the effective milliseconds. Invalid values fail configuration validation at
startup. Embedded compiler diagnostics are cut at 512 bytes, and this release
has no runtime override for that excerpt.
For example, select paths from your existing Xcode or Command Line Tools
installation **before** entering an enforced write boundary:
+1
View File
@@ -267,6 +267,7 @@ npx rea-agents@latest setup
- [التثبيت والإعداد](../installation.md): تسجيل الوكلاء، وتهيئة الموفّرين، والتحديثات، وإلغاء التثبيت.
- [الجاهزية واستكشاف الأخطاء](../installation.md#check-readiness-for-your-task): تشخيص وكيل أو محرك تحليل محدد.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI وEvidence](../cli.md): الأوامر، واختيار الموفّر، واللقطات، والاستيراد/التصدير، وحالات الخروج.
- [عقود MCP](../mcp-contracts.md) و[تعليمات الوكلاء](../mcp-prompts.md): نتائج الأدوات، والجلسات، والاستقصاءات الموجّهة.
+1
View File
@@ -265,6 +265,7 @@ Beginne mit den [praxisnahen Anleitungen](https://rea.tools/de/guides/) auf der
- [Installation und Einrichtung](../installation.md): Agentenregistrierung, Provider-Konfiguration, Updates und Deinstallation.
- [Bereitschaft und Fehlerbehebung](../installation.md#check-readiness-for-your-task): Diagnose eines bestimmten Agenten oder einer Analyse-Engine.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI und Evidence](../cli.md): Befehle, Provider-Auswahl, Snapshots, Import/Export und Exit-Status.
- [MCP-Kontrakte](../mcp-contracts.md) und [Agenten-Prompts](../mcp-prompts.md): Werkzeugergebnisse, Sitzungen und geführte Untersuchungen.
+1
View File
@@ -278,6 +278,7 @@ npx rea-agents@latest setup
- [Εγκατάσταση και ρύθμιση](../installation.md): καταχώριση agents, ρύθμιση παρόχων ανάλυσης, ενημερώσεις και απεγκατάσταση.
- [Έλεγχος ετοιμότητας και αντιμετώπιση προβλημάτων](../installation.md#check-readiness-for-your-task): διάγνωση ενός agent ή μιας μηχανής ανάλυσης.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI και Evidence](../cli.md): εντολές, επιλογή παρόχου, στιγμιότυπα, εισαγωγή/εξαγωγή και κωδικοί εξόδου.
- [Προδιαγραφές MCP](../mcp-contracts.md) και [prompts για agents](../mcp-prompts.md): αποτελέσματα εργαλείων, συνεδρίες και καθοδηγούμενες διερευνήσεις.
+1
View File
@@ -265,6 +265,7 @@ Empieza con las [guías prácticas](https://rea.tools/es/guides/) del sitio web.
- [Instalación y configuración](../installation.md): registro de agentes, configuración de proveedores, actualizaciones y desinstalación.
- [Preparación y resolución de problemas](../installation.md#check-readiness-for-your-task): diagnóstico de un agente o motor de análisis concreto.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI y Evidence](../cli.md): comandos, selección de proveedores, instantáneas, importación/exportación y estados de salida.
- [Contratos de MCP](../mcp-contracts.md) y [prompts para agentes](../mcp-prompts.md): resultados de herramientas, sesiones e investigaciones guiadas.
+1
View File
@@ -250,6 +250,7 @@ npx rea-agents@latest setup
- [نصب و راه‌اندازی](../installation.md): ثبت ایجنت، پیکربندی ارائه‌دهنده، به‌روزرسانی و حذف نصب.
- [آمادگی و رفع اشکال](../installation.md#check-readiness-for-your-task): عیب‌یابی یک ایجنت یا موتور تحلیل.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI و شواهد](../cli.md): دستورات، انتخاب ارائه‌دهنده، snapshotها، ورود/خروج داده و وضعیت‌های خروج.
- [قراردادهای MCP](../mcp-contracts.md) و [پرامپت‌های ایجنت](../mcp-prompts.md): نتایج ابزارها، نشست‌ها و بررسی‌های هدایت‌شده.
+1
View File
@@ -269,6 +269,7 @@ Commencez par les [guides pas à pas](https://rea.tools/fr/guides/) du site web.
- [Installation et configuration](../installation.md) : enregistrement des agents, configuration des fournisseurs, mises à jour et désinstallation.
- [Vérification et dépannage](../installation.md#check-readiness-for-your-task) : diagnostiquer un agent ou un moteur d'analyse donné.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI et Evidence](../cli.md) : commandes, choix du fournisseur, snapshots, import/export et codes de sortie.
- [Contrats MCP](../mcp-contracts.md) et [prompts pour agents](../mcp-prompts.md) : résultats des outils, sessions et investigations guidées.
+1
View File
@@ -290,6 +290,7 @@ Untuk mengetahui opsi, prasyarat, dan kontrak hasil secara tepat:
- [Instalasi dan penyiapan](../installation.md): pendaftaran agen, konfigurasi penyedia, pembaruan, dan penghapusan instalasi.
- [Kesiapan dan pemecahan masalah](../installation.md#check-readiness-for-your-task): mendiagnosis satu agen atau mesin analisis.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI dan Bukti](../cli.md): perintah, pemilihan penyedia, snapshot, impor/ekspor, dan status keluar.
- [Kontrak MCP](../mcp-contracts.md) dan [prompt agen](../mcp-prompts.md): hasil alat, sesi, dan investigasi terpandu.
+1
View File
@@ -269,6 +269,7 @@ Inizia dalle [guide pratiche](https://rea.tools/it/guides/) del sito web. Per op
- [Installazione e configurazione](../installation.md): registrazione agente, configurazione provider, aggiornamenti e disinstallazione.
- [Pronto all'uso e risoluzione dei problemi](../installation.md#check-readiness-for-your-task): diagnostica un agente o un motore di analisi.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI ed Evidence](../cli.md): comandi, selezione provider, snapshot, import/export e stati di uscita.
- [Contratti MCP](../mcp-contracts.md) e [prompt agente](../mcp-prompts.md): risultati degli strumenti, sessioni e indagini guidate.
+1
View File
@@ -267,6 +267,7 @@ npx rea-agents@latest setup
- [インストールとセットアップ](../installation.md):エージェントの登録、プロバイダーの設定、更新、アンインストール。
- [準備状況の確認とトラブルシューティング](../installation.md#check-readiness-for-your-task):特定のエージェントや分析エンジンを診断。
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI と Evidence](../cli.md):コマンド、プロバイダーの選択、スナップショット、インポート/エクスポート、終了ステータス。
- [MCP 契約](../mcp-contracts.md)と[エージェント向けプロンプト](../mcp-prompts.md):ツールの結果、セッション、ガイド付き調査。
+1
View File
@@ -280,6 +280,7 @@ npx rea-agents@latest setup
- [설치 및 설정](../installation.md): 에이전트 등록, 제공자 설정, 업데이트, 제거.
- [준비 상태 확인 및 문제 해결](../installation.md#check-readiness-for-your-task): 특정 에이전트 또는 분석 엔진 진단.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI 및 Evidence](../cli.md): 명령, 제공자 선택, 스냅샷, 가져오기/내보내기, 종료 상태.
- [MCP 계약](../mcp-contracts.md) 및 [에이전트 프롬프트](../mcp-prompts.md): 도구 결과, 세션, 안내에 따른 조사.
+1
View File
@@ -283,6 +283,7 @@ npx rea-agents@latest setup
- [Installation and setup](../installation.md): agent registration, provider configuration, updates, uninstall.
- [Readiness and troubleshooting](../installation.md#check-readiness-for-your-task): ഒരു agent-ന്റെയോ analysis engine-ന്റെയോ പ്രശ്നങ്ങൾ കണ്ടെത്തുക.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI and Evidence](../cli.md): commands, provider തിരഞ്ഞെടുക്കൽ, snapshots, import/export, exit statuses.
- [MCP contracts](../mcp-contracts.md), [agent prompts](../mcp-prompts.md): tool results, sessions, guided investigations.
+1
View File
@@ -265,6 +265,7 @@ Zacznij od [przewodników z przykładami](https://rea.tools/pl/guides/) na stron
- [Instalacja i konfiguracja](../installation.md): rejestracja agentów, konfiguracja dostawców analizy, aktualizacje i odinstalowanie.
- [Gotowość i rozwiązywanie problemów](../installation.md#check-readiness-for-your-task): diagnostyka jednego agenta lub silnika analizy.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI i Evidence](../cli.md): polecenia, wybór dostawcy, migawki, import/eksport i kody zakończenia.
- [Kontrakty MCP](../mcp-contracts.md) i [prompty dla agentów](../mcp-prompts.md): wyniki narzędzi, sesje i prowadzone analizy.
+1
View File
@@ -265,6 +265,7 @@ Comece pelos [guias práticos](https://rea.tools/pt-BR/guides/) do site. Para op
- [Instalação e configuração](../installation.md): registro de agentes, configuração de provedores, atualizações e desinstalação.
- [Prontidão e solução de problemas](../installation.md#check-readiness-for-your-task): diagnóstico de um agente ou motor de análise específico.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI e Evidence](../cli.md): comandos, seleção de provedores, snapshots, importação/exportação e status de saída.
- [Contratos MCP](../mcp-contracts.md) e [prompts para agentes](../mcp-prompts.md): resultados das ferramentas, sessões e investigações guiadas.
+1
View File
@@ -265,6 +265,7 @@ npx rea-agents@latest setup
- [Установка и настройка](../installation.md): регистрация агентов, настройка провайдеров, обновление и удаление.
- [Готовность и устранение неполадок](../installation.md#check-readiness-for-your-task): диагностика конкретного агента или движка анализа.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI и Evidence](../cli.md): команды, выбор провайдера, снимки, импорт/экспорт и коды завершения.
- [Контракты MCP](../mcp-contracts.md) и [промпты агентов](../mcp-prompts.md): результаты инструментов, сеансы и исследования с подсказками.
+1
View File
@@ -265,6 +265,7 @@ npx rea-agents@latest setup
- [การติดตั้งและตั้งค่า](../installation.md): การลงทะเบียนเอเจนต์ การตั้งค่าผู้ให้บริการ การอัปเดต และการถอนการติดตั้ง
- [ความพร้อมใช้งานและการแก้ไขปัญหา](../installation.md#check-readiness-for-your-task): ตรวจวินิจฉัยเอเจนต์หรือเอนจินวิเคราะห์หนึ่งตัว
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI และ Evidence](../cli.md): คำสั่ง การเลือกผู้ให้บริการ สแนปช็อต การนำเข้า/ส่งออก และรหัสการสิ้นสุด
- [สัญญา MCP](../mcp-contracts.md) และ[พรอมป์ต์สำหรับเอเจนต์](../mcp-prompts.md): ผลลัพธ์เครื่องมือ เซสชัน และการตรวจสอบตามคำแนะนำ
+1
View File
@@ -270,6 +270,7 @@ Web sitesindeki [uygulamalı rehberlerden](https://rea.tools/tr/guides/) başlay
- [Yükleme ve kurulum](../installation.md): ajan kaydı, sağlayıcı yapılandırması, güncellemeler ve kaldırma.
- [Hazırlık ve sorun giderme](../installation.md#check-readiness-for-your-task): belirli bir ajanı veya analiz motorunu tanılama.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI ve Evidence](../cli.md): komutlar, sağlayıcı seçimi, anlık görüntüler, içe/dışa aktarma ve çıkış durumları.
- [MCP sözleşmeleri](../mcp-contracts.md) ve [ajan istemleri](../mcp-prompts.md): araç sonuçları, oturumlar ve yönlendirmeli araştırmalar.
+1
View File
@@ -265,6 +265,7 @@ npx rea-agents@latest setup
- [Встановлення й налаштування](../installation.md): реєстрація агентів, конфігурація провайдерів, оновлення та видалення.
- [Готовність та усунення неполадок](../installation.md#check-readiness-for-your-task): діагностика конкретного агента або рушія аналізу.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI та Evidence](../cli.md): команди, вибір провайдера, знімки, імпорт/експорт і статуси завершення.
- [Контракти MCP](../mcp-contracts.md) і [промпти агентів](../mcp-prompts.md): результати інструментів, сеанси й керовані дослідження.
+1
View File
@@ -265,6 +265,7 @@ Bắt đầu với [hướng dẫn thực hành](https://rea.tools/vi/guides/) t
- [Cài đặt và thiết lập](../installation.md): đăng ký tác nhân, cấu hình trình phân tích, cập nhật và gỡ cài đặt.
- [Kiểm tra mức sẵn sàng và khắc phục sự cố](../installation.md#check-readiness-for-your-task): chẩn đoán một tác nhân hoặc bộ máy phân tích cụ thể.
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI và Evidence](../cli.md): lệnh, lựa chọn trình phân tích, ảnh chụp trạng thái, nhập/xuất và trạng thái thoát.
- [Hợp đồng MCP](../mcp-contracts.md) và [prompt cho tác nhân](../mcp-prompts.md): kết quả công cụ, phiên làm việc và điều tra có hướng dẫn.
+1
View File
@@ -271,6 +271,7 @@ npx rea-agents@latest setup
- [安裝與設定](../installation.md):AI 代理註冊、提供者組態、更新和卸載。
- [就緒檢查與故障排查](../installation.md#check-readiness-for-your-task):診斷特定 AI 代理或分析引擎。
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI 與 Evidence](../cli.md):命令、提供者選擇、快照、匯入/匯出和退出狀態。
- [MCP 契約](../mcp-contracts.md)和[AI 代理提示詞](../mcp-prompts.md):工具結果、會話和引導式調查。
+1
View File
@@ -271,6 +271,7 @@ npx rea-agents@latest setup
- [安装与设置](../installation.md):智能体注册、提供方配置、更新和卸载。
- [就绪检查与故障排查](../installation.md#check-readiness-for-your-task):诊断特定智能体或分析引擎。
- [CUDA / HIP](../gpu-code-objects.md)
- [GGUF / safetensors](../model-artifacts.md)
- [CLI 与 Evidence](../cli.md):命令、提供方选择、快照、导入/导出和退出状态。
- [MCP 契约](../mcp-contracts.md)和[智能体提示词](../mcp-prompts.md):工具结果、会话和引导式调查。
@@ -320,6 +320,13 @@ close a `packaged-process` obligation. A static-only source cannot close a
runtime or process claim. Marking a claim `blocked` or `out-of-scope` records the
disposition but does not silently count it as verified.
The `summary` counts obligations. `by_status`, `by_application_layer`, and
`by_source_state` place each obligation in exactly one bucket.
`by_evidence_authority` groups obligations by the Evidence `authority` of their
`authority_references`: an obligation counts once under each distinct authority
it cites, so those counts can exceed `total`. A reference's own `state` and the
obligation's `source_state` do not affect its authority bucket.
The tool returns every obligation inline. Its `ledger_id` and `closure_digest`
identify the complete result, including summaries, reports, ownership and
dependency graphs, Evidence links, and limitations.
+3
View File
@@ -126,6 +126,9 @@ matching help retains original one-based line numbers. Empty query selects all.
returns all remaining matches; CLI `--all` and `--limit` conflict.
The window reports examined, matching and returned records and `has_more`.
A filtered empty page is not evidence that the installation has no decoders.
When sigrok reports a selected decoder ID as not installed, help and decoding
return `invalid_request` naming `decoder` or `decoders`; omit `decoder` to list
installed IDs. An installed decoder that fails to load remains an execution failure.
Discovery refuses more than 65,535 line separators before constructing record
objects (65,536-line representation budget), in addition to the 4 MiB output cap.
+6 -1
View File
@@ -57,7 +57,12 @@ triggers or arbitrary SQL.
Rows are optional. `table` selects one exact ordinary-table name. `columns`
(CLI repeated `--column`) selects exact visible column names in the requested
order; omission selects all visible columns, including generated columns.
Unselected cells are not evaluated or projected. Malformed column-name bytes
Unselected cells are not evaluated or projected. A selected virtual generated
column whose expression needs a function REA's connection lacks (extensions and
application functions are not loaded), or raises an SQL error for a sampled
row, is refused as `capability_unavailable`. The error names the table and
each failing selected column with SQLite's message, and recommends only columns
whose sampled values were read; stored and other generated cells stay readable. Malformed column-name bytes
cause a format refusal when all columns are requested. In inventory scope, an explicit valid subset can
still be sampled without decoding unrelated malformed column names.
Column selection requires a
+52 -2
View File
@@ -46,6 +46,24 @@ without executing tests or building. A missing Git base reports how to fetch
it or select another revision.
Source typechecking needs no compiled runtime or generated test metadata.
Typecheck, runtime compilation, and Frida binding verification share
`scripts/run-typescript.mjs`. The pinned tsrs package supplies Linux x64/arm64
(GNU and musl) and macOS arm64 compilers. Windows and other unsupported hosts
retain the existing stable official TypeScript compiler. Its upstream version
can differ from tsrs, so both compiler tasks hash the selected package and version
through `scripts/run-turbo.mjs` before cache lookup. Outputs from different
compilers occupy different cache entries. Repository npm scripts and focused-test
preparation use that entrypoint; a raw Turbo compiler task without matching
identity fails instead of publishing an ambiguous successful cache entry.
A missing binary on a supported host also fails without silently falling back.
The Rust compiler defaults to at most two checker threads. For `--noEmit` checks,
`--maxMemory 3G` also sets a soft memory target; emit builds do not use that target.
Explicit compiler arguments can override the defaults. These controls do not
impose a process-family memory limit. Termux's explicit build, Stryker's compiler
API dependency, and independent fixture compiler oracles keep their existing
TypeScript versions.
`npm run build:cached` compiles the CLI/MCP runtime and its packaged skill.
Turbo's strict task environment preserves caller-selected `GOMAXPROCS`,
`GOMEMLIMIT`, and `UV_THREADPOOL_SIZE` for resource-constrained builds and
@@ -233,6 +251,27 @@ and retain representative compiled CLI parity checks. Every distinct source
scenario still runs through the public workflow; session-lifecycle scenarios
need their own sessions and cleanup assertions.
When a matrix already holds a named tool definition from live `tools/list`,
pass that definition through the SDK's `toolDefinition` call option. This keeps
the selected output-schema validation without eagerly compiling unrelated
catalog schemas. Re-read definitions if the server advertises a changed list.
Artifact verification uses `artifactMcpCall` for both lifecycle and analysis
calls. A fresh SDK client has no held tool listing, so a bare `callTool` can
skip output-schema validation. The helper discovers actual advertised
definitions, rejects missing or ambiguous schemas, and validates the requested
tool without compiling unrelated catalog schemas. REA registers process-static
tool contracts; bindings share one discovery per negotiated connection and are
discarded on reconnect or failed discovery. Keep raw artifact calls on this
shared path and retain explicit request options such as timeouts.
Caller-supplied tool definitions are rejected: the helper owns selection from
the advertised catalog so a stale or permissive schema cannot bypass it.
The regression fixture changes only `canonical_path` in a real stdio response
to a number. The old helper accepts that response; the named schema must reject
it. This client-boundary fault injection does not establish native Apple
behavior, which remains covered by the macOS verification lane.
Report unavailable authority as a named skip. Await owned cleanup before a
verifier reports success. Native process inspectors are released at Vitest
worker teardown; fork termination does not run Node exit hooks, and per-file
@@ -648,11 +687,22 @@ missing entries are omitted as whole entries, and consumers rebuild cache misses
including when every entry is unavailable. Malformed plans, non-file entries,
and I/O errors other than disappearance still fail publication. After a trusted
static run, superseded cross-run Turbo snapshots for that OS and architecture
are deleted down to the two newest generations. The Windows lane relies on the Linux static job for typechecking. They retain the normal build and prepack
commands, which restore matching outputs or rebuild on a cache miss. Native
are deleted down to the two newest generations. The Windows lane also runs the
official compiler's complete source typecheck with two checker threads,
independently of the accelerated Linux checker. Both lanes retain the normal
build and prepack commands, which restore matching outputs or rebuild on a cache
miss. Native
artifacts, dependency installs, package archives, and runtime verification remain
host-local. Keep platform-dependent outputs out of this shared cache.
The Windows native verifier's private-runtime ACL observer reads SID rules once
for both trustee validation and counting. It deliberately rejects the convenience
`Access` property, which resolves account names. Its `private-runtime-acl` stderr
markers report completed phases and elapsed time; inspect these on a timeout
before attributing it to startup, ACL reads, or identity lookup. The original
15-second deadline and all ownership, DACL, and readonly assertions remain in
force. A passing retry alone does not explain a previous timeout.
Record cold and warm preparation separately from fresh test execution when
benchmarking the optional `npm run check:pr` gate. Compare multiple runs on the
same host, including subprocess and queue costs; a cached pass is not a test
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,286 @@
# Static GPU code-object verification
The [receipts](gpu-code-objects-2026-10-21.json) pin the measured runtime tree,
publisher URLs/digests, actual commands, completed answers and resource costs.
Downloaded binaries, toolchains and complete Evidence bundles remain outside Git.
## Correctness
NVIDIA CUDA 12.9.1 redistribution archives were verified against their publisher
manifest. nvcc 12.9.86 compiled a small owned kernel for sm_80 and sm_90a with
uncompressed, size-compressed and speed-compressed fatbins. cuobjdump 12.9.82
independently confirmed two parameters at offsets 0/8 with sizes 8/4, 12 parameter
bytes, 10 physical registers and architecture-dependent shared memory. Separate
PTX/CUBIN and a host ELF object exercised the same parser boundaries.
Clang 14 produced a gfx900 HIP bundle and AMDGPU ELF object; llvm-readelf supplied
the MessagePack metadata oracle. An LLVM 19 offload bundler produced a real CCOB
v2 Zstd bundle. All nine producer artifacts were reread on the final runtime
source with complete payload coverage. CCOB v1/v3 and zlib have portable boundary
fixtures, rather than real producer verification.
Released Linux and Windows nvJPEG 12.4.0.76 artifacts each contain 165 CUBIN and
10 PTX payloads, with 3,968 kernel declarations across 15 target declarations.
Independent extraction matched all 165 decoded CUBIN hashes and all 10 PTX byte
sequences; cuobjdump resource output matched all 3,720 native kernel
name/register/shared-memory tuples. Pre-review MCP workflows rechecked the Linux
sm_100 subset's 248 complete kernel facts against those prior oracle facts.
The pre-review public CLI analyzed the entire Windows DLL, exported complete
Evidence, and reproduced all previously verified facts exactly.
Windows validation ran on Linux: LLVM independently identified the PE section
coordinates, and cuobjdump consumed its extracted fatbin. cuobjdump did not
recognize the DLL directly. This does not establish Windows runtime behavior,
GPU execution, host launch correlation or legacy AMDGPU YAML support.
## Initial completed MCP workflows
Both fresh stdio MCP sessions analyzed only `sm_100` in the same Linux library,
retained 176 inventory objects and 248 kernel facts, queried the same named
kernel, exported Evidence, and repeated the historical query three times.
The complete variant returned the selected-target report; the selected variant
returned a same-call kernel page. Both preserved the exact answer: one parameter
of 168 bytes, 31 physical registers and 9,216 shared bytes.
| Cost | Complete report | Selected page |
| --------------------------------------------------------------- | ----------------: | -----------------: |
| Used input declarations + three primary calls, estimated tokens | 406,862 | 26,106 |
| All input declarations + primary calls, estimated tokens | 635,381 | 254,625 |
| Full advertised catalog + all six calls, estimated tokens | 1,475,258 | 1,094,502 |
| Process-family elapsed seconds | 15.492 | 15.443 |
| Process-family CPU seconds | 18.220 | 18.074 |
| Sampled peak family RSS bytes | 1,139,601,408 | 1,321,566,208 |
| First inspection milliseconds | 8,572 | 8,810 |
| Three warm retained-page calls, milliseconds | 10.7 / 10.6 / 9.8 | 12.0 / 11.4 / 10.6 |
Tokenx estimates include advertised schemas, arguments and full SDK replies;
they are not actual model usage. Text and structured reply representations are
both counted. Initialization and JSON-RPC envelopes are unmeasured. Complete
catalog discovery substantially reduces the overall percentage saving.
These are workflow variants on the same implementation, not a before/after
parser speed benchmark. Node compile caching was disabled, but OS caches and
unrelated host load were uncontrolled. No general latency, CPU or memory
improvement is established. The selected workflow used more peak memory in this
pair. An earlier all-target complete reply exceeded the existing transport guard;
that incomplete attempt is excluded from the completed comparison.
The full Windows DLL CLI task took 4.064 seconds / 4.909 CPU seconds /
503,459,840 peak family RSS bytes. All profiled completed tasks stayed within
the 8 GiB and 420 second guards, using two CPU affinities. Peak RSS sums payload
descendants every 20 ms and is approximate; the observer is excluded.
## Regression checks
- The initial focused run passed 265 tests in 19 files: GPU domain/codec/parser
cases, public CLI/MCP, retained views, output profiles, cancellation and cleanup,
plus shared ELF/PE Go, Rust and IL2CPP consumers.
- Full static checks passed before the catalog/docs-only follow-up; its receipt
records 6,866,296,832 peak family RSS bytes. The final CI run qualifies the PR.
- Documentation/catalog/license checks passed after fixing missing catalog
ownership, translated README links and the new MessagePack dependency notices.
- An earlier combined schema run hit two 30 second timeouts on two CPUs. The
isolated canonical schema suite passed all 15 cases without increasing limits.
This is recorded as a successful isolation, not a proven baseline failure.
Retention caches are scoped to immutable Evidence through a WeakMap; historical
pages do not reopen files. Live inspections always stream a fresh full-file hash
and revalidate the descriptor/path. No live-state freshness cache is claimed.
## Review follow-up
At `36b87ed`, the public CLI reproduced both review findings: selecting
`amdgcn-amd-amdhsa--gfx900` in the real Clang-produced standalone object returned
zero kernels with an unknown target, and malformed `--targets` JSON was reported
at the `view` issue path.
At `09c6062`, AMD ELF note parsing owns target discovery and prepared kernel
declarations. Inventory reads those bounded notes before selection; selected
analysis reuses them without decoding MessagePack twice. Aggregate encoded note
bytes are limited to 8 MiB before reads, with existing per-value/depth limits;
the parsed JavaScript representation expands beyond that source-byte count.
The prepared declarations live only for the current payload inspection. Named
CLI option parsing also supplies the actual parsed input to schema diagnostics.
The real public CLI now inventories the gfx900 target, returns the producer's
one kernel/two parameters/12-byte parameter area/6 SGPR/2 VGPR when selected,
and preserves an unselected object without kernel facts for a different target.
Those three complete CLI tasks together took 5.974 seconds / 6.951 CPU seconds /
383,623,168 sampled peak family RSS bytes, including the Python task driver.
All nine producer artifacts were reread with complete payload coverage.
The review regression run passed 35 cases in four files, including public CLI,
MCP, metadata capacity and cleanup. A further 10 product-catalog cases passed
after adding the missing CLI `analysis` option description identified by CI.
The earlier paired nvJPEG measurements remain pinned to their original runtime;
these follow-ups do not establish a new CUDA performance result.
## Bounded metadata and partial-fact review
The latest qualification at `ad1e17a` rebases onto main `5241597`, preserving
both Rust and GPU retained views. All 70 focused cases in nine files passed,
including both public CLI/MCP workflows; the separate SDK-converted canonical
schema suite passed all 15 cases.
CUDA parsing shares an aggregate 262,144 record/symbol visit budget, 128 MiB
section-read budget and 16 MiB serialized-size accounting budget for index facts.
Equal scoped byte ranges and duplicate register values are coalesced before
index growth; conflicting values survive. Parsed parameter metadata is reused.
The public CLI completed an 861,896-byte CUBIN containing 1,024 headers aliasing
65,536 records and retained the correct physical count. The old 67-million-entry
expansion is source-derived; it was not run to exhaustion.
Decoder coverage now reaches object, report and target pages: absent CUBIN
symbol tables and unknown section names remain partial. AMD note parsing uses
a tagged Result that retains earlier parsed notes on later malformed, capacity,
unsupported or interrupted reads; retained kernel facts are emitted before the
original failure is propagated. Standalone PTX recognition continues beyond the
64 KiB fast path up to its actual 16 MiB text budget and reuses complete probe
bytes. Unsupported/overflowing register counts stay null instead of low totals.
The public CLI verified an AMD valid-note/truncated-note input and a PTX file
with a 70 KiB leading comment, in addition to the alias case. Those three tasks
together took 5.132 seconds / 6.222 CPU seconds / 405,835,776 peak family RSS
bytes, including the Python driver. Regression cases additionally cover invalid
MessagePack and unsupported extensions in standalone and bundled AMD objects.
The revised Windows DLL workflow preserved all 165 decoded CUBIN hashes, all
10 PTX byte identities and all 3,720 native kernel facts. Independently counting
register declarations in cuobjdump-extracted PTX verified all 248 PTX kernels;
20 kernel totals now correctly include previously missed declarations. This
is a virtual-declaration oracle, not physical allocation or GPU execution.
The Linux sm_100 workflows still match all 248 independently checked native
kernel facts. All nine real producer artifacts retain complete payload coverage.
The latest complete/selected MCP pair performed the same six calls and answered
the same question as the initial pair. Receipts retain both measurement stages.
| Cost | Complete report | Selected page |
| --------------------------------------------------------- | --------------: | ------------: |
| Used input declarations + primary calls, estimated tokens | 407,368 | 26,612 |
| All input declarations + primary calls, estimated tokens | 636,495 | 255,739 |
| Full catalog + all six calls, estimated tokens | 1,480,698 | 1,099,942 |
| Process-family elapsed seconds | 15.487 | 15.923 |
| Process-family CPU seconds | 18.404 | 18.760 |
| Peak family RSS bytes | 1,251,254,272 | 1,201,774,592 |
These remain tokenizer estimates and same-code workflow variants. The single
latest pair establishes no general latency/CPU/memory improvement; OS caches
and host contention remain uncontrolled. The Windows CLI rerun completed in
4.078 seconds / 4.880 CPU seconds / 498,380,800 peak family RSS bytes.
Main CI independently reproduced the browser termination and WASM optional-input
assertion failures without GPU changes. Cancelled fail-fast shards are not
counted as passing. Those baseline failures are outside this feature's fixes.
## Target declaration and CUDA ABI qualification
The latest code `f5be46f` is based on main `f801b2f`. Inventory and payload
target declarations remain separate, with typed consistency and inspection
coverage preserved in object pages even with selected fields. A mismatched
outer/inner base target remains partial and annotates affected kernels; CUDA
base-only agreement does not establish agreement on a feature profile. Target
filters select inventory declarations, and unexamined inner targets stay unknown.
Standalone and unknown-ID bundled AMDGPU files use the same bounded target
discovery owner before filtering. Portable controls wrap the same real Clang
ELF with two-separator, one-separator and opaque HIP IDs. All three inventory
then exact-target CLI workflows retain the verified kernel. They are alternate
wrapper controls, not verification of an actual legacy compiler producer.
The shared fixed ELF header owner now retains OS ABI. NVIDIA's released CUBINs
include distinct `0x33` and `0x41` flag layouts. The earlier low-byte-only
interpretation falsely produced `sm_2` for 55 modern payloads; that attempted
target-check implementation and its timings are superseded diagnostic evidence,
not a correctness-qualified benchmark. The ABI-qualified decoder retains raw
flags and exact fixed-header coordinates, and leaves unrecognized patterns unknown.
Header ownership is checked when importing portable Evidence.
All 165 released Windows CUBIN targets and header values were checked against
cuobjdump-extracted files: 110 use OS ABI `0x33`, 55 use `0x41`; no false conflicts
remain. All 3,968 corrected kernel facts and 10 PTX byte identities match the
prior independent oracle. The nine real producer artifacts retain complete
payload coverage. Unsupported layout controls retain their headers and make
unknown target discovery partial in the default MCP summary.
After the shared-header change, 251 cases in nine files passed, including
Go/Rust/IL2CPP consumers, GPU CLI/MCP and provenance forgery. The last uncertainty
propagation check passed 46 public/boundary cases. The separate SDK-converted
canonical schema suite passed all 15 cases on the current base.
The latest qualified complete/selected pair retains the same sm_100 question,
answer and six-call workflow. It runs at `21eaa0d`, before the final unresolved-
discovery-only guard.
| Cost | Complete report | Selected page |
| --------------------------------------------------------- | --------------: | ------------: |
| Used input declarations + primary calls, estimated tokens | 418,967 | 26,653 |
| All input declarations + primary calls, estimated tokens | 648,242 | 255,928 |
| Full catalog + all six calls, estimated tokens | 1,493,041 | 1,100,727 |
| Process-family elapsed seconds | 21.307 | 24.604 |
| Process-family CPU seconds | 23.538 | 26.454 |
| Peak family RSS bytes | 1,146,519,552 | 1,341,333,504 |
Selected replies save context but do not establish faster analysis. The latest
pair used more elapsed/CPU time and peak memory for selected delivery; variability
is visible across the preserved trials. Fresh process lifetime is controlled;
OS caches, frequency and unrelated load are not. A same-code diagnostic timing
repeat varied substantially, and no cause or persistent speedup is inferred.
The tokenizer/client scope and omitted initialization costs remain as above.
The final Windows CLI task took 5.446 seconds / 5.916 CPU seconds / 498,192,384
peak family RSS bytes. The six HIP wrapper CLI tasks took 15.696 seconds /
16.427 CPU seconds / 382,410,752 peak family RSS bytes. All completed tasks stayed
within the 8 GiB family guard.
## Payload admission and PTX uncertainty follow-up
At `893e04d`, PTX parsing owns typed per-kernel coverage and limitations for
unknown parameter layouts, shared declarations and virtual register counts.
Inventory forwards that coverage to objects, reports and historical pages.
The old register-only interpretation in inventory was removed.
A 67-case focused run passed across five files, including parser, public CLI/MCP,
provider boundary and lifecycle tests. Three source admission cases also passed
(one overlaps the focused run). NVIDIA `ptxas 12.9.86` accepted a pointer-parameter
PTX control; the public CLI retained its unknown layout and reported partial
coverage. A released nvJPEG example independently extracted by cuobjdump uses
`.param .u64 .ptr .align 1`; this remains outside supported layout interpretation.
The revised Windows DLL report retains all 176 object factual fields, all 3,968
kernel factual fields and all 10 exact PTX byte identities. Eight PTX objects and
59 kernels now expose their pre-existing unknown parameter layouts with partial
coverage and added limitations. The earlier complete payload-coverage claim for
those PTX objects is superseded; decoded numerical facts are unchanged. Nine real
producer artifacts still retain complete payload coverage.
The shared work owner now reserves uncompressed payload input once, and compressed
input plus decoded output atomically, before reading/allocating. CCOB uses the
same admission owner as payload analysis and AMD note reads. The allowance is
512 MiB of that work, with 128 MiB per input/output buffer. Admitted failed work
remains charged. A real sparse HIP file contains twelve aliases of one 128 MiB
ELF payload: file-read instrumentation observed four full payload reads and eight
capacity refusals with no payload read or decoded digest; the earlier four kernel
facts survive. CUDA and CCOB oversized-output controls likewise prove refusal
before the compressed input read. Old rejected-alias rereads are source-derived;
they were not run to exhaustion.
The nine-producer pass took 1.148 seconds / 1.100 CPU seconds / 122,732,544 peak
family RSS bytes. The pointer-PTX and released-DLL public CLI tasks together took
10.102 seconds / 11.302 CPU seconds / 512,335,872 peak family RSS bytes. The focused
run took 142.441 seconds / 211.528 CPU seconds / 3,478,200,320 peak family RSS bytes,
including the required artifact build. No resource guard stopped these tasks.
The earlier MCP cost pairs remain pinned to their measured revisions; no new
speedup is inferred from this follow-up.
Three verifier attempts are retained as unsuccessful in the JSON record: an
incorrect bare-record assumption for the Evidence bundle, refusal to overwrite
an existing export, and a stale expectation that the now-corrected PTX coverage
would stay complete. The successful comparison requires all factual values and
PTX bytes to match while checking the intended uncertainty changes explicitly.
`npm run check` passed at this source revision, including type/lint/format, module
boundaries and architecture guards: 135.345 seconds / 266.563 CPU seconds /
5,371,879,424 sampled peak family RSS bytes; no resource guard stop.
`npm run docs:check` also passed, including all five README conformance cases.
+3
View File
@@ -178,6 +178,9 @@ sanitized source even with explicit sensitivity options. If that source pass
is interrupted, unchecked bodies are not published as partial Evidence.
Framing overrides apply only to format-owned body coordinates and preserve
exclusions for the transport declared in MIME metadata.
Explicitly excluded or conflicting MIME metadata is not treated as absent:
automatic framing leaves such a non-empty body uninterpreted, adds a record
limitation and keeps `protobuf_coverage` partial unless `framing` is selected.
Unknown or explicitly excluded HTTP coding metadata is not treated as absent:
SAZ/native bodies remain opaque until identity coding is established. Raw
header values and retained body bytes remain available in source Evidence.
@@ -0,0 +1,50 @@
"""Read archived AppKit connector UIDs independently with Python's plistlib."""
import argparse
import hashlib
import json
from pathlib import Path
import plistlib
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--bundle', type=Path, required=True)
parser.add_argument('--document', required=True)
parser.add_argument('--baseline', type=Path, required=True,
help='Complete pre-change 64-document Evidence; equality control, not the oracle')
parser.add_argument('--small-baseline', type=Path, required=True,
help='Complete pre-change 1-document Evidence')
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
data = (args.bundle / args.document).read_bytes()
objects = plistlib.loads(data)['$objects']
connections = []
for index, value in enumerate(objects):
if not isinstance(value, dict) or not {'NSSource', 'NSLabel'} <= value.keys():
continue
classname = objects[value['$class'].data]['$classname']
if classname not in ('NSNibOutletConnector', 'NSNibControlConnector'):
continue
connections.append({
'archive_index': index,
'kind': 'outlet' if classname == 'NSNibOutletConnector' else 'action',
'name': objects[value['NSLabel'].data],
'source': str(value['NSSource'].data),
'destination': str(value['NSDestination'].data),
})
if not connections:
raise ValueError('No supported archived connectors; choose a keyed AppKit document')
args.output.write_text(json.dumps({
'document': args.document,
'archive_sha256': hashlib.sha256(data).hexdigest(),
'connections': connections,
'complete_result_paths': {
'1': str(args.small_baseline.resolve()),
'64': str(args.baseline.resolve()),
},
}, indent=2) + '\n')
if __name__ == '__main__':
main()
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,225 @@
# Retained Interface Builder graph validation
Issue #5855. Baseline `f801b2fbfd28f4e3713adefddc7bd73db44c284c`, Linux
x86_64 / AMD EPYC 7B12 / Node 24.18.0 / npm 11.16.0. This is static archive
analysis on Linux, without executing an app or using ibtool or a native
analysis provider.
## Question and oracle
Identify the AAC settings outlets and VBR action in the official
[XLD 20250302 app](https://tmkk.undo.jp/xld/index_e.html), including archive
source/destination object identities, connection evidence and limitations.
- [Official DMG](https://tmkk.undo.jp/xld/xld-20250302.dmg) SHA-256:
`0032a5470ea4e32a11a35b8077ebf4986102891f8eb82743094f2c6621ad8aeb`.
- Selected document:
`Contents/PlugIns/XLDAacOutput.bundle/Contents/Resources/English.lproj/XLDAacOutput.nib`.
- Archive SHA-256:
`4fdbfba451ee64b1d333c96a4f3d1a0c4b543a36b9b3686e1151b51e4dd14c72`.
- Python's independent `plistlib` reads the actual `$objects` table and
`NSNibOutletConnector` / `NSNibControlConnector` records. Observed connections:
| Kind | Label | Source UID | Destination UID |
| ------ | ------------- | ---------- | --------------- |
| outlet | o_bitrate | 2 | 10 |
| outlet | o_gaplessFlag | 2 | 41 |
| outlet | o_prefPane | 2 | 53 |
| outlet | o_quality | 2 | 62 |
| outlet | o_useVBR | 2 | 58 |
| action | vbrChecked: | 58 | 2 |
The benchmark checks each connector, both endpoint records, relationship kinds,
archive path/digest and page coverage against these independent facts. It also
compares all returned document rows with the exported complete parent; all
64-document normalized results match the pre-change complete CLI Evidence.
The complete exported parent Evidence is equal across versions for both
admission sizes. The reproduction oracle requires a separate pre-change
complete Evidence file for each size, and the harness compares the entire
parent, including its identity and parameters, for the selected limit. Equality is a preservation control, not an independent proof
of every decoded fact.
The default 64-document report contains 6,528 nodes and 1,378 edges. The small
control admits one document from the same unmodified real app, with 80 nodes
and 13 edges. Both reports retain truncation and hierarchy limitations. Resource
sessions have no executable subject or executable digest; archive identities
must not be promoted to executable identity.
## Completed workflow and accounting
`interface-builder-views.mjs` uses the real SDK and compiled stdio MCP server:
connect, discover the full catalog, open bundle resources, decode with summary
delivery, exhaust three 32-row node pages and one edge page for the selected
document, repeat those same four pages as a warm query diagnostic, export the
complete Evidence, verify the answer and complete parent, and await cleanup.
The repeat is included in all whole-task costs. Each run starts a fresh server
and isolated account. Decoding is the first decode in that server; retained
queries follow the summary's first validation. OS caches were not flushed, so
these are not disk-cold measurements.
Artifact acquisition/extraction, build time and independent oracle preparation
are outside this analysis task. No actual model session or billed model token
usage was measured. The SDK benchmark answers a fixed factual question; it does
not establish agent usability or general provider performance.
Experiments ran serially using `resource-run.py --observe-payload-exit`: one CPU
(affinity 15), 100% quota, 4 GiB hard memory / 3 GiB early stop, no swap and a
192-task ceiling. CPU and peak memory cover the SDK, server and observer family.
Peak is cgroup memory.peak, including charged page cache, rather than summed RSS.
Payload-exit cgroup counters are authenticated and cleanup is verified; final
whole-scope counters are unavailable after the transient scope disappears.
Every accepted and failed task has exit/cleanup verification and zero observed
memory high/max/OOM/OOM-kill and PID-limit events. Missing final counters are
not interpreted as zero.
## Observations
Medians across all accepted baseline runs and the measured optimized
production-source cohort. The later harness-only review fix does not change
those production files; its revalidation is recorded separately. Five
earlier candidate runs remain labeled pre-review diagnostics and are excluded
from the final-source medians. One failed initial harness run is also retained.
All individual runs, phase timings, token estimates and raw receipt hashes are
in `interface-builder-views.json`.
| Admitted documents | Revision | Runs | Whole wall s | Family CPU s | Family peak MiB | First four pages ms | Repeated four pages ms |
| ------------------ | -------- | ---- | ------------ | ------------ | --------------- | ------------------- | ---------------------- |
| 64 | baseline | 5 | 30.798 | 30.532 | 1614.7 | 2606.3 | 1817.8 |
| 64 | final | 5 | 27.683 | 27.424 | 1551.1 | 338.8 | 94.3 |
| 1 | baseline | 4 | 15.702 | 15.456 | 952.6 | 389.5 | 101.0 |
| 1 | final | 4 | 18.101 | 17.822 | 944.8 | 367.5 | 101.7 |
The larger sampled task has about 10% lower whole wall/CPU medians and 94.8%
lower repeated-page latency. Whole-task ranges overlap substantially:
27.420–51.481 s baseline and 25.247–45.451 s final. Startup/initial decoding
vary independently of the retained-query change; these observations do not
establish a general whole-task speedup or stable memory reduction. Both
versions' slow first runs remain included.
The small sampled task is slower overall (15.3% wall), with no repeat-page
benefit. In the two later interleaved small pairs, baseline took 16.883/18.317 s
and final took 16.988/20.276 s. Do not interpret the earlier unpaired small
measurements as evidence of a cold-start improvement. Startup variance remains
unexplained; this optimization is justified by repeated large-parent queries,
not a claim that every completed task becomes faster.
An additional production-boundary diagnostic isolates the changed projection
from startup, decoding and transport. It imports both compiled owners, seals
separate clones of the actual exported parent with each owner's snapshot
module, and alternates version order for three epochs. Each epoch projects a
summary and the same eight pages; every resulting payload is equal. Median
projection CPU falls from 157.4 to 30.3 ms for the one-document parent and
6003.5 to 914.7 ms for the 64-document parent. Snapshot preparation, Evidence
authentication and all adapters are excluded from this diagnostic; the public
tasks above account for their whole costs. First large-summary CPU rises from
620.2 to 879.8 ms because representation comparison is paid up front. This is a
reuse tradeoff, not a faster first decode. The diagnostic source/receipt hashes,
per-call wall/CPU times and bounded process-family counters are retained in the
JSON; it does not replace real public workflow evidence.
Selection still scans retained rows to count every match; no additional
per-document index is retained. The whole original graph and serialization
costs still exist.
Public input/output contracts and complete parent Evidence are unchanged.
Tokenizer estimates count the entire 212-tool input catalog once, every RPC's
arguments and text response, including warm diagnostics and export. The raw SDK
catalog, including output schemas, is reported separately. Structured content
and model-specific discovery/exposure may incur different costs. The 64-document task estimates are 282,885 baseline and 282,887 for the
initial candidate (198,492 input-catalog tokens in each); the two-token change
comes solely from the export destination. The first ten RPC requests and
replies are exactly equal. Raw SDK catalog exposure estimates 891,652 tokens
including output schemas. Final-source values and every run are retained in
the JSON. There is no claimed token reduction.
## Ownership and reuse
The Interface Builder view owner validates an unknown normalized result with
its existing schema. Reuse requires the application's seal plus exact owned
immutable snapshot identity and representation-preserving comparison against
the parsed result. A weak set records only successful snapshots. It retains no
second graph, file-path cache, index, transient errors or failed subject
bindings. Every view still checks its current parent's target binding and
constructs current parent provenance and limitations. Different results,
mutable imports, forged seals and schema normalization revalidate normally.
Document selection uses the shared matching pager to count all matching rows
while retaining only the requested page. Unfiltered requests keep direct
slicing. Row IDs, ordering, provenance, decoder truncation and unknowns remain
unchanged. CLI and MCP share the same application/domain implementation.
The snapshot comparison helper's type accepts owning schemas with optional
undefined members; its runtime comparison is unchanged and refuses a parsed
representation that actually adds an undefined field.
## Reproduction and limits
Extract the official app without running it. On the baseline, save complete
CLI JSON with `rea decode-interface-builder <XLD.app> --format json` and a
separate control with `rea decode-interface-builder <XLD.app> --max-documents 1
--format json` (without profiler logging mixed into either JSON). Then:
```sh
python3 experiments/analysis-efficiency/interface-builder-oracle.py \
--bundle '<XLD.app>' \
--document 'Contents/PlugIns/XLDAacOutput.bundle/Contents/Resources/English.lproj/XLDAacOutput.nib' \
--baseline '<complete-64-document-evidence.json>' \
--small-baseline '<complete-1-document-evidence.json>' --output '<oracle.json>'
python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \
node experiments/analysis-efficiency/interface-builder-views.mjs \
--runtime '<absolute baseline or candidate checkout>' --input '<XLD.app>' \
--oracle '<oracle.json>' --output '<new output directory>' --documents 64
```
Repeat with `--documents 1`. Paths must be absolute. The oracle uses standard
Python only; token estimates use offline tiktoken 0.12.0 / cl100k_base. Count
compact JSON tool name/description/inputSchema entries once, compact
`{name,arguments}` for every transcript event, and every returned text block;
these are estimates, not actual model usage.
Failed attempts remain visible. `base64-a` completed all eight page RPCs but the
first harness incorrectly expected an action-source `contains` edge. Production
correctly returned `target_action`; the harness was corrected before accepted
runs. That failure's costs/receipt remain in the JSON. An initial typecheck
refused the snapshot helper's unnecessarily narrow generic constraint for the
optional resource root; the constraint was corrected without changing runtime
comparison. Build resource observations with a still-exiting Turbo child are
not qualified task measurements.
KeepingYouAwake 1.6.8 (official ZIP SHA-256
`8001a149b4490c008fdac19898bce9902d516c4aa6412a7eb0f9a37443b15c6b`)
was also downloaded and smoke-decoded through the real CLI
(17 documents, 1,369 nodes, 542 edges), but its NIBArchive records cannot be read
by this plistlib oracle. It is not a comparative benchmark or independent NIB
correctness claim. macOS ibtool, runtime UI, other archive variants, globally
complete hierarchy/class interpretation, #5720/#5721 and the remaining source
decoder issues are unverified or unchanged here.
## Integration verification
The measured comparison above was pinned to `f801b2fbf`. The branch was then
rebased onto `f5410bdc4` (the separate #5806 scalar-record fix). All six recorded owned-source
fingerprints remained identical across that rebase. On the rebased runtime both real SDK
tasks passed again, with the complete retained parent equal to the original
baseline for both admission sizes. The complete real CLI Evidence and portable
CLI page also equal the baseline/MCP results. These integration runs are recorded
separately in the JSON and excluded from the pinned comparison medians.
The relevant 12-file suite passed 204 tests after the paging refactor. After
rebasing, another six-file run passed 78 tests covering scalar source facts,
keyed archive number metadata, Interface Builder views, production MCP summary
paging and compiled CLI numeric/hierarchy boundaries. Both cohorts verified
owned cleanup with no observed memory/PID limit events; tests used the bounded
two-CPU, 4 GiB guard. Native ibtool/macOS runtime behavior is not established by
these tests.
Greptile identified that the original script only enforced the pre-change
comparison for 64 documents; the one-document equality above had been checked
manually. The oracle now requires both baseline files and the harness compares
the selected complete parent, including its Evidence ID and parameters, for
either limit. Both real workflows passed again. An intentional wrong baseline
(the 64-document parent supplied for the 1-document task) is rejected after all
other real-task checks succeed, with cleanup verified. These review checks and
updated harness fingerprints are recorded separately. Production source and
original comparison medians are unchanged; original measured fingerprints are
preserved rather than relabeled as the new harness.
@@ -0,0 +1,238 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { createWriteStream } from "node:fs";
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { finished } from "node:stream/promises";
import { pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import { createRetainedQueryCaller } from "./retained-query-calls.mjs";
import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs";
const { values } = parseArgs({
options: {
runtime: { type: "string" },
input: { type: "string" },
oracle: { type: "string" },
output: { type: "string" },
documents: { type: "string", default: "64" },
},
});
for (const key of ["runtime", "input", "oracle", "output"])
assert.equal(values[key], resolve(values[key]));
const maxDocuments = Number(values.documents);
assert.ok([1, 64].includes(maxDocuments));
const oracle = JSON.parse(await readFile(values.oracle, "utf8"));
const baselinePath = oracle.complete_result_paths?.[String(maxDocuments)];
assert.equal(
typeof baselinePath,
"string",
"Provide a pre-change baseline for the selected document limit.",
);
assert.equal(baselinePath, resolve(baselinePath));
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
assert.equal(
sha256(await readFile(join(values.input, oracle.document))),
oracle.archive_sha256,
);
const require = createRequire(join(values.runtime, "package.json"));
const { Client } = await import(
pathToFileURL(require.resolve("@modelcontextprotocol/client"))
);
const { StdioClientTransport } = await import(
pathToFileURL(require.resolve("@modelcontextprotocol/client/stdio"))
);
await mkdir(values.output, { mode: 0o700 });
const account = await mkdtemp(join(tmpdir(), "rea-interface-builder-views-"));
const report = {
question:
"Identify archived AAC settings outlets and the VBR action with source/destination object identities, coverage and complete Evidence access.",
max_documents: maxDocuments,
calls: [],
};
const client = new Client({
name: "rea-interface-builder-views",
version: "1",
});
const transport = new StdioClientTransport({
command: process.execPath,
args: [join(values.runtime, "scripts/rea.mjs"), "mcp"],
env: {
HOME: account,
PATH: process.env.PATH,
NODE_OPTIONS: "--max-old-space-size=2048",
UV_THREADPOOL_SIZE: "1",
REA_LOG_LEVEL: "silent",
},
stderr: "pipe",
});
const serverLog = createWriteStream(join(values.output, "server.log"));
const serverLogFinished = finished(serverLog);
serverLogFinished.catch(() => {});
transport.stderr?.pipe(serverLog);
const transcript = [];
const call = createRetainedQueryCaller(client, (receipt, event) => {
report.calls.push(receipt);
transcript.push(event);
});
async function result(name, input, phase = name) {
const reply = await call(name, input, phase);
assert.ok(!reply.isError, JSON.stringify(reply));
assert.ok(reply.structuredContent);
assert.equal(reply.content.length, 1);
assert.equal(reply.content[0].type, "text");
assert.deepEqual(JSON.parse(reply.content[0].text), reply.structuredContent);
return reply.structuredContent;
}
try {
await client.connect(transport);
const catalog = await client.listTools();
assert.equal(catalog.nextCursor, undefined);
report.catalog_tools = catalog.tools.length;
await writeFile(join(values.output, "catalog.json"), JSON.stringify(catalog));
await result("open_bundle_resources", { path: values.input });
const initial = await result("decode_interface_builder", {
max_documents: maxDocuments,
detail: "summary",
});
const summary = initial.normalized_result;
report.parent_evidence_id = summary.parent_evidence_id;
assert.equal(summary.resource_root.executable_sha256, null);
assert.equal(summary.artifact.sha256, null);
assert.equal(summary.summary.truncated, true);
assert.ok(
summary.summary.documents.some(
(d) =>
d.relative_path === oracle.document &&
d.archive_sha256 === oracle.archive_sha256,
),
);
const collections = {};
for (const pass of ["first", "warm"]) {
for (const collection of ["nodes", "edges"]) {
const rows = [];
let offset = 0;
for (;;) {
const selected = await result(
"inspect_analysis_view",
{
source: {
kind: "retained-evidence",
evidence_id: report.parent_evidence_id,
},
view: {
kind: "interface-builder",
collection,
document: oracle.document,
offset,
limit: 32,
},
},
`retained:${pass}:${collection}:${offset}`,
);
const page = selected.normalized_result;
assert.equal(page.parent_evidence_id, report.parent_evidence_id);
assert.equal(page.source_truncated, true);
assert.ok(page.unknowns.length > 0);
rows.push(...page.items);
if (page.coverage.exhausted) {
assert.equal(rows.length, page.coverage.total);
break;
}
assert.ok(page.items.length > 0);
offset += page.items.length;
}
if (pass === "warm") assert.deepEqual(rows, collections[collection]);
else collections[collection] = rows;
}
}
const byId = new Map(collections.nodes.map((n) => [n.id, n]));
for (const expected of oracle.connections) {
const connector = collections.nodes.find(
(n) => n.kind === expected.kind && n.name === expected.name,
);
assert.ok(connector);
const sourceRelation =
expected.kind === "outlet" ? "contains" : "target_action";
const source = collections.edges.find(
(e) => e.to === connector.id && e.relation === sourceRelation,
);
assert.equal(
byId.get(source.from).attributes.interface_builder_object_id,
expected.source,
);
const relation = expected.kind === "outlet" ? "outlet_to" : "target_action";
const destination = collections.edges.find(
(e) =>
e.from === connector.id &&
e.relation === relation &&
byId.get(e.to)?.attributes.interface_builder_object_id ===
expected.destination,
);
assert.equal(
byId.get(destination.to).attributes.interface_builder_object_id,
expected.destination,
);
for (const row of [connector, source, destination])
assert.ok(
row.evidence.some(
(e) =>
e.artifact_path === oracle.document &&
e.artifact_sha256 === oracle.archive_sha256,
),
);
}
report.answer = oracle.connections;
await result("export_evidence_bundle", {
path: join(values.output, "evidence.json"),
});
const bundle = JSON.parse(
await readFile(join(values.output, "evidence.json"), "utf8"),
);
const parent = bundle.records.find(
(e) => e.evidence_id === report.parent_evidence_id,
);
assert.equal(parent.subject, null);
assert.equal(parent.operation, "decode_interface_builder");
const full = parent.normalized_result;
for (const collection of ["nodes", "edges"])
assert.deepEqual(
collections[collection],
full.graph[collection].filter((r) =>
r.evidence.some((e) => e.artifact_path === oracle.document),
),
);
const baselineBytes = await readFile(baselinePath, "utf8");
const expectedParent = JSON.parse(baselineBytes);
report.baseline_sha256 = sha256(baselineBytes);
report.baseline_evidence_id = expectedParent.evidence_id;
const mismatch = `Complete retained Evidence differs from the pre-change ${maxDocuments}-document baseline.`;
assert.equal(parent.evidence_id, expectedParent.evidence_id, mismatch);
assert.deepEqual(parent, expectedParent, mismatch);
report.available_counts = {
nodes: full.graph.nodes.length,
edges: full.graph.edges.length,
};
report.oracle_sha256 = sha256(await readFile(values.oracle));
report.status = "passed";
} catch (cause) {
report.status = "failed";
report.failure = String(cause?.stack ?? cause);
process.exitCode = 1;
} finally {
if (
!(await finalizeRetainedQueryReceipt({
report,
client,
transport,
account,
serverLog,
serverLogFinished,
output: values.output,
transcript,
}))
)
process.exitCode = 1;
}
+36 -1
View File
@@ -2,7 +2,7 @@ REA npm dependency inventory
Prepared from the root package-lock.json; refresh this inventory when locked dependencies change.
License identifiers are upstream lockfile declarations, not a legal assessment.
Runtime includes transitive and optional platform packages; development includes build, test and website tooling.
Locked package entries: 751; runtime: 179.
Locked package entries: 758; runtime: 180.
Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
@algolia/abtesting@1.25.0 | development | MIT
@@ -690,6 +690,11 @@ commander@13.1.0 | runtime | MIT
Package: https://www.npmjs.com/package/@modelcontextprotocol/server/v/2.3.1
Archive: https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz
Integrity: sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw==
@msgpack/msgpack@3.1.3 | runtime | ISC
Lockfile path: node_modules/@msgpack/msgpack
Package: https://www.npmjs.com/package/@msgpack/msgpack/v/3.1.3
Archive: https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz
Integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==
@napi-rs/lzma-linux-x64-gnu@1.5.1 | development, optional | MIT
Lockfile path: node_modules/@napi-rs/lzma-linux-x64-gnu
Package: https://www.npmjs.com/package/@napi-rs/lzma-linux-x64-gnu/v/1.5.1
@@ -1385,6 +1390,31 @@ commander@13.1.0 | runtime | MIT
Package: https://www.npmjs.com/package/@toon-format/toon/v/2.3.1
Archive: https://registry.npmjs.org/@toon-format/toon/-/toon-2.3.1.tgz
Integrity: sha512-sWqEMeAJGMda9qRrfPKrX3C4c33CO9SJuvJzzrcBEn5sbDB+k6pSLkDsfN0rVnLQwR97MV3sDgVbcxQNsq8xVw==
@tsrs/darwin-arm64@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0
Lockfile path: node_modules/@tsrs/darwin-arm64
Package: https://www.npmjs.com/package/@tsrs/darwin-arm64/v/0.18.0-ts7.1.0-dev.20260929
Archive: https://registry.npmjs.org/@tsrs/darwin-arm64/-/darwin-arm64-0.18.0-ts7.1.0-dev.20260929.tgz
Integrity: sha512-sAirMpaGA9uRQm2W+t4zC7PjnijNuFa4PCSIfLKKo0JyAUCRfnMeev0UGBQ9F3g+9Nz187tvikUs5Irh5+eajA==
@tsrs/linux-arm64-gnu@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0
Lockfile path: node_modules/@tsrs/linux-arm64-gnu
Package: https://www.npmjs.com/package/@tsrs/linux-arm64-gnu/v/0.18.0-ts7.1.0-dev.20260929
Archive: https://registry.npmjs.org/@tsrs/linux-arm64-gnu/-/linux-arm64-gnu-0.18.0-ts7.1.0-dev.20260929.tgz
Integrity: sha512-zm9/z9yylig68JiZF4Aq0Cm26n09KEU8YoQRlG7tdDWTBNAIViYw8knxnWYsjMWVZ2XiU4mYq1xdrOKuOgABHg==
@tsrs/linux-arm64-musl@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0
Lockfile path: node_modules/@tsrs/linux-arm64-musl
Package: https://www.npmjs.com/package/@tsrs/linux-arm64-musl/v/0.18.0-ts7.1.0-dev.20260929
Archive: https://registry.npmjs.org/@tsrs/linux-arm64-musl/-/linux-arm64-musl-0.18.0-ts7.1.0-dev.20260929.tgz
Integrity: sha512-Lw7VVU/YQ9JcarODICEiwzMLdFqZprqh5RGTgbS5Uc6I95P635/g5xA1pOC8RVs4efBvAR30J54iDG3l5JEYiw==
@tsrs/linux-x64-gnu@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0
Lockfile path: node_modules/@tsrs/linux-x64-gnu
Package: https://www.npmjs.com/package/@tsrs/linux-x64-gnu/v/0.18.0-ts7.1.0-dev.20260929
Archive: https://registry.npmjs.org/@tsrs/linux-x64-gnu/-/linux-x64-gnu-0.18.0-ts7.1.0-dev.20260929.tgz
Integrity: sha512-EdBwBgNICGu1r1h23ixdcKwsMo6E9ll7ZsY3mNERNYqE1Pwdtcat99d5E7zB1J+oy/Xk6YqiizaB6nolIjpDTw==
@tsrs/linux-x64-musl@0.18.0-ts7.1.0-dev.20260929 | development, optional | Apache-2.0
Lockfile path: node_modules/@tsrs/linux-x64-musl
Package: https://www.npmjs.com/package/@tsrs/linux-x64-musl/v/0.18.0-ts7.1.0-dev.20260929
Archive: https://registry.npmjs.org/@tsrs/linux-x64-musl/-/linux-x64-musl-0.18.0-ts7.1.0-dev.20260929.tgz
Integrity: sha512-LHk+AOAtXuTlhJhOJ2+elriEIX17ycXljojeNxrrzckOipTG1LksoVWuCSas2nu00EnWAzrdKpt9oqPylUKZfQ==
@turbo/darwin-64@2.11.7 | development, optional | MIT
Lockfile path: node_modules/@turbo/darwin-64
Package: https://www.npmjs.com/package/@turbo/darwin-64/v/2.11.7
@@ -3395,6 +3425,11 @@ tslib@2.8.1 | development | 0BSD
Package: https://www.npmjs.com/package/tslib/v/2.8.1
Archive: https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz
Integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
tsrs@0.18.0-ts7.1.0-dev.20260929 | development | Apache-2.0
Lockfile path: node_modules/tsrs
Package: https://www.npmjs.com/package/tsrs/v/0.18.0-ts7.1.0-dev.20260929
Archive: https://registry.npmjs.org/tsrs/-/tsrs-0.18.0-ts7.1.0-dev.20260929.tgz
Integrity: sha512-9T0b4Uam/yMFIK16GUx0fUrJqYZnJ/gvSmV9JB8NTyDePvwCI45WFWxyEV/UMV3KfdNRJG/PBsm3fSIB9WmWsA==
tsx@4.23.15 | development | MIT
Lockfile path: node_modules/tsx
Package: https://www.npmjs.com/package/tsx/v/4.23.15
+15 -1
View File
@@ -1,7 +1,7 @@
REA third-party npm runtime copyright, license and NOTICE texts
Collected from integrity-verified locked npm archives. Release-specific supplements and upstream notice gaps are identified in their entries.
External engines are separately obtained; see THIRD_PARTY_NOTICES.md.
Runtime dependency fingerprint (SHA-256): 4125777159e02cb749e17c959e77f59528cfffe3f7a5220b8a21ce3c49ad3061
Runtime dependency fingerprint (SHA-256): d0a0286bb8c158a670b019cdb56ee120542cb072bbecf3741c172b5f06b2301b
===== @babel/helper-string-parser@8.0.6 =====
Declared license: MIT
@@ -1822,6 +1822,20 @@ CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
the full license text.
===== @msgpack/msgpack@3.1.3 =====
Declared license: ISC
Archive: https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz
Integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==
Scope: npm runtime dependency, including optional platform packages.
--- package/LICENSE ---
Copyright 2019 The MessagePack Community.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
===== @pinojs/redact@0.4.0 =====
Declared license: MIT
Archive: https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz
+128
View File
@@ -18,6 +18,7 @@
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/client": "2.3.1",
"@modelcontextprotocol/server": "2.3.1",
"@msgpack/msgpack": "3.1.3",
"@xmldom/xmldom": "0.9.12",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
@@ -73,6 +74,7 @@
"oxfmt": "0.72.0",
"oxlint": "1.87.0",
"rolldown": "1.2.12",
"tsrs": "0.18.0-ts7.1.0-dev.20260929",
"tsx": "4.23.15",
"turbo": "2.11.7",
"typescript": "7.0.2",
@@ -2239,6 +2241,15 @@
"node": ">=20"
}
},
"node_modules/@msgpack/msgpack": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
"integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==",
"license": "ISC",
"engines": {
"node": ">= 18"
}
},
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
@@ -4397,6 +4408,103 @@
"integrity": "sha512-sWqEMeAJGMda9qRrfPKrX3C4c33CO9SJuvJzzrcBEn5sbDB+k6pSLkDsfN0rVnLQwR97MV3sDgVbcxQNsq8xVw==",
"license": "MIT"
},
"node_modules/@tsrs/darwin-arm64": {
"version": "0.18.0-ts7.1.0-dev.20260929",
"resolved": "https://registry.npmjs.org/@tsrs/darwin-arm64/-/darwin-arm64-0.18.0-ts7.1.0-dev.20260929.tgz",
"integrity": "sha512-sAirMpaGA9uRQm2W+t4zC7PjnijNuFa4PCSIfLKKo0JyAUCRfnMeev0UGBQ9F3g+9Nz187tvikUs5Irh5+eajA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@tsrs/linux-arm64-gnu": {
"version": "0.18.0-ts7.1.0-dev.20260929",
"resolved": "https://registry.npmjs.org/@tsrs/linux-arm64-gnu/-/linux-arm64-gnu-0.18.0-ts7.1.0-dev.20260929.tgz",
"integrity": "sha512-zm9/z9yylig68JiZF4Aq0Cm26n09KEU8YoQRlG7tdDWTBNAIViYw8knxnWYsjMWVZ2XiU4mYq1xdrOKuOgABHg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@tsrs/linux-arm64-musl": {
"version": "0.18.0-ts7.1.0-dev.20260929",
"resolved": "https://registry.npmjs.org/@tsrs/linux-arm64-musl/-/linux-arm64-musl-0.18.0-ts7.1.0-dev.20260929.tgz",
"integrity": "sha512-Lw7VVU/YQ9JcarODICEiwzMLdFqZprqh5RGTgbS5Uc6I95P635/g5xA1pOC8RVs4efBvAR30J54iDG3l5JEYiw==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@tsrs/linux-x64-gnu": {
"version": "0.18.0-ts7.1.0-dev.20260929",
"resolved": "https://registry.npmjs.org/@tsrs/linux-x64-gnu/-/linux-x64-gnu-0.18.0-ts7.1.0-dev.20260929.tgz",
"integrity": "sha512-EdBwBgNICGu1r1h23ixdcKwsMo6E9ll7ZsY3mNERNYqE1Pwdtcat99d5E7zB1J+oy/Xk6YqiizaB6nolIjpDTw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@tsrs/linux-x64-musl": {
"version": "0.18.0-ts7.1.0-dev.20260929",
"resolved": "https://registry.npmjs.org/@tsrs/linux-x64-musl/-/linux-x64-musl-0.18.0-ts7.1.0-dev.20260929.tgz",
"integrity": "sha512-LHk+AOAtXuTlhJhOJ2+elriEIX17ycXljojeNxrrzckOipTG1LksoVWuCSas2nu00EnWAzrdKpt9oqPylUKZfQ==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@turbo/darwin-64": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.11.7.tgz",
@@ -9822,6 +9930,26 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tsrs": {
"version": "0.18.0-ts7.1.0-dev.20260929",
"resolved": "https://registry.npmjs.org/tsrs/-/tsrs-0.18.0-ts7.1.0-dev.20260929.tgz",
"integrity": "sha512-9T0b4Uam/yMFIK16GUx0fUrJqYZnJ/gvSmV9JB8NTyDePvwCI45WFWxyEV/UMV3KfdNRJG/PBsm3fSIB9WmWsA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsrs": "bin/tsrs"
},
"engines": {
"node": ">=16.20.0"
},
"optionalDependencies": {
"@tsrs/darwin-arm64": "0.18.0-ts7.1.0-dev.20260929",
"@tsrs/linux-arm64-gnu": "0.18.0-ts7.1.0-dev.20260929",
"@tsrs/linux-arm64-musl": "0.18.0-ts7.1.0-dev.20260929",
"@tsrs/linux-x64-gnu": "0.18.0-ts7.1.0-dev.20260929",
"@tsrs/linux-x64-musl": "0.18.0-ts7.1.0-dev.20260929"
}
},
"node_modules/tsx": {
"version": "4.23.15",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
+23 -21
View File
@@ -104,31 +104,31 @@
"metadata:generate": "node scripts/run-exclusive.mjs artifacts node scripts/generate-package-metadata.mjs",
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
"mcp-catalog:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog",
"mcp-catalog:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run mcp-catalog:check:built",
"mcp-catalog:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:mcp-catalog",
"mcp-catalog:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run mcp-catalog:check:built",
"prebuild": "npm run deps:check",
"build": "node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills --force",
"build": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:skills --force",
"precheck": "npm run deps:check",
"check": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards",
"check": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards",
"precheck:fast": "npm run deps:check",
"check:fast": "turbo run typecheck lint --output-logs=errors-only",
"check:fast": "node scripts/run-turbo.mjs run typecheck lint --output-logs=errors-only",
"precheck:changed": "npm run deps:check",
"check:changed": "npm run check:fast && npm run test:changed",
"precheck:test": "npm run deps:check",
"check:test": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint:built format:check knip metadata:check test:run",
"check:test": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check",
"check:pr": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos",
"check:pr": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos",
"precheck:ci": "npm run deps:check",
"precheck:ci:fast": "npm run deps:check",
"precheck:ci:deep": "npm run deps:check",
"check:ci": "npm run check:ci:fast && npm run check:ci:deep",
"check:ci:fast": "node scripts/run-exclusive.mjs artifacts npm run check:ci:fast:tasks",
"check:ci:fast:tasks": "turbo run typecheck lint:built format:check",
"check:ci:fast:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check",
"check:ci:deep": "node scripts/run-exclusive.mjs artifacts npm run check:ci:deep:tasks && npm run verify:architecture-guards && npm run verify:test-discovery && npm run test:python",
"check:ci:deep:tasks": "turbo run knip metadata:check jscpd scan:todos",
"check:ci:tasks": "turbo run typecheck lint:built format:check knip metadata:check jscpd scan:todos",
"check:ci:deep:tasks": "node scripts/run-turbo.mjs run knip metadata:check jscpd scan:todos",
"check:ci:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check jscpd scan:todos",
"ci:static-context": "node scripts/ci/static-task-context.mjs",
"build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills",
"build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:skills",
"build:termux": "node scripts/run-exclusive.mjs artifacts npm run build:termux:run",
"build:termux:run": "npm run compile:termux && npm run artifacts:skills",
"format": "oxfmt .",
@@ -151,7 +151,7 @@
"test:built": "node scripts/ci/test-group.mjs built",
"pretest:boundary": "npm run test:prepare",
"test:boundary": "vitest run --project boundary --project cli-boundary --project process-boundary --project mcp-boundary --project process-global",
"pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills artifacts:mcp-catalog",
"pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:skills artifacts:mcp-catalog",
"test:mcp": "vitest run --project mcp-boundary",
"pretest:acceptance": "npm run build:cached",
"test:acceptance": "vitest run --project acceptance",
@@ -174,7 +174,7 @@
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
"pretest:watch:all": "npm run test:prepare",
"test:watch:all": "vitest --changed",
"typecheck": "tsc --noEmit",
"typecheck": "node scripts/run-typescript.mjs --noEmit",
"verify:test-temp-hygiene": "npm run test:prepare && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs",
@@ -213,7 +213,7 @@
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
"verify:frida": "npm run build:cached && node scripts/verify-frida.mjs",
"verify:frida:bindings": "tsc --noEmit -p tsconfig.frida-verification.json",
"verify:frida:bindings": "node scripts/run-typescript.mjs --noEmit -p tsconfig.frida-verification.json",
"verify:browser:dom": "node scripts/verify-browser-dom-destinations.mjs",
"verify:browser:network": "npm run build:cached && node scripts/verify-browser-network.mjs",
"verify:electron": "npm run build:cached && node scripts/verify-real-electron.mjs",
@@ -256,17 +256,17 @@
"probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs",
"verify:published": "node scripts/verify-published-package.mjs",
"config:print": "node scripts/print-mcp-config.mjs",
"evidence:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:managed-evidence",
"evidence:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run evidence:check:built",
"evidence:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:managed-evidence",
"evidence:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run evidence:check:built",
"predocs:dev": "npm run docs:generate",
"docs:dev": "vitepress dev docs",
"predocs:build": "npm run docs:generate",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"docs:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts npm run docs:prepare",
"docs:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run docs:check:built && npm run test:local -- tests/conformance/readmeConsistency.test.ts",
"docs:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run docs:check:built && npm run test:local -- tests/conformance/readmeConsistency.test.ts",
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check && npm run licenses:check",
"docs:error-schema": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:error-schema",
"docs:error-schema": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:error-schema",
"prepack": "node scripts/prepack.mjs",
"prepare": "node scripts/prepare.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
@@ -305,16 +305,16 @@
"verify:solidity:metadata": "npm run build:cached && node scripts/verify-solidity-metadata.mjs",
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
"compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs",
"compile": "node scripts/clean-build-output.mjs && node scripts/run-typescript.mjs -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs",
"compile:termux": "node scripts/clean-build-output.mjs && node --max-old-space-size=3072 node_modules/typescript-termux/lib/tsc.js -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs",
"artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs",
"artifacts:skills": "node scripts/generate-skill-metadata.mjs",
"artifacts:product-catalog": "node scripts/generate-product-catalog.mjs",
"artifacts:managed-evidence": "node scripts/generate-completion-ledger.mjs",
"artifacts:error-schema": "node scripts/generate-error-schema.mjs",
"test:prepare": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog artifacts:product-catalog artifacts:managed-evidence",
"test:prepare": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:mcp-catalog artifacts:product-catalog artifacts:managed-evidence",
"evidence:check:built": "node scripts/generate-completion-ledger.mjs --check",
"docs:prepare": "node scripts/generate-package-metadata.mjs && turbo run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema",
"docs:prepare": "node scripts/generate-package-metadata.mjs && node scripts/run-turbo.mjs run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema",
"verify:web:qlog": "npm run build:cached && node scripts/verify-qlog-captures.mjs",
"licenses:generate": "node scripts/generate-third-party-inventory.mjs",
"licenses:check": "node scripts/generate-third-party-inventory.mjs --check"
@@ -336,6 +336,7 @@
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/client": "2.3.1",
"@modelcontextprotocol/server": "2.3.1",
"@msgpack/msgpack": "3.1.3",
"@xmldom/xmldom": "0.9.12",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
@@ -395,6 +396,7 @@
"oxfmt": "0.72.0",
"oxlint": "1.87.0",
"rolldown": "1.2.12",
"tsrs": "0.18.0-ts7.1.0-dev.20260929",
"tsx": "4.23.15",
"turbo": "2.11.7",
"typescript": "7.0.2",
+25
View File
@@ -63,6 +63,31 @@ function classifyPackage(base, head) {
const after = JSON.parse(git("show", `${head}:package.json`));
const { scripts: beforeScripts, ...beforeRest } = before;
const { scripts: afterScripts, ...afterRest } = after;
const compilerDependencies = (metadata) => ({
dependencies: {
typescript: metadata.dependencies?.typescript,
tsrs: metadata.dependencies?.tsrs,
},
devDependencies: {
typescript: metadata.devDependencies?.typescript,
tsrs: metadata.devDependencies?.tsrs,
},
});
const compilerScripts = [
"compile",
"typecheck",
"build",
"build:cached",
"test:prepare",
"docs:prepare",
"verify:frida:bindings",
].flatMap((name) => [name, `pre${name}`, `post${name}`]);
selected.package ||=
JSON.stringify(compilerDependencies(before)) !==
JSON.stringify(compilerDependencies(after)) ||
compilerScripts.some(
(name) => beforeScripts?.[name] !== afterScripts?.[name],
);
if (JSON.stringify(beforeRest) !== JSON.stringify(afterRest)) {
full("Package metadata or dependencies changed");
return;
+12
View File
@@ -92,6 +92,17 @@ export const SCOPES = {
],
// Four-host installed-package matrix and the Windows curated lane.
package: [
// Both host compilers must be exercised when their installation, options,
// or entrypoints change; Linux's accelerated checker cannot stand in for Go.
"scripts/run-typescript.*",
"scripts/run-turbo.mjs",
"turbo.json",
"tsconfig.json",
"tsconfig.build.json",
"tsconfig.frida-verification.json",
"package-lock.json",
".nvmrc",
".npmrc",
".github/actions/publish-mcp/**",
".github/actions/prepare-release-package/**",
".github/workflows/release-recovery.yml",
@@ -135,6 +146,7 @@ export const SCOPES = {
"src/application/DshCordis*.ts",
"src/cli/interactiveSetup.ts",
"src/cli/setupCommands.ts",
"tests/boundary/filesystem/typescriptCompiler.test.ts",
"tests/boundary/filesystem/binaryTarget.test.ts",
"tests/boundary/filesystem/safeOutputTree.test.ts",
"tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts",
+75 -4
View File
@@ -4,6 +4,67 @@ import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { closeVerifierResources } from "./verifier-cleanup.mjs";
// REA registers process-static tool contracts. Bind their parsed definitions to
// the SDK's negotiated connection identity, including when a Client reconnects.
const toolBindings = new WeakMap();
const advertisedTool = async (client, name) => {
const connection = client.getServerVersion();
assert.ok(connection, "MCP must be initialized before binding tool schemas");
let binding = toolBindings.get(client);
if (binding?.connection !== connection) {
binding = {
connection,
definitions: client.listTools().then(({ tools }) => {
const definitions = new Map();
for (const tool of tools) {
assert.ok(
!definitions.has(tool.name),
`Duplicate MCP tool: ${tool.name}`,
);
definitions.set(tool.name, tool);
}
return definitions;
}),
};
toolBindings.set(client, binding);
}
let definitions;
try {
definitions = await binding.definitions;
assert.equal(
client.getServerVersion(),
connection,
"MCP connection changed while binding tool schemas",
);
} catch (cause) {
if (toolBindings.get(client) === binding) toolBindings.delete(client);
throw cause;
}
const tool = definitions.get(name);
assert.ok(tool, `MCP catalog did not advertise ${name}`);
return tool;
};
/** Call an advertised tool with SDK output validation of its actual schema. */
export async function artifactMcpCall(client, parameters, options = {}) {
options.signal?.throwIfAborted();
assert.equal(
options.toolDefinition,
undefined,
"Artifact verification owns the advertised schema; pass request options only",
);
const toolDefinition = await advertisedTool(client, parameters.name);
assert.notEqual(
toolDefinition.outputSchema,
undefined,
`MCP tool omitted its output schema: ${parameters.name}`,
);
options.signal?.throwIfAborted();
return client.callTool(parameters, { ...options, toolDefinition });
}
const environment = () => {
const compileCache =
@@ -66,9 +127,10 @@ const withOpenedArtifactMcp = async (target, openTool, verify) => {
stderr: "pipe",
});
const client = new Client({ name: "artifact-real-e2e", version: "1" });
let failure;
try {
await client.connect(transport);
const opened = await client.callTool({
const opened = await artifactMcpCall(client, {
name: openTool,
arguments: { path: target },
});
@@ -76,9 +138,15 @@ const withOpenedArtifactMcp = async (target, openTool, verify) => {
if (openTool === "open_bundle_resources")
assert.equal(opened.structuredContent?.result?.caller_path, target);
await verify(client);
} catch (cause) {
failure = cause;
throw cause;
} finally {
await client.close();
await transport.close();
toolBindings.delete(client);
await closeVerifierResources(
[() => client.close(), () => transport.close()],
failure,
);
}
};
@@ -94,7 +162,10 @@ export async function withBundleResourcesMcp(target, verify) {
/** Require a successful structured MCP tool result rather than its text rendering. */
export async function artifactMcpResult(client, name, arguments_ = {}) {
const result = await client.callTool({ name, arguments: arguments_ });
const result = await artifactMcpCall(client, {
name,
arguments: arguments_,
});
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(
result.structuredContent?.normalized_result,
+5
View File
@@ -34,6 +34,7 @@ export const toolFamilyCatalog = (sources) => {
...sources.desktopContracts.DESKTOP_APPLICATION_TOOL_CONTRACTS,
...sources.evmContracts.EVM_TOOL_CONTRACTS,
...sources.goContracts.GO_TOOL_CONTRACTS,
...sources.gpuContracts.GPU_TOOL_CONTRACTS,
...sources.il2cppContracts.IL2CPP_TOOL_CONTRACTS,
...sources.modelContracts.MODEL_TOOL_CONTRACTS,
...sources.rustContracts.RUST_TOOL_CONTRACTS,
@@ -268,6 +269,10 @@ export const providerCatalog = (sources) => {
identity: sources.goProvider.GO_BINARY_PROVIDER_IDENTITY,
contracts: sources.goContracts.GO_TOOL_CONTRACTS,
},
{
identity: sources.gpuProvider.GPU_CODE_OBJECTS_PROVIDER_IDENTITY,
contracts: sources.gpuContracts.GPU_TOOL_CONTRACTS,
},
{
identity: sources.il2cppProvider.IL2CPP_METADATA_PROVIDER_IDENTITY,
contracts: sources.il2cppContracts.IL2CPP_TOOL_CONTRACTS,
+2
View File
@@ -69,6 +69,8 @@ const SOURCE_PATHS = {
evmProvider: "dist/evm/EvmoleRelease.js",
goContracts: "dist/contracts/go/goToolContracts.js",
goProvider: "dist/go/GoBinaryProvider.js",
gpuContracts: "dist/contracts/gpu/gpuToolContracts.js",
gpuProvider: "dist/gpu/GpuCodeObjectsProvider.js",
modelContracts: "dist/contracts/model/modelToolContracts.js",
modelProvider: "dist/model/ModelArtifactProvider.js",
il2cppContracts: "dist/contracts/il2cpp/il2cppToolContracts.js",
+1
View File
@@ -10,6 +10,7 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
"go",
"il2cpp",
"model",
"gpu",
"rust",
"wasm",
"captures",
+1 -1
View File
@@ -17,7 +17,7 @@ export const SOURCE_TEST_PROJECTS = [
group: "source",
include: [
"src/*.test.ts",
"src/{apktool,jeb,adb,artifacts,browser,dotnet,electron,evm,firmware,flutter,pcap,ghidra,go,hopper,ida,inspector,javascript,native,process,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
"src/{apktool,jeb,adb,artifacts,browser,dotnet,electron,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,native,process,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts",
],
},
{
+94 -39
View File
@@ -80,7 +80,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
writeStdout(result.output);
process.exitCode = result.exitCode;
} else {
const { cliConfigurationError, createCli } =
const { cliConfigurationError, closeCliJavaScriptResources, createCli } =
await import("../dist/cli-runtime/cli.js");
const { createCliResultOutput } =
await import("../dist/cli-runtime/cliResultOutput.js");
@@ -94,47 +94,97 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
const cliArgs = normalizeCliArguments(args);
const outputArguments = validateCliOutputArguments(cliArgs);
const configurationError = cliConfigurationError(process.env);
// Doctor is the readiness report for this failure. Other commands still stop
// here so they do not run with an unusable log level.
if (configurationError !== undefined && !isDoctorCommand(cliArgs)) {
process.stderr.write(`${configurationError}\n`);
process.exitCode = 1;
} else if (!outputArguments.ok) {
writeStdout(renderCliOutputArgumentError(outputArguments));
process.exitCode = 1;
} else {
let wroteOutput = false;
const resultOutput = createCliResultOutput(cliArgs, process.stdout);
try {
await createCli(process.env, resultOutput).serve(cliArgs, {
stdout: (output) => {
if (resultOutput?.handled) {
if (resultOutput.failed && !stdoutClosed)
process.stderr.write(output);
return;
}
if (output.length > 0) wroteOutput = true;
writeStdout(output);
},
// A command failure caused by the closed pipe keeps the pipe's status.
exit: (code) => process.exit(stdoutClosed ? SIGPIPE_EXIT_STATUS : code),
});
} catch (cause) {
if (wroteOutput) throw cause;
const recovered = await resultOutput
?.recoverFormattingFailure(cause)
.catch((writeFailure) => {
// Recovery writes outside Incur's command boundary. A reader that
// declined this document keeps the same quiet SIGPIPE outcome.
if (!stdoutClosed) throw writeFailure;
return true;
let dispatchFailure;
// Incur can exit from the callback below before this function returns.
// That callback owns the status, including a closed-pipe exit of 141.
let exiting = false;
try {
// Doctor is the readiness report for this failure. Other commands still stop
// here so they do not run with an unusable log level.
if (configurationError !== undefined && !isDoctorCommand(cliArgs)) {
process.stderr.write(`${configurationError}\n`);
process.exitCode = 1;
} else if (!outputArguments.ok) {
writeStdout(renderCliOutputArgumentError(outputArguments));
process.exitCode = 1;
} else {
let wroteOutput = false;
const resultOutput = createCliResultOutput(cliArgs, process.stdout);
try {
await createCli(process.env, resultOutput).serve(cliArgs, {
stdout: (output) => {
if (resultOutput?.handled) {
if (resultOutput.failed && !stdoutClosed)
process.stderr.write(output);
return;
}
if (output.length > 0) wroteOutput = true;
writeStdout(output);
},
// A command failure caused by the closed pipe keeps the pipe's status.
// Worker shutdown is shared with the dispatch finally below. A failed
// shutdown is reported here because this callback is what exits.
exit: (code) => {
exiting = true;
const status = stdoutClosed ? SIGPIPE_EXIT_STATUS : code;
void Promise.resolve()
.then(() => closeCliJavaScriptResources())
.then(
() => {
process.exit(status);
},
(cleanup) => {
process.stderr.write(
`Failed to close the JavaScript analysis worker: ${cleanupFailureText(cleanup)}\n`,
);
process.exit(
stdoutClosed
? SIGPIPE_EXIT_STATUS
: status === 0
? 1
: status,
);
},
);
},
});
if (!recovered) throw cause;
process.exitCode = stdoutClosed ? SIGPIPE_EXIT_STATUS : 1;
} catch (cause) {
if (wroteOutput) throw cause;
const recovered = await resultOutput
?.recoverFormattingFailure(cause)
.catch((writeFailure) => {
// Recovery writes outside Incur's command boundary. A reader that
// declined this document keeps the same quiet SIGPIPE outcome.
if (!stdoutClosed) throw writeFailure;
return true;
});
if (!recovered) throw cause;
process.exitCode = stdoutClosed ? SIGPIPE_EXIT_STATUS : 1;
}
if (!wroteOutput && !resultOutput?.handled)
writeStdout(renderEmptyFilteredCliOutput(cliArgs) ?? "");
}
} catch (cause) {
dispatchFailure = cause;
} finally {
// Serve and output rendering can throw after a worker is parked. Stop it
// on that path too, and keep the dispatch failure when both fail.
try {
await closeCliJavaScriptResources();
} catch (cleanup) {
// The exit callback reports this same shared failure and then exits.
if (!exiting) {
dispatchFailure =
dispatchFailure === undefined
? cleanup
: new AggregateError(
[dispatchFailure, cleanup],
"CLI dispatch failed and the JavaScript analysis worker remained owned",
);
}
}
if (!wroteOutput && !resultOutput?.handled)
writeStdout(renderEmptyFilteredCliOutput(cliArgs) ?? "");
}
if (!exiting && dispatchFailure !== undefined) throw dispatchFailure;
}
/** True when the first positional command is `doctor`, after global output flags. */
@@ -164,6 +214,11 @@ function isDoctorCommand(arguments_) {
* with the status a SIGPIPE-terminated writer reports. MCP stdio keeps its own
* client lifecycle.
*/
function cleanupFailureText(cleanup) {
if (cleanup instanceof Error) return cleanup.stack ?? cleanup.message;
return String(cleanup);
}
function guardClosedStdout() {
process.stdout.on("error", (cause) => {
if (cause?.code === "EPIPE") {
+1 -1
View File
@@ -59,7 +59,7 @@ try {
"scripts/run-exclusive.mjs",
"artifacts",
process.execPath,
resolve("node_modules/turbo/bin/turbo"),
resolve("scripts/run-turbo.mjs"),
"run",
...(plan.needsBuild ? ["artifacts:skills"] : []),
...plan.artifactTasks,
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env node
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import { resolveTypeScriptCompiler } from "./run-typescript.mjs";
// Compute the actual host compiler before Turbo hashes or restores its tasks.
process.env.REA_TYPESCRIPT_COMPILER = resolveTypeScriptCompiler().identity;
const require = createRequire(import.meta.url);
await import(pathToFileURL(require.resolve("turbo/bin/turbo")).href);
+11
View File
@@ -0,0 +1,11 @@
export function selectTypeScriptCompiler(
platform: string,
arch: string,
optionalDependencies: Readonly<Record<string, string>>,
): "tsrs" | "typescript";
export function resolveTypeScriptCompiler(): {
readonly name: "tsrs" | "typescript";
readonly identity: string;
readonly launcher: string;
};
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { availableParallelism } from "node:os";
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
/** Use the pinned package's platform inventory; let its launcher select libc. */
export const selectTypeScriptCompiler = (
platform,
arch,
optionalDependencies,
) => {
if (platform === "win32") return "typescript";
const prefix = `@tsrs/${platform}-${arch}`;
return Object.keys(optionalDependencies).some(
(name) => name === prefix || name.startsWith(`${prefix}-`),
)
? "tsrs"
: "typescript";
};
export const resolveTypeScriptCompiler = () => {
const require = createRequire(import.meta.url);
const rustPackagePath = require.resolve("tsrs/package.json");
const rustPackage = JSON.parse(readFileSync(rustPackagePath, "utf8"));
const compiler = selectTypeScriptCompiler(
process.platform,
process.arch,
rustPackage.optionalDependencies,
);
const packagePath =
compiler === "tsrs"
? rustPackagePath
: require.resolve("typescript/package.json");
const metadata =
compiler === "tsrs"
? rustPackage
: JSON.parse(readFileSync(packagePath, "utf8"));
const command = compiler === "tsrs" ? "tsrs" : "tsc";
return {
name: compiler,
identity: `${compiler}@${metadata.version}`,
launcher: resolve(dirname(packagePath), metadata.bin[command]),
};
};
const main = async () => {
const compiler = resolveTypeScriptCompiler();
if (
process.env.TURBO_HASH !== undefined &&
process.env.REA_TYPESCRIPT_COMPILER !== compiler.identity
)
throw new Error(
"Compiler identity is missing or stale. Run Turbo through scripts/run-turbo.mjs.",
);
if (compiler.name === "tsrs") {
const args = process.argv.slice(2);
// Rust consumes --maxMemory once; duplicating it becomes an unknown option.
// Preserve explicit resource choices rather than injecting another value.
const defaults = [
["--checkers", String(Math.min(2, availableParallelism()))],
["--maxMemory", "3G"],
].flatMap(([option, value]) =>
args.includes(option) ? [] : [option, value],
);
process.argv.splice(2, 0, ...defaults);
}
// Delegate execution, diagnostics, status and signals to the package's launcher.
// An unavailable binary on a supported host must fail rather than hide drift.
await import(pathToFileURL(compiler.launcher).href);
};
if (process.argv[1] === fileURLToPath(import.meta.url)) await main();
+3 -1
View File
@@ -10,6 +10,7 @@ import { promisify } from "node:util";
import {
artifactCli,
artifactMcpResult,
artifactMcpCall,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
@@ -225,7 +226,8 @@ const checkHardenedRuntime = async (directory, binary) => {
assert.equal(denied.code, "capability_unavailable");
assert.match(denied.details.reason, /^debugger-attach-denied: /u);
await withArtifactMcp(blocked, async (client) => {
const deniedMcp = await client.callTool(
const deniedMcp = await artifactMcpCall(
client,
{
name: "observe_native_calls",
arguments: INPUT,
+6
View File
@@ -555,6 +555,12 @@ async function verifyPageScopedTransport(provider, proxy, origin) {
throw new Error(
"Page-scoped CDP disconnect must report transport loss without claiming a target crash",
);
const remaining = await provider.listTargets(
listBrowserTargetsInputSchema.parse({ cdp_endpoint: proxy.endpoint }),
);
if (!remaining.ok) throw remaining.error;
if (!remaining.value.targets.some(({ target_id }) => target_id === target))
throw new Error("Page-scoped CDP disconnect removed its live target");
}
async function browserExecutable() {
+20 -5
View File
@@ -39,6 +39,19 @@ const environment = {
TEMP: root.path,
};
const client = new Client({ name: "signal-recording-verifier", version: "1" });
// Real sigrok refusals of an unknown decoder ID must name the caller's field.
const assertRejectedSelector = (response, operation, field) => {
assert.equal(response.isError, true);
const text = response.content.find((item) => item.type === "text")?.text;
const { error } = JSON.parse(text ?? "{}");
assert.equal(error.code, "invalid_request");
assert.equal(error.details.operation, operation);
assert.deepEqual(
error.details.issues.map((issue) => issue.path),
[[field]],
);
assert.equal(error.details.captured_output.exit_code, 1);
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
@@ -244,16 +257,18 @@ try {
assert.equal(await readFile(pagedPath, "utf8"), pagedSource);
cases++;
}
for (const input of [
{ path, decoders: "no_such_decoder" },
{ path: join(root.path, "absent.vcd"), decoders: "uart" },
{ path, decoders: "uart", driver: "demo" },
for (const [input, rejectedField] of [
[{ path, decoders: "no_such_decoder" }, "decoders"],
[{ path: join(root.path, "absent.vcd"), decoders: "uart" }],
[{ path, decoders: "uart", driver: "demo" }],
]) {
const response = await client.callTool({
name: advertised.name,
arguments: input,
});
assert.equal(response.isError, true);
if (rejectedField !== undefined)
assertRejectedSelector(response, advertised.name, rejectedField);
await client.ping();
cases++;
}
@@ -378,7 +393,7 @@ try {
name: discoveryTool.name,
arguments: { decoder: "rea_no_such_decoder" },
});
assert.equal(unavailable.isError, true);
assertRejectedSelector(unavailable, discoveryTool.name, "decoder");
cases++;
await client.close();
assert.deepEqual((await readdir(root.path)).sort(), [
+32 -1
View File
@@ -369,7 +369,38 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
"-NoProfile",
"-NonInteractive",
"-Command",
"$ErrorActionPreference='Stop'; $p=$env:REA_TEST_PRIVATE_RUNTIME_PATH; $a=Get-Acl -LiteralPath $p; $u=[Security.Principal.WindowsIdentity]::GetCurrent().User; [IO.File]::SetAttributes($p,[IO.FileAttributes]([int][IO.File]::GetAttributes($p) -bor 1)); @{protected=$a.AreAccessRulesProtected; ownerMatchesUser=($a.GetOwner([Security.Principal.SecurityIdentifier]).Value -eq $u.Value); trusteesOnlyUserAndSystem=(@($a.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]) | Where-Object {$_.IdentityReference.Value -ne $u.Value -and $_.IdentityReference.Value -ne 'S-1-5-18'}).Count -eq 0); count=@($a.Access).Count; rootReadonly=(([int][IO.File]::GetAttributes($p) -band 1) -ne 0)} | ConvertTo-Json -Compress",
String.raw`
$ErrorActionPreference = 'Stop'
$clock = [Diagnostics.Stopwatch]::StartNew()
function Trace-AclPhase([string]$phase) {
[Console]::Error.WriteLine('private-runtime-acl: ' + $phase + ' ' + $clock.ElapsedMilliseconds + 'ms')
}
Trace-AclPhase 'started'
$p = $env:REA_TEST_PRIVATE_RUNTIME_PATH
$a = Get-Acl -LiteralPath $p
Trace-AclPhase 'acl-read'
$u = [Security.Principal.WindowsIdentity]::GetCurrent().User
$owner = $a.GetOwner([Security.Principal.SecurityIdentifier])
Trace-AclPhase 'owner-sid-read'
# Guard the independent observer against accidental account-name resolution.
# Access is a PowerShell convenience property that requests NTAccount rules.
Update-TypeData -TypeName ($a.GetType().FullName) -MemberType ScriptProperty -MemberName Access -Value { throw 'ACL observation must use SID rules' } -Force
if ($a.PSObject.Properties['Access'].MemberType -ne [Management.Automation.PSMemberTypes]::ScriptProperty) {
throw 'ACL observation Access guard was not installed'
}
$rules = @($a.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier]))
Trace-AclPhase 'rule-sids-read'
[IO.File]::SetAttributes($p, [IO.FileAttributes]([int][IO.File]::GetAttributes($p) -bor 1))
Trace-AclPhase 'readonly-set'
@{
protected = $a.AreAccessRulesProtected
ownerMatchesUser = ($owner.Value -eq $u.Value)
trusteesOnlyUserAndSystem = (@($rules | Where-Object { $_.IdentityReference.Value -ne $u.Value -and $_.IdentityReference.Value -ne 'S-1-5-18' }).Count -eq 0)
count = $rules.Count
rootReadonly = (([int][IO.File]::GetAttributes($p) -band 1) -ne 0)
} | ConvertTo-Json -Compress
Trace-AclPhase 'complete'
`,
],
{
encoding: "utf8",
+2 -1
View File
@@ -9,6 +9,7 @@ import { promisify } from "node:util";
import {
artifactCli,
artifactMcpResult,
artifactMcpCall,
withArtifactMcp,
withBundleResourcesMcp,
} from "../../lib/artifact-e2e.mjs";
@@ -97,7 +98,7 @@ try {
offset: first.next_offset,
});
assert.deepEqual([...first.records, ...next.records], cli.records);
const invalid = await client.callTool({
const invalid = await artifactMcpCall(client, {
name: "inspect_asset_catalog",
arguments: { offset: -1 },
});
+2 -1
View File
@@ -9,6 +9,7 @@ import { createHash } from "node:crypto";
import {
artifactCli,
artifactMcpResult,
artifactMcpCall,
withArtifactMcp,
} from "../../lib/artifact-e2e.mjs";
import {
@@ -53,7 +54,7 @@ try {
});
assert.deepEqual(page.objects, [graph.objects[1]]);
assert.equal(page.next_offset, 2);
const invalid = await client.callTool({
const invalid = await artifactMcpCall(client, {
name: "inspect_keyed_archive",
arguments: { path: "../other.plist" },
});
+2 -1
View File
@@ -5,6 +5,7 @@ import { relative, sep } from "node:path";
import { parseArtifactInventoryEvidence } from "../../../dist/domain/artifactInventoryEvidence.js";
import {
artifactCliEvidence,
artifactMcpCall,
withArtifactMcp,
} from "../../lib/artifact-e2e.mjs";
@@ -53,7 +54,7 @@ export async function verifyMachOOccurrences(app, executable) {
assert.equal(node.sha256, direct.manifest.root_sha256);
assert.equal(node.format, root.artifact_format);
await withArtifactMcp(app, async (client) => {
const reply = await client.callTool({
const reply = await artifactMcpCall(client, {
name: "inspect_artifact",
arguments: {},
});
+2 -1
View File
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
import {
artifactCliEvidence,
artifactMcpResult,
artifactMcpCall,
withArtifactMcp,
} from "../../lib/artifact-e2e.mjs";
@@ -180,7 +181,7 @@ export async function verifyDylibResolution(app) {
await artifactMcpResult(client, "trace_dylib_resolution"),
trace,
);
const invalid = await client.callTool({
const invalid = await artifactMcpCall(client, {
name: "trace_dylib_resolution",
arguments: { roots: ["../outside"] },
});
+10 -15
View File
@@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises";
import { toolContract } from "../../../dist/contracts/toolContracts.js";
import {
artifactCliEvidence,
artifactMcpCall,
withArtifactMcp,
withBundleResourcesMcp,
} from "../../lib/artifact-e2e.mjs";
@@ -27,7 +28,7 @@ export async function verifyNativeProvenance(app, executable) {
assert.equal(resourcePlistCli.subject, null);
assert.equal(resourcePlistCli.raw_result, null);
await withBundleResourcesMcp(app, async (client) => {
const response = await client.callTool({
const response = await artifactMcpCall(client, {
name: "inspect_plist",
arguments: {},
});
@@ -58,13 +59,10 @@ export async function verifyNativeProvenance(app, executable) {
assert.equal(binaryPlistCli.raw_result, null);
assert.equal(binaryPlistCli.normalized_result.resource_root, undefined);
await withArtifactMcp(app, async (client) => {
const definitions = new Map(
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
);
const plistResponse = await client.callTool(
{ name: "inspect_plist", arguments: {} },
{ toolDefinition: definitions.get("inspect_plist") },
);
const plistResponse = await artifactMcpCall(client, {
name: "inspect_plist",
arguments: {},
});
assert.notEqual(plistResponse.isError, true, JSON.stringify(plistResponse));
const binaryPlistMcp = toolContract("inspect_plist").outputSchema.parse(
plistResponse.structuredContent,
@@ -98,13 +96,10 @@ export async function verifyNativeProvenance(app, executable) {
arguments_,
),
);
const response = await client.callTool(
{
name: operation,
arguments: operation === "demangle_swift" ? { symbols } : {},
},
{ toolDefinition: definitions.get(operation) },
);
const response = await artifactMcpCall(client, {
name: operation,
arguments: operation === "demangle_swift" ? { symbols } : {},
});
assert.notEqual(response.isError, true, JSON.stringify(response));
const mcp = toolContract(operation).outputSchema.parse(
response.structuredContent,
+34 -16
View File
@@ -18,7 +18,7 @@ describe("parseAdbDevicesOutput", () => {
].join("\n");
const { devices, unparsedLines, daemonStarted } =
parseAdbDevicesOutput(output);
expect(unparsedLines).toEqual([]);
expect(unparsedLines).toEqual({ sample: [], total: 0 });
expect(daemonStarted).toBe(false);
expect(devices).toEqual([
{
@@ -88,7 +88,7 @@ describe("ADB daemon startup notices", () => {
stderr,
);
expect(daemonStarted).toBe(true);
expect(unparsedLines).toEqual([]);
expect(unparsedLines).toEqual({ sample: [], total: 0 });
expect(devices).toHaveLength(1);
});
@@ -98,7 +98,7 @@ describe("ADB daemon startup notices", () => {
"* daemon started successfully *\r\n",
);
expect(daemonStarted).toBe(true);
expect(unparsedLines).toEqual([]);
expect(unparsedLines).toEqual({ sample: [], total: 0 });
});
it("does not treat a daemon startup error containing the success phrase as success", () => {
@@ -107,7 +107,7 @@ describe("ADB daemon startup notices", () => {
"* daemon not started successfully\r\n",
);
expect(daemonStarted).toBe(false);
expect(unparsedLines).toEqual([]);
expect(unparsedLines).toEqual({ sample: [], total: 0 });
});
it("ignores non-startup lines and device strings on stderr", () => {
@@ -126,7 +126,7 @@ describe("ADB daemon startup notices", () => {
stderr,
);
expect(daemonStarted).toBe(false);
expect(unparsedLines).toEqual([]);
expect(unparsedLines).toEqual({ sample: [], total: 0 });
expect(devices).toHaveLength(1);
expect(devices[0]?.serial).toBe("emulator-5554");
});
@@ -158,11 +158,14 @@ describe("ADB device states", () => {
expect(devices.map(({ serial, state }) => [serial, state])).toEqual([
["serial-no-permission", "no permissions"],
]);
expect(unparsedLines).toEqual([
"serial-bad-device deviceXYZ ",
"serial-bad-offline offline-ish",
"serial-bad-auth unauthorizedXYZ",
]);
expect(unparsedLines).toEqual({
sample: [
"serial-bad-device deviceXYZ ",
"serial-bad-offline offline-ish",
"serial-bad-auth unauthorizedXYZ",
],
total: 3,
});
});
it("returns unrecognized lines instead of dropping them", () => {
@@ -172,7 +175,10 @@ describe("ADB device states", () => {
"",
].join("\n");
const { unparsedLines } = parseAdbDevicesOutput(output);
expect(unparsedLines).toEqual(["this is not a device line"]);
expect(unparsedLines).toEqual({
sample: ["this is not a device line"],
total: 1,
});
});
it("reports a non-numeric transport_id as unknown", () => {
@@ -259,7 +265,7 @@ describe("parsePackageListOutput", () => {
"",
].join("\n");
const { packages, unparsedLines } = parsePackageListOutput(output);
expect(unparsedLines).toEqual([]);
expect(unparsedLines).toEqual({ sample: [], total: 0 });
expect(packages).toEqual([
{
package_name: "com.example",
@@ -278,7 +284,19 @@ describe("parsePackageListOutput", () => {
"package:/data/app/x/base.apk=com.ok\n garbage line \n",
);
expect(packages).toHaveLength(1);
expect(unparsedLines).toEqual([" garbage line "]);
expect(unparsedLines).toEqual({ sample: [" garbage line "], total: 1 });
});
it("samples the first ten unrecognized lines and counts every one", () => {
const rows = Array.from(
{ length: 11 },
(_, index) => `row ${String(index + 1)}`,
);
const { packages, unparsedLines } = parsePackageListOutput(
["package:/data/app/x/base.apk=com.ok", ...rows].join("\n"),
);
expect(packages).toHaveLength(1);
expect(unparsedLines).toEqual({ sample: rows.slice(0, 10), total: 11 });
});
});
@@ -291,7 +309,7 @@ describe("parsePmPathOutput", () => {
"",
].join("\n");
const { paths, unparsedLines } = parsePmPathOutput(output);
expect(unparsedLines).toEqual([]);
expect(unparsedLines).toEqual({ sample: [], total: 0 });
expect(paths).toEqual([
{
device_path: "/data/app/~~a==/com.example.b==/base.apk",
@@ -334,7 +352,7 @@ describe("parsePmPathOutput", () => {
},
]);
expect(rejectedPaths).toEqual([]);
expect(unparsedLines).toEqual([]);
expect(unparsedLines).toEqual({ sample: [], total: 0 });
});
it("rejects an empty final component without normalizing the source", () => {
@@ -349,6 +367,6 @@ describe("parsePmPathOutput", () => {
it("retains malformed lines including surrounding whitespace", () => {
expect(
parsePmPathOutput(" package:/data/app/pkg/base.apk \n").unparsedLines,
).toEqual([" package:/data/app/pkg/base.apk "]);
).toEqual({ sample: [" package:/data/app/pkg/base.apk "], total: 1 });
});
});
+20 -16
View File
@@ -2,14 +2,18 @@
* Parsers for the exact adb and Package Manager output formats REA consumes.
*
* Each parser preserves what the tool actually printed: unparseable lines are
* retained rather than silently dropped, and derived classifications carry
* their evidence basis.
* sampled and counted rather than silently dropped, and derived
* classifications carry their evidence basis.
*/
import {
adbHostFileName,
type AdbHostFileNameReason,
} from "./AdbHostFileName.js";
import {
collectUnparsedLines,
type AdbUnparsedLines,
} from "./AdbUnparsedLines.js";
import {
ADB_DEVICE_STATES,
type AdbDeviceState,
@@ -44,18 +48,18 @@ const metadataValue = (remainder: string, key: string): string | null => {
/**
* Parse `adb devices -l` device lines. The banner, blank lines, and the
* daemon startup notices adb prints on first use are recognized separately;
* every other unrecognized line is returned for honest reporting.
* every other unrecognized line is sampled and counted for honest reporting.
*/
export const parseAdbDevicesOutput = (
stdout: string,
stderr = "",
): {
devices: readonly AdbDeviceEntry[];
unparsedLines: readonly string[];
unparsedLines: AdbUnparsedLines;
daemonStarted: boolean;
} => {
const devices: AdbDeviceEntry[] = [];
const unparsedLines: string[] = [];
const unparsed = collectUnparsedLines();
let daemonStarted = false;
for (const rawLine of stdout.split(/\r?\n/u)) {
const line = normalizeAdbOutputLine(rawLine);
@@ -74,7 +78,7 @@ export const parseAdbDevicesOutput = (
return next === undefined || /\s/u.test(next);
});
if (match === null || state === undefined) {
if (rawLine.trim() !== "") unparsedLines.push(rawLine);
if (rawLine.trim() !== "") unparsed.add(rawLine);
continue;
}
const serial = match[1]!;
@@ -122,7 +126,7 @@ export const parseAdbDevicesOutput = (
}
}
}
return { devices, unparsedLines, daemonStarted };
return { devices, unparsedLines: unparsed.facts(), daemonStarted };
};
/**
@@ -184,21 +188,21 @@ export interface AdbPackageEntry {
const PACKAGE_LINE = /^package:(.+)=([^\s=]+)$/u;
/** Parse `pm list packages -f` lines, retaining unrecognized lines. */
/** Parse `pm list packages -f` lines, sampling and counting unrecognized lines. */
export const parsePackageListOutput = (
stdout: string,
): {
packages: readonly AdbPackageEntry[];
unparsedLines: readonly string[];
unparsedLines: AdbUnparsedLines;
} => {
const packages: AdbPackageEntry[] = [];
const unparsedLines: string[] = [];
const unparsed = collectUnparsedLines();
for (const rawLine of stdout.split(/\r?\n/u)) {
const line = rawLine.trim();
if (line === "") continue;
const match = PACKAGE_LINE.exec(line);
if (match === null) {
unparsedLines.push(rawLine);
unparsed.add(rawLine);
continue;
}
packages.push({
@@ -206,7 +210,7 @@ export const parsePackageListOutput = (
base_apk_device_path: match[1]!,
});
}
return { packages, unparsedLines };
return { packages, unparsedLines: unparsed.facts() };
};
/** Quote raw device paths for prose without allowing bidi or invisible text. */
@@ -248,19 +252,19 @@ export const parsePmPathOutput = (
device_path: string;
reason: AdbHostFileNameReason;
}[];
unparsedLines: readonly string[];
unparsedLines: AdbUnparsedLines;
} => {
const paths: AdbPackagePathEntry[] = [];
const rejectedPaths: {
device_path: string;
reason: AdbHostFileNameReason;
}[] = [];
const unparsedLines: string[] = [];
const unparsed = collectUnparsedLines();
for (const rawLine of stdout.split(/\r?\n/u)) {
if (rawLine === "") continue;
const match = /^package:(.*)$/u.exec(rawLine);
if (match === null) {
unparsedLines.push(rawLine);
unparsed.add(rawLine);
continue;
}
const devicePath = match[1]!;
@@ -284,5 +288,5 @@ export const parsePmPathOutput = (
: "unknown",
});
}
return { paths, rejectedPaths, unparsedLines };
return { paths, rejectedPaths, unparsedLines: unparsed.facts() };
};
+11 -6
View File
@@ -1,5 +1,10 @@
import { isUtf8 } from "node:buffer";
import {
collectUnparsedLines,
type AdbUnparsedLines,
} from "./AdbUnparsedLines.js";
interface AdbDirectoryEntry {
readonly name: string;
readonly kind: "directory" | "file" | "symlink" | "other";
@@ -71,35 +76,35 @@ export const parseAdbDirectoryListing = (
stdout: Buffer,
): {
entries: readonly AdbDirectoryEntry[];
unparsedLines: readonly string[];
unparsedLines: AdbUnparsedLines;
} => {
const entries: AdbDirectoryEntry[] = [];
const unparsedLines: string[] = [];
const unparsed = collectUnparsedLines();
let offset = 0;
while (offset < stdout.length) {
const metadataEnd = stdout.indexOf(0, offset);
const nameEnd = metadataEnd < 0 ? -1 : stdout.indexOf(0, metadataEnd + 1);
const targetEnd = nameEnd < 0 ? -1 : stdout.indexOf(0, nameEnd + 1);
if (targetEnd < 0) {
unparsedLines.push(describeUnparsedRecord(stdout.subarray(offset)));
unparsed.add(describeUnparsedRecord(stdout.subarray(offset)));
break;
}
const rawRecord = stdout.subarray(offset, targetEnd + 1);
if (!isUtf8(rawRecord)) {
unparsedLines.push(describeUnparsedRecord(rawRecord));
unparsed.add(describeUnparsedRecord(rawRecord));
} else {
const entry = parseEntry(
stdout.toString("utf8", offset, metadataEnd),
stdout.toString("utf8", metadataEnd + 1, nameEnd),
stdout.toString("utf8", nameEnd + 1, targetEnd),
);
if (entry === null) unparsedLines.push(rawRecord.toString("utf8"));
if (entry === null) unparsed.add(rawRecord.toString("utf8"));
else entries.push(entry);
}
offset = targetEnd + 1;
}
return { entries, unparsedLines };
return { entries, unparsedLines: unparsed.facts() };
};
const describeUnparsedRecord = (record: Buffer): string =>
+70 -25
View File
@@ -1049,34 +1049,78 @@ exit 0`,
},
);
it("pushes a file and verifies the device-side digest", async () => {
const { binary, root } = await writeAdbStub(
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
const PUSHED_DIGEST =
"8748b5d17d1ebcec7d46674fa6a8b92c0f95598a552f27c1c8a132eb58c04043";
it.each([
{
answer: "a digest line",
stdout: `${PUSHED_DIGEST} /data/local/tmp/payload.bin\n`,
exit: 0,
verified: true,
},
{
// GNU sha256sum prefixes a line whose echoed name it escaped.
answer: "an escaped digest line",
stdout: `\\${PUSHED_DIGEST} /data/local/tmp/pay\\\\load.bin\n`,
exit: 0,
verified: true,
},
{ answer: "empty output", stdout: "", exit: 0, verified: false },
{
answer: "a malformed line",
stdout: `${PUSHED_DIGEST.slice(1)} /data/local/tmp/payload.bin\n`,
exit: 0,
verified: false,
},
{ answer: "a failed command", stdout: "", exit: 127, verified: false },
])(
"derives the push digest source from $answer",
async ({ stdout, exit, verified }) => {
const { binary, root } = await writeAdbStub(
(stubRoot) => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
if [ "$1" = "-s" ] && [ "$4" = "test" ]; then exit 1; fi
if [ "$1" = "-s" ] && [ "$3" = "push" ]; then exit 0; fi
if [ "$1" = "-s" ] && [ "$4" = "sha256sum" ]; then printf '%s %s\\n' "8748b5d17d1ebcec7d46674fa6a8b92c0f95598a552f27c1c8a132eb58c04043" "$5"; exit 0; fi
if [ "$1" = "-s" ] && [ "$4" = "sha256sum" ]; then cat "${join(stubRoot, "sha256sum.out")}"; exit ${exit}; fi
exit 1`,
);
const localFile = join(root, "payload.bin");
await writeFile(localFile, "push-payload");
const digest = createHash("sha256").update("push-payload").digest("hex");
const pushed = await evidenceResult(providerOver(binary), {
operation: "push_adb_file",
input: {
serial: "emulator-5554",
local_path: localFile,
device_path: "/data/local/tmp/payload.bin",
overwrite: false,
},
});
expect(pushed).toMatchObject({
bytes: 12,
sha256: digest,
device_sha256: digest,
overwritten: false,
device_digest_source: "device_sha256sum",
});
});
);
await writeFile(join(root, "sha256sum.out"), stdout);
const localFile = join(root, "payload.bin");
await writeFile(localFile, "push-payload");
const result = await providerOver(binary).execute({
operation: "push_adb_file",
input: {
serial: "emulator-5554",
local_path: localFile,
device_path: "/data/local/tmp/payload.bin",
overwrite: false,
},
});
if (!result.ok) throw new Error(result.error.message);
expect(result.value.result).toMatchObject({
bytes: 12,
sha256: PUSHED_DIGEST,
overwritten: false,
...(verified
? {
device_sha256: PUSHED_DIGEST,
device_digest_source: "device_sha256sum",
}
: { device_sha256: null, device_digest_source: "unavailable" }),
});
expect(
result.value.limitations.some((line) =>
line.includes("not device-side verified"),
),
).toBe(!verified);
// Unrecognized checksum output stays inspectable instead of being dropped.
expect(result.value.rawResult).toEqual(
!verified && exit === 0
? { device_sha256sum: { stdout, stderr: "" } }
: null,
);
},
);
it("pushes the same private bytes that its reported digest identifies", async () => {
const { binary, root } = await writeAdbStub(
@@ -1617,6 +1661,7 @@ it("resolves packages by case-insensitive substring", async () => {
},
]);
expect(result.exact_match).toBe(false);
expect(result.unparsed_lines).toEqual([]);
expect(result.coverage).toBe("complete");
});
+115 -86
View File
@@ -117,10 +117,12 @@ import {
parseResolveActivityOutput,
parseServiceListOutput,
parseSettingsGetOutput,
parseSha256sumOutput,
parseWindowFocusOutput,
parseWmDensityOutput,
parseWmSizeOutput,
} from "./AdbShellOutput.js";
import type { AdbUnparsedLines } from "./AdbUnparsedLines.js";
/** Stable identity for the ADB device-acquisition provider. */
export const ADB_PROVIDER_IDENTITY = {
@@ -152,7 +154,6 @@ const SCREEN_TIMEOUT_MS = 60_000;
const SCREEN_MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
const BUGREPORT_TIMEOUT_MS = 600_000;
const BUGREPORT_MAX_OUTPUT_BYTES = 1024 * 1024;
const UNPARSED_LINES_REPORTED = 10;
/** Carry the exact digest owner through caller-specific partial-result mapping. */
class AdbDigestFailure extends Error {
@@ -272,6 +273,21 @@ const adbDigestError = (
type AdbFileDigest = Awaited<ReturnType<typeof hashStableFile>>;
/**
* A pushed file's device-side digest together with its source, so a receipt
* cannot name `sha256sum` as the source of a digest that was never parsed.
*/
type AdbDeviceDigest =
| {
readonly device_sha256: string;
readonly device_digest_source: "device_sha256sum";
}
| {
readonly device_sha256: null;
readonly device_digest_source: "unavailable";
readonly reason: string;
};
const partialObservationWithDigest = (
observation:
| AdbAcquiredFilePartialObservation
@@ -381,23 +397,35 @@ const clientIdentityResult = (
type ClientIdentity = ReturnType<typeof clientIdentityResult>;
type ProviderIdentity = { id: string; name: string; version: string | null };
const reported = (
unparsedLines: readonly string[],
coverage: "complete" | "partial",
) => ({
unparsed_lines: unparsedLines.slice(0, UNPARSED_LINES_REPORTED),
coverage:
coverage === "partial" || unparsedLines.length > 0 ? "partial" : "complete",
});
const unparsedOverflowLimitation = (
unparsedLines: readonly string[],
): string[] =>
unparsedLines.length > UNPARSED_LINES_REPORTED
? [
`unparsed_lines reports the first ${String(UNPARSED_LINES_REPORTED)} of ${String(unparsedLines.length)} unrecognized lines`,
]
: [];
/**
* Build a parsed-listing execution whose `unparsed_lines`, coverage, and
* truncation limitation all derive from the parser's unrecognized-line facts,
* so the limitation can never describe a field the result does not carry.
*/
const parsedListingExecution = (
result: Readonly<Record<string, unknown>>,
unparsed: AdbUnparsedLines,
provider: ProviderIdentity,
limitations: readonly string[] = [],
) =>
createAnalysisExecution(
{
...result,
unparsed_lines: unparsed.sample,
coverage: unparsed.total > 0 ? "partial" : "complete",
},
provider,
{
limitations: [
...limitations,
...(unparsed.total > unparsed.sample.length
? [
`unparsed_lines reports the first ${String(unparsed.sample.length)} of ${String(unparsed.total)} unrecognized lines`,
]
: []),
],
},
);
/** Materialize the caller's output directory, refusing existing files inside. */
const prepareOutputDirectory = async (
@@ -1355,10 +1383,10 @@ export class AdbProvider {
ran.value.stdout,
ran.value.stderr,
);
if (unparsedLines.length > 0)
if (unparsedLines.total > 0)
return err(
new AnalysisProtocolError(
`adb devices reported an unrecognized line: ${unparsedLines[0]}`,
`adb devices reported an unrecognized line: ${unparsedLines.sample[0]!}`,
),
);
return ok(
@@ -1435,21 +1463,18 @@ export class AdbProvider {
ran.value.stdout,
);
return ok(
createAnalysisExecution(
parsedListingExecution(
{
client,
serial: request.input.serial,
scope: request.input.scope,
packages,
...reported(unparsedLines, "complete"),
},
unparsedLines,
provider,
{
limitations: [
"base_apk_device_path is a device filesystem path, not a host-local path",
...unparsedOverflowLimitation(unparsedLines),
],
},
[
"base_apk_device_path is a device filesystem path, not a host-local path",
],
),
);
}
@@ -1603,16 +1628,10 @@ export class AdbProvider {
ran.value.stdout,
);
return ok(
createAnalysisExecution(
{
client,
serial: request.input.serial,
columns,
processes,
...reported(unparsedLines, "complete"),
},
parsedListingExecution(
{ client, serial: request.input.serial, columns, processes },
unparsedLines,
provider,
{ limitations: unparsedOverflowLimitation(unparsedLines) },
),
);
}
@@ -1642,21 +1661,18 @@ export class AdbProvider {
ran.value.stdout,
);
return ok(
createAnalysisExecution(
parsedListingExecution(
{
client,
serial: request.input.serial,
device_path: request.input.device_path,
entries,
...reported(unparsedLines, "complete"),
},
unparsedLines,
provider,
{
limitations: [
"Entry kinds derive from stat permission mode; directory contents and link targets may change during capture",
...unparsedOverflowLimitation(unparsedLines),
],
},
[
"Entry kinds derive from stat permission mode; directory contents and link targets may change during capture",
],
),
);
}
@@ -1670,15 +1686,10 @@ export class AdbProvider {
ran.value.stdout,
);
return ok(
createAnalysisExecution(
{
client,
serial: request.input.serial,
features,
...reported(unparsedLines, "complete"),
},
parsedListingExecution(
{ client, serial: request.input.serial, features },
unparsedLines,
provider,
{ limitations: unparsedOverflowLimitation(unparsedLines) },
),
);
}
@@ -1692,15 +1703,10 @@ export class AdbProvider {
ran.value.stdout,
);
return ok(
createAnalysisExecution(
{
client,
serial: request.input.serial,
services,
...reported(unparsedLines, "complete"),
},
parsedListingExecution(
{ client, serial: request.input.serial, services },
unparsedLines,
provider,
{ limitations: unparsedOverflowLimitation(unparsedLines) },
),
);
}
@@ -1830,7 +1836,7 @@ export class AdbProvider {
entry.package_name.toLowerCase().includes(needle),
);
return ok(
createAnalysisExecution(
parsedListingExecution(
{
client,
serial: request.input.serial,
@@ -1839,15 +1845,12 @@ export class AdbProvider {
exact_match:
matches.length === 1 &&
matches[0]!.package_name.toLowerCase() === needle,
coverage: unparsedLines.length > 0 ? "partial" : "complete",
},
unparsedLines,
provider,
{
limitations: [
"Matching is a case-insensitive substring over package names; application display labels are not part of pm output",
...unparsedOverflowLimitation(unparsedLines),
],
},
[
"Matching is a case-insensitive substring over package names; application display labels are not part of pm output",
],
),
);
}
@@ -1952,7 +1955,7 @@ export class AdbProvider {
}),
);
}
if (parsed.paths.length === 0 && parsed.unparsedLines.length === 0)
if (parsed.paths.length === 0 && parsed.unparsedLines.total === 0)
return err(
new AnalysisUnsupportedTargetError(
operation,
@@ -1960,10 +1963,10 @@ export class AdbProvider {
`Package Manager reported no APK paths for ${packageName}; it may not be installed for user 0`,
),
);
if (parsed.unparsedLines.length > 0)
if (parsed.unparsedLines.total > 0)
return err(
new AnalysisProtocolError(
`pm path reported an unrecognized line: ${parsed.unparsedLines[0]}`,
`pm path reported an unrecognized line: ${parsed.unparsedLines.sample[0]!}`,
{ capturedOutput },
),
);
@@ -2449,9 +2452,13 @@ export class AdbProvider {
);
if (!pushed.ok) return pushed;
const receipt = (
deviceSha256: string | null,
source: "device_sha256sum" | "unavailable",
extra: readonly string[] = [],
deviceDigest: AdbDeviceDigest,
rawResult?: {
readonly device_sha256sum: {
readonly stdout: string;
readonly stderr: string;
};
},
) =>
createAnalysisExecution(
{
@@ -2461,15 +2468,18 @@ export class AdbProvider {
device_path: input.device_path,
bytes: digest.bytes,
sha256: digest.sha256,
device_sha256: deviceSha256,
device_sha256: deviceDigest.device_sha256,
overwritten: deviceExists,
device_digest_source: source,
device_digest_source: deviceDigest.device_digest_source,
},
provider,
{
...(rawResult === undefined ? {} : { rawResult }),
limitations: [
"Push writes a private snapshot of the caller's file to the device",
...extra,
...(deviceDigest.device_digest_source === "unavailable"
? [deviceDigest.reason]
: []),
],
},
);
@@ -2484,11 +2494,13 @@ export class AdbProvider {
if (!deviceDigest.ok) {
const cancelled =
deviceDigest.error instanceof AnalysisCancelledError;
const completed = receipt(null, "unavailable", [
cancelled
const completed = receipt({
device_sha256: null,
device_digest_source: "unavailable",
reason: cancelled
? "The device-side digest check was cancelled after push completed"
: "The device did not answer sha256sum, so the transfer is not device-side verified",
]);
});
if (cancelled)
deviceDigest.error.retainPartialObservation(
createEvidence(
@@ -2507,11 +2519,28 @@ export class AdbProvider {
);
return cancelled ? err(deviceDigest.error) : ok(completed);
}
const digestMatch = /^([a-f0-9]{64})\s/u.exec(
deviceDigest.value.stdout,
);
const completed = receipt(digestMatch?.[1] ?? null, "device_sha256sum");
if (digestMatch !== null && digestMatch[1] !== digest.sha256)
const parsedDigest = parseSha256sumOutput(deviceDigest.value.stdout);
const completed =
parsedDigest === null
? receipt(
{
device_sha256: null,
device_digest_source: "unavailable",
reason:
"The device's sha256sum output contained no recognizable digest, so the transfer is not device-side verified",
},
{
device_sha256sum: {
stdout: deviceDigest.value.stdout,
stderr: deviceDigest.value.stderr,
},
},
)
: receipt({
device_sha256: parsedDigest,
device_digest_source: "device_sha256sum",
});
if (parsedDigest !== null && parsedDigest !== digest.sha256)
return err(
new AnalysisArtifactChangedError(
operation,
+26 -14
View File
@@ -2,13 +2,17 @@ import type {
AdbWindowFocusContext,
AdbWindowFocusObservation,
} from "../domain/adb/adbDeviceAnalysis.js";
import {
collectUnparsedLines,
type AdbUnparsedLines,
} from "./AdbUnparsedLines.js";
/**
* Parsers for fixed observation commands REA runs through `adb shell`.
*
* Formats follow toybox output on Android 8+ (verified against an Android 14
* emulator). Unparseable lines are retained rather than dropped so callers
* can see the format drift instead of silently losing rows.
* emulator). Unparseable lines are sampled and counted rather than dropped so
* callers can see the format drift instead of silently losing rows.
*/
export interface AdbProcessEntry {
@@ -29,10 +33,10 @@ export const parseProcessListOutput = (
): {
columns: string | null;
processes: readonly AdbProcessEntry[];
unparsedLines: readonly string[];
unparsedLines: AdbUnparsedLines;
} => {
const processes: AdbProcessEntry[] = [];
const unparsedLines: string[] = [];
const unparsed = collectUnparsedLines();
let columns: string | null = null;
for (const rawLine of stdout.split(/\r?\n/u)) {
const line = rawLine.trimEnd();
@@ -43,7 +47,7 @@ export const parseProcessListOutput = (
}
const match = PROCESS_LINE.exec(line);
if (match === null) {
unparsedLines.push(line);
unparsed.add(line);
continue;
}
processes.push({
@@ -55,7 +59,7 @@ export const parseProcessListOutput = (
name: match[9]!.trim(),
});
}
return { columns, processes, unparsedLines };
return { columns, processes, unparsedLines: unparsed.facts() };
};
export interface AdbFeatureEntry {
@@ -73,16 +77,16 @@ export const parseFeatureListOutput = (
stdout: string,
): {
features: readonly AdbFeatureEntry[];
unparsedLines: readonly string[];
unparsedLines: AdbUnparsedLines;
} => {
const features: AdbFeatureEntry[] = [];
const unparsedLines: string[] = [];
const unparsed = collectUnparsedLines();
for (const rawLine of stdout.split(/\r?\n/u)) {
const line = rawLine.trim();
if (line === "") continue;
const match = FEATURE_LINE.exec(line);
if (match === null) {
unparsedLines.push(line);
unparsed.add(line);
continue;
}
features.push({
@@ -90,7 +94,7 @@ export const parseFeatureListOutput = (
version: match[2] === undefined ? null : parseFeatureVersion(match[2]!),
});
}
return { features, unparsedLines };
return { features, unparsedLines: unparsed.facts() };
};
export interface AdbServiceEntry {
@@ -105,17 +109,17 @@ export const parseServiceListOutput = (
stdout: string,
): {
services: readonly AdbServiceEntry[];
unparsedLines: readonly string[];
unparsedLines: AdbUnparsedLines;
} => {
const services: AdbServiceEntry[] = [];
const unparsedLines: string[] = [];
const unparsed = collectUnparsedLines();
for (const rawLine of stdout.split(/\r?\n/u)) {
const line = rawLine.trimEnd();
if (line.trim() === "" || /^Found \d+ services:/u.test(line.trim()))
continue;
const match = SERVICE_LINE.exec(line);
if (match === null) {
if (line.trim() !== "") unparsedLines.push(line.trim());
if (line.trim() !== "") unparsed.add(line.trim());
continue;
}
services.push({
@@ -123,7 +127,7 @@ export const parseServiceListOutput = (
interface: match[2] === "" ? null : match[2]!,
});
}
return { services, unparsedLines };
return { services, unparsedLines: unparsed.facts() };
};
const SIZE_PAIR = /^Physical size:\s+(\d+)x(\d+)$/u;
@@ -377,6 +381,14 @@ export const parseBugreportCopiedPath = (stdout: string): string | null => {
return match === null ? null : match[1]!;
};
/**
* Read the digest from one `sha256sum FILE` line, or null when the output
* names none. GNU sha256sum starts the line with a backslash when it escaped
* the echoed name; the digest itself is never escaped.
*/
export const parseSha256sumOutput = (stdout: string): string | null =>
/^\\?([a-f0-9]{64}) [ *]/u.exec(stdout)?.[1] ?? null;
/** Read the device's `Failure [REASON]` verdict from package-change output. */
export const packageChangeFailureReason = (output: string): string | null =>
/Failure \[([^\]]+)\]/u.exec(output)?.[1] ?? null;
+27
View File
@@ -0,0 +1,27 @@
/** Most unrecognized output lines a parser retains as source evidence. */
const UNPARSED_LINE_SAMPLE_LIMIT = 10;
/**
* Unrecognized output lines one parser observed: the first lines verbatim, up
* to {@link UNPARSED_LINE_SAMPLE_LIMIT}, and the exact number observed.
*/
export interface AdbUnparsedLines {
readonly sample: readonly string[];
readonly total: number;
}
/** Accumulate unrecognized lines without retaining more than the sample. */
export const collectUnparsedLines = (): {
add: (line: string) => void;
facts: () => AdbUnparsedLines;
} => {
const sample: string[] = [];
let total = 0;
return {
add: (line) => {
total += 1;
if (sample.length < UNPARSED_LINE_SAMPLE_LIMIT) sample.push(line);
},
facts: () => ({ sample, total }),
};
};
+10 -1
View File
@@ -544,13 +544,22 @@ const resolveJar = async (
environment: Readonly<Record<string, string | undefined>>,
): Promise<string> => {
const jar = environment.REA_JADX_MCP_JAR;
if (jar === undefined || !isAbsolute(jar))
if (jar === undefined)
throw configurationFailure(
"not_configured",
JADX_JAR_CONFIGURATION_REMEDIATION,
{ jar_configured: false },
"jar",
);
// A supplied selection stays configured even when rejected, so callers can
// tell it from an omitted setting and see the exact value REA refused.
if (!isAbsolute(jar))
throw configurationFailure(
"open_options_invalid",
`REA_JADX_MCP_JAR must be an absolute path; received ${JSON.stringify(jar)}. ${JADX_JAR_CONFIGURATION_REMEDIATION}`,
{ jar_configured: true, jar_path: jar, path_requirement: "absolute" },
"jar",
);
try {
await access(jar, constants.R_OK);
if (!(await stat(jar)).isFile())
+51 -8
View File
@@ -41,12 +41,14 @@ import {
import { hasGodotPckHeader } from "../domain/godotPckFormat.js";
import {
hasZipSignature,
locateTrailingZipArchive,
isZipPackageFormat,
zipPackageFormatForPath,
zipRootPackageFormat,
} from "../domain/zipPackageFormat.js";
import {
parseCodeViewData,
claimsExecutableHeader,
parseExecutableHeader,
parsePeRecord,
peDebugDirectoryEntry,
@@ -219,12 +221,32 @@ export const parseBinaryTarget = async (
hostArchitecture,
signal,
);
if (!detected.ok)
if (!detected.ok) {
// Only bytes no other format claims may lead a located ZIP, so an
// executable with an appended archive keeps its executable route.
if (
!(await claimsLeadingExecutableHeader(
handle,
metadata.size,
signal,
)) &&
(await locateTrailingZipArchive(metadata.size, (position, length) =>
readHandleRange(handle, position, length, signal),
)) !== undefined
)
return ok({
path,
sourcePath,
sha256: await sha256Handle(handle, path, metadata, signal),
kind: "archive",
format: zipRootPackageFormat(path),
});
return err(
new BinaryTargetError(path, detected.error, {
constraint: "file_format_rejected",
}),
);
}
return ok({
path,
sourcePath,
@@ -356,12 +378,8 @@ const detectArtifactFormat = async (
"WebAssembly v1 artifacts are not supported by the native opening route",
{ constraint: "wasm_requires_static_inspection" },
);
if (hasZipSignature(magic.subarray(0, observed.bytesRead))) {
// The HarmonyOS App Pack suffix `.app` is a root-input concept; a nested
// ZIP member ending in `.app` keeps the generic zip classification.
if (lower.endsWith(".app")) return "app-pack";
return zipPackageFormatForPath(lower) ?? "zip";
}
if (hasZipSignature(magic.subarray(0, observed.bytesRead)))
return zipRootPackageFormat(lower);
if (hasGodotPckHeader(magic.subarray(0, observed.bytesRead)))
return "godot-pck";
// Binary plists, including keyed archives, often use other extensions.
@@ -446,6 +464,31 @@ const isArchiveFormat = (
isZipPackageFormat(format) ||
["asar", "godot-pck", "dmg", "pkg"].includes(format);
/** Whether the leading bytes begin any executable header family, valid or not. */
const claimsLeadingExecutableHeader = async (
handle: FileHandle,
size: number,
signal?: AbortSignal,
): Promise<boolean> =>
claimsExecutableHeader(
Buffer.from(await readHandleRange(handle, 0, Math.min(size, 4096), signal)),
size,
);
/** Read one bounded range, honoring cancellation around the host read. */
const readHandleRange = async (
handle: FileHandle,
position: number,
length: number,
signal?: AbortSignal,
): Promise<Uint8Array> => {
throwIfTargetResolutionCancelled(signal);
const bytes = Buffer.alloc(length);
const observed = await handle.read(bytes, 0, length, position);
throwIfTargetResolutionCancelled(signal);
return bytes.subarray(0, observed.bytesRead);
};
const sha256Handle = async (
handle: FileHandle,
path: string,
+1
View File
@@ -415,6 +415,7 @@ const workflowAvailabilityFor = ({
name === "inspect_go_binary" ||
name === "inspect_il2cpp_metadata" ||
name === "inspect_model_artifact" ||
name === "inspect_gpu_code_objects" ||
name === "inspect_rust_binary" ||
name === "inspect_electron_fuses"
)
+4 -8
View File
@@ -46,15 +46,11 @@ export const exportEvidenceBundleCommand = async (
);
if (written.ok) return projectWrite(loaded.value, written);
let cleanupSucceeded = false;
try {
await resources.close();
cleanupSucceeded = true;
} catch {
// The original error cause retains the same staged-output owner for diagnostics.
}
// Confirmed release resolves the bound error to its primary outcome; a
// failed retry leaves that error incomplete with its exact retained owner.
await resources.close().catch(() => undefined);
if (
cleanupSucceeded &&
!written.error.cleanupIncomplete &&
written.error instanceof EvidenceFileError &&
written.error.published
) {
+37 -11
View File
@@ -29,6 +29,11 @@ import { NonRegularFileReadError } from "../filesystem/RegularFile.js";
import { RegularFileCleanupFailure } from "./RegularFileRead.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js";
import {
type AnalysisCleanupLease,
AnalysisCleanupLeaseGroup,
} from "../artifacts/AnalysisCleanupLease.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { OwnedStagedOutput } from "../filesystem/OwnedStagedOutput.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
@@ -212,13 +217,14 @@ export function writeTextParts(
cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined
? cause.cleanup
: undefined;
return err(
new EvidenceFileError("write", "io", {
cause,
path: absoluteCallerPath(path),
...(cleanup === undefined ? {} : { cleanup }),
}),
);
const error = new EvidenceFileError("write", "io", {
cause,
path: absoluteCallerPath(path),
...(cleanup === undefined ? {} : { cleanup }),
});
// A failed admission retry names its exact retained leases.
bindArtifactFileCleanup(cause, error);
return err(error);
});
}
@@ -268,28 +274,31 @@ const writeTextPartsOwned = async (
resources: [cause.owner.directory],
};
if (cause.outcome.ok)
return err(
return stagedCleanupFailure(
new EvidenceFileError("write", "io", {
cause,
path: requestedPath,
cleanup,
published: { ...cause.outcome.value, path: requestedPath },
}),
cause.lease,
);
if (cause.outcome.error instanceof AnalysisCancelledError)
return err(
return stagedCleanupFailure(
new AnalysisCancelledError(cause.outcome.error.operation, {
cause: cause.outcome.error.cause,
cleanupPrimary: cause.outcome.error,
cleanup,
}),
cause.lease,
);
return err(
return stagedCleanupFailure(
new EvidenceFileError("write", missingOrIo(cause.outcome.error), {
cause,
path: requestedPath,
cleanup,
}),
cause.lease,
);
}
if (
@@ -314,6 +323,17 @@ const writeTextPartsOwned = async (
}
};
/** The retained staging lease clears this error only when a retry confirms release. */
const stagedCleanupFailure = <
Failure extends EvidenceFileError | AnalysisCancelledError,
>(
error: Failure,
lease: AnalysisCleanupLease,
): Result<never, Failure> => {
new AnalysisCleanupLeaseGroup(error, [lease]);
return err(error);
};
/** A missing path or parent is a selection error, not a permission failure. */
const missingOrIo = (cause: unknown): "missing" | "io" => {
const code = fileErrorCode(cause);
@@ -373,7 +393,12 @@ const publishFile = async (
output: owner,
});
if (released.kind === "failed")
throw new StagedOutputCleanupFailure(outcome, released.cause, owner);
throw new StagedOutputCleanupFailure(
outcome,
released.cause,
owner,
released.lease,
);
if (!outcome.ok) throw outcome.error;
return outcome.value.bytes;
};
@@ -383,6 +408,7 @@ class StagedOutputCleanupFailure extends Error {
readonly outcome: Result<PublishedEvidenceFile, unknown>,
readonly cleanupCause: unknown,
readonly owner: OwnedStagedOutput,
readonly lease: AnalysisCleanupLease,
) {
super("Staged output cleanup failed", {
cause: outcome.ok ? cleanupCause : outcome.error,
@@ -11,6 +11,8 @@ import { describe, expect, it } from "vitest";
import { createEvidence } from "../domain/evidence.js";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
import { emptyUnverifiedProcessCapture } from "../domain/process/processCapture.fixture.js";
import { FINALIZED_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCaptureExample.js";
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import {
digestProcessCommitment,
@@ -178,6 +180,40 @@ describe("reconstruction obligation candidates", () => {
`Process capture ${evidence.evidence_id} has no validated executable artifact identity; packaged-process lifecycle obligation was not bound to an artifact.`,
);
});
it("names the first rejected capture field and the remaining issue count", () => {
const {
manifest: { completed_at: _completedAt, ...manifest },
exit,
...rest
} = FINALIZED_PROCESS_CAPTURE_EXAMPLE;
const evidence = createEvidence(undefined, PROCESS_PROVIDER, {
predicateType: "rea.process-capture",
operation: "capture_process_scenario",
parameters: {},
result: jsonValueSchema.parse({
...rest,
manifest,
exit: {
...exit,
finalization: {
...exit.finalization,
signals: [
{ ...exit.finalization.signals[0], reason: "not allowed here" },
],
},
},
}),
});
const generated = deriveReconstructionObligationCandidates(
createEvidenceBundle([evidence]),
[],
);
expect(generated.limitations).toEqual([
`Process Evidence ${evidence.evidence_id} could not generate candidates: at manifest.completed_at: Invalid input: expected string, received undefined (1 more issues)`,
]);
});
});
it("keeps each semantic candidate bound to its own file and authority context", () => {
@@ -4,6 +4,7 @@ import canonicalize from "canonicalize";
import type { Evidence } from "../domain/evidence.js";
import {
describeValidationFailure,
parseEvidenceBundle,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
@@ -166,7 +167,7 @@ const deriveApplicationCandidates = (
);
} catch (cause: unknown) {
limitations.add(
`Application Evidence ${evidence.evidence_id} could not generate candidates: ${errorMessage(cause)}`,
`Application Evidence ${evidence.evidence_id} could not generate candidates: ${describeValidationFailure(cause)}`,
);
}
};
@@ -258,7 +259,7 @@ const deriveProcessCandidate = (
);
} catch (cause: unknown) {
limitations.add(
`Process Evidence ${evidence.evidence_id} could not generate candidates: ${errorMessage(cause)}`,
`Process Evidence ${evidence.evidence_id} could not generate candidates: ${describeValidationFailure(cause)}`,
);
}
};
@@ -464,6 +465,3 @@ const digest = (value: unknown): string => {
throw new TypeError("Reconstruction obligation key is not canonical JSON");
return createHash("sha256").update(encoded).digest("hex");
};
const errorMessage = (cause: unknown): string =>
cause instanceof Error ? cause.message : "unknown validation failure";
@@ -1,73 +1,17 @@
import { describe, expect, it } from "vitest";
import {
boundProofEvidence,
build,
originalCases,
processEvidence,
proofEvidence,
readyProcessLedgerRequest,
request,
reviewedObligation,
} from "./ReconstructionObligationLedger.fixture.js";
import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js";
import { evaluateReconstructionObligationLedger } from "./ReconstructionObligationLedgerEvaluation.js";
/** Build a request whose packaged-process obligation closes as ready. */
const readyProcessLedgerRequest = () => {
const capture = processEvidence();
const initial = build(request([capture]));
const obligation = initial.obligations[0];
if (obligation === undefined) throw new Error("Expected process obligation");
const fixtureIds = obligation.required_case_kinds.map(
(caseKind) => `packaged.${caseKind}`,
);
const proof = boundProofEvidence("packaged", {
obligation_ids: [obligation.obligation_id],
fixture_ids: fixtureIds,
case_kinds: obligation.required_case_kinds,
verifier_ids: ["verifier.packaged"],
claim_ids: ["claim.process"],
});
const ledgerRequest = request([capture, proof], {
manifest: {
bindings: [
{
obligation_id: obligation.obligation_id,
owner: {
module_path: "src/processOwner.ts",
symbol: "runProcess",
owner_sha256: "2".repeat(64),
},
parser_type: null,
original_cases: originalCases(obligation, capture.evidence_id),
fixtures: obligation.required_case_kinds.map((caseKind) => ({
fixture_id: `packaged.${caseKind}`,
case_kind: caseKind,
authority: "packaged-process",
evidence_ids: [proof.evidence_id],
})),
verifier: {
verifier_id: "verifier.packaged",
claim_id: "claim.process",
command: "npm run verify:package -- process",
authority: "packaged-process",
status: "pass",
result_evidence_id: proof.evidence_id,
enumerated_obligation_ids: [obligation.obligation_id],
nondeterminism: {
mode: "partial-order",
specification:
"Startup precedes teardown; worker completion is unordered.",
},
},
},
],
contradictions: [],
},
});
return ledgerRequest;
};
describe("reconstruction obligation ledger", () => {
it("emits the complete deterministic ledger and closure identity", () => {
const evidence = proofEvidence("review");
@@ -120,6 +120,63 @@ export const originalCases = (
location: `/normalized_result/cases/${caseKind}`,
}));
/** Build a request whose packaged-process obligation closes as ready. */
export const readyProcessLedgerRequest =
(): ReconstructionObligationLedgerInput => {
const capture = processEvidence();
const initial = build(request([capture]));
const obligation = initial.obligations[0];
if (obligation === undefined)
throw new Error("Expected process obligation");
const fixtureIds = obligation.required_case_kinds.map(
(caseKind) => `packaged.${caseKind}`,
);
const proof = boundProofEvidence("packaged", {
obligation_ids: [obligation.obligation_id],
fixture_ids: fixtureIds,
case_kinds: obligation.required_case_kinds,
verifier_ids: ["verifier.packaged"],
claim_ids: ["claim.process"],
});
return request([capture, proof], {
manifest: {
bindings: [
{
obligation_id: obligation.obligation_id,
owner: {
module_path: "src/processOwner.ts",
symbol: "runProcess",
owner_sha256: "2".repeat(64),
},
parser_type: null,
original_cases: originalCases(obligation, capture.evidence_id),
fixtures: obligation.required_case_kinds.map((caseKind) => ({
fixture_id: `packaged.${caseKind}`,
case_kind: caseKind,
authority: "packaged-process",
evidence_ids: [proof.evidence_id],
})),
verifier: {
verifier_id: "verifier.packaged",
claim_id: "claim.process",
command: "npm run verify:package -- process",
authority: "packaged-process",
status: "pass",
result_evidence_id: proof.evidence_id,
enumerated_obligation_ids: [obligation.obligation_id],
nondeterminism: {
mode: "partial-order",
specification:
"Startup precedes teardown; worker completion is unordered.",
},
},
},
],
contradictions: [],
},
});
};
/** Create a minimal reviewed obligation for ledger closure scenarios. */
export const reviewedObligation = (
obligationId: string,
@@ -149,3 +206,63 @@ export const reviewedObligation = (
disposition: "active",
review_evidence_ids: [evidenceId],
});
const authorityEvidence = (
label: string,
authority: "shipped-artifact" | "controlled-replay",
) =>
createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "observe",
parameters: { label },
result: { present: true },
confidence: "observed",
authority,
},
);
/**
* Request with one single-reference shipped-artifact obligation and one
* obligation citing two shipped-artifact records and one controlled-replay
* record. Shipped-artifact review references have candidate state while both
* reviewed obligations have reviewed source state.
*/
export const mixedAuthorityRequest =
(): ReconstructionObligationLedgerInput => {
const shipped = authorityEvidence("shipped", "shipped-artifact");
const otherShipped = authorityEvidence("other-shipped", "shipped-artifact");
const replay = authorityEvidence("replay", "controlled-replay");
return request([shipped, otherShipped, replay], {
reviewed_obligations: [
reviewedObligation("review.shipped", shipped.evidence_id),
{
...reviewedObligation("review.mixed", shipped.evidence_id),
review_evidence_ids: [
shipped.evidence_id,
replay.evidence_id,
otherShipped.evidence_id,
],
},
],
});
};
/** Summary buckets expected for {@link mixedAuthorityRequest}. */
export const MIXED_AUTHORITY_SUMMARY = {
total: 2,
by_evidence_authority: [
{ key: "shipped-artifact", count: 2 },
{ key: "controlled-replay", count: 1 },
{ key: "historical-reference", count: 0 },
{ key: "external-service", count: 0 },
{ key: "analyst-inference", count: 0 },
],
by_source_state: [
{ key: "candidate", count: 0 },
{ key: "observed", count: 0 },
{ key: "reviewed", count: 2 },
{ key: "unknown", count: 0 },
],
} as const;
@@ -7,6 +7,7 @@ import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
type Diagnostic = ReconstructionObligation["diagnostics"][number];
type Binding = NonNullable<ReconstructionObligation["binding"]>;
type Contradiction = ReconstructionObligationManifest["contradictions"][number];
export const groupManifestBindings = (
manifest: ReconstructionObligationManifest,
@@ -22,12 +23,12 @@ export const groupManifestBindings = (
export const groupManifestContradictions = (
manifest: ReconstructionObligationManifest,
): ReadonlyMap<string, readonly string[]> => {
const grouped = new Map<string, string[]>();
): ReadonlyMap<string, readonly Contradiction[]> => {
const grouped = new Map<string, Contradiction[]>();
for (const contradiction of manifest.contradictions)
grouped.set(contradiction.obligation_id, [
...(grouped.get(contradiction.obligation_id) ?? []),
...contradiction.evidence_ids,
contradiction,
]);
return grouped;
};
@@ -59,26 +60,33 @@ export const uniqueObligationDiagnostics = (
export const contradictionDiagnostics = (
obligationId: string,
contradictions: ReadonlyMap<string, readonly string[]>,
contradictions: ReadonlyMap<string, readonly Contradiction[]>,
evidenceById: ReadonlyMap<string, Evidence>,
): Diagnostic[] => {
const evidenceIds = contradictions.get(obligationId) ?? [];
if (evidenceIds.length === 0) return [];
const missing = evidenceIds.filter(
(evidenceId) => !evidenceById.has(evidenceId),
// Each manifest entry is a separate claim. Absent Evidence leaves only its
// own entry unestablished; it must not hide a sibling whose Evidence exists.
const entries = contradictions.get(obligationId) ?? [];
const missing = new Set(
entries.flatMap(({ evidence_ids: ids }) =>
ids.filter((evidenceId) => !evidenceById.has(evidenceId)),
),
);
return missing.length > 0
? [
{
code: "contradiction-evidence-missing",
detail: `Contradiction references missing Evidence: ${missing.join(", ")}.`,
},
]
: [
{
code: "contradiction",
detail:
"Authenticated contradictory Evidence reopens this obligation.",
},
];
const diagnostics: Diagnostic[] = [];
if (missing.size > 0)
addObligationDiagnostic(
diagnostics,
"contradiction-evidence-missing",
`Contradiction references missing Evidence: ${[...missing].join(", ")}.`,
);
if (
entries.some(({ evidence_ids: ids }) =>
ids.every((evidenceId) => evidenceById.has(evidenceId)),
)
)
addObligationDiagnostic(
diagnostics,
"contradiction",
"Authenticated contradictory Evidence reopens this obligation.",
);
return diagnostics;
};
@@ -47,7 +47,7 @@ interface EvaluationContext {
readonly candidates: ReadonlyMap<string, ReconstructionObligationCandidate>;
readonly evidence: ReadonlyMap<string, Evidence>;
readonly bindings: ReadonlyMap<string, readonly Binding[]>;
readonly contradictions: ReadonlyMap<string, readonly string[]>;
readonly contradictions: ReturnType<typeof groupManifestContradictions>;
}
export const evaluateReconstructionObligationLedger = ({
@@ -1,6 +1,9 @@
import { expect, it } from "vitest";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import {
projectAnalysisError,
projectAnalysisErrorWithoutCapturedOutput,
} from "../../domain/analysisErrorProjection.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { err } from "../../domain/result.js";
@@ -59,18 +62,26 @@ it("preserves the original explanation and collected output alongside cleanup un
reason: "Process termination was not confirmed",
});
const combined = analysisErrorWithCleanupFailure(primary, cleanup);
expect(projectAnalysisError(combined)).toMatchObject({
const projected = projectAnalysisError(combined);
expect(projected).toMatchObject({
code: "cleanup_incomplete",
message: primary.userMessage,
details: {
resources: ["owned-process"],
captured_output: primary.capturedOutput,
diagnostics: {
primary_error: projectAnalysisError(primary),
primary_error: projectAnalysisErrorWithoutCapturedOutput(primary),
cleanup_error: projectAnalysisError(cleanup),
},
},
});
expect(projected).not.toHaveProperty(
"details.diagnostics.primary_error.details.captured_output",
);
expect(JSON.stringify(projected).split("partial observation")).toHaveLength(
2,
);
expect(JSON.stringify(projected).split("native reason")).toHaveLength(2);
expect(combined.cause).toBeInstanceOf(AggregateError);
expect(combined.cause).toMatchObject({
cause: primary,
@@ -5,6 +5,7 @@ import {
createAnalysisExecution,
type AnalysisClient,
type AnalysisProviderCandidate,
type AnalysisProviderInspectionOptions,
type CapabilityDescriptor,
type ProviderAvailability,
type ProviderIdentity,
@@ -825,7 +826,9 @@ it("joins cancelled readiness finalization and shutdown before admitting another
const registry = new AnalysisProviderRegistry([
{
...base,
inspectAvailability: async ({ signal } = {}) => {
inspectAvailability: async ({
signal,
}: AnalysisProviderInspectionOptions = {}) => {
calls++;
if (calls === 1) {
observed = signal;
@@ -982,7 +985,9 @@ it("registers selection before async readiness can race shutdown", async () => {
const registry = new AnalysisProviderRegistry([
{
...base,
inspectAvailability: async ({ signal } = {}) => {
inspectAvailability: async ({
signal,
}: AnalysisProviderInspectionOptions = {}) => {
calls++;
observed = signal;
return base.inspectAvailability();
@@ -2,59 +2,72 @@ import { expect, it } from "vitest";
import { FirmwareAnalysisService } from "./FirmwareAnalysisService.js";
import { createAnalysisExecution } from "../AnalysisProvider.js";
import { firmwareResultSchemas } from "../../domain/firmware/firmwareAnalysis.js";
import {
firmwareResultSchemas,
type FirmwareOperation,
} from "../../domain/firmware/firmwareAnalysis.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { err, ok } from "../../domain/result.js";
const fixture = (
operation: FirmwareOperation,
budget: number | null = 120_000,
) => {
const path = "/owned-fixture/input.bin";
const input = {
path,
...(operation === "extract_firmware"
? { output_directory: "/owned-fixture/output" }
: {}),
};
const engine = {
name: "fixture",
version: "1",
executable_path: "/fixture/engine",
executable_sha256: "b".repeat(64),
source_revision: null,
worker_count: 1,
};
const normalized = firmwareResultSchemas[operation].parse(
operation === "inspect_firmware_regions"
? { engine, input_size: 1, regions: [], coverage: "signature_scan" }
: {
engine,
selection: { offset: 0, length: 1, sha256: "a".repeat(64) },
output_directory: "/owned-fixture/output",
coverage: "complete",
files: [],
unpublished_entries: [],
chunks: [],
derivations: [],
depth_limited_paths: [],
diagnostics: [],
extraction_gaps: [],
extraction_continuations: [],
published_bytes: 0,
},
);
const execution = createAnalysisExecution(
budget === null
? normalized
: { ...normalized, analysis_timeout_ms: budget },
{ id: "fixture", name: "Fixture firmware", version: "1" },
{
subject: {
path,
format: "file",
sha256: "a".repeat(64),
},
rawResult: { source: "controlled producer" },
},
);
return { input, execution };
};
it.each(["inspect_firmware_regions", "extract_firmware"] as const)(
"retains %s Evidence after late cancellation and preserves provider failure precedence",
async (operation) => {
const path = "/owned-fixture/input.bin";
const input = {
path,
...(operation === "extract_firmware"
? { output_directory: "/owned-fixture/output" }
: {}),
};
const engine = {
name: "fixture",
version: "1",
executable_path: "/fixture/engine",
executable_sha256: "b".repeat(64),
source_revision: null,
worker_count: 1,
};
const normalized = firmwareResultSchemas[operation].parse(
operation === "inspect_firmware_regions"
? { engine, input_size: 1, regions: [], coverage: "signature_scan" }
: {
engine,
selection: { offset: 0, length: 1, sha256: "a".repeat(64) },
output_directory: "/owned-fixture/output",
coverage: "complete",
files: [],
unpublished_entries: [],
chunks: [],
derivations: [],
depth_limited_paths: [],
diagnostics: [],
extraction_gaps: [],
extraction_continuations: [],
published_bytes: 0,
},
);
const execution = createAnalysisExecution(
normalized,
{ id: "fixture", name: "Fixture firmware", version: "1" },
{
subject: {
path,
format: "file",
sha256: "a".repeat(64),
},
rawResult: { source: "controlled producer" },
},
);
const { input, execution } = fixture(operation);
const control = await new FirmwareAnalysisService({
execute: async () => ok(execution),
}).execute(operation, input);
@@ -89,3 +102,72 @@ it.each(["inspect_firmware_regions", "extract_firmware"] as const)(
).toEqual(err(failure));
},
);
for (const operation of [
"inspect_firmware_regions",
"extract_firmware",
] as const) {
it(`preserves a selected ${operation} analysis budget independently of publication`, async () => {
const { input, execution } = fixture(operation, 1234);
const service = new FirmwareAnalysisService({
execute: async (request) => {
expect(request.input).toMatchObject({ analysis_timeout_ms: 1234 });
if (request.operation === "extract_firmware")
expect(request.input.publication_timeout_ms).toBe(120_000);
return ok(execution);
},
});
const result = await service.execute(operation, {
...input,
analysis_timeout_ms: 1234,
});
if (!result.ok) throw result.error;
expect(result.value.normalized_result).toMatchObject({
analysis_timeout_ms: 1234,
});
expect(result.value.parameters).toMatchObject({
analysis_timeout_ms: 1234,
});
});
it(`retains the known ${operation} default without inventing an explicit input`, async () => {
const { input, execution } = fixture(operation);
const result = await new FirmwareAnalysisService({
execute: async () => ok(execution),
}).execute(operation, input);
if (!result.ok) throw result.error;
expect(result.value.normalized_result).toMatchObject({
analysis_timeout_ms: 120_000,
});
expect(result.value.parameters).not.toHaveProperty("analysis_timeout_ms");
});
it.each([
{ selected: undefined, reported: null },
{ selected: undefined, reported: 1234 },
{ selected: 1234, reported: null },
{ selected: 1234, reported: 120_000 },
])(
`rejects missing or changed fresh ${operation} settings: %j`,
async ({ selected, reported }) => {
const { input, execution } = fixture(operation, reported);
const result = await new FirmwareAnalysisService({
execute: async () => ok(execution),
}).execute(operation, {
...input,
...(selected === undefined ? {} : { analysis_timeout_ms: selected }),
});
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
});
},
);
it(`keeps historical ${operation} budget omissions unknown`, () => {
const { execution } = fixture(operation, null);
expect(
firmwareResultSchemas[operation].parse(execution.result),
).not.toHaveProperty("analysis_timeout_ms");
});
}
@@ -2,6 +2,8 @@ import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { FirmwareAnalysisPort } from "./FirmwareAnalysisPort.js";
import {
firmwareRequestSchema,
firmwareExecutionBudgetSchema,
DEFAULT_FIRMWARE_ANALYSIS_TIMEOUT_MS,
type FirmwareOperation,
} from "../../domain/firmware/firmwareAnalysis.js";
import {
@@ -53,6 +55,21 @@ export class FirmwareAnalysisService {
"Firmware execution did not bind its source artifact",
),
);
// Providers own normalized facts. Check only fresh execution metadata here,
// without reparsing/copying potentially large extraction inventories.
const budget = firmwareExecutionBudgetSchema.safeParse(execution.result);
if (
!budget.success ||
budget.data.analysis_timeout_ms !==
(parsed.data.input.analysis_timeout_ms ??
DEFAULT_FIRMWARE_ANALYSIS_TIMEOUT_MS)
)
return err(
new AnalysisOutputError(
operation,
"Firmware execution did not retain its selected analysis budget",
),
);
return completeProviderEvidence(
createEvidence(execution.subject, execution.provider, {
operation,
@@ -0,0 +1,57 @@
import type { ProviderIdentity } from "../AnalysisProvider.js";
import type {
GpuCodeObjects,
GpuCodeObjectsInput,
GpuCodeObjectsPartial,
} from "../../domain/gpu/gpuCodeObjects.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
const gpuParameters = (parameters: GpuCodeObjectsInput) => ({
path: parameters.path,
analysis: parameters.analysis,
...(parameters.object_ids === undefined
? {}
: { object_ids: parameters.object_ids }),
...(parameters.targets === undefined ? {} : { targets: parameters.targets }),
...(parameters.expected_sha256 === undefined
? {}
: { expected_sha256: parameters.expected_sha256 }),
});
/** One Evidence interpretation for success and observations retained by file finalization failures. */
export const createGpuCodeObjectsEvidence = (
report: GpuCodeObjects,
provider: ProviderIdentity,
parameters: GpuCodeObjectsInput,
): Evidence =>
createEvidence(
{
path: report.artifact.path,
sha256: report.artifact.sha256,
format: "file",
},
provider,
{
operation: "inspect_gpu_code_objects",
parameters: gpuParameters(parameters),
result: report,
confidence: "observed",
locations: [{ kind: "artifact-path", path: report.artifact.path }],
limitations: report.limitations,
},
);
export const createGpuCodeObjectsPartialEvidence = (
report: GpuCodeObjectsPartial,
provider: ProviderIdentity,
parameters: GpuCodeObjectsInput,
): Evidence =>
createEvidence(undefined, provider, {
operation: "inspect_gpu_code_objects",
parameters: gpuParameters(parameters),
result: report,
confidence: "observed",
subjectUnavailableReason:
"The interrupted inspection did not complete final file identity verification; root digest and completeness are unknown.",
locations: [{ kind: "artifact-path", path: report.artifact.path }],
limitations: report.limitations,
});
+20
View File
@@ -0,0 +1,20 @@
import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
GpuCodeObjects,
GpuCodeObjectsInput,
} from "../../domain/gpu/gpuCodeObjects.js";
import type { Result } from "../../domain/result.js";
/** Read-only, local GPU declarations; no GPU runtime, target execution or downloads. */
export interface GpuCodeObjectsPort {
readonly identity: ProviderIdentity;
inspect(
input: GpuCodeObjectsInput,
options?: ExecutionOptions,
): Promise<Result<GpuCodeObjects, AnalysisError>>;
close(): Promise<void>;
}
@@ -0,0 +1,97 @@
import { inspectGpuCodeObjectsInputSchema } from "../../domain/gpu/gpuCodeObjects.js";
import type { GpuCodeObjectsPort } from "./GpuCodeObjectsPort.js";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import { gpuCodeObjectsSchema } from "../../domain/gpu/gpuCodeObjects.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import type { Evidence } from "../../domain/evidence.js";
import { createGpuCodeObjectsEvidence } from "./GpuCodeObjectsEvidence.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
requireAnalysisViewRetention,
projectRetainedAnalysis,
type AnalysisViewRetention,
} from "../analysisView/AnalysisViewService.js";
/** Shared input, Evidence and retained delivery boundary for CLI, MCP and SDK callers. */
export class GpuCodeObjectsService {
constructor(readonly provider: GpuCodeObjectsPort) {}
close(): Promise<void> {
return this.provider.close();
}
async inspect(
raw: unknown,
options?: ExecutionOptions,
retention: AnalysisViewRetention = {
evidenceById: undefined,
recordEvidence: undefined,
},
): Promise<Result<Evidence, AnalysisError>> {
const input = inspectGpuCodeObjectsInputSchema.safeParse(raw);
if (!input.success)
return err(
analysisInputErrorFromIssues(
"inspect_gpu_code_objects",
input.error.issues,
raw,
{ cause: input.error },
),
);
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError("inspect_gpu_code_objects", {
cause: options.signal.reason,
}),
);
const { detail, view, ...parameters } = input.data;
if (detail === "summary") {
const ready = requireAnalysisViewRetention(
"inspect_gpu_code_objects",
retention,
);
if (!ready.ok) return ready;
}
const inspected = await this.provider.inspect(parameters, options);
if (!inspected.ok) return inspected;
const checked = gpuCodeObjectsSchema.safeParse(inspected.value);
if (
!checked.success ||
checked.data.artifact.path !== parameters.path ||
checked.data.analysis !== parameters.analysis ||
(parameters.expected_sha256 !== undefined &&
checked.data.artifact.sha256 !== parameters.expected_sha256)
)
return err(
new AnalysisOutputError(
"inspect_gpu_code_objects",
"GPU reader changed the selected input or returned invalid facts",
),
);
const report = checked.data;
const evidence = createGpuCodeObjectsEvidence(
report,
this.provider.identity,
parameters,
);
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError("inspect_gpu_code_objects", {
cause: options.signal.reason,
partialObservation: evidence,
}),
);
return detail === "summary"
? projectRetainedAnalysis(
evidence,
retention,
view === undefined
? { kind: "summary" }
: { kind: "gpu-code-objects", ...view },
)
: ok(evidence);
}
}
@@ -158,7 +158,7 @@ export const reconstructJavaScriptArtifact = async (
? {}
: { fileNodes: input.max_semantic_nodes_per_file }),
});
const worker = createJavaScriptSourceAnalysis(
const worker = await createJavaScriptSourceAnalysis(
{
heapMb:
input.max_heap_mb ?? JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.heapMb,
@@ -169,6 +169,7 @@ export const reconstructJavaScriptArtifact = async (
files.files.filter(
(file) => file.kind === "javascript" && file.text.included,
).length,
{ scope },
);
let outcome;
let cleanupFailure: AnalysisError | undefined;
@@ -55,6 +55,89 @@ it("waits for admitted operations and shares concurrent close", async () => {
expect(closed).toBe(true);
});
it("stops a close hook even when retained cleanup is still failing", async () => {
const scope = new ArtifactResourceScope();
let releases = 0;
const owner = {
kind: "lease" as const,
resource: "stuck lease",
lease: {
async release() {
releases += 1;
if (releases < 3) throw new Error("still owned");
},
},
};
expect(await scope.release(owner)).toMatchObject({ kind: "failed" });
let hookRuns = 0;
scope.addCloseHook(async () => {
hookRuns += 1;
if (hookRuns === 1) throw new Error("hook still stopping");
});
const closeFailure = await scope.close().then(
() => {
throw new Error("close succeeded while cleanup and the hook failed");
},
(cause: unknown) => cause,
);
expect(closeFailure).toMatchObject({
message: "Artifact cleanup remains incomplete: stuck lease, close hook",
failedOwners: [owner],
cleanup: {
reason: "still owned; hook still stopping",
resources: ["stuck lease", "close hook"],
},
});
expect(closeFailure).toBeInstanceOf(ArtifactResourceRetryFailure);
const causes = (closeFailure as ArtifactResourceRetryFailure).cause;
expect(causes).toBeInstanceOf(AggregateError);
expect(
(causes as AggregateError).errors.map((error) =>
error instanceof Error ? error.message : String(error),
),
).toEqual([
"Artifact cleanup remains incomplete: stuck lease",
"hook still stopping",
]);
expect(releases).toBe(2);
expect(hookRuns).toBe(1);
await scope.close();
expect(releases).toBe(3);
expect(hookRuns).toBe(2);
});
it("runs later close hooks when an earlier hook fails", async () => {
const scope = new ArtifactResourceScope();
const order: string[] = [];
scope.addCloseHook(async () => {
order.push("first");
throw new Error("first hook failed");
});
scope.addCloseHook(async () => {
order.push("second");
throw new Error("second hook failed");
});
const failure = await scope.close().then(
() => {
throw new Error("close succeeded while hooks failed");
},
(cause: unknown) => cause,
);
expect(order).toEqual(["first", "second"]);
expect(failure).toBeInstanceOf(AggregateError);
expect(
(failure as AggregateError).errors.map((error) =>
error instanceof Error ? error.message : String(error),
),
).toEqual(["first hook failed", "second hook failed"]);
order.length = 0;
await expect(scope.close()).rejects.toBeInstanceOf(AggregateError);
expect(order).toEqual(["first", "second"]);
});
it("retains the same failed reader owner in a retry failure and retries before admitting work", async () => {
const scope = new ArtifactResourceScope();
let closeAttempts = 0;
+95 -1
View File
@@ -95,6 +95,8 @@ export class ArtifactResourceScope {
>();
readonly #active = new Set<Promise<unknown>>();
#draining: Promise<void> | undefined;
#closeInFlight: Promise<void> | undefined;
readonly #closeHooks: Array<() => Promise<void>> = [];
#closed = false;
get hasPending(): boolean {
@@ -203,10 +205,102 @@ export class ArtifactResourceScope {
return this.#draining;
}
/**
* Run after retained owners are released. Hooks stay registered so a later
* close retries them, and a failing hook does not skip the ones after it.
* Concurrent closes share one in-flight attempt.
*/
addCloseHook(hook: () => Promise<void>): void {
this.#closeHooks.push(hook);
}
/** Stop admission, wait for active operations, and retry every retained owner. */
close(): Promise<void> {
this.#closed = true;
return this.retryCleanup();
if (this.#closeInFlight !== undefined) return this.#closeInFlight;
const attempt = this.#finishClose();
let published: Promise<void>;
published = attempt.finally(() => {
if (this.#closeInFlight === published) this.#closeInFlight = undefined;
});
this.#closeInFlight = published;
return published;
}
/** Every registered hook runs. One failure is returned unchanged; several are aggregated. */
async #runCloseHooks(): Promise<unknown> {
const failures: unknown[] = [];
for (const hook of this.#closeHooks) {
try {
await hook();
} catch (cause: unknown) {
failures.push(cause);
}
}
if (failures.length === 0) return undefined;
if (failures.length === 1) return failures[0];
return new AggregateError(
failures,
failures
.map((cause) =>
cause instanceof Error ? cause.message : String(cause),
)
.join("; "),
);
}
async #finishClose(): Promise<void> {
// A parked analyzer is not a pending owner. Stop it even when another
// retained cleanup is still failing. A later close retries both.
const cleanup = await this.retryCleanup().then(
() => undefined,
(cause: unknown) => cause,
);
const hookFailure = await this.#runCloseHooks();
if (cleanup !== undefined && hookFailure !== undefined)
throw this.#composeCloseFailures(cleanup, hookFailure);
if (hookFailure !== undefined) throw hookFailure;
if (cleanup !== undefined) throw cleanup;
}
/**
* Report both outstanding owners. The pending set and the close hook stay
* registered, so the next close retries each failed path.
*/
#composeCloseFailures(
cleanup: unknown,
hookFailure: unknown,
): ArtifactReaderFailure {
const retained = ArtifactReaderFailure.cleanupObservation(
cleanup,
"retained artifact cleanup",
);
const hook = ArtifactReaderFailure.cleanupObservation(
hookFailure,
"close hook",
);
const resources = [...new Set([...retained.resources, ...hook.resources])];
const options = {
cause: new AggregateError(
[cleanup, hookFailure],
"Retained cleanup and a close hook both failed",
),
cleanup: {
reason: `${retained.reason}; ${hook.reason}`,
resources,
},
};
const message = `Artifact cleanup remains incomplete: ${resources.join(", ")}`;
if (cleanup instanceof ArtifactResourceRetryFailure)
return new ArtifactResourceRetryFailure(
cleanup.failedOwners.map((owner) => ({
owner,
lease: this.#lease(owner),
})),
message,
options,
);
return new ArtifactReaderFailure("unavailable", message, options);
}
async #drainAndRetry(): Promise<void> {
@@ -122,6 +122,9 @@ function* xmlPreflightTokens(xml: string) {
}
}
/** Deepest binary plist value nesting the reference walk admits. */
export const MAX_BINARY_PLIST_NESTING_DEPTH = 128;
/** Resource or structural failure found while preflighting a plist. */
export type BinaryPlistExpansionFailure = "cycle" | "depth" | "expansion";
@@ -299,7 +302,7 @@ export const estimateBinaryPlistExpansion = (
continue;
}
pendingObjectCount -= 1;
if (entry.depth > 128)
if (entry.depth > MAX_BINARY_PLIST_NESTING_DEPTH)
throw fail("depth", `binary plist nesting exceeds ${budget}`);
if (active.has(entry.object))
throw fail("cycle", `binary plist reference cycle exceeds ${budget}`);
Loaded 100 of 284 files, more files were not shown because too many files have changed in this diff. Show more