mirror of
https://github.com/morluto/rea.git
synced 2026-11-02 21:06:04 +00:00
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:
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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` /
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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): نتائج الأدوات، والجلسات، والاستقصاءات الموجّهة.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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): αποτελέσματα εργαλείων, συνεδρίες και καθοδηγούμενες διερευνήσεις.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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): نتایج ابزارها، نشستها و بررسیهای هدایتشده.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):ツールの結果、セッション、ガイド付き調査。
|
||||
|
||||
@@ -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): 도구 결과, 세션, 안내에 따른 조사.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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): результаты инструментов, сеансы и исследования с подсказками.
|
||||
|
||||
@@ -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): ผลลัพธ์เครื่องมือ เซสชัน และการตรวจสอบตามคำแนะนำ
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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): результати інструментів, сеанси й керовані дослідження.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):工具結果、會話和引導式調查。
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+128
@@ -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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -10,6 +10,7 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
|
||||
"go",
|
||||
"il2cpp",
|
||||
"model",
|
||||
"gpu",
|
||||
"rust",
|
||||
"wasm",
|
||||
"captures",
|
||||
|
||||
@@ -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
@@ -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") {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(), [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
|
||||
@@ -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"] },
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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() };
|
||||
};
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
};
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,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
Reference in new issue
Block a user