diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md index 12a47c3896..3499424275 100644 --- a/.agents/skills/reverse-engineer-anything/SKILL.md +++ b/.agents/skills/reverse-engineer-anything/SKILL.md @@ -251,6 +251,14 @@ explicit path or endpoint and do not need it. absence of that table, not proof the image is unsigned in every catalog sense. Signatures are not verified. See the [PE certificate guide](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md). +- PE import and delay-load tables: `inspect_pe_imports` with the explicit + local PE32/PE32+ file. This built-in target-free inspection requires no + Windows SDK. Preserve DLL names, named and ordinal functions, ILT/IAT RVAs, + delay-load descriptors, and the pefile import hash of the standard import + table. A missing import directory is observed absence of that table, not + proof the image never imported APIs through another table. Bound snapshots + and loaded IAT contents remain unobserved. See the + [PE import guide](https://github.com/morluto/rea/blob/main/docs/pe-imports.md). - ELF note tables: `inspect_elf_notes` with the explicit local ELF32/ELF64 file. This built-in target-free inspection requires no disassembler. Preserve PT_NOTE and SHT_NOTE ranges, owners, types, descriptor SHA-256, and GNU @@ -267,8 +275,10 @@ explicit path or endpoint and do not need it. - Erlang BEAM IFF container: `inspect_erlang_beam` with the explicit local `.beam` file. This built-in target-free inspection requires no Erlang installation. Preserve chunk names, atom order, export/import/lambda tables, - and the Code header. Missing optional tables stay null. Bytecode is not - executed. See the + and the Code header. For selected facts, prefer `detail: "summary"` plus a + `beam` view (collection, names/indices, fields and paging), retaining the full + Evidence. CLI summary requires `--evidence_output`. Missing table counts stay + null; decoded empty tables count zero. Bytecode is not executed. See the [Erlang BEAM guide](https://github.com/morluto/rea/blob/main/docs/erlang-beam.md). - Explicit local SQLite database snapshot: `inspect_sqlite_database`. Inspect diff --git a/.agents/skills/reverse-engineer-anything/references/javascript-applications.md b/.agents/skills/reverse-engineer-anything/references/javascript-applications.md index e1894c1b63..9c3508571d 100644 --- a/.agents/skills/reverse-engineer-anything/references/javascript-applications.md +++ b/.agents/skills/reverse-engineer-anything/references/javascript-applications.md @@ -17,9 +17,10 @@ the complete artifact inventory and identity but reads source text and analyzes only selected files. The result and summary record `source_selection`; files outside scope and their relationships remain unknown, so partial application coverage does not mean the selected source failed. Use repeated `--source-path` -on either static application CLI route. Omit the selection when the question -requires complete application analysis; `source_paths: []` is invalid, not an -unfiltered request. `detail: "summary"` reduces delivery +on either static application CLI route, or `--source-paths-json` with a JSON +file for selections too long for one command line. Omit the selection when the +question requires complete application analysis; `source_paths: []` is invalid, +not an unfiltered request. `detail: "summary"` reduces delivery and does not by itself reduce analysis work. ASAR header/index admission defaults to 16 MiB / 16777216 bytes; diff --git a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md index fdf876fa20..1dc7b97d40 100644 --- a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md +++ b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md @@ -112,6 +112,19 @@ not proof the image is unsigned in every catalog or overlay sense. Signatures, chains, timestamps, and Authenticode digest coverage remain unobserved. See [PE Authenticode certificate tables](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md). +## PE import and delay-load tables + +Use `inspect_pe_imports` for an explicit local PE32/PE32+ image when the +question concerns imported DLL and function names, ordinals, delay-load +descriptors, or the pefile import hash. No `open_binary`, Windows SDK, or +target execution is required. The CLI equivalent is `inspect-pe-imports PATH`. +Preserve artifact SHA-256, directory file ranges, original DLL spellings, and +ILT/IAT RVAs. A missing `IMAGE_DIRECTORY_ENTRY_IMPORT` directory is observed +absence of that table, not proof the image never imported APIs through another +table or overlay. Bound snapshots, loaded IAT contents, and export-forward +resolution remain unobserved. See +[PE import and delay-load tables](https://github.com/morluto/rea/blob/main/docs/pe-imports.md). + ## ELF note tables Use `inspect_elf_notes` for an explicit local ELF32/ELF64 image when the @@ -140,7 +153,11 @@ question concerns IFF chunks, atom names, exports, imports, lambdas, or the Code header. No `open_binary`, Erlang installation, or bytecode execution is required. The CLI equivalent is `inspect-erlang-beam PATH`. Preserve artifact SHA-256, chunk file ranges, 1-based atom indices, and original atom spellings. -Missing optional tables stay null. See +Missing optional tables stay null. For a few facts, use `detail: "summary"` with +a `beam` view selecting a collection, exact names/indices and fields; read later +pages from `parent_evidence_id` or the complete Evidence bundle. Same-name +exports retain every arity and original atom index. CLI summary delivery requires +`--evidence_output`. Null table counts remain distinct from decoded empty tables. See [Erlang BEAM files](https://github.com/morluto/rea/blob/main/docs/erlang-beam.md). ## SQLite snapshots diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05d2c19bd1..5f0a39565a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -789,7 +789,7 @@ jobs: id: npm-cache uses: ./.github/actions/npm-cache - run: npm ci - - name: Run the public CLI/MCP reconstruction journey + - name: Verify static CLI/MCP journeys and synthetic report conformance env: REA_READINESS_REPORT_PATH: readiness-report.json run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ba6cc84db..f665669614 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,12 +81,29 @@ jobs: echo "frozen=true" >> "$GITHUB_OUTPUT" fi - - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + - name: Check out proposal controller and complete Git history + if: steps.editorial.outputs.frozen != 'true' + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 if: steps.editorial.outputs.frozen != 'true' with: - token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} - target-branch: main - skip-github-release: true + node-version-file: .nvmrc + package-manager-cache: false + + - name: Install proposal controller dependencies + if: steps.editorial.outputs.frozen != 'true' + run: npm ci + + - name: Update release proposal + if: steps.editorial.outputs.frozen != 'true' + env: + GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} + SOURCE_SHA: ${{ github.sha }} + run: node scripts/release-proposal.mjs --target-branch main --source-sha "$SOURCE_SHA" merged-release: name: Release reviewed main merge @@ -259,15 +276,25 @@ jobs: node scripts/release-notes.mjs publication --repository "$GH_REPO" \ --source "$SOURCE_SHA" - - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 - id: release - with: + - name: Prepare checkpoint release proposal + if: inputs.phase == 'prepare' + id: proposal + env: # A dedicated credential starts PR CI automatically; the built-in # token creates approval-required PR runs for maintainers to approve. + GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} + RELEASE_BRANCH: ${{ inputs.release_branch }} + SOURCE_SHA: ${{ steps.branch.outputs.sha }} + run: node scripts/release-proposal.mjs --target-branch "$RELEASE_BRANCH" --source-sha "$SOURCE_SHA" + + - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + if: inputs.phase == 'publish' + id: release + with: token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} target-branch: ${{ inputs.release_branch }} - skip-github-release: ${{ inputs.phase == 'prepare' }} - skip-github-pull-request: ${{ inputs.phase == 'publish' }} + skip-github-release: false + skip-github-pull-request: true - name: Require a merged release PR for publication if: inputs.phase == 'publish' && steps.release.outputs.release_created != 'true' @@ -287,26 +314,26 @@ jobs: fi - name: Check out release pull request - if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' + if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true' uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: - ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }} + ref: ${{ fromJSON(steps.proposal.outputs.pr).headBranchName }} fetch-depth: 0 persist-credentials: false - name: Set up Node.js for generated documentation - if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' + if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true' uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version-file: .nvmrc package-manager-cache: false - name: Install dependencies - if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' + if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true' run: npm ci - name: Validate prepared release version and migration notes - if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' + if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true' env: RELEASE_BRANCH: ${{ inputs.release_branch }} SOURCE_SHA: ${{ steps.branch.outputs.sha }} @@ -316,11 +343,11 @@ jobs: --source-sha "$SOURCE_SHA" - name: Regenerate release documentation - if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' + if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true' run: npm run docs:generate - name: Validate generated release documentation - if: inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true' + if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true' run: npm run docs:check && git diff --exit-code qualification: diff --git a/README.md b/README.md index 13ccf9f28e..5afad7d976 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,7 @@ Additional tools and host support depend on the target: | 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 | | Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](docs/pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](docs/pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](docs/elf-notes.md) | | Erlang BEAM | IFF chunks, atoms, exports, imports, lambdas and the Code header | Built-in static BEAM inspection; [Erlang BEAM](docs/erlang-beam.md) | | WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` | diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index e46e10acb3..9ae116bdbb 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -16,6 +16,7 @@ notices supplied with the software you actually obtain or redistribute. | Material | Use and licensing records | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | npm dependencies | [Complete inventory](licenses/npm-dependencies.txt) of every entry in `package-lock.json`, including transitive, optional platform, development, and website dependencies. [Runtime notices](licenses/npm-runtime-notices.txt) preserve the copyright, license, and NOTICE documents from integrity-verified release archives, plus the MIT notice for Rolldown's generated runtime helpers. Development dependencies are inventoried but are not all distributed in REA's npm package. | +| pefile ordinal tables | Import-hash ordinal names are derived from [pefile 2024.8.26](https://github.com/erocarrera/pefile/tree/4b3b1e2e568a88d4f1897d694d684f23d9e270c4), copyright 2004–2024 Ero Carrera. Preserve the [MIT license](licenses/pefile-2024.8.26-LICENSE.txt). The Python package is not a runtime dependency. | | NativeAOT parser | REA's derived parser uses the MIT-licensed [ghidra-nativeaot](https://github.com/Washi1337/ghidra-nativeaot/tree/effeb734fc570c32650f88b159608979dc7b423e) work of Washi and the NativeAOT layout definitions in [dotnet/runtime](https://github.com/dotnet/runtime/tree/a3b2d40328be0be3f8dd43f0e5cc925b8827b044). Preserve [Washi's MIT notice](licenses/nativeaot-parser-MIT.txt) and the [.NET Foundation MIT notice](licenses/dotnet-runtime-MIT.txt). | | EVM analysis | `evmole@0.9.3`, MIT, copyright 2023 Maxim Andreev. The published npm archive omits its license document; REA retains the [original upstream license](third_party/evmole/LICENSE) and [provenance](third_party/evmole/README.md). It is also included in the runtime notices. | | Windows native addon | The release addon is compiled with MinGW-w64 and statically links GCC runtime libraries. Preserve the [MinGW-w64 runtime notices](licenses/MinGW-w64-runtime.txt), [GPLv3 text](licenses/GPL-3.0.txt), and [GCC Runtime Library Exception 3.1](licenses/GCC-Runtime-Library-Exception-3.1.txt). The exception governs eligible compiled combinations; it does not relicense the runtime itself under MIT. The Node-API build headers are from Node 22.19.0; its [original license and component notices](licenses/Node-22.19.0-LICENSE.txt) are retained. Node itself is supplied by the user. Snapshot provenance and release-toolchain review requirements are in [licenses/README.md](licenses/README.md). | diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 253bf6ff80..7120eab1fe 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -33,6 +33,7 @@ export default defineConfig({ { text: "Model artifacts", link: "/model-artifacts" }, { text: "Rust dependencies", link: "/rust-binaries" }, { text: "PE certificates", link: "/pe-certificates" }, + { text: "PE imports", link: "/pe-imports" }, { text: "ELF notes", link: "/elf-notes" }, { text: "CPython pyc files", link: "/python-pyc" }, { text: "Erlang BEAM", link: "/erlang-beam" }, diff --git a/docs/agent-environments.md b/docs/agent-environments.md index 8a7b39d5dc..b362648386 100644 --- a/docs/agent-environments.md +++ b/docs/agent-environments.md @@ -957,8 +957,14 @@ remain unchanged. Clock-format comparison and respelling recognize four-digit ISO years `0000`–`0099` as well as later years, with normal calendar validation. Respelling preserves the original value when the target zone would require a year outside `0000`–`9999`; generated writes refuse a clock outside that format. -Capture-state extraction still uses millisecond coordinates for request intervals -and their ordering, and generated-clock progression compares milliseconds. +Capture-state start ordering uses the same exact recorded timestamp facts, +including minute-only times and fractions longer than nine digits. Recorded +spellings may use `T` or a space, `Z` or an extended/compact offset, or no zone +(interpreted as UTC). Equal starts use ordinal order; a missing or unreadable +start makes the capture use record order. Request interval and overlap checks +still use millisecond coordinates, and generated-clock progression compares +milliseconds. Derived ISO clock formats still require seconds and at most nine +fractional digits. Comparison keeps response representations distinct: JSON values compare structurally, text compares as text, and a retained base64 body compares by bytes (including with UTF-8 text carrying those same bytes), never by its base64 diff --git a/docs/apktool-resource-analysis.md b/docs/apktool-resource-analysis.md index 9d1c6b115a..b8969142d8 100644 --- a/docs/apktool-resource-analysis.md +++ b/docs/apktool-resource-analysis.md @@ -93,6 +93,10 @@ still suppresses all string projection, including in this mode. `target.bytes` bind the observation to the exact APK that was decoded. The Evidence subject carries the same snapshot digest and caller-selected path, including when the original file changes during decoding. + A change during snapshot acquisition reports retryable `artifact_changed`; + wait for a stable input before retrying. Host read permission failures report + `access_denied`, while a missing or non-regular selected path reports + `invalid_request` with the rejected `path` field. - **Metadata comes from apktool.yml as apktool printed it.** Version codes keep their own spelling (`'7'` versus `7`); `package_name` is read from the decoded manifest, not inferred. diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid index 6ab14e680c..d38bd957f2 100644 --- a/docs/architecture.mermaid +++ b/docs/architecture.mermaid @@ -86,8 +86,8 @@ graph TD TESTS["tests/
Vitest suite
fixtures/ holds fake launcher, bridge, and CDP seams"] end - subgraph "Evaluation" - EVALUATION["src/evaluation/
Agent release evaluation
session transcript scoring"] + subgraph "Development Evaluation" + EVALUATION["tests/evaluation/model-evals/
Agent release evaluation
session transcript scoring"] end DISPATCH --> CLI diff --git a/docs/cli.md b/docs/cli.md index 6886010a5d..c849410d30 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -290,6 +290,17 @@ embedded X.509 serial, issuer, subject, and validity fields. A missing security directory is observed absence of that table. See [PE Authenticode certificate tables](pe-certificates.md). +Inspect PE import and delay-load tables without executing the image: + +```bash +rea inspect-pe-imports /absolute/path/to/application.exe --json +``` + +The workflow reports DLL names, named and ordinal functions, delay-load +descriptors, and a pefile-compatible import hash of the standard import table. +A missing import directory is observed absence of that table. See +[PE import and delay-load tables](pe-imports.md). + Inspect ELF PT_NOTE and SHT_NOTE tables without executing the image: ```bash diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 1395e2bc9b..97faaf85c7 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -305,15 +305,17 @@ for the supported policy values. When direct launcher paths prove the same executable or Node entrypoint, setup retains the existing caller-selected spelling. An otherwise aligned registration therefore needs no rewrite or recovery backup merely because -REA was invoked through another symlink. Unresolved identity does not prove -equivalence. +REA was invoked through another symlink. Setup also retains that spelling when +another field, such as an invalid timeout, needs repair. Unresolved identity +does not prove equivalence. -| Settings | Setup ownership | -| ----------------------------------------- | --------------------------------------------------------------------- | -| Launcher, transport and activation | Refreshes the REA stdio connection and enables it. | -| Explicit server environment | Preserves configured overrides and merges selected provider settings. | -| Codex positive timeouts and client policy | Retains caller choices; adds missing recommended REA timeouts. | -| Unrelated or future server settings | Retains their parsed values. | +| Settings | Setup ownership | +| ------------------------------------------- | --------------------------------------------------------------------------------------- | +| Launcher, transport and activation | Refreshes the REA stdio connection and enables it. | +| Explicit server environment | Preserves configured overrides and merges selected provider settings. | +| Codex timeouts | Retains positive values; repairs missing or invalid values to recommended REA defaults. | +| Codex approval policies and tool selections | Retains caller choices; refuses known malformed values. | +| Unrelated or future server settings | Retains their parsed values. | Dry-run applies the same policy validation and writes no files. A successful refresh keeps an exact recovery snapshot of the original configuration; diff --git a/docs/client-verification.md b/docs/client-verification.md index e1496e5e92..6561752e74 100644 --- a/docs/client-verification.md +++ b/docs/client-verification.md @@ -458,6 +458,11 @@ real local Codex CLI with: npm run verify:agent ``` +The verifier's scenarios, transcript scoring, fixture rubrics, and report gates +are development verification code under `tests/evaluation/model-evals/`. The +source-owned evaluator runs alongside the compiled REA tools; it is excluded +from the shipped runtime package. + The lane runs scenarios sequentially in standalone Codex (`--no-daemon`) with a disposable HOME and CODEX_HOME under `~/.cache/rea-agent-evaluations/`. Set `REA_AGENT_EVAL_ROOT` to choose another parent directory outside the OS temporary @@ -487,7 +492,7 @@ boundary instead of stopping at Codex approval. Its scenario contract accepts one `provider_unavailable` open refusal without a close, or one successful open followed by a successful close. Other errors, an unclosed session, and a close without acquisition fail. These outcomes and the prompt are owned together in -`src/evaluation/AgentEvaluationScenarios.ts`; generic successful-tool sequences +`tests/evaluation/model-evals/AgentEvaluationScenarios.ts`; generic successful-tool sequences still apply to scenarios that require them. The provider scenario checks routing, reported limitations, and cleanup; free-form answer correctness remains unassessed. It does not validate a real native engine or authorize runtime execution. diff --git a/docs/erlang-beam.md b/docs/erlang-beam.md index b8c6837dd3..0ca020f748 100644 --- a/docs/erlang-beam.md +++ b/docs/erlang-beam.md @@ -54,6 +54,65 @@ rules apply to the last occurrence of each chunk name; an empty final occurrence does not revive an earlier payload of that name. Nonempty truncated tables remain format failures. +## Select retained facts + +Complete output remains the default. Request a summary with a `beam` view to +read a few facts in the first call while retaining the complete inspection: + +```json +{ + "name": "inspect_erlang_beam", + "arguments": { + "path": "/artifacts/app.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module_info"], + "fields": ["function_index", "function", "arity", "label"], + "limit": 20 + } + } +} +``` + +`normalized_result.page.items` contains the matching rows. `source_index` is +zero-based in the original collection and scoped to `parent_evidence_id` and +that collection. It does not replace the original one-based atom `index` or a +lambda's own `index`. Every matching function name survives, including different +arities. Names are exact retained display strings; raw indexes remain available +when names are unresolved or displayed names collide. + +Collections are `atoms`, `exports`, `imports`, `lambdas`, and `chunks`. `names` +selects atom/chunk names or function names; imports additionally support exact +`modules`. Filters intersect. `indices` selects original collection indexes; +`fields` selects that collection's own fields. Empty selector arrays match +nothing, and `fields: []` returns only `source_index`. Offset/limit page matching +rows in original order (defaults 0/64). Unfiltered pages slice the retained +collection; queries do not decode the file again. + +`beam` keeps the module, form/Code bounds, original source coverage, and table +counts. A null count means no table was decoded; zero means a decoded empty +table. Missing tables and unresolved atom names remain explicit, and an empty +page does not establish runtime absence. Page coverage is separate from the +source's examined chunks and parser budgets. + +CLI summary delivery requires a destination for the complete Evidence bundle: + +```sh +rea inspect-erlang-beam /artifacts/app.beam --detail summary \ + --view '{"kind":"beam","collection":"exports","names":["module_info"],"fields":["function","arity"]}' \ + --evidence_output /reports/app-beam.evidence.json --json +rea inspect-analysis-view '{"source":{"kind":"evidence-file","path":"/reports/app-beam.evidence.json"},"view":{"kind":"beam","collection":"imports","modules":["erlang"]}}' --json +``` + +The bundle retains the complete report. Subsequent MCP queries may use +`source: {kind: "retained-evidence", evidence_id: parent_evidence_id}` or an +Evidence file. Retained and offline views still work after the original BEAM +file is deleted. Supplying `view` requires `detail: "summary"`; omitting it +returns only the structural summary. Selection changes delivery, not parsing or +coverage, and executes no bytecode. + ## Limits and failures The selected file is limited to 64 MiB by default (512 MiB maximum). At most diff --git a/docs/index.md b/docs/index.md index 0230b11d09..1a83f408fa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md). | Task | Guides | | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) | -| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [ELF notes](elf-notes.md), [Erlang BEAM](erlang-beam.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) | +| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [ELF notes](elf-notes.md), [Erlang BEAM](erlang-beam.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) | | Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) | | Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) | | Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) | diff --git a/docs/installation.md b/docs/installation.md index 915efa7fc6..3881ba13eb 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -88,11 +88,11 @@ Setup also retains valid environment overrides in REA's existing server entry, such as `REA_MCP_INPUT_SCHEMA_PROFILE`, `REA_MCP_OUTPUT_SCHEMA_PROFILE`, and `REA_MCP_MAX_RESPONSE_BYTES`. Detected provider settings replace only matching environment keys. -Setup manages REA's launcher, selected provider settings, and supported timeout -defaults. Client approval policies remain user choices, but the current Codex -refresh drops those policies inside the REA entry; see the -[Codex preservation limitation](client-configuration.md#codex-and-devin) before -refreshing a customized registration. +Setup manages REA's launcher, selected provider settings, and recommended timeout +defaults. Codex approval policies, tool selections, and other caller-owned server +settings are preserved. Known malformed policy values block setup before it +writes the configuration or a backup; see +[Codex settings preservation](client-configuration.md#codex-and-devin). Setup and doctor report a malformed REA environment map or invalid runtime setting before setup writes files. Repair the named setting in the client configuration; setup does not silently discard the rest of the map. diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md index 4a4c622f5b..8dd1b603ff 100644 --- a/docs/javascript-application-workflows.md +++ b/docs/javascript-application-workflows.md @@ -427,8 +427,11 @@ value. Earlier `break`/`continue` paths can leave a var initializer unexecuted. Legacy non-strict block-function copies are indexed on their outer variable owner, while strict/module and lexical-shadowing boundaries keep their values. A catch-shadowed var declaration keeps declaration ownership separate from its -initializer's lexical assignment. Ordinary functions own an implicit arguments -object; arrows retain lexical arguments lookup. Promise source identity uses +initializer's lexical assignment, including object and array destructuring slots. +Projected initializer facts retain their source and alias identity on the lexical +recipient, so a later mutation through that alias prevents reporting the original +object field as an unchanged constant. Ordinary functions own an implicit +arguments object; arrows retain lexical arguments lookup. Promise source identity uses the same read-position selection and requires an unmodified intrinsic factory. Rebound values are evaluated at their read position: saving an incoming parameter diff --git a/docs/javascript-artifact-reconstruction.md b/docs/javascript-artifact-reconstruction.md index 129e5b1091..413dd696e6 100644 --- a/docs/javascript-artifact-reconstruction.md +++ b/docs/javascript-artifact-reconstruction.md @@ -101,6 +101,18 @@ caller's request and first-occurrence priority, analyzing the file once. Omit the selection for the existing complete application workflow. +For a long selection, pass the paths as a JSON array with +`--source-paths-json`, either inline or as a JSON file path. A file keeps the +command below host command-line limits, such as the 8,191-character limit of +the `cmd.exe` shim that `npx` uses on Windows. Its entries follow any +`--source-path` values and stay artifact-relative; they are not resolved +against the working directory: + +```bash +rea analyze-javascript-application /absolute/path/to/app.asar \ + --source-paths-json /absolute/path/to/paths.json --json +``` + The result and summary retain `source_selection.requested_paths` and `source_selection.unselected_files`. Selecting every relevant file leaves this count at zero and adds no unselected-source caveat. Every relevant file keeps its @@ -109,8 +121,8 @@ original inventory identity; unselected files report `text_status: coverage is partial while any file remains outside scope. Selection does not claim whole-application completeness or change integrity policy. Summary detail only reduces delivery; source selection reduces text acquisition and analysis. -Explicit native provider or snapshot routing rejects `--source-path` rather -than ignoring it. +Explicit native provider or snapshot routing rejects `--source-path` and +`--source-paths-json` rather than ignoring them. For directory input, a permission-denied child file remains an unavailable inventory occurrence, and an unreadable child directory records an unknown @@ -301,7 +313,10 @@ Selected bounded text is then parsed as inert data to recover: - local HTML script entrypoints; - Webpack and Rspack chunk registrations and module factories represented as AST literals, plus esbuild `__commonJS` module tables; recovered esbuild - CommonJS exports use its factory's `exports`-then-`module` parameter order; + CommonJS exports use its factory's `exports`-then-`module` parameter order. + Export receivers and loader helpers retain those calling-convention roles only + while the binding at the read still holds its original parameter value; + replacing a parameter does not give the replacement object that role; - static imports, dynamic imports, CommonJS `require` calls, workers, and service workers (only `new Worker(new URL(path, import.meta.url))` resolves relative to the calling module; string worker and service-worker paths use diff --git a/docs/native-investigation.md b/docs/native-investigation.md index fe2a1d5b48..125d03ea4c 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -426,8 +426,12 @@ Native target admission checks Mach-O slice headers before assigning FAT identity. A shared `CAFEBABE` prefix whose declarations do not identify Mach-O images receives the generic unsupported-format refusal, including JVM class files larger than the bounded admission probe. This does not identify a partial -prefix as a validated JVM class. Malformed recognized FAT declarations retain -their format-specific reasons. +prefix as a validated JVM class. For files sharing `CAFEBABE`, target admission +checks complete JVM framing within the same 16 MiB class identity bound as the +Mach-O image reader. A complete class remains unsupported even if its constant +pool resembles a known FAT CPU declaration. Above that bound, an ambiguous +malformed FAT interpretation remains unresolved. Malformed recognized FAT +declarations retain their format-specific reasons. `inspect_artifact` and `extract_artifact` retain universal Mach-O slices under their exact `lipo` names, including 32-bit ARM implementations such as `armv7`, @@ -464,24 +468,26 @@ numbers requiring XML type facts retain the `plutil` route. Invalid UTF-8 and encoding-declaration mismatches remain explicit failures. Direct-source provenance records `file` without claiming conversion; conversion limitations and invocations appear only when conversion actually ran. -Malformed XML structure remains an input failure; malformed scalar literals -retain their exact source text, reason and counted unknown-scalar limitation, -using the same scalar interpretation as archive readers. +XML sources must also pass native format validation, with native refusals retaining +their diagnostics and successful validation recorded in provenance. Original +unknown scalar text, reasons and limitations survive successful validation; +output-budget omissions remain omissions. Source input is limited to 64 MiB. Binary and direct XML decoding reserve an estimated 64 MiB decode budget before parsing; this is not a hard process-memory bound. JSON decode memory has not been measured and has no equivalent decode estimate. A refused input reports non-retryable `resource_constraint` with `reported_limits.maximum_input_bytes` or `maximum_decoded_bytes` as applicable. -Ordinary direct-source inspection admits 998 source nesting levels, derived from -the existing 1000-level JSON result capacity by reserving one level for typed -scalar fields and one for the result's `value` field. Deeper input reports +Ordinary direct-source decoding has a ceiling of 998 source nesting levels, +derived from the existing 1000-level JSON result capacity by reserving one level +for typed scalar fields and one for the result's `value` field. Deeper input reports `resource_constraint` with `reported_limits.maximum_nesting_depth: 998` and names -the fixed nesting allowance. Compatibility conversions can encounter an earlier -native utility refusal; their diagnostics remain explicit. Archive and Interface -Builder decoding retain their separate 128-level allowance. Worker limits and -host memory do not raise these fixed allowances, and the unchanged request fails -the same way. +the fixed nesting allowance. XML admission also requires native validation, which +can refuse input below this decoding ceiling. Compatibility conversions can +likewise encounter an earlier native utility refusal; both retain native +diagnostics. Archive and Interface Builder decoding retain their separate +128-level allowance. Worker limits and host memory do not raise these fixed +allowances, and the unchanged request fails the same way. 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 @@ -724,9 +730,10 @@ complete observation. examined type records and fully decoded declarations. Class vtable recovery reads the same descriptors, so an unreadable type record also keeps `swift_class_vtable_descriptors` partial. - Nominal declarations, conformance type references and class vtables share one - snapshot-scoped name/context reader. Conformance `type_name` and dispatch - `owner` retain enclosing named types; conformances carry the known module. + Nominal declarations, conformance type and protocol references, and class + vtables share one snapshot-scoped name/context reader. Conformance `type_name`, + `protocol_name`, and dispatch `owner` retain enclosing named types; + conformances carry the known module. Unresolved parents preserve the leaf name and a partial identity reason, while independently readable witness implementations remain available. Protocol instance properties use the same property-list reader as classes. diff --git a/docs/pe-imports.md b/docs/pe-imports.md new file mode 100644 index 0000000000..628ef07100 --- /dev/null +++ b/docs/pe-imports.md @@ -0,0 +1,73 @@ +# PE import and delay-load tables + +`inspect_pe_imports` / `inspect-pe-imports` answers whether one selected local +PE32/PE32+ image declares an `IMAGE_DIRECTORY_ENTRY_IMPORT` table, which DLLs +and functions that table names, which delay-load descriptors are present, and +the pefile-compatible import hash of the standard import table. It reads the +file without executing it and does not require a Windows SDK or disassembler. + +```bash +rea inspect-pe-imports /absolute/path/to/application.exe --json +``` + +The CLI resolves relative paths against the caller's working directory. MCP +and the advertised input schema require an absolute filesystem path and +preserve that exact selection: + +```json +{ + "name": "inspect_pe_imports", + "arguments": { "path": "/artifacts/application.exe" } +} +``` + +## Observations + +The inline Evidence result contains the artifact path, SHA-256, and byte length. +`import_directory` and `delay_import_directory` report the data directory RVA +and size, or `null` when a directory is absent or zero. Their `locations` are +the file spans actually read for the descriptor array and its terminator. When +consecutive descriptors map to discontiguous file bytes (for example across a +section boundary), each span is reported separately, and each becomes its own +Evidence file-offset range. +If the first descriptor cannot be examined, the directory declaration remains +present with no consumed `locations`, and coverage is partial. Descriptors, +thunks and NUL-terminated names stay within PE's 32-bit RVA space, including +PE32+ images; earlier valid rows survive an overflowing descriptor walk. + +Each `imports[]` item reports the DLL name, descriptor RVAs, and named or +ordinal functions with ILT/IAT RVAs. `delay_imports[]` reports RVA-based +delay-load descriptors separately. `imphash` is pefile's `get_imphash()` over +the standard import table only; delay-load rows do not participate. Ordinal +imports use pefile's ordinal name tables and otherwise its `ord` +fallback. + +`imphash.md5` is `null` with a `reason` whenever the value would not be +pefile's hash of this image: the standard import table is partial, or pefile +would read a different row sequence (names it treats as invalid or truncates, +ordinal 0, descriptors without functions, or tables its bounds and import +symbol cap truncate). Delay-load issues do not withhold the hash. + +A missing import directory is observed absence of that table. It is not proof +the image never imported APIs through another table or overlay. + +## Limits and failures + +The selected file is limited to 64 MiB by default (512 MiB maximum). At most +256 import and delay-load descriptors are examined by default (4096 maximum), +and at most 8192 functions across every table (262144 maximum). Malformed PE +headers and exhausted DLL or function budgets fail without partial success. +Decoded DLL and function names are limited to a fixed 16 MiB of retained text +with no runtime override; `max_file_bytes`, `max_dlls` and `max_functions` do +not raise it. Resource failures report the exhausted allowance as `limit` +(`dlls`, `functions` or `retained_text`) with matching recovery. + +Unreadable DLL names, hint/name entries, or thunks are reported as partial +coverage with `issues`. A descriptor whose required table RVA is zero (a +standard `FirstThunk`, or a delay-load import address or name table) is +reported as `malformed_entry` with no functions, never as a complete empty +inventory. Delay-load descriptors that store virtual addresses rather than RVAs +are recorded as `unsupported_form` and are not decoded. + +This workflow does not resolve bound imports, loaded IAT contents, or +export-forward names in another DLL. diff --git a/docs/readme/README_ar.md b/docs/readme/README_ar.md index 0b686fa96c..164e69b363 100644 --- a/docs/readme/README_ar.md +++ b/docs/readme/README_ar.md @@ -154,6 +154,7 @@ npx rea-agents@latest setup | الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](../native-investigation.md) | | بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | diff --git a/docs/readme/README_de.md b/docs/readme/README_de.md index 13f510f5cf..f3f0f40b52 100644 --- a/docs/readme/README_de.md +++ b/docs/readme/README_de.md @@ -154,6 +154,7 @@ REA benötigt Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) oder 26+ (>=26.12.1) so | Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](../native-investigation.md) | | Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | diff --git a/docs/readme/README_el.md b/docs/readme/README_el.md index 5fa717d055..fe21e44cfb 100644 --- a/docs/readme/README_el.md +++ b/docs/readme/README_el.md @@ -156,6 +156,7 @@ npx rea-agents@latest setup | Δυαδικά αρχεία Go | Ενσωματωμένη έκδοση μεταγλωττιστή, modules, αντικαταστάσεις και ρυθμίσεις μεταγλώττισης | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· δεν απαιτείται εγκατάσταση του Go | | Δυαδικά αρχεία Rust | Ενσωματωμένα ονόματα, εκδόσεις, πηγές και σχέσεις εξάρτησης των crates από το cargo-auditable | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· [δυαδικά αρχεία Rust](../rust-binaries.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | | Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) | | Αρχείο WASM | Έλεγχο εγκυρότητας, ενότητες, ακριβείς μορφές εισαγωγών/εξαγωγών σε WAT και αποκωδικοποιημένο WAT | WABT 1.0.42 που παρέχει ο χρήστης· `inspect_wasm_artifact` | diff --git a/docs/readme/README_es.md b/docs/readme/README_es.md index 9e56d60ac2..dd4b489ba2 100644 --- a/docs/readme/README_es.md +++ b/docs/readme/README_es.md @@ -154,6 +154,7 @@ REA requiere Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) o 26+ (>=26.12.1), adem | Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](../native-investigation.md) | | Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | diff --git a/docs/readme/README_fa.md b/docs/readme/README_fa.md index 51c3a11184..2b830fccbc 100644 --- a/docs/readme/README_fa.md +++ b/docs/readme/README_fa.md @@ -146,6 +146,7 @@ REA به Node.js 22.x (>=22.23.4)، 24.x (>=24.22.1) یا 26+ (>=26.12.1)، به | باینری‌های بومی | شبه‌کد، اسمبلی، رشته‌ها، نمادها، فراخوانی‌ها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](../native-investigation.md) | | ساختار ELF آفلاین | بخش‌ها، سگمنت‌ها، نمادها و relocationهای اصلی و گزینه‌های احتمالی کاهش آسیب‌پذیری ایستا | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | بایت‌کد EVM | selectorهای dispatch، offsetهای بایتی، آرگومان‌های استنباط‌شده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | diff --git a/docs/readme/README_fr.md b/docs/readme/README_fr.md index a5d7d46a1b..d33c322cd9 100644 --- a/docs/readme/README_fr.md +++ b/docs/readme/README_fr.md @@ -153,6 +153,7 @@ REA nécessite Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) ou 26+ (>=26.12.1), ai | Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](../native-investigation.md) | | Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | Artefact WASM | Validation, sections, formes WAT exactes d'import/export et WAT décodé | WABT 1.0.42 fourni par l'utilisateur ; `inspect_wasm_artifact` | | Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | diff --git a/docs/readme/README_id.md b/docs/readme/README_id.md index 808a5ae616..8715456c94 100644 --- a/docs/readme/README_id.md +++ b/docs/readme/README_id.md @@ -156,6 +156,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis: | Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](../native-investigation.md) | | Tata letak ELF offline | Bagian, segmen, simbol/relokasi asli, dan kandidat mitigasi statis | pwntools yang disediakan pemanggil pada Linux x64; [diagnostik biner](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | Artefak WASM | Validasi, bagian, bentuk WAT import/ekspor yang tepat, dan WAT yang didekode | WABT 1.0.42 yang disediakan pemanggil; `inspect_wasm_artifact` | | Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | diff --git a/docs/readme/README_it.md b/docs/readme/README_it.md index cde6e8d7f1..ed3e7a201a 100644 --- a/docs/readme/README_it.md +++ b/docs/readme/README_it.md @@ -153,6 +153,7 @@ REA richiede Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) o 26+ (>=26.12.1), oltre | Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](../native-investigation.md) | | Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | Artefatto WASM | Convalida, sezioni, forme WAT di import/export esatte e WAT decodificato | WABT 1.0.42 fornito dal chiamante; `inspect_wasm_artifact` | | Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | diff --git a/docs/readme/README_ja.md b/docs/readme/README_ja.md index 1d31db25b9..f37aa8ae52 100644 --- a/docs/readme/README_ja.md +++ b/docs/readme/README_ja.md @@ -154,6 +154,7 @@ REA には Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)、または 26+ (>= | ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](../native-investigation.md) | | オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | diff --git a/docs/readme/README_ko.md b/docs/readme/README_ko.md index 638fc6add9..8af51c5f15 100644 --- a/docs/readme/README_ko.md +++ b/docs/readme/README_ko.md @@ -156,6 +156,7 @@ REA에는 Node.js 22.x(>=22.23.4), 24.x(>=24.22.1) 또는 26+ (>=26.12.1)와 npm | Go 바이너리 | 내장된 컴파일러 버전, 모듈, `replace` 지시어, 빌드 설정 | REA 내장 ELF/PE/thin Mach-O 정적 검사; Go 설치 불필요 | | Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | | Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) | | WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` | diff --git a/docs/readme/README_ml.md b/docs/readme/README_ml.md index 8abd927804..11fbe51174 100644 --- a/docs/readme/README_ml.md +++ b/docs/readme/README_ml.md @@ -156,6 +156,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), അല്ല | Go binaries | ഉൾച്ചേർത്ത compiler version, modules, replacements, build settings | Built-in static ELF/PE/thin Mach-O പരിശോധന; Go installation ആവശ്യമില്ല | | Rust binaries | ഉൾച്ചേർത്ത cargo-auditable crate പേരുകൾ, versions, sources, dependency edges | Built-in static ELF/PE/thin Mach-O പരിശോധന; [Rust binaries](../rust-binaries.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | | Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) | | WASM artifact | Validation, sections, കൃത്യമായ import/export WAT രൂപങ്ങൾ, decoded WAT | Caller നൽകുന്ന WABT 1.0.42; `inspect_wasm_artifact` | diff --git a/docs/readme/README_pl.md b/docs/readme/README_pl.md index f0de869c65..2b509cfeee 100644 --- a/docs/readme/README_pl.md +++ b/docs/readme/README_pl.md @@ -154,6 +154,7 @@ REA wymaga Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) lub 26+ (>=26.12.1) oraz n | Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](../native-investigation.md) | | Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | diff --git a/docs/readme/README_pt-BR.md b/docs/readme/README_pt-BR.md index f1b1a6847d..26bf804f94 100644 --- a/docs/readme/README_pt-BR.md +++ b/docs/readme/README_pt-BR.md @@ -154,6 +154,7 @@ O REA requer Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) ou 26+ (>=26.12.1), alé | Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](../native-investigation.md) | | Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | diff --git a/docs/readme/README_ru.md b/docs/readme/README_ru.md index a84f158a91..90f210a068 100644 --- a/docs/readme/README_ru.md +++ b/docs/readme/README_ru.md @@ -156,6 +156,7 @@ REA требует Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) или 26+ (>=26.1 | Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | | Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) | | Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) | diff --git a/docs/readme/README_th.md b/docs/readme/README_th.md index e3b12949aa..02b428cb1a 100644 --- a/docs/readme/README_th.md +++ b/docs/readme/README_th.md @@ -154,6 +154,7 @@ REA ต้องใช้ Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) หรื | ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](../native-investigation.md) | | โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | diff --git a/docs/readme/README_tr.md b/docs/readme/README_tr.md index 8a8a447bf5..c280e828e2 100644 --- a/docs/readme/README_tr.md +++ b/docs/readme/README_tr.md @@ -154,6 +154,7 @@ REA; Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) veya 26+ (>=26.12.1) ve npm gere | Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez | | Rust ikili dosyaları | Gömülü cargo-auditable crate adları, sürümleri, kaynakları ve bağımlılık ilişkileri | Yerleşik statik ELF/PE/thin Mach-O incelemesi; [Rust ikili dosyaları](../rust-binaries.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | | Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) | | WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` | diff --git a/docs/readme/README_uk.md b/docs/readme/README_uk.md index 0703767054..7907ed37ec 100644 --- a/docs/readme/README_uk.md +++ b/docs/readme/README_uk.md @@ -154,6 +154,7 @@ REA потребує Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) або 26+ (>=26 | Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](../native-investigation.md) | | Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | diff --git a/docs/readme/README_ur-Latn.md b/docs/readme/README_ur-Latn.md index 00573989b8..0dc7604067 100644 --- a/docs/readme/README_ur-Latn.md +++ b/docs/readme/README_ur-Latn.md @@ -196,6 +196,7 @@ Mazeed tools aur host support target par depend karte hain: | Godot PCK | Archive inventory aur extraction, original header/directory facts aur selected content identities | Built-in static formats 2, 3 aur 4; Godot zaroori nahi | | Rust binaries | Embedded cargo-auditable crate names, versions, sources aur dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](../rust-binaries.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries aur PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import aur delay-load DLL names, ordinals, IAT RVAs aur pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id aur ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | | Erlang BEAM | IFF chunks, atoms, exports, imports, lambdas aur Code header | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) | | WASM artifact | Validation, sections, exact import/export WAT forms aur decoded WAT | Aapka diya hua WABT 1.0.42; `inspect_wasm_artifact` | diff --git a/docs/readme/README_vi.md b/docs/readme/README_vi.md index 283e3db3da..66f357e7ac 100644 --- a/docs/readme/README_vi.md +++ b/docs/readme/README_vi.md @@ -156,6 +156,7 @@ REA yêu cầu Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) hoặc 26+ (>=26.12.1) | Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) | | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | | Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) | | Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) | diff --git a/docs/readme/README_zh-TW.md b/docs/readme/README_zh-TW.md index 2b3984e399..ca9000bfda 100644 --- a/docs/readme/README_zh-TW.md +++ b/docs/readme/README_zh-TW.md @@ -157,6 +157,7 @@ REA 需要 Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)或 26+ (>=26.12.1) | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | | 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) | | 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) | diff --git a/docs/readme/README_zh.md b/docs/readme/README_zh.md index 6209778852..39f16fb696 100644 --- a/docs/readme/README_zh.md +++ b/docs/readme/README_zh.md @@ -157,6 +157,7 @@ REA 需要 Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)或 26+ (>=26.12.1) | Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) | | Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) | | PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) | +| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) | | ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) | | 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) | | 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) | diff --git a/docs/reconstruction-readiness.md b/docs/reconstruction-readiness.md index eec1d33dff..5c80371904 100644 --- a/docs/reconstruction-readiness.md +++ b/docs/reconstruction-readiness.md @@ -1,19 +1,25 @@ -# Reconstruction readiness conformance +# Reconstruction readiness verification -Run the source-owned native, JavaScript CLI, Electron, Evidence, comparison, -obligation, replay, and cleanup conformance verifier from a clean checkout: +Run the public static-analysis journeys and synthetic report-evaluator checks +from a clean checkout: ```sh npm ci npm run verify:readiness ``` -The command builds the redistributable native conformance fixtures, inventories -the native executable through the public CLI, analyzes the generated JavaScript -CLI and Electron fixtures through both CLI and MCP, evaluates an internal -nine-stage conformance report, and proves stale/tampered replay rejection. The -report evaluator is part of the repository verifier, not a public REA tool. It -prints one compact JSON summary. +The command builds the redistributable native conformance fixtures, inspects +the native executable through the public CLI, and analyzes the JavaScript CLI +and Electron fixture sources through both CLI and MCP. It checks matching +CLI/MCP Evidence identities for those two application analyses. The fixture +applications are not executed, and native MCP parity is not exercised. + +Separately, the command evaluates a wholly synthetic nine-stage input and +checks evaluator determinism, Evidence tampering rejection, and stale-source +digest rejection. These checks concern report evaluation; they do not establish +application runtime behavior, authority recovery, cancellation cleanup, +behavioral equivalence, or reconstruction closure for the analyzed fixtures. +The command prints one compact JSON summary. Set `REA_READINESS_REPORT_PATH` to retain the complete machine-readable report: @@ -24,15 +30,37 @@ REA_READINESS_REPORT_PATH=readiness-report.json npm run verify:readiness CI runs this command on Linux, publishes the compact summary, and retains the full report as the `reconstruction-readiness` artifact. -## Project conformance contract +## Report scope and ownership The verifier runs as `npm run verify:readiness`; it is not exposed as a public -CLI command or MCP tool. The synthetic report input and evaluator exist to -check project-owned conformance fixtures and fail-closed report behavior. +CLI command or MCP tool. Its report separates two kinds of verification: -## Required stages +- `observed_journeys` retains the actual static-analysis Evidence and the + observed CLI/MCP identity checks, with their coverage limits. +- `evaluator_conformance` retains the synthetic baseline report and the + evaluator checks. Its nine-stage `pass` describes acceptance of supplied + synthetic observations. -| Stage | Required proof | +The verifier's overall success means its listed checks passed. It is not an +end-to-end reconstruction-readiness verdict. Actual analysis Evidence is not +grafted into the synthetic report or used to close its checks. + +The retained artifact now uses `schema: "ReconstructionReadinessVerificationReport"` +and `schema_version: 1`. The previous unversioned artifact was the nine-stage +evaluator report itself. Consumers should read the two explicit sections; the +synthetic nine-stage report now lives at `evaluator_conformance.baseline_report`. +Observed CLI and MCP Evidence are retained in separate bundles under +`observed_journeys.evidence_bundles`, preserving each surface's records. + +The evaluator, schemas, synthetic input, and semantic tests live under +`tests/conformance/readiness/report/`. They are development verification code, +excluded from the compiled runtime package. The evaluator authenticates Evidence +commitments and checks supplied observations for consistency; it does not +independently observe the events described by those observations. + +## Synthetic evaluator stages + +| Stage | Required supplied observations | | ------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `discover-classify` | target-free startup, artifact inventory, compatible workflow selection, and exact visible limits | | `diagnose-environment` | broken runtime separated from analysis failure, version skew detected, and revision changed after recovery | @@ -44,13 +72,13 @@ check project-owned conformance fixtures and fail-closed report behavior. | `verify-reconstruction-closure` | incomplete reconstruction detected, incremental verifier closure, and authority-delegating candidates rejected | | `export-replay` | CLI/MCP parity, process/structured failure parity, deterministic replay, tamper rejection, and stale-input rejection | -Every stage and required check must occur exactly once and link to Evidence in -the embedded Evidence bundle. `fail`, `unknown`, `unsupported`, `truncated`, -and `skipped` never aggregate to `pass`. +Within the synthetic input, every stage and required check must occur exactly +once and link to Evidence in its embedded bundle. `fail`, `unknown`, +`unsupported`, `truncated`, and `skipped` never aggregate to `pass`. ## Fail-closed rules -The evaluator rejects or withholds readiness when: +The evaluator rejects or withholds a passing synthetic report when: - a bounded capability omits its effective numeric limits; - a fixture lacks exactly one compatible selected workflow; @@ -63,8 +91,8 @@ The evaluator rejects or withholds readiness when: - contradiction Evidence or its affected comparison is missing; - the reconstruction obligation ledger is not `ready`, has no required obligation, or retains an unowned/unverified required obligation; -- a process exceeds its declared target/lifecycle, cancellation loses diagnostics, or owned - resources remain; +- a denied or cancelled grant reports a process launch, a cancelled cleanup + observation omits diagnostic Evidence, or owned resources remain; - replay is nondeterministic, tampering/staleness is not detected, or the expected source digest differs. @@ -95,19 +123,27 @@ Differing transport status is checked on each observation separately. ## Fixtures -The verifier uses only source-owned, redistributable fixtures: +The static-analysis journeys use source-owned, redistributable fixtures: -- `tests/conformance` native executables with stable symbols and deliberate - version divergence; +- `build/conformance/c`, built from `tests/conformance/c/fixture.c`, with stable + symbols and strings; - `tests/conformance/readiness/javascript-cli`, which exposes prompts, alternate-screen behavior, configuration precedence, child process, persistence, loopback HTTP, and signal teardown to static reconstruction; - `tests/conformance/readiness/electron`, which exposes main, preload, renderer, utility, IPC, renderer restart, blocked shell effect, and deep-link - surfaces; -- the shared incomplete reconstruction and broken-runtime observations in - `RECONSTRUCTION_READINESS_EXAMPLE`. + surfaces to static analysis. -The synthetic example is a public typed input, not a global equivalence claim. -It demonstrates that every deliberate fault remains represented in the final -report and that only comparable Evidence can close the associated obligation. +The separate `RECONSTRUCTION_READINESS_EXAMPLE` supplies synthetic incomplete +reconstruction, broken-runtime, authority, comparison, cleanup, and closure +observations. Its semantic tests preserve malformed-Evidence rejection, +fail-closed stage results, contradiction handling, repeated CLI/MCP outcomes, +and closure-history consistency. Run those source tests independently: + +```sh +npm run test:local -- tests/conformance/readiness/report/reconstructionReadiness.test.ts +``` + +Passing evaluator tests does not establish real-provider or fixture runtime +behavior. Use the applicable [real-toolchain lanes](testing.md#real-toolchain-verification-lanes) +for those claims. diff --git a/docs/releasing.md b/docs/releasing.md index 44081875bb..72b51e5cb4 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -90,6 +90,23 @@ releases are outside this recovery path. For a release that needs an independently frozen application checkpoint, use the optional manual path below. It retains explicit prepare and publish phases. +Automatic main proposals and manual checkpoint preparation use +`scripts/release-proposal.mjs` with the pinned release-please dependency. +The controller applies `patches/release-please+17.3.0.patch` only when invoked; +normal dependency installation and published-package installation do not apply it. +The patch avoids commit-file backfills and path splitting for a single root Node +package with the default changelog, default or always-bump-minor versioning, +no path exclusions and no explicit plugins. Other configurations retain the +upstream file attribution workflow. Associated PR metadata and commit overrides +still come from GitHub. The controller sizes commit search from the complete +selected Git history, and a missing known release boundary fails before proposal +creation. Publication continues through the pinned release-please action. + +When upgrading release-please, review the upstream manifest implementation, +refresh the versioned patch and run the release proposal and workflow boundary +tests. Version mismatch or a patch that neither applies cleanly nor matches an +already-applied patch fails closed. + ## Contributor-aware release notes Use [the rea-changelog-update skill](https://github.com/morluto/rea/blob/main/.agents/skills/rea-changelog-update/SKILL.md) diff --git a/docs/sqlite-databases.md b/docs/sqlite-databases.md index 418a321536..f089347bbd 100644 --- a/docs/sqlite-databases.md +++ b/docs/sqlite-databases.md @@ -184,6 +184,11 @@ retains the completed Evidence and the resource owner for cleanup retry. MCP records that Evidence before delivering the error, which carries its retained evidence ID for later retrieval or export. +If a worker fails and cleanup remains incomplete, the error retains the original +cancellation, timeout, diagnostic overflow or memory failure. Successful cleanup +retry restores that failure's classification; it does not make the inspection +successful. + Changed input identities, special files, malformed databases, unsupported formats and resource failures are reported as errors with the selected path and meaningful reason. Worker resource refusals expose the caller-selected file in diff --git a/docs/testing.md b/docs/testing.md index 770271d76f..20e18e4636 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -169,14 +169,15 @@ rendered declarations. Keep canonical rejection checks separate from the intentionally reduced compact advertisement, and probe actual client renderers when changing schema representations. -The reused CLI boundary workers cap V8 old space at 384 MiB for ordinary runs -and 512 MiB with coverage enabled. Covered ADB fixtures exhaust the ordinary -384 MiB budget; those fixtures also complete with the shared CLI graph preloaded -at 512 MiB. This is a measured capacity control, not a suite-wide speedup claim. -Both profiles keep the -same bounded worker count, and spawned REA processes retain their own heap -settings. Explicitly disabling coverage retains the ordinary profile; coverage -provider options alone do not enable instrumentation. +The reused CLI boundary workers cap V8 old space at 512 MiB, with or without +coverage. Ordinary CI also exhausted the former 384 MiB budget after earlier +files loaded the shared source/contract graph, while later command registration +and schema/help rendering still needed transient allocation space. This is a +measured capacity control, not a suite-wide speedup claim. The worker count +remains bounded at two, and spawned REA processes retain their own heap settings. +Coverage enablement still controls instrumentation, test deadlines and coverage +thresholds; explicitly disabling it or selecting only a provider does not enable +instrumentation. Vitest worker limits do not bound subprocesses launched within a test. Bound those batches separately so repeated CLI validation cannot oversubscribe the @@ -233,13 +234,16 @@ the next command starts. IL2CPP metadata, and dylib-resolution commands use that session. File teardown stops the shared CLI process, including startup still in flight, and does not open another after close. -`exportedFunctionBindingsCli.test.ts` keeps its own compiled CLI parity -process for the file and still compares that process's JSON command results -with the MCP session. A command that does not respond is rejected. That -process stays owned until it exits, and another process does not start while -it remains. A process that stays running after SIGKILL remains owned so a -later close can retry. Closing the session reports a failure when the process -does not exit after stdin closes, once the child has been stopped. +`exportedFunctionBindingsCli.test.ts` uses the shared compiled CLI session for +its file and compares its JSON command results with the MCP session. It selects +20-second startup and command deadlines; other callers retain their existing +deadline behavior. Retired processes stay owned until exit, block replacement, +and remain available for a later close to retry after failed termination. +Shared session cleanup closes stdin so the worker can release JavaScript +resources, waits the bounded grace, then escalates to SIGTERM and SIGKILL only +if needed. Confirmed exit succeeds; an unconfirmed exit reports cleanup failure. +When startup or a command fails and cleanup also fails, the reported aggregate +retains both failures and the original failure as its cause. `applicationWorkflowCli.test.ts` keeps one compiled CLI process for its JSON commands, including a non-zero status whose stdout is still a JSON document, and closes that process after the file. @@ -545,12 +549,16 @@ uses the same ownership rule to find retained fixtures under a private temporary root. Third-party caches remain outside that ownership rule. Real Hopper, Ghidra, IDA, browser, package, and managed-code claims belong to their -explicit `npm run verify:*` lanes. The reconstruction-readiness lane also -checks deterministic rerun, tamper, and stale-input handling; those checks do -not execute extracted JavaScript modules. When application runtime behavior is -needed, exercise the actual target through browser, Electron, or process -capture. Real model trials are manual; Vitest covers deterministic evaluator -logic. +explicit `npm run verify:*` lanes. The reconstruction-readiness lane separates +observed native CLI inspection and JavaScript/Electron static CLI/MCP analysis +from synthetic report-evaluator conformance. Its determinism, tamper, and +stale-source checks concern the synthetic evaluator; they do not execute fixture +applications or verify runtime authority, cleanup, comparison, or reconstruction +closure. The evaluator and its semantic tests belong to +`tests/conformance/readiness/report/`, outside the shipped runtime. When +application runtime behavior is needed, exercise the actual target through +browser, Electron, or process capture. Real model trials are manual; Vitest +covers deterministic evaluator logic. `verify:managed` runs the portable PE byte-fixture conformance entrypoint under `scripts/verify/managed/`, with its byte builder under @@ -1695,6 +1703,15 @@ See [Packet capture analysis](pcap-analysis.md) for contracts and budgets. ## Apple native metadata and UI +`tests/boundary/mcp/nativeDispatchBoundTarget.test.ts` uses the production +stdio MCP server on macOS to bind an inert, self-authored Mach-O, then replace +its path with missing, directory and symlink-loop states. Each refusal must +report `artifact_changed`; restoring the same bytes must succeed in the same +session. A separate non-root case checks real read permission denial as +`access_denied`. These tests read the target without executing it. Controlled +provider tests retain the physical acquisition failure through descriptor close +denial and confirm cleanup retry releases the original owner. + `npm run verify:apple-dispatch` compiles the Objective-C fixture (classes, protocols, a property and an `NSString` category) and the Swift conformance/vtable fixture. diff --git a/docs/tool-design.md b/docs/tool-design.md index cb78da95a5..d3c0f9fe1f 100644 --- a/docs/tool-design.md +++ b/docs/tool-design.md @@ -201,8 +201,10 @@ providers need not expose identical state machines. [`finishAnalysisCommand`](https://github.com/morluto/rea/blob/main/src/cli/finishAnalysisCommand.ts) where its contract applies rather than duplicate close/finally handling. - Give descriptor-backed reads an owner before opening the file. A rejected - close must retain the exact handle and preserve any earlier failure. If an - abortable caller stops waiting, keep the underlying work in that owner's + close must retain the exact handle and preserve any earlier failure. + Stable file reads expose completed values separately from cancellation so + consumers retain acquired identity facts after successful close or cleanup retry. + If an abortable caller stops waiting, keep the underlying work in that owner's lifetime so shutdown cannot report release before finalization settles. Admit the entire inspection once, including later reads and finalization. Helpers must borrow that admission or use an operation child scope; re-entering @@ -254,6 +256,10 @@ File-error wrappers retain the format owner's specific diagnostic and recovery text. Strict JSON file input, for example, keeps the byte-order-mark explanation and UTF-8 recovery through CLI/MCP projection rather than replacing it with generic invalid-file advice. Consumers do not classify parser message text. +When an artifact boundary falls back to an I/O error, retain the original cause +on the typed error so provider callers can inspect its filesystem evidence. +Keep public error fields under the canonical projector's ownership; retaining +a cause does not require exposing it in transport output. Update canonical contracts, output schemas, examples, generated artifacts, and relevant docs together. Verify converted advertised schemas and success, diff --git a/docs/user-visible-errors.csv b/docs/user-visible-errors.csv index c69819462f..ab9bb79967 100644 --- a/docs/user-visible-errors.csv +++ b/docs/user-visible-errors.csv @@ -119,3 +119,4 @@ Then rerun this installer.",implemented,verified by shell branch inspection 90.7,src/ghidra/GhidraProviderClient.ts:projectPrimarySessionError,One procedure's decompile reaches REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS,"Ghidra decompilation of {entry} exceeded the {N}-second limit set by REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS. Only this procedure's decompilation was stopped; the Ghidra session remains usable.",Low risk; names the setting and keeps the session,Keep current copy,implemented,provider projection test and real-provider lane 91,src/application/binaryDiagnostics/ByteEntropyService.ts,Invalid file or window,"Expected a regular file.; Selected file could not be found.; Selected byte window starts beyond the end of the file.; Selected byte window extends beyond the end of the file.; File size exceeds exact numeric offset representation.",Invalid selection cannot produce complete entropy Evidence,Choose a readable regular file and a window within its byte size,implemented,entropy service and MCP boundary tests 92,src/domain/analysisView/byteEntropyView.ts,Non-summary entropy view,Byte entropy Evidence supports the summary view; retrieve parent Evidence for the complete block series.,Unsupported view selection,Use the summary view or retrieve complete parent Evidence,implemented,typed input rejection +93,src/windows/WindowsNativeLoader.ts:load refused,Windows refuses to load the digest-verified native addon (for example Smart App Control or App Control policy),"REA Windows native controls are unavailable: Windows refused to load the verified native/windows/build/rea-windows-x64.node: {OS error} The file matched the package manifest, so restoring or reinstalling it will not help. ...",Previous artifact copy told users to restore or rebuild files that were already intact,"Name the OS refusal, say restoring cannot help, state that Ghidra and JADX need native controls while setup can still register clients, and point to a policy-allowed host or a maintainer-signed build",implemented,simulated loader refusal unit test; real Smart App Control block not reproduced diff --git a/docs/verification/javascript-source-paths-json-2026-10-24.md b/docs/verification/javascript-source-paths-json-2026-10-24.md new file mode 100644 index 0000000000..4f6e61f077 --- /dev/null +++ b/docs/verification/javascript-source-paths-json-2026-10-24.md @@ -0,0 +1,86 @@ +# JavaScript source-selection JSON verification — 2026-10-24 + +## Scope and integration + +PR #5029 adds `--source-paths-json` to the two static application CLI routes. +One inline JSON array or selected local JSON file appends exact inventoried +paths after repeated `--source-path` values. The application `source_paths` +contract, ordering, duplicate requests, artifact identity and MCP contract are +preserved. This does not add folders, globs or implicit dependencies; #5048 is +separate. + +The CLI adapter reuses the existing JSON file reader and domain array schema. +Entries remain artifact-relative. It preserves cancellation and shared file +ownership rather than introducing another reader/parser. The native lane +refuses source-selection options before starting an engine. Main integration +retains existing delivery validation, the caller's summary/view settings, +environment selection and process-owned JavaScript worker cleanup. + +The initial main `7f70797b` integration passes **44** selected CLI, filesystem +JSON-input and actual SDK source-selection checks, plus full `npm run check`. +The JSON-file acceptance case now analyzes an ASAR and verifies the original +package's path and SHA-256; the inline case covers an extracted directory. +Both combine repeated and JSON entries and preserve their requested order. +Invalid shapes, empty entries, native-lane refusal and cancellation are covered. +This is static source analysis; the selected application is not executed. + +## Completed-task comparison + +Compare main `7f70797b98c226009163358f91a9cfcedf03b390` with feature integration +`e886393ae698d4ed411179430c8dbb10ce6c5647`. Three alternating pairs analyze the +same ASAR, selecting `package.json` followed by 210 named JavaScript files. +The baseline repeats 211 path options; the candidate uses one repeated option +plus an existing JSON file with the other 210 paths. All **six** tasks finish. + +Every complete Evidence object is exactly equal across all runs, including its +ID, selected paths, provenance, coverage, uncertainty and limitations. Each +result reports 210 parsed JavaScript files, no unselected files, and retains all +210 expected named functions. The ASAR remains 75,620 bytes with SHA-256 +`f1089a49e40f4477e0850787654411c0ec208934262837a19e65c2aa4dc5f035`. + +| Completed CLI task cost | Repeated options | Existing JSON file | +| ----------------------------------------- | ---------------: | -----------------: | +| Displayed invocation characters | 17,977 | 201 | +| Estimated complete Evidence stdout tokens | 1,861,409 | 1,861,409 | +| Estimated invocation tokens | 5,078 | 62 | +| Estimated stdout + invocation tokens | 1,866,487 | 1,861,471 | +| Elapsed median s | 19.149 | 18.389 | +| Waited-child CPU median s | 18.250 | 17.340 | +| Sampled family RSS median MiB | 601.4 | 594.1 | + +This solves command-line capacity without reducing the complete analysis +result. Microsoft documents an +[8,191-character Cmd.exe command-line limit](https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/command-line-string-limitation). +The ASCII fixture's displayed invocation crosses that limit before the change +and stays below it with a file. This is an inference from the documented limit; +the completed tasks ran on Linux, not through Windows Cmd.exe or `npx`. + +The caller-provided selection file already exists in this timed task; its +reading/validation is included. If an agent must create it first, the file +contents add an estimated **4,411 tokens**, plus an extra write call's framing +and execution costs, which are unmeasured. Do not treat the shorter invocation +as a whole-workflow token saving in that scenario. File creation, discovery, +prompts, rendering and client JSON parsing are outside the measured scope. + +No speedup is claimed: elapsed ranges are 19.142–19.841 s before and +18.277–21.384 s after; CPU ranges are 18.140–18.821 s and 17.278–21.026 s. +The ranges overlap and include a slower candidate run. Tokenx 1.3.0 estimates +stdout and invocation text; these are not billed/model token counts. + +Each task starts a fresh Node 24.22.1 CLI and JavaScript worker with CPU affinity +0,1. OS caches were not flushed and the host is shared. These are fresh-process +runs with uncontrolled warm filesystem caches; cold-disk and warm persistent +worker costs are unmeasured. CPU is waited-child user plus system time. RSS is +a 50 ms recursive process-family sample, which can double-count shared pages +and miss short-lived or unobserved descendants. Token estimation and equality +checks run outside timing. Later main changes are outside these pinned costs. + +## Final integration checks + +Integration with main `cd10ee1f8a8e124be4864e77750a16e76c561809` passes **58** +checks, extending the initial selection with active JSON-read cancellation, +process-level error status and existing route/delivery coverage. This takes +167.935 s, 182.796 s waited-child CPU and 2,056.1 MiB sampled family RSS; these +are suite costs rather than analyst-task costs. Full `npm run check`, +`npm run docs:check` and test discovery pass. These selections overlap the +earlier checks. Later upstream integrations remain outside the cost comparison. diff --git a/docs/verification/pe-imports-2026-10-24.md b/docs/verification/pe-imports-2026-10-24.md new file mode 100644 index 0000000000..439a3f038a --- /dev/null +++ b/docs/verification/pe-imports-2026-10-24.md @@ -0,0 +1,255 @@ +# PE import inventory verification — 2026-10-24 + +The analyst task is to list a local PE's standard and delay imports, keep source +locations and partial facts, and obtain a pefile-compatible imphash only when +the retained standard import sequence supports it. CLI and MCP use the same +application service and static parser. Targets are read without execution. + +## Revisions and ownership + +- Original PR: `0b65cbcfe2bf78a808d41e12fa5aee9c0fc93da4`. +- Main integration and mapping index: `336f83cbb08dcc02a2d4c932f9c82f2c7b83554e`. +- Measured baseline: `073e2612e783dc38d3603cd7dcc9dc721c0d9412`, the preceding + integration with provider identity imports and artifact-failure classification + repaired so the runtime builds. The measured candidate uses the same provider. +- Measured candidate: `105f4ea66ebc82e559fe1000aabe75ced3024a41`. + +`readPeImage` owns machine, image base, directories, alignments and declared raw +section pointers. The import parser consumes those facts rather than reading the +PE header again or substituting a missing base with zero. The shared mapping +index preserves canonical ambiguous/unmapped failures and adjacent mappings. +Its construction is O(n log n), with O(log n) nonempty range queries; both normal +and failed lookups avoid a linear fallback. Indexed and canonical CString reads +share one implementation and can reconstruct strings across physical sections. +These structures belong to one parse, with no cross-request cache or invalidation. + +Derived ILT/IAT slots are checked against the 32-bit RVA space before a row is +returned. A malformed table preserves preceding valid rows and reports partial +coverage instead of failing the output schema. Named thunks are checked before +conversion from bigint, retaining the exact source hex when reserved high bits +are set. The format's named hint/name pointer uses 31 bits; see the +[Microsoft PE format specification](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#import-lookup-table). + +Early cancellation preserves the caller's reason before input validation in +PE imports and the two adjacent certificate/ELF-note services. Artifact failure +classification consumes the shared owner's origin facts and regular-file error +predicate; cleanup ownership and retry remain with `ArtifactResourceScope`. + +## Independent oracle and regressions + +The oracle is pefile **2024.8.26**, installed without dependencies into the +temporary verification directory. Its `pefile.py` SHA-256 is +`0d0eb68f0f169182613dc64b3ab50b20855508c1c1d2faafe8fccf23edb6a345`. +The release's upstream commit is +[`4b3b1e2e568a88d4f1897d694d684f23d9e270c4`](https://github.com/erocarrera/pefile/tree/4b3b1e2e568a88d4f1897d694d684f23d9e270c4). +All **696** copied ordinal names match its three complete maps: 425 oleaut32, +196 ws2_32 and 75 wsock32. Own-key lookup and `ord` fallback are retained. +The upstream MIT license is preserved in `licenses/` and the notices inventory. + +Independent byte fixtures establish these differences: + +- A cross-section function name is `Function128` in native mappings; pefile + reads `Functi`. A cross-section DLL name is `KERNEL32.dll` natively and + `KERNEL32.d` in pefile. Complete native facts remain useful, while hashing them + would produce a different valid-looking MD5. +- pefile stops after 2,048 section headers. With imports in section 2,049, native + parsing retains both functions while pefile finds no import table. +- pefile also stops at a null section header or a section with at least three + of its structural warnings. Native parsing can still see a later import table. + +The candidate checks byte-mapping compatibility from the parsed owner facts, +including pointer/address normalization, section clipping and those stop rules. +Unproven geometry withholds only the hash, with an explicit reason. Delay-only +issues do not invalidate the standard table's hash. This is conservative +compatibility checking, not universal qualification of every unusual PE layout. + +PE32/PE32+ IAT overflow and exact wide-thunk regressions fail on the old producer. +The three section-stop cases fail on the precursor before its stop guard was +added; the independent oracle sees no imports in each. All pass after correction. +Each of the three early-cancellation services fails its null-reason control +before the fix; afterward both null and caller-object identity are preserved. +The existing retained-text failure already propagates through its tagged error +and is not claimed as a newly fixed defect. + +Executed checks at the candidate stages: + +- **58** parser/mapping/image/cancellation checks pass. Randomized mapping + comparisons use the canonical mapper as an independent semantic oracle. +- **172** selected domain/service/filesystem/CLI/MCP/artifact-lifecycle checks + pass before the final section-stop guard; sets overlap and are not added. +- The final parser file passes after the stress fixture's header/section ranges + are made disjoint. **5** public CLI cases then pass, including both IAT widths, + a complete cross-section name, section-cap and null-section hash refusals. +- Full static gates pass after provider integration and after the compatibility + guard. The initial missing artifact-failure API compile error was corrected. + +The public CLI checks parse actual responses with the named output contract. +MCP tests exercise the real SDK and advertised schema. The artifact-consumer +suite covers physical read errors, cancellation, cleanup failure and retry with +the PE provider included. No deep native engine or Windows runtime was newly +qualified by these static-format checks. + +## Shipped PE artifacts + +The two launchers come from the official +[pip 24.0 wheel](https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl), +SHA-256 `ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc`. +They were extracted as analysis artifacts, without installation or execution. + +| Artifact | Bytes | SHA-256 | Oracle result | +| -------- | ------: | ------------------------------------------------------------------ | --------------------------------------------------------- | +| t32.exe | 97,792 | `6b4195e640a85ac32eb6f9628822a622057df1e459df7c17a12f97aeabc9415b` | 2 DLLs / 85 functions; `5e24f42b46c247f13d78f0f21a4a2bf7` | +| t64.exe | 108,032 | `81a618f21cb87db9076134e70388b6e9cb7c2106739011b6a51772d22cae06b7` | 2 DLLs / 86 functions; `c51d659b4b1142d4af3795d09f1d63f7` | + +For both launchers the ordered DLLs, names, ordinals, hints, IAT RVAs, machine, +bitness and imphash agree with pefile. The benchmark verifies the complete named +rows, not merely the function count or MD5. + +## Completed-task costs + +Three alternating pairs per fixture execute one fresh public CLI process: +`rea inspect-pe-imports PATH --json`. All **24** tasks complete. Complete Evidence +objects are equal across all six runs of each fixture, including provenance, +coverage, limitations, locations and identity; nothing is stripped for equality. +Artifact digests remain unchanged. The stress fixture contains 2,048 independent +sections and 2,048 functions, SHA-256 +`0540676161eb7408805277a75e4d47e3d752732b6d5f294c59c8c670c7c1e905`. +The empty fixture is 3,072 bytes, SHA-256 +`b32058d9121313554af54bc0aed762871d3d8eb5e2d9eeab6a365f0518c6f897`. + +| Complete CLI task | Estimated stdout + invocation tokens, before → after | Elapsed median s | CPU median s | Sampled family RSS median MiB | +| -------------------------- | ---------------------------------------------------: | ---------------: | ------------: | ----------------------------: | +| No imports | 676 → 676 | 1.817 → 1.867 | 2.157 → 2.219 | 364.6 → 369.9 | +| Shipped PE32 | 3,678 → 3,678 | 1.868 → 1.867 | 2.201 → 2.221 | 365.2 → 366.4 | +| Shipped PE32+ | 3,715 → 3,715 | 1.868 → 1.868 | 2.238 → 2.220 | 367.1 → 370.3 | +| 2,048 sections / functions | 70,342 → 70,342 | 1.969 → 2.069 | 2.413 → 2.482 | 388.4 → 391.2 | + +Tokens use tokenx 1.3.0 estimates after timing, not actual model usage. The task +uses known CLI commands without catalog/schema/help or continuation calls. +Prompts, model framing, rendering and client JSON parsing are unmeasured. +Node is 24.22.1 on Linux 6.1.0-54 x86-64, affinity CPUs 0–1, on a shared host. +Every process is fresh; OS caches were not flushed and had prior oracle reads. +There is no warm provider-cache measurement. Build/setup costs are separate. + +Elapsed ranges overlap, and the 50 ms profiler poll quantizes short-task timing; +no speedup is claimed. The largest case's median increases about 0.10 s elapsed, +0.07 s CPU and 2.9 MiB RSS for the additional correctness checks. RSS is the +50 ms observed sum across descendants: shared pages may be counted twice and +short/unobserved processes missed. CPU accounts for waited children. No resource +stop occurred. The mapping index was already in the baseline, so these numbers +do not isolate its speedup or qualify a 65,535-section worst case. + +## Main integration verification + +Integration `a5a1f919e896488d0651e621f334efec84e9132f` includes main +`044c9705eadf62f7c47ddd42a0b2dc065b26b228`. The PE refinement is a type +intersection with main's new container union; runtime PE interpretation is +unchanged. The newly added Roman Urdu README receives the same PE imports link +as the canonical table. The full static gate passes; documentation/license +validation and test discovery pass after this link correction. The complete +selected parser/mapping/image/service/filesystem/CLI/MCP/lifecycle selection +passes **201** checks in 54.062 s (60.363 s waited-child CPU). Sets above overlap. +Later main-integration performance was not remeasured; the cost table remains +pinned to its recorded revisions. These tests use built-in static parsing, not +a simulated claim of deep-engine behavior. + +## Additional RVA review and independent ordinal control + +The pre-merge review found a real missing boundary: PE32+ virtual mappings could +carry descriptors, their terminator, or a DLL name beyond the 32-bit RVA end. +Five new controls fail before correction (four standard/delay directory cases +and one NUL beyond the RVA end). The producer now owns one RVA-span predicate +used by primitive reads and partial row admission. CString reads include only +bytes within that space. A name whose NUL is the final legal byte still passes. + +An unexamined first descriptor retains the declared directory with zero consumed +locations and partial coverage. The schema represents that observation; the +service rejects an empty consumed-span list claimed as complete. Four public +CLI cases and the real MCP SDK/advertised AJV schema exercise both zero-span and +preceding-row outcomes. The expanded selected workflow passes **213** checks +in 71.408 s (71.595 s waited-child CPU). The full static gate passes. The earlier +cost table remains pinned to its revisions; these later RVA checks were not +included in that first measurement; the separate comparison below covers them. + +The ordinal-mask suggestion was tested rather than applied: installed pefile +2024.8.26 itself uses `addr_of_data & 0x7FFFFFFF` in its ordinal corruption +heuristic, including PE32+. Independent images with both ILT/IAT words rewritten +produce these results: + +| Raw PE32+ thunk | pefile result | Native result | +| ------------------ | ----------------------------------------------------------- | -------------------------------------- | +| `8000000100000001` | ordinal 1 + ExitProcess; `72d9cd67504bb3b9f29c37a226bf4331` | identical, complete raw thunk retained | +| `8000000000010001` | import table discarded; empty hash | hash withheld | +| `8000000000000001` | ordinal 1 + ExitProcess; `72d9cd67504bb3b9f29c37a226bf4331` | identical | + +All three controls already pass before the RVA correction. Expanding the mask +to 63 payload bits would diverge from this pinned pefile behavior. The reason +text now specifies the low 31-bit payload; the raw thunk remains available, and +matching pefile does not claim loader validity for reserved bits. + +## RVA-guard completed-task follow-up + +To measure the final guard separately from other integration changes, repeat +three alternating pairs of the same four completed CLI tasks, now comparing +`a7406eca8137c9fae58a12cb8354f042047f5045` against +`feda57ae4ef420bc72e8e04393b75810da86cc3c` on their common main integration. +All **24** tasks complete. All complete Evidence objects match across each +fixture's six runs, and every ordered inventory field still matches the +independent pefile oracle. Artifact digests stay unchanged; stderr is empty. +The token estimator, invocation/discovery scope, process/cache conditions and +profiler limitations are the same as the earlier measurement. + +| Complete CLI task | Estimated stdout + invocation tokens, before → after | Elapsed median s | CPU median s | Sampled family RSS median MiB | +| -------------------------- | ---------------------------------------------------: | ---------------: | ------------: | ----------------------------: | +| No imports | 676 → 676 | 2.220 → 2.019 | 2.383 → 2.382 | 367.6 → 364.0 | +| Shipped PE32 | 3,678 → 3,678 | 1.918 → 1.918 | 2.253 → 2.203 | 366.8 → 367.9 | +| Shipped PE32+ | 3,715 → 3,715 | 1.973 → 1.968 | 2.363 → 2.293 | 368.9 → 369.3 | +| 2,048 sections / functions | 70,342 → 70,342 | 2.825 → 2.221 | 2.423 → 2.428 | 382.9 → 389.0 | + +No speedup is claimed. In the largest case, elapsed ranges are 2.019–3.085 s +before and 2.019–2.776 s after. CPU medians differ by about 0.004 s; sampled +RSS increases about 6.1 MiB (1.6%). Shared-host variance dominates the apparent +elapsed improvement. This validates the measured completed tasks, not a maximum +format-size or transport-buffer qualification. Later main changes remain +outside these pinned measurements. + +## Coverage consistency + +Three new service controls reject contradictory standard-import, delay-import, +and complete descriptor counts; all three fail before the correction. The +service now compares counts with the retained rows before issuing Evidence. +Partial results may still report more examined descriptors than retained DLLs, +as verified with an unsupported delay descriptor. Complete results must agree +and carry no issues. Existing function-count traversals are reused; the change +adds no further row scan. These validation checks postdate the pinned cost +measurements above. + +The expanded selection passes **217** checks in 73.179 s (70.360 s +waited-child CPU), with a sampled process-family peak of 2,039.3 MiB. Full +`npm run check` passes. These counts overlap earlier selections. A subsequent +main integration at `7f70797b98c226009163358f91a9cfcedf03b390` includes the +upstream plist-fixture correction for the three unrelated native-provider +failures seen in CI run 40055148308. + +## Descriptor mapping stops + +A later review identified the same partial-result boundary when a descriptor's +uint32 RVA is legal but lacks a file mapping. Four controls (standard/delay, +first descriptor/after one row) all threw before correction. Both walks now +use one descriptor admission function: range failures and mapping failures +append explicit issues, then preserve the directory declaration, consumed +spans and preceding rows. Only mapping failures are caught; unrelated failures +continue to propagate. Standard-table failures withhold imphash; delay-table +failures preserve an independently complete standard hash. + +The public CLI covers all four mapping-stop cases alongside the RVA cases. +The actual MCP SDK and advertised output schema cover an unmapped first +descriptor. These later validation changes are outside the pinned performance +comparisons above; no additional speed or maximum-size claim is made. + +Final integration at main `7f70797b` passes **277** selected checks: the +expanded PE/shared lifecycle selection plus native-provider tests that cover +the upstream plist-fixture correction. This takes 126.978 s, 127.567 s +waited-child CPU and 2,061.9 MiB sampled process-family RSS. These are test +suite costs, not analyst-task costs. Full `npm run check`, `npm run docs:check` +and test discovery pass; discovery accounts for all 2,323 files exactly once. diff --git a/docs/windows-ghidra-p0.md b/docs/windows-ghidra-p0.md index d5d8881aea..c98103633d 100644 --- a/docs/windows-ghidra-p0.md +++ b/docs/windows-ghidra-p0.md @@ -80,8 +80,11 @@ Windows x64 package; changing MCP registration cannot supply that artifact. A filesystem or access failure identifies the failed native constraint. An incompatible host uses `unsupported_host`. Missing, mismatched or unloadable native artifacts use `runtime_missing` and doctor classification -`missing_dependency`. A failed native invocation or invalid inspection uses -`runtime_unavailable` and doctor classification `runtime_failure`; it does not +`missing_dependency`. A verified artifact that Windows refuses to load, for +example under Smart App Control or an App Control policy, reports failure stage +`load`; restoring or reinstalling the file cannot fix it. A failed native +invocation or invalid inspection uses `runtime_unavailable` and doctor +classification `runtime_failure`; it does not mean Windows x64 itself is unsupported. Provider diagnostics retain the failure stage under `windows_native_failure`, together with the corresponding remediation. Self-test failures preserve the native operation and OS error text. Check the diff --git a/experiments/analysis-efficiency/byte-entropy-costs.py b/experiments/analysis-efficiency/byte-entropy-costs.py new file mode 100644 index 0000000000..60050574e7 --- /dev/null +++ b/experiments/analysis-efficiency/byte-entropy-costs.py @@ -0,0 +1,94 @@ +"""Verify 18 completed entropy tasks and report whole-task resource/token costs.""" +import argparse +from pathlib import Path +import hashlib, json, statistics +import tiktoken +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--receipts', type=Path, required=True) +parser.add_argument('--output', type=Path, required=True) +options = parser.parse_args() +b = options.receipts +cohort = b / 'cohort' +index = json.loads((cohort / 'index.json').read_text()) +assert len(index) == 18 and all((r['exit'] == 0 for r in index)) +assert len({item['name'] for item in index}) == 18 +enc = tiktoken.get_encoding('cl100k_base') +compact = lambda d: json.dumps(d, ensure_ascii=False, separators=(',', ':'), allow_nan=False) +count = lambda s: len(enc.encode(s, disallowed_special=())) +digest = lambda p: hashlib.sha256(p.read_bytes()).hexdigest() +rows = [] +for item in index: + p = cohort / item['name'] + r = json.loads((p / 'report.json').read_text()) + catalog = json.loads((p / 'catalog.json').read_text()) + tx = json.loads((p / 'transcript.json').read_text()) + scope = next((json.loads(l) for l in reversed((cohort / (item['name'] + '.log')).read_text().splitlines()) if l.startswith('{"resource_scope":'))) + obs = scope['payload_resource_observation'] + events = obs['resource_events'] + assert r['oracle_sha256'] == digest(b / f"{item['case']}-oracle.json") + assert item['exit'] == 0 and r['status'] == 'passed' and r['complete_source_export_verified'] and (not r['cleanup_failures']) and (not r['receipt_failures']) + assert scope['abort'] is None and scope['payload_resource_observation_verified'] and (obs['payload_exit_code'] == 0) and obs['payload_cleanup_verified'] + assert all((events['memory.events'][k] == 0 for k in ['high', 'max', 'oom', 'oom_kill'])) and events['pids.events']['max'] == 0 + assert [e['name'] for e in tx] == ['inspect_byte_entropy', 'export_evidence_bundle'] + assert all((not e['reply'].get('isError', False) for e in tx)) + inp = count(compact([{k: t[k] for k in ['name', 'description', 'inputSchema']} for t in catalog['tools']])) + args = sum((count(compact({'name': e['name'], 'arguments': e['input']})) for e in tx)) + replies = sum((count(c['text']) for e in tx for c in e['reply']['content'] if c['type'] == 'text')) + prompt = count(r['question']) + instructions = count(r['server_instructions']) + rows.append({ + **item, + 'catalog_tools': len(catalog['tools']), + 'catalog_sha256': digest(p / 'catalog.json'), + 'parent_evidence_id': r['parent_evidence_id'], + 'source_report_sha256': r['source_report_sha256'], + 'runtime_identity_sha256': r['runtime_identity_sha256'], + 'wall_seconds': scope['elapsed_seconds'], + 'cpu_seconds': obs['cpu_stat']['usage_usec'] / 1000000.0, + 'kernel_family_peak_bytes': obs['memory_peak_bytes'], + 'inspection_ms': r['calls'][0]['elapsed_ms'], + 'discovery_ms': r['phases']['discovery_ms'], + 'token_estimate': { + 'question': prompt, + 'instructions': instructions, + 'directory_once': inp, + 'arguments': args, + 'text_replies': replies, + 'whole_task': prompt + instructions + inp + args + replies, + 'raw_catalog': count(compact(catalog)), + }, + 'receipt_sha256': {n: digest(p / n) for n in ['report.json', 'transcript.json', 'catalog.json', 'source.evidence.json']}, + 'payload_exit_events': events, + 'payload_cleanup_verified': True, + 'whole_scope_events_final': scope['resource_events_final'], + }) +summary = {} +for case in ['llvm-small-block', 'llvm-large-block', 'ls']: + rs = [r for r in rows if r['case'] == case] + for k in ['catalog_sha256', 'source_report_sha256', 'parent_evidence_id']: + assert len({r[k] for r in rs}) == 1, (case, k) + summary[case] = {} + for lane in ['before', 'after']: + ls = [r for r in rs if r['lane'] == lane] + assert len(ls) == 3 + assert {r['repeat'] for r in ls} == {1, 2, 3} + metrics = {k: [r[k] for r in ls] for k in ['wall_seconds', 'cpu_seconds', 'kernel_family_peak_bytes', 'inspection_ms', 'discovery_ms']} + metrics['whole_task_tokens'] = [r['token_estimate']['whole_task'] for r in ls] + summary[case][lane] = {k: {'median': statistics.median(v), 'min': min(v), 'max': max(v)} for k, v in metrics.items()} +identities = {lane: json.loads((b / f'{lane}-runtime-identity.json').read_text()) for lane in ['before', 'after']} +assert all(identity['source_clean'] for identity in identities.values()) +for row in rows: + assert row['runtime_identity_sha256'] == digest(b / f"{row['lane']}-runtime-identity.json") +result = { + 'tiktoken': tiktoken.__version__, + 'encoding': enc.name, + 'actual_model_usage': None, + 'revisions': {l: i['source_revision'] for l, i in identities.items()}, + 'identity_receipts': {l: digest(b / f'{l}-runtime-identity.json') for l in identities}, + 'oracles': {c: {'sha256': digest(b / f'{c}-oracle.json'), 'artifact': json.loads((b / f'{c}-oracle.json').read_text())['expected']['artifact']} for c in summary}, + 'summary': summary, + 'recordings': rows, + 'measurement': 'Three alternating fresh production stdio SDK sessions per lane/case; exact discovered toolDefinition/default validator, fresh HOME, Node compile cache disabled, OS caches unflushed. Each task discovers complete catalog, inspects bytes and exports/compares full source Evidence; all oracle facts checked. Whole process-family CPU and kernel peak include client/server, runtime fingerprint verification, serialization, export and cleanup. Oracle generation and tokenization outside measured tasks. One-CPU quota, 3GiB early stop/4GiB hard/no swap; payload exit/cleanup/events verified, final whole-scope counters unavailable. Token serialization estimate includes question/instructions/all input name+description+schema once/all arguments/all text replies, raw catalog separately. Host framing, replay, model inference quality and billing unmeasured.', +} +options.output.write_text(json.dumps(result, indent=2) + '\n') +print(json.dumps(summary, indent=2)) diff --git a/experiments/analysis-efficiency/byte-entropy-oracle.py b/experiments/analysis-efficiency/byte-entropy-oracle.py new file mode 100644 index 0000000000..a3032b478b --- /dev/null +++ b/experiments/analysis-efficiency/byte-entropy-oracle.py @@ -0,0 +1,56 @@ +"""Independent Python histogram oracle; reads artifact bytes without execution.""" +import argparse +from collections import Counter +import hashlib +import json +import math +from pathlib import Path + +def entropy(counts, size): + if not size: + return 0 + value = -sum((counts[k] / size) * math.log2(counts[k] / size) for k in sorted(counts) if counts[k]) + return math.floor(value * 10000 + 0.5) / 10000 + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--path', required=True) + parser.add_argument('--output', required=True) + parser.add_argument('--block-size', type=int, required=True) + parser.add_argument('--offset', type=int, default=0) + parser.add_argument('--length', type=int) + args = parser.parse_args() + path = Path(args.path) + before = path.stat() + counts = Counter() + digest = hashlib.sha256() + with path.open('rb') as source: + while chunk := source.read(1024 * 1024): + digest.update(chunk) + counts.update(chunk) + length = args.length if args.length is not None else before.st_size - args.offset + assert 0 <= args.offset <= before.st_size and 0 <= length <= before.st_size - args.offset + blocks, runs = [], [] + source.seek(args.offset) + for offset in range(args.offset, args.offset + length, args.block_size): + chunk = source.read(min(args.block_size, args.offset + length - offset)) + value = entropy(Counter(chunk), len(chunk)) + block = {'offset': offset, 'length': len(chunk), 'entropy': value} + blocks.append(block) + classification = 'high' if value >= 7.2 else 'low' if value <= 1 else None + if classification is not None: + if runs and runs[-1]['classification'] == classification and runs[-1]['offset'] + runs[-1]['length'] == offset: + runs[-1]['length'] += len(chunk) + else: + runs.append({'offset': offset, 'length': len(chunk), 'classification': classification}) + after = path.stat() + assert (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns) + expected = {'artifact': {'path': args.path, 'bytes': before.st_size, 'sha256': digest.hexdigest()}, 'block_size':args.block_size,'window':{'offset':args.offset,'length':length},'file_entropy':entropy(counts,before.st_size),'thresholds':{'high':7.2,'low':1},'blocks':blocks,'runs':runs} + request = {'path':args.path,'block_size':args.block_size,'offset':args.offset} + if args.length is not None: + request['length'] = args.length + Path(args.output).write_text(json.dumps({'input':request,'expected':expected},indent=2)+'\n') + print(json.dumps({'path':args.path,'sha256':digest.hexdigest(),'blocks':len(blocks),'runs':len(runs)})) + +if __name__ == '__main__': + main() diff --git a/experiments/analysis-efficiency/byte-entropy-reads.json b/experiments/analysis-efficiency/byte-entropy-reads.json new file mode 100644 index 0000000000..150429774e --- /dev/null +++ b/experiments/analysis-efficiency/byte-entropy-reads.json @@ -0,0 +1,1430 @@ +{ + "tiktoken": "0.14.0", + "encoding": "cl100k_base", + "actual_model_usage": null, + "revisions": { + "before": "1d896e03b776453282beca7fd89338be778bfca2", + "after": "3ed3276d0ee79c291e1006901ace5b06626c1d4e" + }, + "identity_receipts": { + "before": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "after": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a" + }, + "oracles": { + "llvm-small-block": { + "sha256": "8c3edeb30472bb1092ed250412b33ffeaacbba58da474c010c76390628241aad", + "artifact": { + "path": "/usr/lib/x86_64-linux-gnu/libLLVM-16.so.1", + "bytes": 123379936, + "sha256": "f62d254b7f2bf42df8c8b07d46ee3bb4c2cafeca436b2e6bc6ccbe4581f58f40" + } + }, + "llvm-large-block": { + "sha256": "9aab6dba368a251f3f54feefd9147de148c0f6cdc60e6344cf765c7a4844cb80", + "artifact": { + "path": "/usr/lib/x86_64-linux-gnu/libLLVM-16.so.1", + "bytes": 123379936, + "sha256": "f62d254b7f2bf42df8c8b07d46ee3bb4c2cafeca436b2e6bc6ccbe4581f58f40" + } + }, + "ls": { + "sha256": "ff4e6b59cb10b98f6687120b77224e2e33a9615bfaa307eadec3b6a3da8a73f5", + "artifact": { + "path": "/usr/bin/ls", + "bytes": 151344, + "sha256": "cb30d69b24245bf2ecdc9e7f53bbad19159999970b6d82c0c00c7d32d9e37aa4" + } + } + }, + "summary": { + "llvm-small-block": { + "before": { + "wall_seconds": { + "median": 29.842, + "min": 28.541, + "max": 33.29 + }, + "cpu_seconds": { + "median": 25.286236, + "min": 23.600276, + "max": 25.428612 + }, + "kernel_family_peak_bytes": { + "median": 660090880, + "min": 650514432, + "max": 668438528 + }, + "inspection_ms": { + "median": 24862.077908, + "min": 24796.278229, + "max": 29515.395641 + }, + "discovery_ms": { + "median": 1062.3440920000003, + "min": 1056.1659610000002, + "max": 1339.2179160000003 + }, + "whole_task_tokens": { + "median": 62000, + "min": 62000, + "max": 62000 + } + }, + "after": { + "wall_seconds": { + "median": 8.584, + "min": 7.981, + "max": 10.396 + }, + "cpu_seconds": { + "median": 6.358016, + "min": 6.048826, + "max": 6.63289 + }, + "kernel_family_peak_bytes": { + "median": 705114112, + "min": 698036224, + "max": 705949696 + }, + "inspection_ms": { + "median": 4213.348502999999, + "min": 3920.4952740000003, + "max": 5781.0953389999995 + }, + "discovery_ms": { + "median": 1112.952316, + "min": 1106.7913999999998, + "max": 1396.260179 + }, + "whole_task_tokens": { + "median": 62000, + "min": 62000, + "max": 62000 + } + } + }, + "llvm-large-block": { + "before": { + "wall_seconds": { + "median": 12.314, + "min": 9.095, + "max": 17.957 + }, + "cpu_seconds": { + "median": 7.580575, + "min": 6.934466, + "max": 8.476001 + }, + "kernel_family_peak_bytes": { + "median": 700346368, + "min": 663851008, + "max": 705323008 + }, + "inspection_ms": { + "median": 7798.0581010000005, + "min": 5327.810836000001, + "max": 7879.026692 + }, + "discovery_ms": { + "median": 1442.5241819999999, + "min": 1119.990668, + "max": 2455.198091 + }, + "whole_task_tokens": { + "median": 91900, + "min": 91900, + "max": 91900 + } + }, + "after": { + "wall_seconds": { + "median": 12.115, + "min": 8.285, + "max": 13.319 + }, + "cpu_seconds": { + "median": 7.325135, + "min": 7.049975, + "max": 7.446041 + }, + "kernel_family_peak_bytes": { + "median": 707096576, + "min": 702083072, + "max": 708579328 + }, + "inspection_ms": { + "median": 6899.755944, + "min": 4762.259958000001, + "max": 7711.982530000001 + }, + "discovery_ms": { + "median": 1430.1016910000003, + "min": 1088.6852219999998, + "max": 1525.502455 + }, + "whole_task_tokens": { + "median": 91900, + "min": 91900, + "max": 91900 + } + } + }, + "ls": { + "before": { + "wall_seconds": { + "median": 7.075, + "min": 6.469, + "max": 9.892 + }, + "cpu_seconds": { + "median": 5.804089, + "min": 5.564724, + "max": 5.868173 + }, + "kernel_family_peak_bytes": { + "median": 691068928, + "min": 664076288, + "max": 691322880 + }, + "inspection_ms": { + "median": 2788.714276, + "min": 2753.4078409999997, + "max": 4401.935402 + }, + "discovery_ms": { + "median": 1071.0256309999997, + "min": 988.3230369999999, + "max": 1323.905776 + }, + "whole_task_tokens": { + "median": 67215, + "min": 67215, + "max": 67215 + } + }, + "after": { + "wall_seconds": { + "median": 8.286, + "min": 6.169, + "max": 9.188 + }, + "cpu_seconds": { + "median": 5.528156, + "min": 5.401958, + "max": 5.749299 + }, + "kernel_family_peak_bytes": { + "median": 691326976, + "min": 684163072, + "max": 693678080 + }, + "inspection_ms": { + "median": 3604.7330589999997, + "min": 2798.7924759999996, + "max": 3954.776057 + }, + "discovery_ms": { + "median": 1103.981563, + "min": 987.2837319999999, + "max": 1400.601354 + }, + "whole_task_tokens": { + "median": 67215, + "min": 67215, + "max": 67215 + } + } + } + }, + "recordings": [ + { + "name": "llvm-small-block-1-before", + "case": "llvm-small-block", + "lane": "before", + "repeat": 1, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-static-tool-audit-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-small-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/before-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-small-block-1-before" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_4a27946fd5072db2f8ec817b55b0cb24e11213542828a895ab719d77a5cc6d05", + "source_report_sha256": "00fed6fa92201271f7f2187b13d16686c782482fca922c772ec353fb2e321868", + "runtime_identity_sha256": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "wall_seconds": 29.842, + "cpu_seconds": 25.428612, + "kernel_family_peak_bytes": 660090880, + "inspection_ms": 24796.278229, + "discovery_ms": 1056.1659610000002, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 80, + "text_replies": 4690, + "whole_task": 62000, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "c8ac36fec41db539fcdc73fa06210cf1aa275b5c523239b3529ef04455ca61e7", + "transcript.json": "cd7a625762d76dee5c2e6dcf02c2f13bb007ed5eba2bfcd72954992107b0e74b", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "e250e777492bb82153cb3d57ece0d8fb8ec9d2eb238f0d648761592fd4441ff7" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-small-block-1-after", + "case": "llvm-small-block", + "lane": "after", + "repeat": 1, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-entropy-reads-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-small-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/after-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-small-block-1-after" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_4a27946fd5072db2f8ec817b55b0cb24e11213542828a895ab719d77a5cc6d05", + "source_report_sha256": "00fed6fa92201271f7f2187b13d16686c782482fca922c772ec353fb2e321868", + "runtime_identity_sha256": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a", + "wall_seconds": 8.584, + "cpu_seconds": 6.63289, + "kernel_family_peak_bytes": 705949696, + "inspection_ms": 3920.4952740000003, + "discovery_ms": 1112.952316, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 80, + "text_replies": 4690, + "whole_task": 62000, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "d266fbb30bc2f79ed4fa9e4ea6e801a8aafe67f96d2855bb90d105c32314cb13", + "transcript.json": "54b367813185f8ff84885e330f26e601ae6c7177da8fcaf4bb2061046d1af368", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "e250e777492bb82153cb3d57ece0d8fb8ec9d2eb238f0d648761592fd4441ff7" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-small-block-2-after", + "case": "llvm-small-block", + "lane": "after", + "repeat": 2, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-entropy-reads-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-small-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/after-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-small-block-2-after" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_4a27946fd5072db2f8ec817b55b0cb24e11213542828a895ab719d77a5cc6d05", + "source_report_sha256": "00fed6fa92201271f7f2187b13d16686c782482fca922c772ec353fb2e321868", + "runtime_identity_sha256": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a", + "wall_seconds": 7.981, + "cpu_seconds": 6.358016, + "kernel_family_peak_bytes": 705114112, + "inspection_ms": 4213.348502999999, + "discovery_ms": 1106.7913999999998, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 80, + "text_replies": 4690, + "whole_task": 62000, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "7acbf49fbb7c72fcccfce9dfe8762e4e28b51d2bdd39bb47bf64cbdb50dbe59b", + "transcript.json": "4559f30a6d47c5535e339033493b2b57f107b7c04c92ce494afce9fc81e6dbc1", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "e250e777492bb82153cb3d57ece0d8fb8ec9d2eb238f0d648761592fd4441ff7" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-small-block-2-before", + "case": "llvm-small-block", + "lane": "before", + "repeat": 2, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-static-tool-audit-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-small-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/before-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-small-block-2-before" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_4a27946fd5072db2f8ec817b55b0cb24e11213542828a895ab719d77a5cc6d05", + "source_report_sha256": "00fed6fa92201271f7f2187b13d16686c782482fca922c772ec353fb2e321868", + "runtime_identity_sha256": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "wall_seconds": 28.541, + "cpu_seconds": 23.600276, + "kernel_family_peak_bytes": 668438528, + "inspection_ms": 24862.077908, + "discovery_ms": 1339.2179160000003, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 80, + "text_replies": 4690, + "whole_task": 62000, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "ca2e55932d806e289210015e570abd70a6e00c5831e57e6839f2da4724c9302c", + "transcript.json": "aa32660dc0970c41dad7b4ddd0f8051253e5a95fbb2f332a34b379823e12e5dd", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "e250e777492bb82153cb3d57ece0d8fb8ec9d2eb238f0d648761592fd4441ff7" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-small-block-3-before", + "case": "llvm-small-block", + "lane": "before", + "repeat": 3, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-static-tool-audit-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-small-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/before-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-small-block-3-before" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_4a27946fd5072db2f8ec817b55b0cb24e11213542828a895ab719d77a5cc6d05", + "source_report_sha256": "00fed6fa92201271f7f2187b13d16686c782482fca922c772ec353fb2e321868", + "runtime_identity_sha256": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "wall_seconds": 33.29, + "cpu_seconds": 25.286236, + "kernel_family_peak_bytes": 650514432, + "inspection_ms": 29515.395641, + "discovery_ms": 1062.3440920000003, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 80, + "text_replies": 4690, + "whole_task": 62000, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "26acb427b1b4e264ad092501028d2543eecaae79a8c37dd00f7ac30111ddf40f", + "transcript.json": "896af044ad686d2e7fcd3f1b4352ae8e3044de2604baee2031c006ebae5db1f7", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "e250e777492bb82153cb3d57ece0d8fb8ec9d2eb238f0d648761592fd4441ff7" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-small-block-3-after", + "case": "llvm-small-block", + "lane": "after", + "repeat": 3, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-entropy-reads-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-small-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/after-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-small-block-3-after" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_4a27946fd5072db2f8ec817b55b0cb24e11213542828a895ab719d77a5cc6d05", + "source_report_sha256": "00fed6fa92201271f7f2187b13d16686c782482fca922c772ec353fb2e321868", + "runtime_identity_sha256": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a", + "wall_seconds": 10.396, + "cpu_seconds": 6.048826, + "kernel_family_peak_bytes": 698036224, + "inspection_ms": 5781.0953389999995, + "discovery_ms": 1396.260179, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 80, + "text_replies": 4690, + "whole_task": 62000, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "2fd342b60e88b5e0265221f5a69df4adf545107fe69df4f52f2bdb0a59b63cd0", + "transcript.json": "b6e2e0eb119b483ca690cadf4b638d33d0aefd49cf382c4aaad1d766d7b37f95", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "e250e777492bb82153cb3d57ece0d8fb8ec9d2eb238f0d648761592fd4441ff7" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-large-block-1-before", + "case": "llvm-large-block", + "lane": "before", + "repeat": 1, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-static-tool-audit-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-large-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/before-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-large-block-1-before" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_95c2526af2a7e197f1bd05b94e254da37631fb93d41c0738dd6e7ada141994a7", + "source_report_sha256": "4caca811267410bbc81ad5eba5f5cb7fb80b1bb064729d91adf6fbb2b8b7a611", + "runtime_identity_sha256": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "wall_seconds": 17.957, + "cpu_seconds": 8.476001, + "kernel_family_peak_bytes": 663851008, + "inspection_ms": 7879.026692, + "discovery_ms": 2455.198091, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 76, + "text_replies": 34594, + "whole_task": 91900, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "ad2f1a74a64518b5a88536f0aca629e4dee83c9f053a53000fdaecc425eee48f", + "transcript.json": "2afc5c5daabaaabed6bed2d1f12b125d858196257d20b84059341bee2aa920c3", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "d45a4a687adce34a893dc0978c7596bb713fd018d477524a99e086b47d11ca9d" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-large-block-1-after", + "case": "llvm-large-block", + "lane": "after", + "repeat": 1, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-entropy-reads-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-large-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/after-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-large-block-1-after" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_95c2526af2a7e197f1bd05b94e254da37631fb93d41c0738dd6e7ada141994a7", + "source_report_sha256": "4caca811267410bbc81ad5eba5f5cb7fb80b1bb064729d91adf6fbb2b8b7a611", + "runtime_identity_sha256": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a", + "wall_seconds": 12.115, + "cpu_seconds": 7.049975, + "kernel_family_peak_bytes": 707096576, + "inspection_ms": 6899.755944, + "discovery_ms": 1525.502455, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 76, + "text_replies": 34594, + "whole_task": 91900, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "643c97711d54f9c63fa77637e2ccae186a5ea245962126cdba5113d9091b6ff3", + "transcript.json": "fb0dd5d99181c0f3ed242b96ffa93410226ebad5bddb4db20441c69f1f3e2e80", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "d45a4a687adce34a893dc0978c7596bb713fd018d477524a99e086b47d11ca9d" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-large-block-2-after", + "case": "llvm-large-block", + "lane": "after", + "repeat": 2, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-entropy-reads-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-large-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/after-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-large-block-2-after" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_95c2526af2a7e197f1bd05b94e254da37631fb93d41c0738dd6e7ada141994a7", + "source_report_sha256": "4caca811267410bbc81ad5eba5f5cb7fb80b1bb064729d91adf6fbb2b8b7a611", + "runtime_identity_sha256": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a", + "wall_seconds": 13.319, + "cpu_seconds": 7.325135, + "kernel_family_peak_bytes": 702083072, + "inspection_ms": 7711.982530000001, + "discovery_ms": 1430.1016910000003, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 76, + "text_replies": 34594, + "whole_task": 91900, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "e1beda23320577291a8bbb75246ea142e0846219e6b70486fc9820d025e9e0b6", + "transcript.json": "e351f27922938c10cec3c7ccf1f3acf07a3f0accecf9f317f7be7f4bfd964d38", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "d45a4a687adce34a893dc0978c7596bb713fd018d477524a99e086b47d11ca9d" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-large-block-2-before", + "case": "llvm-large-block", + "lane": "before", + "repeat": 2, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-static-tool-audit-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-large-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/before-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-large-block-2-before" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_95c2526af2a7e197f1bd05b94e254da37631fb93d41c0738dd6e7ada141994a7", + "source_report_sha256": "4caca811267410bbc81ad5eba5f5cb7fb80b1bb064729d91adf6fbb2b8b7a611", + "runtime_identity_sha256": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "wall_seconds": 12.314, + "cpu_seconds": 6.934466, + "kernel_family_peak_bytes": 700346368, + "inspection_ms": 7798.0581010000005, + "discovery_ms": 1442.5241819999999, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 76, + "text_replies": 34594, + "whole_task": 91900, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "15b9da16f1f471440debf3a37a3de3c6b3f323936bda30291337cce238502d54", + "transcript.json": "a2b958397689f03e5ef3a0c9dc634c5e9d444bc865993a8756a883692c320a69", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "d45a4a687adce34a893dc0978c7596bb713fd018d477524a99e086b47d11ca9d" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-large-block-3-before", + "case": "llvm-large-block", + "lane": "before", + "repeat": 3, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-static-tool-audit-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-large-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/before-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-large-block-3-before" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_95c2526af2a7e197f1bd05b94e254da37631fb93d41c0738dd6e7ada141994a7", + "source_report_sha256": "4caca811267410bbc81ad5eba5f5cb7fb80b1bb064729d91adf6fbb2b8b7a611", + "runtime_identity_sha256": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "wall_seconds": 9.095, + "cpu_seconds": 7.580575, + "kernel_family_peak_bytes": 705323008, + "inspection_ms": 5327.810836000001, + "discovery_ms": 1119.990668, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 76, + "text_replies": 34594, + "whole_task": 91900, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "6d322a8fb97b69fe21b784b82a48965cfae4dfc68a4d7b8488da11e0b0ad3275", + "transcript.json": "2487fd135bb1e703b240dd46b73dec42a09daedc54ac9537df1d5c6aca83569b", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "d45a4a687adce34a893dc0978c7596bb713fd018d477524a99e086b47d11ca9d" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "llvm-large-block-3-after", + "case": "llvm-large-block", + "lane": "after", + "repeat": 3, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-entropy-reads-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/llvm-large-block-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/after-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/llvm-large-block-3-after" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_95c2526af2a7e197f1bd05b94e254da37631fb93d41c0738dd6e7ada141994a7", + "source_report_sha256": "4caca811267410bbc81ad5eba5f5cb7fb80b1bb064729d91adf6fbb2b8b7a611", + "runtime_identity_sha256": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a", + "wall_seconds": 8.285, + "cpu_seconds": 7.446041, + "kernel_family_peak_bytes": 708579328, + "inspection_ms": 4762.259958000001, + "discovery_ms": 1088.6852219999998, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 76, + "text_replies": 34594, + "whole_task": 91900, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "1dacfb55d2a0b2ea1568fb657163b449e7d6ae1acdc0940a2a41e4a42f6c4b72", + "transcript.json": "ba4d1c9d7480765b515b75534a582a4dfbcaf0c4832b5cec9d6a2c6ff25f0ceb", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "d45a4a687adce34a893dc0978c7596bb713fd018d477524a99e086b47d11ca9d" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "ls-1-before", + "case": "ls", + "lane": "before", + "repeat": 1, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-static-tool-audit-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/ls-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/before-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/ls-1-before" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_a69f96696a9f0c7f4ef7d28eacac454a3177b54ffc3f0171f48481c95e54bb00", + "source_report_sha256": "bdecdc567e749c52d6d4f31c59032946c59719d0c817b4ad555ba79896bb0e83", + "runtime_identity_sha256": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "wall_seconds": 6.469, + "cpu_seconds": 5.804089, + "kernel_family_peak_bytes": 691068928, + "inspection_ms": 2753.4078409999997, + "discovery_ms": 988.3230369999999, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 61, + "text_replies": 9924, + "whole_task": 67215, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "8fa0a65917d28c9a1be93fe6fc5d2649f97a29c1401c798c465607f30bcc2510", + "transcript.json": "72c7694e07465c7f434b8d1f7853d4c010917f5a50f9f82baf1321791d8931d9", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "8e5a9243caaca7bea1fd709c781fe313f4334bc103dd0dbc06660195f8dde420" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "ls-1-after", + "case": "ls", + "lane": "after", + "repeat": 1, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-entropy-reads-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/ls-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/after-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/ls-1-after" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_a69f96696a9f0c7f4ef7d28eacac454a3177b54ffc3f0171f48481c95e54bb00", + "source_report_sha256": "bdecdc567e749c52d6d4f31c59032946c59719d0c817b4ad555ba79896bb0e83", + "runtime_identity_sha256": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a", + "wall_seconds": 8.286, + "cpu_seconds": 5.749299, + "kernel_family_peak_bytes": 684163072, + "inspection_ms": 3604.7330589999997, + "discovery_ms": 1103.981563, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 61, + "text_replies": 9924, + "whole_task": 67215, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "238427864393640abec8333755af1e6fb824518e3afc3b1a9af89a469778bfd6", + "transcript.json": "882e34a885df0ead76f137290d6846054882b99ef2be738837a6c3a784410344", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "8e5a9243caaca7bea1fd709c781fe313f4334bc103dd0dbc06660195f8dde420" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "ls-2-after", + "case": "ls", + "lane": "after", + "repeat": 2, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-entropy-reads-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/ls-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/after-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/ls-2-after" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_a69f96696a9f0c7f4ef7d28eacac454a3177b54ffc3f0171f48481c95e54bb00", + "source_report_sha256": "bdecdc567e749c52d6d4f31c59032946c59719d0c817b4ad555ba79896bb0e83", + "runtime_identity_sha256": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a", + "wall_seconds": 9.188, + "cpu_seconds": 5.401958, + "kernel_family_peak_bytes": 691326976, + "inspection_ms": 3954.776057, + "discovery_ms": 1400.601354, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 61, + "text_replies": 9924, + "whole_task": 67215, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "b369e16e21a54f6f6a312c2b8f294eb2110c324614b4bb3ac187650fe8870e3f", + "transcript.json": "fd50a0a729e26186c50d45944207533d3b97ade2e81ec2f4ce6d250e9a7e7906", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "8e5a9243caaca7bea1fd709c781fe313f4334bc103dd0dbc06660195f8dde420" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "ls-2-before", + "case": "ls", + "lane": "before", + "repeat": 2, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-static-tool-audit-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/ls-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/before-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/ls-2-before" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_a69f96696a9f0c7f4ef7d28eacac454a3177b54ffc3f0171f48481c95e54bb00", + "source_report_sha256": "bdecdc567e749c52d6d4f31c59032946c59719d0c817b4ad555ba79896bb0e83", + "runtime_identity_sha256": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "wall_seconds": 9.892, + "cpu_seconds": 5.564724, + "kernel_family_peak_bytes": 664076288, + "inspection_ms": 4401.935402, + "discovery_ms": 1323.905776, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 61, + "text_replies": 9924, + "whole_task": 67215, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "c4b3eb936d5b386cb3a1487d1635ad773472242b315eac9aee5bd1dc3d4e6662", + "transcript.json": "b6262b8224d6e32e38ac706952a79161b84bde11c97d49114090b804c2be8fd7", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "8e5a9243caaca7bea1fd709c781fe313f4334bc103dd0dbc06660195f8dde420" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "ls-3-before", + "case": "ls", + "lane": "before", + "repeat": 3, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-static-tool-audit-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/ls-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/before-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/ls-3-before" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_a69f96696a9f0c7f4ef7d28eacac454a3177b54ffc3f0171f48481c95e54bb00", + "source_report_sha256": "bdecdc567e749c52d6d4f31c59032946c59719d0c817b4ad555ba79896bb0e83", + "runtime_identity_sha256": "7a14d7a9282c44ab720b080a1d2d5fc2c3acb84f1ca8185a95afcb8ccbbd73b9", + "wall_seconds": 7.075, + "cpu_seconds": 5.868173, + "kernel_family_peak_bytes": 691322880, + "inspection_ms": 2788.714276, + "discovery_ms": 1071.0256309999997, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 61, + "text_replies": 9924, + "whole_task": 67215, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "f52001fe53650d3062a1e440c5c5f91e43ba12382835651fb8b6a4ad1b5943cf", + "transcript.json": "7afb6207f443a736d8ab11368947614b2f40bb8e22594dbcd02c4e88039b557b", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "8e5a9243caaca7bea1fd709c781fe313f4334bc103dd0dbc06660195f8dde420" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + }, + { + "name": "ls-3-after", + "case": "ls", + "lane": "after", + "repeat": 3, + "command": [ + "python3", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-entropy-reads-20261024/experiments/analysis-efficiency/byte-entropy-reads.mjs", + "--runtime", + "/home/yann/yann/rea-perf-entropy-reads-20261024", + "--oracle", + "/tmp/rea-perf-byte-entropy-20261024/ls-oracle.json", + "--identity", + "/tmp/rea-perf-byte-entropy-20261024/after-runtime-identity.json", + "--output", + "/tmp/rea-perf-byte-entropy-20261024/cohort/ls-3-after" + ], + "exit": 0, + "catalog_tools": 222, + "catalog_sha256": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "parent_evidence_id": "ev_a69f96696a9f0c7f4ef7d28eacac454a3177b54ffc3f0171f48481c95e54bb00", + "source_report_sha256": "bdecdc567e749c52d6d4f31c59032946c59719d0c817b4ad555ba79896bb0e83", + "runtime_identity_sha256": "a2d2a57495756565a50d7d3098066bf4210991ac79d5c3ab96f81cc8a40d128a", + "wall_seconds": 6.169, + "cpu_seconds": 5.528156, + "kernel_family_peak_bytes": 693678080, + "inspection_ms": 2798.7924759999996, + "discovery_ms": 987.2837319999999, + "token_estimate": { + "question": 32, + "instructions": 513, + "directory_once": 56685, + "arguments": 61, + "text_replies": 9924, + "whole_task": 67215, + "raw_catalog": 838375 + }, + "receipt_sha256": { + "report.json": "54474d0bc47458aead95f872ea3c94a651f2419280ab35857d2393c7ffcdbb18", + "transcript.json": "04a63c8e7e9b916c1dae3299d9f1952aef8e7f4cf1e227109a6ab6b3a5f4c458", + "catalog.json": "4b2ac38e76ee3cbbdd6763230e1e5abbbbb0c760c8476efb5e036957d5021644", + "source.evidence.json": "8e5a9243caaca7bea1fd709c781fe313f4334bc103dd0dbc06660195f8dde420" + }, + "payload_exit_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "whole_scope_events_final": null + } + ], + "measurement": "Three alternating fresh production stdio SDK sessions per lane/case; exact discovered toolDefinition/default validator, fresh HOME, Node compile cache disabled, OS caches unflushed. Each task discovers complete catalog, inspects bytes and exports/compares full source Evidence; all oracle facts checked. Whole process-family CPU and kernel peak include client/server, runtime fingerprint verification, serialization, export and cleanup. Oracle generation and tokenization outside measured tasks. One-CPU quota, 3GiB early stop/4GiB hard/no swap; payload exit/cleanup/events verified, final whole-scope counters unavailable. Token serialization estimate includes question/instructions/all input name+description+schema once/all arguments/all text replies, raw catalog separately. Host framing, replay, model inference quality and billing unmeasured." +} diff --git a/experiments/analysis-efficiency/byte-entropy-reads.md b/experiments/analysis-efficiency/byte-entropy-reads.md new file mode 100644 index 0000000000..8c28981d4a --- /dev/null +++ b/experiments/analysis-efficiency/byte-entropy-reads.md @@ -0,0 +1,158 @@ +# Byte entropy: separate physical reads from report resolution + +Issue: [#6947](https://github.com/morluto/rea/issues/6947). + +The question is to return the full-file SHA-256 and entropy, every block in a +selected byte window, and adjacent high/low runs, preserving complete source +Evidence. `ByteEntropyService` previously used `block_size` for both reporting +and physical reads, including outside the selected window. Small report blocks +therefore caused hundreds of thousands of asynchronous reads of a large file. + +The shared service now fills a bounded 64 KiB buffer before calculating the +caller-selected blocks. Supported power-of-two block sizes divide that buffer; +the window starts its own block sequence, and only its final block can be short. +Short physical reads are joined before interpretation. The same stable-file, +whole-file hash, cancellation, capacity and descriptor-cleanup owners remain. +No new cache, parser, contract or output selection is introduced. + +## Fixed comparison + +Baseline: `1d896e03b776453282beca7fd89338be778bfca2`. +Measured implementation: `3ed3276d0ee79c291e1006901ace5b06626c1d4e`. +Both were clean and built with Node 24.22.1, npm 11.16.0 and SDK 2.3.1. +The runtime receipt fingerprints 2,582 compiled/catalog/entrypoint/dependency +and driver files in each lane, checked before and after every task. + +Real artifacts are installed Linux x64 binaries, inspected without execution: + +| Artifact | Bytes | SHA-256 | +| ----------------------------------------------------- | ----------: | ------------------------------------------------------------------ | +| LLVM 16 (`/usr/lib/x86_64-linux-gnu/libLLVM-16.so.1`) | 123,379,936 | `f62d254b7f2bf42df8c8b07d46ee3bb4c2cafeca436b2e6bc6ccbe4581f58f40` | +| GNU ls (`/usr/bin/ls`) | 151,344 | `cb30d69b24245bf2ecdc9e7f53bbad19159999970b6d82c0c00c7d32d9e37aa4` | + +`byte-entropy-oracle.py` independently computes Python byte histograms, +Shannon entropy, selected blocks, rounded-value run classifications and digests. +It checks file state around its reads. Every returned fact agrees with this +oracle, and both lanes have identical complete report hashes and Evidence IDs. + +There are three alternating fresh production stdio MCP sessions per lane/case, +18 tasks total. Each discovers all 222 tools, uses exact discovered +`toolDefinition` validation, inspects the bytes, exports the complete Evidence +bundle and verifies its parent record. Fresh HOME/client/server, Node compile +cache disabled, OS caches unflushed. These are fresh sessions; no warm-runtime +speedup is claimed. Cached-catalog SDK bulk validation was not measured here. + +| Task (3 samples per lane) | Whole elapsed median, s | Family CPU median, s | Kernel family peak median, MiB | Whole-task estimated tokens | +| ----------------------------------------------------------- | ----------------------: | -------------------: | -----------------------------: | --------------------------: | +| LLVM: 256-byte blocks, offset 3, length 65,539 (257 blocks) | 29.842 → 8.584 | 25.286 → 6.358 | 629.5 → 672.4 | 62,000 → 62,000 | +| LLVM: whole file, 65,536-byte blocks (1,883 blocks) | 12.314 → 12.115 | 7.581 → 7.325 | 667.9 → 674.3 | 91,900 → 91,900 | +| ls: whole file, 256-byte blocks (592 blocks) | 7.075 → 8.286 | 5.804 → 5.528 | 659.1 → 659.3 | 67,215 → 67,215 | + +The large fine-resolution task improves elapsed median by 71.2% and CPU by +74.9%. Elapsed ranges are 28.541–33.290 → 7.981–10.396 seconds; CPU ranges +23.600–25.429 → 6.049–6.633 seconds. Its kernel peak increases by about +43 MiB (ranges 620.4–637.5 → 665.7–673.2 MiB). This is a speed/CPU improvement +with higher measured peak memory, not a memory optimization. + +The other controls do not establish a stable whole-task speedup. The small +file's elapsed median is worse; ranges overlap at 6.469–9.892 → 6.169–9.188 +seconds. No general latency or memory benefit is claimed. Full-file reading +and hashing remain necessary even when the selected window is short or empty. + +A separate single real-file service probe delegated to actual `open/stat/read` +while counting reads: 481,955 → 1,885 physical reads of all 123,379,936 bytes, +maximum request 256 → 65,536 bytes, service elapsed 17.09 → 1.34 seconds. +The complete Evidence objects were equal and independently checked. This probe +locates the work removed; it is not the repeated public-task performance table. + +The [machine-readable report](./byte-entropy-reads.json) preserves ranges, +per-sample phase timings, calls, receipt hashes, artifact identities and commands. +`cl100k_base` via tiktoken 0.14.0 counts the fixed question, server instructions, +all tool names/descriptions/input schemas once, every argument and every text +reply including export. Input-directory cost is 56,685 tokens; raw catalog with +output schemas is separately 838,375. Actual model billing, host framing/replay, +model reasoning quality and inference latency were not measured. No token saving +is claimed: replies and catalog remain unchanged. + +## Resource and correctness verification + +All tasks ran serially under a one-CPU quota, 3 GiB early memory stop, 4 GiB hard +limit, zero swap and 192-task ceiling. Whole process-family CPU and kernel memory +peak include client/server, runtime fingerprint reads, serialization, source +export and cleanup; kernel peak includes charged file cache. Independent oracle +generation and tokenization are outside timed tasks. Payload exits, remaining +owned processes and memory/PID-limit events were checked: all task payloads +exited 0, cleanup verified, no guard stop/OOM. Final whole-scope counters were +unavailable after transient-scope removal and are not represented as verified. + +Executed additional checks: + +- Six real CLI calls (three cases in both lanes): every oracle fact, complete + CLI Evidence equality and matching MCP parent identity passed. +- 36 tests in four exact source/public-boundary files passed, including short + reads, unaligned windows across buffers, partial tails at minimum/intermediate/ + maximum block sizes, empty windows, cancellation through finalization, + identity changes, capacity refusal, retained summaries and cleanup retry. +- Six serial static tasks (typecheck, lint including module boundaries, format, + knip, metadata and required compilation), plus architecture guards passed. + Static family peak was 1.96 GiB; focused-test/preparation peak was 2.15 GiB. +- An initial typecheck failure in the new short-read test's injected callback + was fixed with an explicitly typed positional boundary. Its failed receipt is + retained, not counted as a successful run. Initial local formatter path and + early profile verification attempts also failed before their intended checks; + corrected checks are recorded separately. + +The public experiments qualify static byte inspection on these Linux files. +They do not establish behavior on every filesystem or operating system and do +not execute LLVM or ls. CLI/MCP contracts, Evidence fields, input limits and +provider authority remain unchanged; generated catalogs and builds are ignored. + +After integrating main `21a20306b`, the six serial static tasks, architecture +guards and all 36 focused cases passed again. Three fresh real MCP/source-export +tasks and three CLI calls matched every oracle fact and the frozen cohort's +report/parent identity. The integrated test/preparation family peaked at +2.23 GiB with verified payload exit/cleanup and no guard stop/OOM. These are +integration qualification runs; they do not replace the fixed performance pins. +The committed cost verifier also reproduced the complete measurement JSON. + +## Reproduction + +Build both pinned checkouts using their locked toolchain, serially. Supply an +independent installed dependency tree matching the lock, without upgrading host +software. Use the exact artifact digests above. The oracle accepts explicit +`--path`, `--block-size`, `--offset`, `--length` and `--output`; omit length for +the two whole-file controls. For example: + +```sh +python3 experiments/analysis-efficiency/byte-entropy-oracle.py \ + --path /usr/lib/x86_64-linux-gnu/libLLVM-16.so.1 \ + --block-size 256 --offset 3 --length 65539 --output /absolute/oracle.json +python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \ + node experiments/analysis-efficiency/byte-entropy-reads.mjs \ + --runtime /absolute/pinned-checkout --oracle /absolute/oracle.json \ + --identity /absolute/runtime-identity.json --output /absolute/new-task-directory +``` + +Runtime identity JSON contains `source_revision`, `source_clean`, +`node_sha256` and `files: [{path, sha256}]` with absolute paths. Fingerprint the +entire compiled runtime and catalogs, scripts, lock/toolchain files, actual SDK +JavaScript dependencies and the driver/helpers. Both sessions must use the same +Node and dependency contents. Use the same one-CPU/memory policy for the harness; +the historical runs additionally pinned the payload to CPUs 0–1 and the initial +observer to CPU 15. Do not compare an unguarded default-concurrency run as an +equivalent resource measurement. + +Retained raw receipts are on the investigation host at +`/tmp/rea-perf-byte-entropy-20261024/`. This includes `cohort/index.json`, +per-task catalog/transcript/report/source export/guard logs, runtime identities, +three oracle JSONs, CLI stdout/stderr, boundary profiles and verification logs. +The cost verifier takes that receipt directory and a separate output: + +```sh +python3 experiments/analysis-efficiency/byte-entropy-costs.py \ + --receipts /absolute/receipt-directory --output /absolute/costs.json +``` + +It requires all 18 successful verified samples, matching parent/report/catalog +identity across lanes, successful source exports and oracle/runtime receipts. +Failed or stopped attempts cannot enter the published cohort. diff --git a/experiments/analysis-efficiency/byte-entropy-reads.mjs b/experiments/analysis-efficiency/byte-entropy-reads.mjs new file mode 100644 index 0000000000..f6eaa8529e --- /dev/null +++ b/experiments/analysis-efficiency/byte-entropy-reads.mjs @@ -0,0 +1,154 @@ +/** Complete entropy question through production stdio, with full source export. */ +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 { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { finished } from "node:stream/promises"; +import { parseArgs } from "node:util"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { createRetainedQueryCaller } from "./retained-query-calls.mjs"; +import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs"; + +const { values } = parseArgs({ + options: Object.fromEntries( + ["runtime", "oracle", "identity", "output"].map((name) => [ + name, + { type: "string" }, + ]), + ), +}); +for (const value of Object.values(values)) assert.equal(value, resolve(value)); +const sha256 = (value) => createHash("sha256").update(value).digest("hex"); +const oracleBytes = await readFile(values.oracle); +const oracle = JSON.parse(oracleBytes); +const identityBytes = await readFile(values.identity); +const identity = JSON.parse(identityBytes); +await mkdir(values.output, { mode: 0o700 }); +const account = await mkdtemp(join(tmpdir(), "rea-entropy-task-")); +const client = new Client({ name: "entropy-complete-task", version: "1" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [join(values.runtime, "scripts/rea.mjs"), "mcp"], + maxBufferSize: 10485760, + env: { + HOME: account, + TMPDIR: account, + PATH: process.env.PATH, + REA_LOG_LEVEL: "silent", + NODE_OPTIONS: "--max-old-space-size=2048", + NODE_DISABLE_COMPILE_CACHE: "1", + UV_THREADPOOL_SIZE: "2", + }, + stderr: "pipe", +}); +const serverLog = createWriteStream(join(values.output, "server.log")); +const serverLogFinished = finished(serverLog); +serverLogFinished.catch(() => {}); +transport.stderr?.pipe(serverLog); +const report = { + status: "running", + question: + "Return the file SHA-256, file-wide entropy, every entropy block and adjacent high/low run in the requested byte window; preserve the complete source Evidence.", + request: oracle.input, + calls: [], + phases: {}, + client: + "Production stdio SDK, exact discovered toolDefinition and default validator", + runtime_identity_sha256: sha256(identityBytes), + oracle_sha256: sha256(oracleBytes), + driver_sha256: sha256(await readFile(new URL(import.meta.url))), +}; +const transcript = []; +let definitions; +const call = createRetainedQueryCaller( + { + callTool(params, options) { + return client.callTool(params, { + ...options, + toolDefinition: definitions.get(params.name), + }); + }, + }, + (receipt, event) => { + report.calls.push(receipt); + transcript.push(event); + }, +); +const observe = async (name, args, phase) => { + const result = await call(name, args, phase); + assert.notEqual(result.isError, true, JSON.stringify(result)); + assert.ok(result.structuredContent); + return result.structuredContent; +}; +const verifyRuntime = async () => { + assert.equal(sha256(await readFile(values.oracle)), sha256(oracleBytes)); + assert.equal(sha256(await readFile(values.identity)), sha256(identityBytes)); + assert.equal(sha256(await readFile(process.execPath)), identity.node_sha256); + assert.equal( + sha256(await readFile(new URL(import.meta.url))), + report.driver_sha256, + ); + for (const item of identity.files) + assert.equal(sha256(await readFile(item.path)), item.sha256, item.path); +}; +try { + await verifyRuntime(); + const start = performance.now(); + await client.connect(transport); + report.server_instructions = client.getInstructions() ?? ""; + const catalog = await client.listTools(); + definitions = new Map(catalog.tools.map((tool) => [tool.name, tool])); + await writeFile(join(values.output, "catalog.json"), JSON.stringify(catalog)); + report.phases.discovery_ms = performance.now() - start; + const parent = await observe("inspect_byte_entropy", oracle.input, "inspect"); + for (const [key, value] of Object.entries(oracle.expected)) + assert.deepEqual(parent.normalized_result[key], value, key); + report.parent_evidence_id = parent.evidence_id; + report.source_report_sha256 = sha256( + JSON.stringify(parent.normalized_result), + ); + report.artifact = parent.normalized_result.artifact; + await observe( + "export_evidence_bundle", + { path: join(values.output, "source.evidence.json") }, + "export", + ); + const bundle = JSON.parse( + await readFile(join(values.output, "source.evidence.json")), + ); + assert.deepEqual( + bundle.records.find((item) => item.evidence_id === parent.evidence_id), + parent, + ); + report.complete_source_export_verified = true; + report.status = "passed"; +} catch (cause) { + report.status = "failed"; + report.error = String(cause); +} finally { + try { + await verifyRuntime(); + } catch (cause) { + report.status = "failed"; + report.identity_failure = String(cause); + } + await finalizeRetainedQueryReceipt({ + client, + transport, + account, + serverLog, + serverLogFinished, + report, + transcript, + output: values.output, + }); + if ( + report.status !== "passed" || + report.cleanup_failures.length || + report.receipt_failures.length + ) + process.exitCode = 1; +} diff --git a/experiments/analysis-efficiency/erlang-beam-costs.py b/experiments/analysis-efficiency/erlang-beam-costs.py new file mode 100644 index 0000000000..565d777aff --- /dev/null +++ b/experiments/analysis-efficiency/erlang-beam-costs.py @@ -0,0 +1,111 @@ +"""Verify completed BEAM selection/export tasks and count a declared token projection.""" +import argparse +import hashlib +import json +from pathlib import Path +import statistics + +import tiktoken + + +def compact(value): + return json.dumps(value, ensure_ascii=False, separators=(',', ':'), allow_nan=False) + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def inspect(path, encoding): + report = json.loads((path / 'report.json').read_text()) + catalog = json.loads((path / 'catalog.json').read_text()) + transcript = json.loads((path / 'transcript.json').read_text()) + scope = next(json.loads(line) for line in reversed(path.with_suffix('.log').read_text().splitlines()) + if line.startswith('{"resource_scope":')) + observed = scope.get('payload_resource_observation') or {} + events = observed.get('resource_events', {}) + assert report['status'] == 'passed' and report['runtime_identity_sha256'] + assert report['complete_source_export_verified'] is True + assert not report['cleanup_failures'] and not report['receipt_failures'] + assert scope['abort'] is None and scope['payload_resource_observation_verified'] is True + assert observed['payload_exit_code'] == 0 and observed['payload_cleanup_verified'] is True + assert all(events.get('memory.events', {}).get(key) == 0 for key in ['high', 'max', 'oom', 'oom_kill']) + assert events.get('pids.events', {}).get('max') == 0 + assert len(catalog['tools']) == report['catalog_tools'] == 221 + assert transcript[0]['name'] == 'inspect_erlang_beam' + assert transcript[-1]['name'] == 'export_evidence_bundle' + assert all(event['name'] == 'inspect_analysis_view' for event in transcript[1:-1]) + assert all('reply' in event and not event['reply'].get('isError', False) for event in transcript) + count = lambda text: len(encoding.encode(text, disallowed_special=())) + directory = [{key: tool[key] for key in ['name', 'description', 'inputSchema']} for tool in catalog['tools']] + request = transcript[0]['input'] + question = report['question'] + assert ('detail' in request) == (report['lane'] == 'selected') + arguments = sum(count(compact({'name': event['name'], 'arguments': event['input']})) for event in transcript) + replies = sum(count(block['text']) for event in transcript for block in event['reply']['content'] if block['type'] == 'text') + inputs, prompt = count(compact(directory)), count(question) + instructions = count(report['server_instructions']) + return dict(case=path.name, lane=report['lane'], artifact=report['artifact'], + answer_count=report['answer_count'], answer_sha256=report['answer_sha256'], + parent_evidence_id=report['parent_evidence_id'], source_report_sha256=report['source_report_sha256'], + runtime_identity_sha256=report['runtime_identity_sha256'], catalog_sha256=digest(path / 'catalog.json'), + calls=report['calls'], phases=report['phases'], wall_seconds=scope['elapsed_seconds'], + cpu_seconds=observed['cpu_stat']['usage_usec'] / 1e6, + kernel_family_peak_bytes=observed['memory_peak_bytes'], + events_at_payload_exit=events, payload_cleanup_verified=True, + final_whole_scope_events=scope.get('resource_events_final'), + token_proxy=dict(question=prompt, server_instructions=instructions, input_catalog_once=inputs, arguments=arguments, + text_replies=replies, combined=prompt + instructions + inputs + arguments + replies, + complete_transport_catalog=count(compact(catalog))), + receipt_sha256={file.name: digest(file) for file in [path / 'report.json', path / 'catalog.json', + path / 'transcript.json', path / 'evidence.json', path.with_suffix('.log')]}) + + +def summarize(rows): + result = {} + for artifact in sorted({row['artifact']['sha256'] for row in rows}): + cases = [row for row in rows if row['artifact']['sha256'] == artifact] + for field in ['answer_count', 'answer_sha256', 'parent_evidence_id', 'source_report_sha256']: + assert len({row[field] for row in cases}) == 1, field + result[artifact] = {} + for lane in ['complete', 'selected']: + samples = [row for row in cases if row['lane'] == lane] + assert len(samples) == 3 + for field in ['runtime_identity_sha256', 'catalog_sha256']: + assert len({row[field] for row in samples}) == 1, field + metrics = {key: [row[key] for row in samples] for key in ['wall_seconds', 'cpu_seconds', 'kernel_family_peak_bytes']} + metrics['projected_tokens'] = [row['token_proxy']['combined'] for row in samples] + metrics['mcp_reply_bytes'] = [sum(call['reply_bytes'] for call in row['calls']) for row in samples] + result[artifact][lane] = {key: dict(median=statistics.median(values), minimum=min(values), maximum=max(values)) + for key, values in metrics.items()} + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--cohort', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + args = parser.parse_args() + manifest = json.loads((args.cohort / 'manifest.json').read_text()) + index = json.loads((args.cohort / 'index.json').read_text()) + assert len(index) == 18 and all(case['exit'] == 0 for case in index) + for file, expected in manifest['driver_files'].items(): + assert digest(Path(file)) == expected, file + identities = {} + for lane, path in manifest['runtime_identities'].items(): + identity = json.loads(Path(path).read_text()) + assert identity['source_clean'] is True + identities[lane] = dict(sha256=digest(Path(path)), source_revision=identity['source_revision']) + encoding = tiktoken.get_encoding('cl100k_base') + rows = [inspect(args.cohort / case['name'], encoding) for case in index] + assert all(row['runtime_identity_sha256'] == identities[row['lane']]['sha256'] for row in rows) + receipt = dict(tiktoken=tiktoken.__version__, encoding=encoding.name, actual_model_usage=None, + cohort=manifest, commands=index, runtime_identities=identities, + projection='Scripted fresh SDK tasks, not model runs. One fixed question, server instructions and complete input directory (name/description/inputSchema), every argument and MCP text reply including export. Raw catalog including output schemas reported separately. No framing, host injection, history replay, inference quality or billable usage measured.', + resources='CPU quota100%, payload affinity0-1, initial observer15. Early stop3GiB, hard4GiB, swap0. Fresh HOME/client/server; Node compile cache disabled, OS caches unflushed. Whole process-family CPU and kernel memory peak include file cache, runtime identity checks, hashing, assertions, SDK/server serialization and cleanup. Oracle generation and tokenization outside timed tasks. Payload-exit counters verified; final whole-scope counters unavailable.', + summary=summarize(rows), recordings=rows) + args.output.write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n') + + +if __name__ == '__main__': + main() diff --git a/experiments/analysis-efficiency/erlang-beam-oracle.py b/experiments/analysis-efficiency/erlang-beam-oracle.py new file mode 100644 index 0000000000..f283d33475 --- /dev/null +++ b/experiments/analysis-efficiency/erlang-beam-oracle.py @@ -0,0 +1,108 @@ +"""Independent struct oracle for the fixed public OTP 27 benchmark artifacts. + +Reads bytes only. It deliberately refuses compact atom layouts; format-wide +coverage belongs to the production reader and its separate regressions. +""" +from pathlib import Path +import getopt +import hashlib +import json +import struct +import sys + + +def decode(path): + data = path.read_bytes() + assert len(data) <= 64 * 1024 * 1024 + u32 = lambda at: struct.unpack_from('>I', data, at)[0] + assert data[:4] == b'FOR1' and data[8:12] == b'BEAM' + assert u32(4) + 8 == len(data) + chunks, selected, at = [], {}, 12 + while at < len(data): + name = data[at:at + 4].decode('ascii') + size = u32(at + 4) + end = at + 8 + size + padded = (end + 3) & ~3 + assert end <= padded <= len(data) + chunks.append(dict(index=len(chunks), name=name, + location=dict(offset=at, bytes=padded - at), + payload=dict(location=dict(offset=at + 8, bytes=size), + bytes=size, sha256=hashlib.sha256(data[at + 8:end]).hexdigest()))) + selected[name] = (at + 8, end) + at = padded + assert at == len(data) + atom_name = 'AtU8' if 'AtU8' in selected else 'Atom' + start, end = selected[atom_name] + n = u32(start) + assert n < 65536, 'Fixed OTP 27 oracle does not decode compact atom tables' + at, atoms = start + 4, [] + for i in range(n): + size = data[at] + at += 1 + name = data[at:at + size].decode('utf-8' if atom_name == 'AtU8' else 'latin1') + atoms.append(dict(index=i + 1, name=name)) + at += size + assert at == end + + def atom(i): + return atoms[i - 1]['name'] if 0 < i <= len(atoms) else None + + def table(name, keys): + if name not in selected: + return None + start, end = selected[name] + if name == 'FunT' and start == end: + return None + n, width = u32(start), len(keys) + assert start + 4 + n * 4 * width == end + rows = [] + for i in range(n): + values = struct.unpack_from('>' + 'I' * width, data, start + 4 + i * width * 4) + row = dict(zip(keys, values)) + if 'module_index' in row: + row['module'] = atom(row['module_index']) + row['function'] = atom(row['function_index']) + rows.append(row) + return rows + + exports = table('ExpT', ['function_index', 'arity', 'label']) + imports = table('ImpT', ['module_index', 'function_index', 'arity']) + lambdas = table('FunT', ['function_index', 'arity', 'label', 'index', 'free_vars', 'old_unique']) + code = None + if 'Code' in selected: + start, end = selected['Code'] + size = u32(start) + assert size >= 16 and start + 4 + size <= end + code = dict(zip(['instruction_set', 'opcode_max', 'labels', 'functions'], + struct.unpack_from('>IIII', data, start + 4))) + code.update(extra_header_bytes=size - 16, + instructions=dict(offset=start + 4 + size, bytes=end - start - 4 - size)) + return dict(artifact=dict(path=str(path), sha256=hashlib.sha256(data).hexdigest(), bytes=len(data)), + form=dict(location=dict(offset=0, bytes=len(data)), size=len(data) - 8), + chunks=chunks, atoms=atoms, atom_encoding='utf8' if atom_name == 'AtU8' else 'latin1', + exports=exports, imports=imports, lambdas=lambdas, code=code, + module=atoms[0]['name'], + coverage=dict(status='complete', examined_chunks=len(chunks), atoms=len(atoms))) + + +def main(): + options, remaining = getopt.getopt(sys.argv[1:], '', ['input=', 'output=', 'name=']) + assert not remaining + inputs = [v for k, v in options if k == '--input'] + outputs = [v for k, v in options if k == '--output'] + names = [v for k, v in options if k == '--name'] + assert len(inputs) == len(outputs) == 1 and names + path = Path(inputs[0]) + assert path.is_absolute() + expected = decode(path) + answer = [dict(source_index=i, **row) for i, row in enumerate(expected['exports'] or []) + if row['function'] in names] + result = dict(oracle='Independent Python struct decoding of fixed OTP27 bytes; no BEAM execution. ' + 'Every table, chunk range/hash and Code header checked. Other encodings/malformed inputs unqualified.', + source='https://www.erlang.org/doc/apps/stdlib/beam_lib.html', + expected=expected, names=names, answer=answer) + Path(outputs[0]).write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n') + + +if __name__ == '__main__': + main() diff --git a/experiments/analysis-efficiency/erlang-beam-sdk-cache.json b/experiments/analysis-efficiency/erlang-beam-sdk-cache.json new file mode 100644 index 0000000000..c7656b747b --- /dev/null +++ b/experiments/analysis-efficiency/erlang-beam-sdk-cache.json @@ -0,0 +1,534 @@ +{ + "method": "3alternating freshsessions perlane; SDK cachesentirecatalog thenusesdefaultvalidator withouttoolDefinition override. Diagnostic client startup control, separate from18exactnamedtasks. Samequestion/fullsourceexports/oracle; allpayloadexit/cleanupverified.", + "summary": { + "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55": { + "complete": { + "wall_seconds": { + "median": 19.764, + "minimum": 17.257, + "maximum": 21.577 + }, + "cpu_seconds": { + "median": 16.276557, + "minimum": 15.287268, + "maximum": 16.561897 + }, + "kernel_family_peak_bytes": { + "median": 1073950720, + "minimum": 1041948672, + "maximum": 1078980608 + }, + "projected_tokens": { + "median": 76907, + "minimum": 76907, + "maximum": 76907 + }, + "mcp_reply_bytes": { + "median": 147003, + "minimum": 147003, + "maximum": 147003 + } + }, + "selected": { + "wall_seconds": { + "median": 19.375, + "minimum": 18.052, + "maximum": 22.397 + }, + "cpu_seconds": { + "median": 16.867381, + "minimum": 16.139353, + "maximum": 17.069666 + }, + "kernel_family_peak_bytes": { + "median": 1105092608, + "minimum": 1059520512, + "maximum": 1117036544 + }, + "projected_tokens": { + "median": 58090, + "minimum": 58090, + "maximum": 58090 + }, + "mcp_reply_bytes": { + "median": 7469, + "minimum": 7469, + "maximum": 7469 + } + } + } + }, + "recordings": [ + { + "case": "erl_lint-1-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam" + }, + "phase": "inspect", + "elapsed_ms": 18092.161941, + "reply_bytes": 146627, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/sdk-cache-cohort/erl_lint-1-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 52.40052199999991, + "reply_bytes": 376, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1018.838796 + }, + "wall_seconds": 21.577, + "cpu_seconds": 16.561897, + "kernel_family_peak_bytes": 1073950720, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 78, + "text_replies": 20875, + "combined": 76907, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "3717e681ac50a70db8bd7303ac18d87bd2028d4321104a793421695a82579290", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "c718ac6e9b6c63533bc28d18c90c73a03abc1697516ae216a444b6dd55827255", + "evidence.json": "ca66367cd1bb325d7d4e47f958c86234d01aaf749b68778ac5212cac246aacb3", + "erl_lint-1-complete.log": "a90d1345cd09e90865c8a75f732abe2561c2299d0084718e91205f2cb904f38d" + } + }, + { + "case": "erl_lint-1-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module", "exprs", "expr"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 18394.333454, + "reply_bytes": 7093, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/sdk-cache-cohort/erl_lint-1-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 58.59494199999972, + "reply_bytes": 376, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1056.9951270000001 + }, + "wall_seconds": 22.397, + "cpu_seconds": 17.069666, + "kernel_family_peak_bytes": 1117036544, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 120, + "text_replies": 1066, + "combined": 58090, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "c3c17495f33ee4ea9daabd7a353f698fa0a804451c4bbdbe9eb1adf905ab483a", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "fd8c589a6e0dbcd0783ef5a526b51db7a3c208cd4067986ed99804bda06c4498", + "evidence.json": "2373bc2e57e40aac881f3ef91fa77187b098dd61d6cf44d37a4b8fe56d022b40", + "erl_lint-1-selected.log": "fdd55def718ffa7c972b3cb9b6be1b7f66a7fa5fb1c371cc492051ffb0d08f62" + } + }, + { + "case": "erl_lint-2-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module", "exprs", "expr"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 15135.096698, + "reply_bytes": 7093, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/sdk-cache-cohort/erl_lint-2-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 37.63045200000124, + "reply_bytes": 376, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1335.57118 + }, + "wall_seconds": 19.375, + "cpu_seconds": 16.139353, + "kernel_family_peak_bytes": 1059520512, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 120, + "text_replies": 1066, + "combined": 58090, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "d25a255b106e1bb232237474915e2900e313ba354aded0d27dad81ea74cfe0c6", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "2c0f92c25f97bb825b99133b84d792c7aac315b4c100986d75cb01939af6ce77", + "evidence.json": "2373bc2e57e40aac881f3ef91fa77187b098dd61d6cf44d37a4b8fe56d022b40", + "erl_lint-2-selected.log": "011553e956860fb3ce9d4e54ca7d4a2baf88a260786d82f60b1d0b70974fdbb7" + } + }, + { + "case": "erl_lint-2-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam" + }, + "phase": "inspect", + "elapsed_ms": 15133.678018999999, + "reply_bytes": 146627, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/sdk-cache-cohort/erl_lint-2-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 44.53432299999986, + "reply_bytes": 376, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1000.9708420000002 + }, + "wall_seconds": 19.764, + "cpu_seconds": 15.287268, + "kernel_family_peak_bytes": 1041948672, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 78, + "text_replies": 20875, + "combined": 76907, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "61aa7d7c78db145af4399b22d57aa39c2008c479530b28a49d401a6d564e46f7", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "3729bcc5547ab06f80b578c24bbb7937a438b0c190ee6680d0d7868cb5885159", + "evidence.json": "ca66367cd1bb325d7d4e47f958c86234d01aaf749b68778ac5212cac246aacb3", + "erl_lint-2-complete.log": "7bb8d637df3cb7ba542345c9d93ea09fe967d11c90cf5383083db7cd2c5fafb8" + } + }, + { + "case": "erl_lint-3-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam" + }, + "phase": "inspect", + "elapsed_ms": 13658.772345000001, + "reply_bytes": 146627, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/sdk-cache-cohort/erl_lint-3-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 48.42420299999867, + "reply_bytes": 376, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1121.564358 + }, + "wall_seconds": 17.257, + "cpu_seconds": 16.276557, + "kernel_family_peak_bytes": 1078980608, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 78, + "text_replies": 20875, + "combined": 76907, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "d683aab80d34c4f0fb474981c5715a4ba1ee706bf4e74be10bf69c40358ec011", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "12380e1e67368b8df7787d54be828c953d4e2ef63007c04cd9fc912b42cc42b5", + "evidence.json": "ca66367cd1bb325d7d4e47f958c86234d01aaf749b68778ac5212cac246aacb3", + "erl_lint-3-complete.log": "404f609e4dc10bbf0d8f1d3076751b2cb8b271ff401b00ea161a00944a55d558" + } + }, + { + "case": "erl_lint-3-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module", "exprs", "expr"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 14405.966256000002, + "reply_bytes": 7093, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/sdk-cache-cohort/erl_lint-3-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 31.32568199999878, + "reply_bytes": 376, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1107.032897 + }, + "wall_seconds": 18.052, + "cpu_seconds": 16.867381, + "kernel_family_peak_bytes": 1105092608, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 120, + "text_replies": 1066, + "combined": 58090, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "732e5b4f880d3a981d5362ece88578a2da9e68e80e97eca2dec2ae5856ebbdee", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "e62dc35d939b21418989093a8addadbe533d358a046feb7d99cecab10cedc2d4", + "evidence.json": "2373bc2e57e40aac881f3ef91fa77187b098dd61d6cf44d37a4b8fe56d022b40", + "erl_lint-3-selected.log": "2f35909a9ad5efa9e8ddeffe665887e8a9e9c97c13b4a030b765dd1ceecb14ef" + } + } + ] +} diff --git a/experiments/analysis-efficiency/erlang-beam-views.json b/experiments/analysis-efficiency/erlang-beam-views.json new file mode 100644 index 0000000000..9fcdcf758e --- /dev/null +++ b/experiments/analysis-efficiency/erlang-beam-views.json @@ -0,0 +1,2118 @@ +{ + "tiktoken": "0.14.0", + "encoding": "cl100k_base", + "actual_model_usage": null, + "cohort": { + "runtime_identities": { + "complete": "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "selected": "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json" + }, + "driver_files": { + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs": "6751a6f7112fc17602302f864a35bb7accf2abf5e41f8cd902fe696a0c149ef3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-oracle.py": "b57bb5a492b535b8e1c323e967394399a33b365b6c3d41797cfc21b8476dcbad", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/retained-query-calls.mjs": "6cef1ceda424622d6a9654e388ff22d246eeb6f9d7b182b2122a83a2378fdaf7", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/retained-query-finalization.mjs": "901092ae6dc7fda1a8869d62362a0a9f560e03d5ef5667a5c6ed660db000b9a2" + }, + "base": "e09b06369d923e9cccedd91f6839ba643dfbd893", + "feature": "bcc3f761127e4a369e36ed17b4eb766578400562", + "host": "Linux-6.1.0-54-cloud-amd64-x86_64-with-glibc2.36", + "sampling": "3 alternating fresh complete/selected sessions per real artifact; same question and complete source export in both lanes. Nodecompilecache disabled, OS caches unflushed. One full input directory once and every argument/textreply; no actual model run. Exact discovered SDK toolDefinition/default validator in main cohort;6separatecachedcatalog controls.", + "package": { + "url": "https://deb.debian.org/debian/pool/main/e/erlang/erlang-base_27.3.4.1%2Bdfsg-1%2Bdeb13u3_amd64.deb", + "package_bytes": 11425380, + "package_sha256": "4ebf03a400bc44420df4f1b507718413fcb42ed64b979bca05778722e777da13", + "members_observed": 294, + "execution": "No Erlang executable or selected bytecode run; public package read as an archive." + }, + "oracle_files": { + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_lint-oracle.json": "39b1f1155ade024eb1d2eab83d74f0b0a965e250840bf2881a3e58478caba1a5", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_parse-oracle.json": "ce2d2a52179066e74e667b8de7522ad1294ba4199e9896058cddeee06deccb3d", + "/tmp/rea-perf-static-tool-audit-20261024/clean-pg2-oracle.json": "a5a5bdbe2bf4e1bf1ed52e21faf2e231e836e0031cd4861c74ed49d5891aabd1" + } + }, + "commands": [ + { + "name": "erl_lint-1-complete", + "lane": "complete", + "case": "erl_lint", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-6938-base-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_lint-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-1-complete", + "--lane", + "complete" + ], + "exit": 0 + }, + { + "name": "erl_lint-1-selected", + "lane": "selected", + "case": "erl_lint", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-beam-views-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_lint-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-1-selected", + "--lane", + "selected" + ], + "exit": 0 + }, + { + "name": "erl_parse-1-complete", + "lane": "complete", + "case": "erl_parse", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-6938-base-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_parse-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-1-complete", + "--lane", + "complete" + ], + "exit": 0 + }, + { + "name": "erl_parse-1-selected", + "lane": "selected", + "case": "erl_parse", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-beam-views-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_parse-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-1-selected", + "--lane", + "selected" + ], + "exit": 0 + }, + { + "name": "pg2-1-complete", + "lane": "complete", + "case": "pg2", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-6938-base-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-pg2-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-1-complete", + "--lane", + "complete" + ], + "exit": 0 + }, + { + "name": "pg2-1-selected", + "lane": "selected", + "case": "pg2", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-beam-views-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-pg2-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-1-selected", + "--lane", + "selected" + ], + "exit": 0 + }, + { + "name": "erl_lint-2-selected", + "lane": "selected", + "case": "erl_lint", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-beam-views-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_lint-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-2-selected", + "--lane", + "selected" + ], + "exit": 0 + }, + { + "name": "erl_lint-2-complete", + "lane": "complete", + "case": "erl_lint", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-6938-base-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_lint-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-2-complete", + "--lane", + "complete" + ], + "exit": 0 + }, + { + "name": "erl_parse-2-selected", + "lane": "selected", + "case": "erl_parse", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-beam-views-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_parse-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-2-selected", + "--lane", + "selected" + ], + "exit": 0 + }, + { + "name": "erl_parse-2-complete", + "lane": "complete", + "case": "erl_parse", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-6938-base-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_parse-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-2-complete", + "--lane", + "complete" + ], + "exit": 0 + }, + { + "name": "pg2-2-selected", + "lane": "selected", + "case": "pg2", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-beam-views-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-pg2-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-2-selected", + "--lane", + "selected" + ], + "exit": 0 + }, + { + "name": "pg2-2-complete", + "lane": "complete", + "case": "pg2", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-6938-base-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-pg2-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-2-complete", + "--lane", + "complete" + ], + "exit": 0 + }, + { + "name": "erl_lint-3-complete", + "lane": "complete", + "case": "erl_lint", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-6938-base-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_lint-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-3-complete", + "--lane", + "complete" + ], + "exit": 0 + }, + { + "name": "erl_lint-3-selected", + "lane": "selected", + "case": "erl_lint", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-beam-views-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_lint-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-3-selected", + "--lane", + "selected" + ], + "exit": 0 + }, + { + "name": "erl_parse-3-complete", + "lane": "complete", + "case": "erl_parse", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-6938-base-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_parse-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-3-complete", + "--lane", + "complete" + ], + "exit": 0 + }, + { + "name": "erl_parse-3-selected", + "lane": "selected", + "case": "erl_parse", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-beam-views-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-erl_parse-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-3-selected", + "--lane", + "selected" + ], + "exit": 0 + }, + { + "name": "pg2-3-complete", + "lane": "complete", + "case": "pg2", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-6938-base-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-complete-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-pg2-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-3-complete", + "--lane", + "complete" + ], + "exit": 0 + }, + { + "name": "pg2-3-selected", + "lane": "selected", + "case": "pg2", + "command": [ + "python3", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/resource-run.py", + "--observe-payload-exit", + "--", + "/tmp/rea-perf-native-exports-20261023/run-task.sh", + "env", + "NODE_DISABLE_COMPILE_CACHE=1", + "node", + "/home/yann/yann/rea-perf-beam-views-20261024/experiments/analysis-efficiency/erlang-beam-views.mjs", + "--runtime", + "/home/yann/yann/rea-perf-beam-views-20261024", + "--identity", + "/tmp/rea-perf-static-tool-audit-20261024/paired-selected-runtime-identity.json", + "--oracle", + "/tmp/rea-perf-static-tool-audit-20261024/clean-pg2-oracle.json", + "--output", + "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-3-selected", + "--lane", + "selected" + ], + "exit": 0 + } + ], + "runtime_identities": { + "complete": { + "sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "source_revision": "e09b06369d923e9cccedd91f6839ba643dfbd893" + }, + "selected": { + "sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "source_revision": "bcc3f761127e4a369e36ed17b4eb766578400562" + } + }, + "projection": "Scripted fresh SDK tasks, not model runs. One fixed question, server instructions and complete input directory (name/description/inputSchema), every argument and MCP text reply including export. Raw catalog including output schemas reported separately. No framing, host injection, history replay, inference quality or billable usage measured.", + "resources": "CPU quota100%, payload affinity0-1, initial observer15. Early stop3GiB, hard4GiB, swap0. Fresh HOME/client/server; Node compile cache disabled, OS caches unflushed. Whole process-family CPU and kernel memory peak include file cache, runtime identity checks, hashing, assertions, SDK/server serialization and cleanup. Oracle generation and tokenization outside timed tasks. Payload-exit counters verified; final whole-scope counters unavailable.", + "summary": { + "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55": { + "complete": { + "wall_seconds": { + "median": 6.773, + "minimum": 6.267, + "maximum": 6.974 + }, + "cpu_seconds": { + "median": 5.580634, + "minimum": 5.009249, + "maximum": 5.608298 + }, + "kernel_family_peak_bytes": { + "median": 699400192, + "minimum": 664604672, + "maximum": 752414720 + }, + "projected_tokens": { + "median": 76907, + "minimum": 76907, + "maximum": 76907 + }, + "mcp_reply_bytes": { + "median": 146997, + "minimum": 146997, + "maximum": 146997 + } + }, + "selected": { + "wall_seconds": { + "median": 6.87, + "minimum": 6.67, + "maximum": 7.277 + }, + "cpu_seconds": { + "median": 5.312143, + "minimum": 5.179784, + "maximum": 5.333158 + }, + "kernel_family_peak_bytes": { + "median": 661504000, + "minimum": 660738048, + "maximum": 669630464 + }, + "projected_tokens": { + "median": 58090, + "minimum": 58090, + "maximum": 58090 + }, + "mcp_reply_bytes": { + "median": 7463, + "minimum": 7463, + "maximum": 7463 + } + } + }, + "729cce2b1688a1613d1ee71f5d0d99a9430d163d08fbe1aa5148dbbe6494f458": { + "complete": { + "wall_seconds": { + "median": 6.367, + "minimum": 6.167, + "maximum": 8.18 + }, + "cpu_seconds": { + "median": 5.62721, + "minimum": 5.219992, + "maximum": 5.939738 + }, + "kernel_family_peak_bytes": { + "median": 698499072, + "minimum": 667934720, + "maximum": 703078400 + }, + "projected_tokens": { + "median": 72499, + "minimum": 72499, + "maximum": 72499 + }, + "mcp_reply_bytes": { + "median": 110451, + "minimum": 110451, + "maximum": 110451 + } + }, + "selected": { + "wall_seconds": { + "median": 6.974, + "minimum": 5.761, + "maximum": 7.678 + }, + "cpu_seconds": { + "median": 5.554719, + "minimum": 5.198619, + "maximum": 5.905078 + }, + "kernel_family_peak_bytes": { + "median": 689467392, + "minimum": 657203200, + "maximum": 716496896 + }, + "projected_tokens": { + "median": 58087, + "minimum": 58087, + "maximum": 58087 + }, + "mcp_reply_bytes": { + "median": 7375, + "minimum": 7375, + "maximum": 7375 + } + } + }, + "f01d6f2da046796ec572657ca10cb73c3193d42663315d5446f29aeb5821b5ef": { + "complete": { + "wall_seconds": { + "median": 6.365, + "minimum": 5.561, + "maximum": 7.682 + }, + "cpu_seconds": { + "median": 5.29498, + "minimum": 5.074642, + "maximum": 5.501023 + }, + "kernel_family_peak_bytes": { + "median": 679653376, + "minimum": 651247616, + "maximum": 685228032 + }, + "projected_tokens": { + "median": 57733, + "minimum": 57733, + "maximum": 57733 + }, + "mcp_reply_bytes": { + "median": 11089, + "minimum": 11089, + "maximum": 11089 + } + }, + "selected": { + "wall_seconds": { + "median": 7.576, + "minimum": 6.672, + "maximum": 8.883 + }, + "cpu_seconds": { + "median": 5.614821, + "minimum": 5.189659, + "maximum": 5.695883 + }, + "kernel_family_peak_bytes": { + "median": 657383424, + "minimum": 653221888, + "maximum": 658235392 + }, + "projected_tokens": { + "median": 58018, + "minimum": 58018, + "maximum": 58018 + }, + "mcp_reply_bytes": { + "median": 7081, + "minimum": 7081, + "maximum": 7081 + } + } + } + }, + "recordings": [ + { + "case": "erl_lint-1-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam" + }, + "phase": "inspect", + "elapsed_ms": 2807.781777, + "reply_bytes": 146627, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-1-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 44.688083000000006, + "reply_bytes": 370, + "is_error": false + } + ], + "phases": { + "discovery_ms": 921.1063750000003 + }, + "wall_seconds": 6.267, + "cpu_seconds": 5.580634, + "kernel_family_peak_bytes": 752414720, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 78, + "text_replies": 20875, + "combined": 76907, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "914ce0d8be1703bdbd93d76b0fc7d0c8b9984ebd9ad4fecfe1687c9eb7f61704", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "c8f9b98cac403fa0939d1c949d045ebd4d9bb66ad73ef5f75dbbf55f834ce293", + "evidence.json": "ca66367cd1bb325d7d4e47f958c86234d01aaf749b68778ac5212cac246aacb3", + "erl_lint-1-complete.log": "312c99204694dff112ce7b57e056ac50844a44e72e9f07a0f75c4e66af8cc49c" + } + }, + { + "case": "erl_lint-1-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module", "exprs", "expr"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 3240.8563719999997, + "reply_bytes": 7093, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-1-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 45.78090299999985, + "reply_bytes": 370, + "is_error": false + } + ], + "phases": { + "discovery_ms": 909.0800340000001 + }, + "wall_seconds": 6.87, + "cpu_seconds": 5.312143, + "kernel_family_peak_bytes": 669630464, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 120, + "text_replies": 1066, + "combined": 58090, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "6a021a13839197688bca5acf0c6d86af52be7af34d143fa19e971a7f392a0514", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "00020bbd5dee73763812c9213c0a3a9e4be2653eb09d6228f45a375546246844", + "evidence.json": "2373bc2e57e40aac881f3ef91fa77187b098dd61d6cf44d37a4b8fe56d022b40", + "erl_lint-1-selected.log": "c8b05bd5f163ce6151efccb01afec03e47e7d198a15e75752762cb7c69650e02" + } + }, + { + "case": "erl_parse-1-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam", + "sha256": "729cce2b1688a1613d1ee71f5d0d99a9430d163d08fbe1aa5148dbbe6494f458", + "bytes": 608240 + }, + "answer_count": 3, + "answer_sha256": "10947c408b5056ca1a7cb6a8f8db6747618cad99d877075667e8bc24a1fdc262", + "parent_evidence_id": "ev_f9415ed479d5f05c97f9b2304ddcc56f8415a901a6ba5c95b29b55ca26cc84dd", + "source_report_sha256": "02b1af7e202d3886b2787ae1863ee608e4693a7492f04d3131caa9e2a7e174a3", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam" + }, + "phase": "inspect", + "elapsed_ms": 2747.3048309999995, + "reply_bytes": 110079, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-1-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 25.273911000000226, + "reply_bytes": 372, + "is_error": false + } + ], + "phases": { + "discovery_ms": 959.8128179999999 + }, + "wall_seconds": 6.167, + "cpu_seconds": 5.62721, + "kernel_family_peak_bytes": 698499072, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 35, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 76, + "text_replies": 16466, + "combined": 72499, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "d83d0ab94fec05829b14199eaa108a839d4b09e24b645084ba5f6350e000d4b6", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "8e39b9bb9214a33f8413461bccc9af819dd5fcb90d2d3dfc4eb3edf3055f5edd", + "evidence.json": "21070fe9430738da3b19f42185901c623782dc90622dfe1c32e68e3ef77f9078", + "erl_parse-1-complete.log": "6cdcd1a28b882ed828dec1d0df2b19698da0ffbf5db34921b87f3f65ef9999a9" + } + }, + { + "case": "erl_parse-1-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam", + "sha256": "729cce2b1688a1613d1ee71f5d0d99a9430d163d08fbe1aa5148dbbe6494f458", + "bytes": 608240 + }, + "answer_count": 3, + "answer_sha256": "10947c408b5056ca1a7cb6a8f8db6747618cad99d877075667e8bc24a1fdc262", + "parent_evidence_id": "ev_f9415ed479d5f05c97f9b2304ddcc56f8415a901a6ba5c95b29b55ca26cc84dd", + "source_report_sha256": "02b1af7e202d3886b2787ae1863ee608e4693a7492f04d3131caa9e2a7e174a3", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["parse_form", "parse_exprs", "parse_term"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 2753.1975509999997, + "reply_bytes": 7003, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-1-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 23.967811999999867, + "reply_bytes": 372, + "is_error": false + } + ], + "phases": { + "discovery_ms": 959.427747 + }, + "wall_seconds": 5.761, + "cpu_seconds": 5.554719, + "kernel_family_peak_bytes": 689467392, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 35, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 121, + "text_replies": 1059, + "combined": 58087, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "7cc0fac187d1f193e3b733f3f52e592d5ad5330453e1c230a0fb3cfb33689545", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "79dd907ad43301817ad7a061d246d3708a43938dc0a0e0386300918f56a544d7", + "evidence.json": "c23a115afa5c44f6421f925cd86c5405631a91dc0ccfdbeb4f59e38aead4b2de", + "erl_parse-1-selected.log": "da51d679fa2a89ce95e64d42168c7ad7f0666df66c24cd2fb2a12b4382a21045" + } + }, + { + "case": "pg2-1-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam", + "sha256": "f01d6f2da046796ec572657ca10cb73c3193d42663315d5446f29aeb5821b5ef", + "bytes": 1152 + }, + "answer_count": 2, + "answer_sha256": "592c2e565db9436b2ba65df4d1b7787a28e717588bc597c179d6c8d08cd77801", + "parent_evidence_id": "ev_f866b82f64a80921545dbd60b5d00292999d64b4fca4f168c0639fbe4c0253f5", + "source_report_sha256": "81ff98b2487b8b3d77217312e9f626a9bda6569e7ee3fd541248b07774a21bd7", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam" + }, + "phase": "inspect", + "elapsed_ms": 2568.0045200000004, + "reply_bytes": 10731, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-1-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 20.659660999999687, + "reply_bytes": 358, + "is_error": false + } + ], + "phases": { + "discovery_ms": 924.166424 + }, + "wall_seconds": 5.561, + "cpu_seconds": 5.29498, + "kernel_family_peak_bytes": 685228032, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 28, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 72, + "text_replies": 1711, + "combined": 57733, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "89d0fcfd6d4ac2ecab12d4511a260d1dc84e99e398a8437a713863a58d0b303b", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "bd72b26d42043bbc6abb28dd50b25f6d00eb99810b817eaf5e510091523446e7", + "evidence.json": "eee0a4a2e733e8f51edb6f2bfbd8176b2ab5608207cbd8178406aae9ee432d87", + "pg2-1-complete.log": "286265fbd0a62f5a21576eab12052b8b8a3e0571ee9ca7989d29066d51cf5d17" + } + }, + { + "case": "pg2-1-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam", + "sha256": "f01d6f2da046796ec572657ca10cb73c3193d42663315d5446f29aeb5821b5ef", + "bytes": 1152 + }, + "answer_count": 2, + "answer_sha256": "592c2e565db9436b2ba65df4d1b7787a28e717588bc597c179d6c8d08cd77801", + "parent_evidence_id": "ev_f866b82f64a80921545dbd60b5d00292999d64b4fca4f168c0639fbe4c0253f5", + "source_report_sha256": "81ff98b2487b8b3d77217312e9f626a9bda6569e7ee3fd541248b07774a21bd7", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module_info"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 3693.8386549999996, + "reply_bytes": 6723, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-1-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 33.49674200000027, + "reply_bytes": 358, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1004.845028 + }, + "wall_seconds": 8.883, + "cpu_seconds": 5.614821, + "kernel_family_peak_bytes": 657383424, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 28, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 110, + "text_replies": 1008, + "combined": 58018, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "457c1972f926aa43a7d21c03d03f37b6ee99dc90be83c79435b1472369da263d", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "20092db4d525db2bc3a5b608c17566b3bf1e904f74b0dba19148b23e329319d7", + "evidence.json": "e67fb26194e0e2c77053e315a593c699695753cee22e8d48ff43794afa546b51", + "pg2-1-selected.log": "1883d0b5d2213b994c934472ad8a60f639acd538b108267afb227562ffd7908e" + } + }, + { + "case": "erl_lint-2-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module", "exprs", "expr"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 3805.7522, + "reply_bytes": 7093, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-2-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 45.88289199999963, + "reply_bytes": 370, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1020.4027999999998 + }, + "wall_seconds": 7.277, + "cpu_seconds": 5.179784, + "kernel_family_peak_bytes": 661504000, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 120, + "text_replies": 1066, + "combined": 58090, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "d70998963fafe90b5d785f5bd6b4d4e87fe9310e8584f96ee13ffe8658b87e73", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "3c99bf6fb9432c15d8d817d99daae58bea32b2d2c5904e51c48a6fc8e0808223", + "evidence.json": "2373bc2e57e40aac881f3ef91fa77187b098dd61d6cf44d37a4b8fe56d022b40", + "erl_lint-2-selected.log": "ecb0aef50083a74aadd62ebf3b99434f4f05ed1fa0e113f2d6d44e75862f6581" + } + }, + { + "case": "erl_lint-2-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam" + }, + "phase": "inspect", + "elapsed_ms": 3191.800223, + "reply_bytes": 146627, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-2-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 30.675680999999713, + "reply_bytes": 370, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1149.250356 + }, + "wall_seconds": 6.974, + "cpu_seconds": 5.608298, + "kernel_family_peak_bytes": 664604672, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 78, + "text_replies": 20875, + "combined": 76907, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "40ae0cd960e7ea9e5ca0a8c3add92d8f907ba5fdcf9ddf89b03b602c8ca848a4", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "08ee5abab00dda432d9c5f04c2a2a2f2978e9e169fc47b82b3421f1c83492cf8", + "evidence.json": "ca66367cd1bb325d7d4e47f958c86234d01aaf749b68778ac5212cac246aacb3", + "erl_lint-2-complete.log": "ed79f95afe948499b5ba842f2d4cfa25c93586c43d317bb4eb72399287f50f74" + } + }, + { + "case": "erl_parse-2-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam", + "sha256": "729cce2b1688a1613d1ee71f5d0d99a9430d163d08fbe1aa5148dbbe6494f458", + "bytes": 608240 + }, + "answer_count": 3, + "answer_sha256": "10947c408b5056ca1a7cb6a8f8db6747618cad99d877075667e8bc24a1fdc262", + "parent_evidence_id": "ev_f9415ed479d5f05c97f9b2304ddcc56f8415a901a6ba5c95b29b55ca26cc84dd", + "source_report_sha256": "02b1af7e202d3886b2787ae1863ee608e4693a7492f04d3131caa9e2a7e174a3", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["parse_form", "parse_exprs", "parse_term"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 3534.790642, + "reply_bytes": 7003, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-2-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 50.63602300000002, + "reply_bytes": 372, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1002.128269 + }, + "wall_seconds": 7.678, + "cpu_seconds": 5.905078, + "kernel_family_peak_bytes": 716496896, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 35, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 121, + "text_replies": 1059, + "combined": 58087, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "ad148478483965c8fe505b378f686955bd9e43e75e69d5f8f8e83b0fc8d45259", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "e675cad4a869743ef96ac2b598869a3b6a54a7fe8ef2ce3e50d3adfb17181139", + "evidence.json": "c23a115afa5c44f6421f925cd86c5405631a91dc0ccfdbeb4f59e38aead4b2de", + "erl_parse-2-selected.log": "23f3ae89276056e051ceb56d98c524c419a2f3ee59da68b3b39d9bfb9e970af4" + } + }, + { + "case": "erl_parse-2-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam", + "sha256": "729cce2b1688a1613d1ee71f5d0d99a9430d163d08fbe1aa5148dbbe6494f458", + "bytes": 608240 + }, + "answer_count": 3, + "answer_sha256": "10947c408b5056ca1a7cb6a8f8db6747618cad99d877075667e8bc24a1fdc262", + "parent_evidence_id": "ev_f9415ed479d5f05c97f9b2304ddcc56f8415a901a6ba5c95b29b55ca26cc84dd", + "source_report_sha256": "02b1af7e202d3886b2787ae1863ee608e4693a7492f04d3131caa9e2a7e174a3", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam" + }, + "phase": "inspect", + "elapsed_ms": 2986.7222790000005, + "reply_bytes": 110079, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-2-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 26.622252000000117, + "reply_bytes": 372, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1018.472458 + }, + "wall_seconds": 6.367, + "cpu_seconds": 5.219992, + "kernel_family_peak_bytes": 703078400, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 35, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 76, + "text_replies": 16466, + "combined": 72499, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "9bbbe5080da4fd1a3e5b25c5b659cefafe3879c15d989b1329067be8500000e9", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "41e37d59bceb8ed7051536d299eec32b9a52486bcd9aa58dc47de5b61353fcd2", + "evidence.json": "21070fe9430738da3b19f42185901c623782dc90622dfe1c32e68e3ef77f9078", + "erl_parse-2-complete.log": "d1522feab9194787c8bc0f7679ef1449bbfc67ba4df77253d1a87b245c41d2f2" + } + }, + { + "case": "pg2-2-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam", + "sha256": "f01d6f2da046796ec572657ca10cb73c3193d42663315d5446f29aeb5821b5ef", + "bytes": 1152 + }, + "answer_count": 2, + "answer_sha256": "592c2e565db9436b2ba65df4d1b7787a28e717588bc597c179d6c8d08cd77801", + "parent_evidence_id": "ev_f866b82f64a80921545dbd60b5d00292999d64b4fca4f168c0639fbe4c0253f5", + "source_report_sha256": "81ff98b2487b8b3d77217312e9f626a9bda6569e7ee3fd541248b07774a21bd7", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module_info"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 2903.7854450000004, + "reply_bytes": 6723, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-2-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 33.95551299999988, + "reply_bytes": 358, + "is_error": false + } + ], + "phases": { + "discovery_ms": 938.619553 + }, + "wall_seconds": 6.672, + "cpu_seconds": 5.695883, + "kernel_family_peak_bytes": 653221888, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 28, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 110, + "text_replies": 1008, + "combined": 58018, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "3621ce7cbee29808d40c726d1bbea664381a0a34630eee786bedb051d967b8a5", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "9f7ae3dda84f41356f77c4dfb66e15baedf89cbd732a804bb9e560d16b7c21dd", + "evidence.json": "e67fb26194e0e2c77053e315a593c699695753cee22e8d48ff43794afa546b51", + "pg2-2-selected.log": "e9a69b140c741020ccf98f97967d24e9f6666b357a0779edf8a41a066a8aace8" + } + }, + { + "case": "pg2-2-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam", + "sha256": "f01d6f2da046796ec572657ca10cb73c3193d42663315d5446f29aeb5821b5ef", + "bytes": 1152 + }, + "answer_count": 2, + "answer_sha256": "592c2e565db9436b2ba65df4d1b7787a28e717588bc597c179d6c8d08cd77801", + "parent_evidence_id": "ev_f866b82f64a80921545dbd60b5d00292999d64b4fca4f168c0639fbe4c0253f5", + "source_report_sha256": "81ff98b2487b8b3d77217312e9f626a9bda6569e7ee3fd541248b07774a21bd7", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam" + }, + "phase": "inspect", + "elapsed_ms": 3838.993407, + "reply_bytes": 10731, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-2-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 19.48830999999973, + "reply_bytes": 358, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1364.953607 + }, + "wall_seconds": 7.682, + "cpu_seconds": 5.501023, + "kernel_family_peak_bytes": 679653376, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 28, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 72, + "text_replies": 1711, + "combined": 57733, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "3d15187f0c7a4048999621040f7c717660ab6cfd5f0a17263f5a673baa08688c", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "d15ea3b28d34406e217bb7bb809e793e1ba5016f482a0e2ce68343c0b4a13346", + "evidence.json": "eee0a4a2e733e8f51edb6f2bfbd8176b2ab5608207cbd8178406aae9ee432d87", + "pg2-2-complete.log": "37f0d0e5d5b379ae1792f3a0cfd0bbff426e2d38fe4787069f21780d83e1651b" + } + }, + { + "case": "erl_lint-3-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam" + }, + "phase": "inspect", + "elapsed_ms": 3250.579772, + "reply_bytes": 146627, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-3-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 36.204091999999946, + "reply_bytes": 370, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1075.498081 + }, + "wall_seconds": 6.773, + "cpu_seconds": 5.009249, + "kernel_family_peak_bytes": 699400192, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 78, + "text_replies": 20875, + "combined": 76907, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "b6a24e7e13d82f6df2a8a9e73e9d54dacf17c6c6521bae12437a4e923065a455", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "f7a008d26bbd41bb923bc69de49e2ae3a947fd9da3b434268bb0869f362ce78f", + "evidence.json": "ca66367cd1bb325d7d4e47f958c86234d01aaf749b68778ac5212cac246aacb3", + "erl_lint-3-complete.log": "cc9f22658c68c18342d687381366f70a3de639f1e6e3ee1d9e162a1b6e5cefba" + } + }, + { + "case": "erl_lint-3-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "sha256": "716d78394b7c3da983e36aac7aadd6673e5041677ba7de20413cc2704d958d55", + "bytes": 233968 + }, + "answer_count": 4, + "answer_sha256": "34ce3091dbea9ac8cd20831cf6443c7a1de6e02b753246cafa3e0e22b83907c9", + "parent_evidence_id": "ev_df4bfa8b8f0391ad6291542fdccf9062ae3bb8dbabf01c2b166ca6f3493051fc", + "source_report_sha256": "db64350ae25bff9e77f5e167f4a4ac095ed872e09c15de5ce3b2728c078a16d2", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_lint.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module", "exprs", "expr"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 3406.196419, + "reply_bytes": 7093, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_lint-3-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 29.59845200000018, + "reply_bytes": 370, + "is_error": false + } + ], + "phases": { + "discovery_ms": 950.8520830000001 + }, + "wall_seconds": 6.67, + "cpu_seconds": 5.333158, + "kernel_family_peak_bytes": 660738048, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 32, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 120, + "text_replies": 1066, + "combined": 58090, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "8c6397ad241bc654fa22f8e3c18620ef48e12e4cb654199226f98855b206a57c", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "4b1d11acf13d8de543232da3a798449a500604ec3fb96b7b42ba65ed6518ab30", + "evidence.json": "2373bc2e57e40aac881f3ef91fa77187b098dd61d6cf44d37a4b8fe56d022b40", + "erl_lint-3-selected.log": "c2764f57ad97f252edfb9d885db629dced2192253b370cbaea14e808239f00a4" + } + }, + { + "case": "erl_parse-3-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam", + "sha256": "729cce2b1688a1613d1ee71f5d0d99a9430d163d08fbe1aa5148dbbe6494f458", + "bytes": 608240 + }, + "answer_count": 3, + "answer_sha256": "10947c408b5056ca1a7cb6a8f8db6747618cad99d877075667e8bc24a1fdc262", + "parent_evidence_id": "ev_f9415ed479d5f05c97f9b2304ddcc56f8415a901a6ba5c95b29b55ca26cc84dd", + "source_report_sha256": "02b1af7e202d3886b2787ae1863ee608e4693a7492f04d3131caa9e2a7e174a3", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam" + }, + "phase": "inspect", + "elapsed_ms": 2915.007422, + "reply_bytes": 110079, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-3-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 43.0001029999994, + "reply_bytes": 372, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1995.13426 + }, + "wall_seconds": 8.18, + "cpu_seconds": 5.939738, + "kernel_family_peak_bytes": 667934720, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 35, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 76, + "text_replies": 16466, + "combined": 72499, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "9fa05b25dcedc1a109d99784d409648952681b4154dc285ab3a7223f34d038e9", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "4ca8439f2942a3dbc6becf3917fd7b9ea0df710e411438335f3968f7105cc8bd", + "evidence.json": "21070fe9430738da3b19f42185901c623782dc90622dfe1c32e68e3ef77f9078", + "erl_parse-3-complete.log": "6fc1ab20ebafff99bdd792a5884a616e03fd930f1d7a444475e6bc6e3e32653d" + } + }, + { + "case": "erl_parse-3-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam", + "sha256": "729cce2b1688a1613d1ee71f5d0d99a9430d163d08fbe1aa5148dbbe6494f458", + "bytes": 608240 + }, + "answer_count": 3, + "answer_sha256": "10947c408b5056ca1a7cb6a8f8db6747618cad99d877075667e8bc24a1fdc262", + "parent_evidence_id": "ev_f9415ed479d5f05c97f9b2304ddcc56f8415a901a6ba5c95b29b55ca26cc84dd", + "source_report_sha256": "02b1af7e202d3886b2787ae1863ee608e4693a7492f04d3131caa9e2a7e174a3", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/erl_parse.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["parse_form", "parse_exprs", "parse_term"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 3316.580943, + "reply_bytes": 7003, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/erl_parse-3-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 58.031253000000106, + "reply_bytes": 372, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1181.213305 + }, + "wall_seconds": 6.974, + "cpu_seconds": 5.198619, + "kernel_family_peak_bytes": 657203200, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 35, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 121, + "text_replies": 1059, + "combined": 58087, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "6eb8158d18dd0d29716d8c7cad582326fe7360c078a12a4aa5d8aa567e6d445c", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "321584f796d26e01d743b0c1810bed2312e07ded31d663999ecabf4aefab6d63", + "evidence.json": "c23a115afa5c44f6421f925cd86c5405631a91dc0ccfdbeb4f59e38aead4b2de", + "erl_parse-3-selected.log": "726bc0356564ce67c3eb97476b5a9f1eb88b89b74da762b18a14a3e056d4230e" + } + }, + { + "case": "pg2-3-complete", + "lane": "complete", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam", + "sha256": "f01d6f2da046796ec572657ca10cb73c3193d42663315d5446f29aeb5821b5ef", + "bytes": 1152 + }, + "answer_count": 2, + "answer_sha256": "592c2e565db9436b2ba65df4d1b7787a28e717588bc597c179d6c8d08cd77801", + "parent_evidence_id": "ev_f866b82f64a80921545dbd60b5d00292999d64b4fca4f168c0639fbe4c0253f5", + "source_report_sha256": "81ff98b2487b8b3d77217312e9f626a9bda6569e7ee3fd541248b07774a21bd7", + "runtime_identity_sha256": "1b4b40330a2e21d31c06a07d10937cb6a3b37cd0bcbf3d129380341d486b916e", + "catalog_sha256": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam" + }, + "phase": "inspect", + "elapsed_ms": 3254.2005499999996, + "reply_bytes": 10731, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-3-complete/evidence.json" + }, + "phase": "export", + "elapsed_ms": 23.99913100000049, + "reply_bytes": 358, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1013.5910859999999 + }, + "wall_seconds": 6.365, + "cpu_seconds": 5.074642, + "kernel_family_peak_bytes": 651247616, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 28, + "server_instructions": 513, + "input_catalog_once": 55409, + "arguments": 72, + "text_replies": 1711, + "combined": 57733, + "complete_transport_catalog": 829075 + }, + "receipt_sha256": { + "report.json": "f7b22fc0017f925e0ba2b309db6962b9ef08882b4b5ec18231ee33020df02ed3", + "catalog.json": "f4a683209df5bed1ed25ae42c690fdd803348742132e88f77ba2e4e8333ca562", + "transcript.json": "3e8d7b66fffbfddf72926e171ec21500b01219f9271f2e072a836188e2033f38", + "evidence.json": "eee0a4a2e733e8f51edb6f2bfbd8176b2ab5608207cbd8178406aae9ee432d87", + "pg2-3-complete.log": "119e8222a29ea127e55eb0df0fb3c8b950401a658ab9fb0fcaa124c9b8cf1533" + } + }, + { + "case": "pg2-3-selected", + "lane": "selected", + "artifact": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam", + "sha256": "f01d6f2da046796ec572657ca10cb73c3193d42663315d5446f29aeb5821b5ef", + "bytes": 1152 + }, + "answer_count": 2, + "answer_sha256": "592c2e565db9436b2ba65df4d1b7787a28e717588bc597c179d6c8d08cd77801", + "parent_evidence_id": "ev_f866b82f64a80921545dbd60b5d00292999d64b4fca4f168c0639fbe4c0253f5", + "source_report_sha256": "81ff98b2487b8b3d77217312e9f626a9bda6569e7ee3fd541248b07774a21bd7", + "runtime_identity_sha256": "a74d203a5a12d7eb82aaa1e0fda6595b9647af2255f318bf116953777052460f", + "catalog_sha256": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "calls": [ + { + "name": "inspect_erlang_beam", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/real-beam/pg2.beam", + "detail": "summary", + "view": { + "kind": "beam", + "collection": "exports", + "names": ["module_info"], + "fields": ["function_index", "function", "arity", "label"], + "offset": 0, + "limit": 64 + } + }, + "phase": "inspect", + "elapsed_ms": 3507.9971619999997, + "reply_bytes": 6723, + "is_error": false + }, + { + "name": "export_evidence_bundle", + "input": { + "path": "/tmp/rea-perf-static-tool-audit-20261024/paired-cohort/pg2-3-selected/evidence.json" + }, + "phase": "export", + "elapsed_ms": 32.01046100000076, + "reply_bytes": 358, + "is_error": false + } + ], + "phases": { + "discovery_ms": 1223.840087 + }, + "wall_seconds": 7.576, + "cpu_seconds": 5.189659, + "kernel_family_peak_bytes": 658235392, + "events_at_payload_exit": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "payload_cleanup_verified": true, + "final_whole_scope_events": null, + "token_proxy": { + "question": 28, + "server_instructions": 513, + "input_catalog_once": 56359, + "arguments": 110, + "text_replies": 1008, + "combined": 58018, + "complete_transport_catalog": 834792 + }, + "receipt_sha256": { + "report.json": "2ead5b0fbd9b312bccaa9d3087a96a8001d971104e23cc81ea7180f5c8326f95", + "catalog.json": "9c09aa2944f10c806df78d1cfa2e1daeddc0a487c432701cb67fa65e14213b0c", + "transcript.json": "e6ce662ef02d378e85209bf0a2637980d8e2d497d84e4ff9c39863f2782f8acb", + "evidence.json": "e67fb26194e0e2c77053e315a593c699695753cee22e8d48ff43794afa546b51", + "pg2-3-selected.log": "627974dbfd44345a2dea208e0ff72c376b58884251d6d9dad3a8ae7ee71c0b32" + } + } + ] +} diff --git a/experiments/analysis-efficiency/erlang-beam-views.md b/experiments/analysis-efficiency/erlang-beam-views.md new file mode 100644 index 0000000000..1e57aea3c8 --- /dev/null +++ b/experiments/analysis-efficiency/erlang-beam-views.md @@ -0,0 +1,125 @@ +# Selected BEAM exports: completed-task measurements + +Issue #6938. The analyst asks for every exported function with a small set of +exact names, preserving arities, original table/atom indexes and labels. Both +lanes export and validate the **complete** parent report, not only the answer. + +Baseline `e09b06369d923e9cccedd91f6839ba643dfbd893`; selected delivery +`bcc3f7611` (full SHA and source-clean runtime identities in the JSON receipt). +The reader, format budgets and complete Evidence identity are unchanged. +Node 24.22.1, SDK client/core/server 2.3.1, npm 11.16.0; Linux x86-64. The +18-task cohort alternates three fresh complete/selected sessions per artifact. +Fresh HOME/client/server and disabled Node compile cache; OS caches unflushed. +This is a scripted client task, not an actual model investigation. + +## Results + +Medians, before → after. Seconds and kernel process-family MiB (includes file +cache), not parent-only RSS. Ranges and every receipt hash are in +[the 18-task JSON](erlang-beam-views.json). + +| Real artifact | Whole-task estimated tokens | Wall s | Family CPU s | Family peak MiB | +| ------------- | --------------------------: | ------------: | ------------: | --------------: | +| erl_lint | 76,907 → 58,090 | 6.773 → 6.870 | 5.581 → 5.312 | 667.0 → 630.9 | +| erl_parse | 72,499 → 58,087 | 6.367 → 6.974 | 5.627 → 5.555 | 666.1 → 657.5 | +| pg2 | 57,733 → 58,018 | 6.365 → 7.576 | 5.295 → 5.615 | 648.2 → 626.9 | + +For erl_lint the estimate drops 24.5%; erl_parse drops 19.9%. Small pg2 grows 0.5% +(285 tokens), so selected delivery does not benefit every input. Complete stays +the default. No stable general elapsed/CPU/memory improvement is established; +the small selected task has a higher wall/CPU median. Full parsing/retention and +cold client setup remain. Matching source-report, parent-ID and answer digests +agree across all six tasks for each artifact. + +The projection counts one question, server instructions, all 221 input-tool +names/descriptions/schemas once, and every argument/text reply including export. +`tiktoken 0.14.0/cl100k_base` serialization estimates are not actual model usage. +Input directory cost grows 55,409→56,359tokens; the complete transport catalog +including output schemas grows 829,075→834,792tokens and is reported separately. +Protocol framing, host injection/history replay, output-schema presentation to +a model and inference quality are unmeasured. Reported context reduction is +specific to this declared client projection. + +## SDK cached-catalog control + +Six additional alternating fresh tasks use the SDK's cached catalog/default +validator without an explicit `toolDefinition`. They answer/export the same +question and pass the same independent oracle and cleanup checks. They are +separate from the 18-task primary cohort; do not pool their startup costs. + +Median wall 19.764→19.375s (ranges 17.257–21.577 / 18.052–22.397); +family CPU 16.277→16.867s (ranges 15.287–16.562 / 16.139–17.070); +family peak 1024.2→1053.9MiB. +No general cached-client startup improvement is claimed. See +[all six control receipts](erlang-beam-sdk-cache.json). + +## Correctness and resources + +The unmodified Debian `erlang-base_27.3.4.1+dfsg-1+deb13u3_amd64.deb` supplies +actual OTP 27 compiler/stdlib modules: erl_lint 233,968 bytes (915 atoms, 119 imports, +112 lambdas), erl_parse 608,240 bytes (1,035 atoms), and pg2 1,152 bytes (4 atoms). +The package URL/SHA256, every selected artifact digest, source revision, driver +hash and runtime fingerprint are retained in the JSON records. The independent +[Python struct oracle](erlang-beam-oracle.py) checks every decoded table, chunk +range/payload digest and Code header, then the selected export answer. It reads +bytes only and deliberately qualifies these fixed OTP 27 layouts, not every +historical/compact format. No Erlang installation, emulator or target execution +was used. Default CLI and selected CLI on all three inputs also matched every +oracle fact and parent ID and published exactly the complete source Evidence. + +Each of the 24 SDK tasks exited 0 with payload cleanup verified and no guard stop, +OOM or task-cap event. Aggregate CPUQuota 100%, payload affinity0–1,3 GiB early +stop/4 GiB hard limit, no swap, TasksMax 192. CPU/peak counters are observed after +the payload exits while the observer lives; final whole-scope counters remain +unavailable. Costs include SDK discovery/schema validation, runtime/dependency +hashing, assertions, source retention/export and cleanup. Sample acquisition, +oracle generation and tokenization occur outside timed tasks. Node, all compiled dist +files, selected entrypoints, 59 actual SDK files and driver files are hashed +before/after; the source revisions were clean when fingerprints were created. + +Public regressions additionally exercise all five collections, duplicate names +and arities, selectors/fields/pages, missing versus empty tables, unresolved atom +names, unavailable retention, source deletion, offline bundles and refusal before +file reads. They establish those boundaries; real new-view qualification here is +OTP 27 on Linux, not every platform or provider. + +## Reproducing the question + +Build clean pinned checkouts with the repository's pinned toolchain and acquire +the public package linked in the JSON. Keep extracted files outside the repo. +Generate each oracle, for example: + +```sh +python3 experiments/analysis-efficiency/erlang-beam-oracle.py \ + --input /samples/erl_lint.beam --output /runs/erl_lint-oracle.json \ + --name module --name exprs --name expr +node experiments/analysis-efficiency/erlang-beam-views.mjs \ + --runtime /built/rea --identity /runs/runtime-identity.json \ + --oracle /runs/erl_lint-oracle.json --output /runs/erl_lint-selected \ + --lane selected +``` + +A runtime fingerprint supplies `compiled_files` and `entrypoints` with relative +paths/SHA256, `driver_files` relative to this experiment directory, actual SDK +`dependency_files` with absolute paths/SHA256, and `node_sha256`. Source revision +and clean-state fields record preparation. Create fingerprints before timing; +the driver verifies them before and after each task. The publication records +fingerprint/receipt hashes and full commands; the original complete fingerprint +files, SDK transcripts, complete Evidence bundles and resource logs remain at +`/tmp/rea-perf-static-tool-audit-20261024/`. The local cohort controller is retained +there too. These local receipts are not shipped binaries or installed providers. + +Wrap each invocation with `resource-run.py --observe-payload-exit`, setting the +pinned Node PATH/heap, process affinity and resource policy as in its recorded +command. Use a fresh output directory/session each time, alternate lanes three +times per artifact, and count with +[erlang-beam-costs.py](erlang-beam-costs.py). `--page-size` can qualify extra +retained-page calls; `--sdk-cache` selects the separate cached-catalog control. +The18 published primary tasks use the exact discovered toolDefinition/default +validator and 64-row pages, requiring two calls each including full-source export. + +The initial exploratory baseline used earlier main `e2463372`; it is not mixed +into this table. Failed development checks remain in the local record: an +incorrect formatter path and copied service import, followed by exact complete +contract narrowing in old test consumers; corrected type/format/public checks +passed. These were not successful benchmark samples. diff --git a/experiments/analysis-efficiency/erlang-beam-views.mjs b/experiments/analysis-efficiency/erlang-beam-views.mjs new file mode 100644 index 0000000000..21cdb6deae --- /dev/null +++ b/experiments/analysis-efficiency/erlang-beam-views.mjs @@ -0,0 +1,255 @@ +/** Compare a completed selected-export question through production stdio MCP. */ +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 { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { finished } from "node:stream/promises"; +import { parseArgs } from "node:util"; +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +const { values } = parseArgs({ + options: { + ...Object.fromEntries( + ["runtime", "oracle", "output", "identity"].map((k) => [ + k, + { type: "string" }, + ]), + ), + lane: { type: "string", default: "complete" }, + "page-size": { type: "string", default: "64" }, + "sdk-cache": { type: "boolean", default: false }, + }, +}); +assert.ok(["complete", "selected"].includes(values.lane)); +const pageSize = Number(values["page-size"]); +assert.ok(Number.isSafeInteger(pageSize) && pageSize > 0); +for (const key of ["runtime", "oracle", "output", "identity"]) + assert.equal(values[key], resolve(values[key])); +const { createRetainedQueryCaller } = + await import("./retained-query-calls.mjs"); +const { finalizeRetainedQueryReceipt } = + await import("./retained-query-finalization.mjs"); +const hash = (x) => createHash("sha256").update(x).digest("hex"), + oracleBytes = await readFile(values.oracle), + expected = JSON.parse(oracleBytes), + identityBytes = await readFile(values.identity), + identity = JSON.parse(identityBytes); +assert.equal( + hash(await readFile(expected.expected.artifact.path)), + expected.expected.artifact.sha256, +); +await mkdir(values.output, { mode: 0o700 }); +const account = await mkdtemp(join(tmpdir(), "rea-beam-task-")); +const client = new Client({ name: "beam-selected-exports", version: "1" }); +const transport = new StdioClientTransport({ + command: process.execPath, + args: [join(values.runtime, "scripts/rea.mjs"), "mcp"], + maxBufferSize: 10485760, + env: { + HOME: account, + TMPDIR: account, + PATH: process.env.PATH, + REA_LOG_LEVEL: "silent", + NODE_OPTIONS: "--max-old-space-size=2048", + NODE_DISABLE_COMPILE_CACHE: "1", + UV_THREADPOOL_SIZE: "2", + }, + stderr: "pipe", +}); +const serverLog = createWriteStream(join(values.output, "server.log")), + serverLogFinished = finished(serverLog); +serverLogFinished.catch(() => {}); +transport.stderr?.pipe(serverLog); +const report = { + status: "running", + lane: values.lane, + artifact: expected.expected.artifact, + calls: [], + phases: {}, + question: `Return every exported function named ${expected.names.join(", ")} with its original export-table index, function atom index, arity and code label. Preserve complete source Evidence.`, + client: values["sdk-cache"] + ? "Production stdio SDK, cached catalog definitions and default validator" + : "Production stdio SDK, exact discovered toolDefinition and default validator", + runtime_identity_sha256: hash(identityBytes), + oracle_sha256: hash(oracleBytes), + driver_sha256: hash(await readFile(new URL(import.meta.url))), +}; +const verifyRuntime = async () => { + assert.equal(hash(await readFile(values.identity)), hash(identityBytes)); + assert.equal(hash(await readFile(values.oracle)), hash(oracleBytes)); + for (const r of [...identity.compiled_files, ...identity.entrypoints]) + assert.equal( + hash(await readFile(join(values.runtime, r.path))), + r.sha256, + r.path, + ); + for (const r of identity.dependency_files ?? []) + assert.equal(hash(await readFile(r.path)), r.sha256, r.path); + for (const r of identity.driver_files) + assert.equal( + hash(await readFile(new URL(r.path, import.meta.url))), + r.sha256, + r.path, + ); + assert.equal(hash(await readFile(process.execPath)), identity.node_sha256); + assert.equal( + hash(await readFile(new URL(import.meta.url))), + report.driver_sha256, + ); +}; +const transcript = []; +let definitions; +const caller = createRetainedQueryCaller( + { + callTool(params, options) { + return client.callTool( + params, + values["sdk-cache"] + ? options + : { + ...options, + toolDefinition: definitions.get(params.name), + }, + ); + }, + }, + (receipt, event) => { + report.calls.push(receipt); + transcript.push(event); + }, +); +const observe = async (name, input, phase) => { + const result = await caller(name, input, phase); + assert.notEqual(result.isError, true, JSON.stringify(result)); + assert.ok(result.structuredContent); + return result.structuredContent; +}; +const verifySource = (source) => { + for (const [key, value] of Object.entries(expected.expected)) + assert.deepEqual(source[key], value, key); + assert.equal(source.coverage.status, "complete"); +}; +try { + await verifyRuntime(); + const started = performance.now(); + await client.connect(transport); + report.server_instructions = client.getInstructions() ?? ""; + const catalog = await client.listTools(); + definitions = new Map(catalog.tools.map((t) => [t.name, t])); + report.catalog_tools = catalog.tools.length; + await writeFile(join(values.output, "catalog.json"), JSON.stringify(catalog)); + report.phases.discovery_ms = performance.now() - started; + const selected = values.lane === "selected"; + const selectedView = (offset) => ({ + kind: "beam", + collection: "exports", + names: expected.names, + fields: ["function_index", "function", "arity", "label"], + offset, + limit: pageSize, + }); + const response = await observe( + "inspect_erlang_beam", + { + path: expected.expected.artifact.path, + ...(selected ? { detail: "summary", view: selectedView(0) } : {}), + }, + "inspect", + ); + report.parent_evidence_id = selected + ? response.normalized_result.parent_evidence_id + : response.evidence_id; + let answer; + if (!selected) { + verifySource(response.normalized_result); + answer = (response.normalized_result.exports ?? []) + .map((r, i) => ({ source_index: i, ...r })) + .filter((r) => expected.names.includes(r.function)); + } else { + answer = []; + let offset = 0, + page = response.normalized_result; + for (;;) { + assert.equal(page.kind, "beam"); + assert.equal(page.page.collection, "exports"); + assert.deepEqual(page.beam.artifact, expected.expected.artifact); + assert.deepEqual(page.beam.coverage, expected.expected.coverage); + assert.equal(page.beam.counts.exports, expected.expected.exports.length); + assert.equal(page.coverage.total, expected.answer.length); + answer.push(...page.page.items); + if (page.coverage.next_offset === null) break; + assert.ok(page.coverage.next_offset > offset); + offset = page.coverage.next_offset; + page = ( + await observe( + "inspect_analysis_view", + { + source: { + kind: "retained-evidence", + evidence_id: report.parent_evidence_id, + }, + view: selectedView(offset), + }, + "selected-page", + ) + ).normalized_result; + } + } + assert.deepEqual(answer, expected.answer); + report.answer_count = answer.length; + report.answer_sha256 = hash( + JSON.stringify( + answer.map((r) => + Object.fromEntries( + Object.entries(r).sort(([a], [b]) => a.localeCompare(b)), + ), + ), + ), + ); + await writeFile(join(values.output, "answer.json"), JSON.stringify(answer)); + await observe( + "export_evidence_bundle", + { path: join(values.output, "evidence.json") }, + "export", + ); + const bundle = JSON.parse( + await readFile(join(values.output, "evidence.json")), + ); + const parent = bundle.records.find( + (r) => r.evidence_id === report.parent_evidence_id, + ); + assert.ok(parent); + assert.equal(parent.operation, "inspect_erlang_beam"); + verifySource(parent.normalized_result); + report.source_report_sha256 = hash(JSON.stringify(parent.normalized_result)); + report.complete_source_export_verified = true; + report.status = "passed"; +} catch (cause) { + report.status = "failed"; + report.error = String(cause); +} finally { + try { + await verifyRuntime(); + } catch (cause) { + report.status = "failed"; + report.runtime_identity_failure = String(cause); + } + await finalizeRetainedQueryReceipt({ + client, + transport, + serverLog, + serverLogFinished, + account, + report, + output: values.output, + transcript, + }); + if ( + report.status !== "passed" || + report.cleanup_failures.length || + report.receipt_failures.length + ) + process.exitCode = 1; +} diff --git a/knip.json b/knip.json index 9eb05c02dc..4d24b67dd5 100644 --- a/knip.json +++ b/knip.json @@ -10,8 +10,6 @@ "src/composition/javascriptAnalysisWorkerProcess.ts", "src/composition/webBundleAnalysisWorkerProcess.ts", "src/contracts/errorSchemas.ts", - "src/evaluation/AgentEvaluationFixtures.ts", - "src/application/RealHopperAssertions.ts", "scripts/verify/fridaBindingsCompatibility.ts", "scripts/verify/termux/browser.mjs", "scripts/verify-real-ghidra-noreturn.mjs", diff --git a/licenses/npm-dependencies.txt b/licenses/npm-dependencies.txt index 4a252cee4a..aaf5608cba 100644 --- a/licenses/npm-dependencies.txt +++ b/licenses/npm-dependencies.txt @@ -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: 775; runtime: 180. +Locked package entries: 956; runtime: 180. Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt. @algolia/abtesting@1.25.0 | development | MIT @@ -355,6 +355,31 @@ obug@3.0.0 | development | MIT Package: https://www.npmjs.com/package/@clack/prompts/v/1.7.0 Archive: https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz Integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A== +@conventional-commits/parser@0.4.1 | development | ISC + Lockfile path: node_modules/@conventional-commits/parser + Package: https://www.npmjs.com/package/@conventional-commits/parser/v/0.4.1 + Archive: https://registry.npmjs.org/@conventional-commits/parser/-/parser-0.4.1.tgz + Integrity: sha512-H2ZmUVt6q+KBccXfMBhbBF14NlANeqHTXL4qCL6QGbMzrc4HDXyzWuxPxPNbz71f/5UkR5DrycP5VO9u7crahg== +@types/unist@2.0.11 | development | MIT + Lockfile path: node_modules/@conventional-commits/parser/node_modules/@types/unist + Package: https://www.npmjs.com/package/@types/unist/v/2.0.11 + Archive: https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz + Integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== +unist-util-is@4.1.0 | development | MIT + Lockfile path: node_modules/@conventional-commits/parser/node_modules/unist-util-is + Package: https://www.npmjs.com/package/unist-util-is/v/4.1.0 + Archive: https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz + Integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== +unist-util-visit@2.0.3 | development | MIT + Lockfile path: node_modules/@conventional-commits/parser/node_modules/unist-util-visit + Package: https://www.npmjs.com/package/unist-util-visit/v/2.0.3 + Archive: https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz + Integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== +unist-util-visit-parents@3.1.1 | development | MIT + Lockfile path: node_modules/@conventional-commits/parser/node_modules/unist-util-visit-parents + Package: https://www.npmjs.com/package/unist-util-visit-parents/v/3.1.1 + Archive: https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz + Integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== @docsearch/css@3.8.2 | development | MIT Lockfile path: node_modules/@docsearch/css Package: https://www.npmjs.com/package/@docsearch/css/v/3.8.2 @@ -530,6 +555,16 @@ commander@13.1.0 | runtime | MIT Package: https://www.npmjs.com/package/@fast-check/vitest/v/0.5.0 Archive: https://registry.npmjs.org/@fast-check/vitest/-/vitest-0.5.0.tgz Integrity: sha512-Kgbj2smuq1P4OhGKuPATs5i2fAxNmGbJ/zSmTrzWhQSLUrHREbKWgv9WGbNUOXcx77q6joLVMmTygyXuwP2Bog== +@google-automations/git-file-utils@3.2.1 | development | Apache-2.0 + Lockfile path: node_modules/@google-automations/git-file-utils + Package: https://www.npmjs.com/package/@google-automations/git-file-utils/v/3.2.1 + Archive: https://registry.npmjs.org/@google-automations/git-file-utils/-/git-file-utils-3.2.1.tgz + Integrity: sha512-LYOjAnGu/xtEkYYh6W3/1RmeK9HB/fV+xMe7nHc+WiDYrnxAJWBfcyUucoJtYS8XAhVUzNWrQvYDPRbxe67DNQ== +@iarna/toml@3.0.0 | development | ISC + Lockfile path: node_modules/@iarna/toml + Package: https://www.npmjs.com/package/@iarna/toml/v/3.0.0 + Archive: https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz + Integrity: sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q== @iconify-json/simple-icons@1.2.99 | development | CC0-1.0 Lockfile path: node_modules/@iconify-json/simple-icons Package: https://www.npmjs.com/package/@iconify-json/simple-icons/v/1.2.99 @@ -640,6 +675,16 @@ commander@13.1.0 | runtime | MIT Package: https://www.npmjs.com/package/@jridgewell/trace-mapping/v/0.3.31 Archive: https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz Integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== +@jsep-plugin/assignment@1.4.0 | development | MIT + Lockfile path: node_modules/@jsep-plugin/assignment + Package: https://www.npmjs.com/package/@jsep-plugin/assignment/v/1.4.0 + Archive: https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.4.0.tgz + Integrity: sha512-nw2Px23Eirx0PFTVoPrb3FEq9Wkmg6vxcDtt38MtBCf1G0tjLCB9cK+3seOXyiw+6+aalgYhvJabQbWwMVKlEQ== +@jsep-plugin/regex@1.1.0 | development | MIT + Lockfile path: node_modules/@jsep-plugin/regex + Package: https://www.npmjs.com/package/@jsep-plugin/regex/v/1.1.0 + Archive: https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.1.0.tgz + Integrity: sha512-2Vkbda+XRjK6VTI6Lrh0sNBuMT8mZ51EhqMvO+V2rSdmqjs7NAGssxPZfkL23EFzYfSrvxyd1ZjOdqPQMM50og== @lydell/node-pty@1.1.0 | runtime | MIT Lockfile path: node_modules/@lydell/node-pty Package: https://www.npmjs.com/package/@lydell/node-pty/v/1.1.0 @@ -705,6 +750,66 @@ commander@13.1.0 | runtime | MIT Package: https://www.npmjs.com/package/@napi-rs/wasm-runtime/v/1.2.5 Archive: https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz Integrity: sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA== +@octokit/auth-token@4.0.0 | development | MIT + Lockfile path: node_modules/@octokit/auth-token + Package: https://www.npmjs.com/package/@octokit/auth-token/v/4.0.0 + Archive: https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz + Integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== +@octokit/core@5.2.2 | development | MIT + Lockfile path: node_modules/@octokit/core + Package: https://www.npmjs.com/package/@octokit/core/v/5.2.2 + Archive: https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz + Integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg== +@octokit/endpoint@9.0.6 | development | MIT + Lockfile path: node_modules/@octokit/endpoint + Package: https://www.npmjs.com/package/@octokit/endpoint/v/9.0.6 + Archive: https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz + Integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw== +@octokit/graphql@7.1.1 | development | MIT + Lockfile path: node_modules/@octokit/graphql + Package: https://www.npmjs.com/package/@octokit/graphql/v/7.1.1 + Archive: https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz + Integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g== +@octokit/openapi-types@24.2.0 | development | MIT + Lockfile path: node_modules/@octokit/openapi-types + Package: https://www.npmjs.com/package/@octokit/openapi-types/v/24.2.0 + Archive: https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz + Integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg== +@octokit/plugin-paginate-rest@11.4.4-cjs.2 | development | MIT + Lockfile path: node_modules/@octokit/plugin-paginate-rest + Package: https://www.npmjs.com/package/@octokit/plugin-paginate-rest/v/11.4.4-cjs.2 + Archive: https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz + Integrity: sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw== +@octokit/plugin-request-log@4.0.1 | development | MIT + Lockfile path: node_modules/@octokit/plugin-request-log + Package: https://www.npmjs.com/package/@octokit/plugin-request-log/v/4.0.1 + Archive: https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz + Integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA== +@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1 | development | MIT + Lockfile path: node_modules/@octokit/plugin-rest-endpoint-methods + Package: https://www.npmjs.com/package/@octokit/plugin-rest-endpoint-methods/v/13.3.2-cjs.1 + Archive: https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz + Integrity: sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ== +@octokit/request@8.4.1 | development | MIT + Lockfile path: node_modules/@octokit/request + Package: https://www.npmjs.com/package/@octokit/request/v/8.4.1 + Archive: https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz + Integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw== +@octokit/request-error@5.1.1 | development | MIT + Lockfile path: node_modules/@octokit/request-error + Package: https://www.npmjs.com/package/@octokit/request-error/v/5.1.1 + Archive: https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz + Integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g== +@octokit/rest@20.1.2 | development | MIT + Lockfile path: node_modules/@octokit/rest + Package: https://www.npmjs.com/package/@octokit/rest/v/20.1.2 + Archive: https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz + Integrity: sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA== +@octokit/types@13.10.0 | development | MIT + Lockfile path: node_modules/@octokit/types + Package: https://www.npmjs.com/package/@octokit/types/v/13.10.0 + Archive: https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz + Integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA== @oxc-parser/binding-android-arm-eabi@0.150.0 | development, optional | MIT Lockfile path: node_modules/@oxc-parser/binding-android-arm-eabi Package: https://www.npmjs.com/package/@oxc-parser/binding-android-arm-eabi/v/0.150.0 @@ -1505,11 +1610,26 @@ commander@13.1.0 | runtime | MIT Package: https://www.npmjs.com/package/@types/mdurl/v/2.0.0 Archive: https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz Integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== +@types/minimist@1.2.5 | development | MIT + Lockfile path: node_modules/@types/minimist + Package: https://www.npmjs.com/package/@types/minimist/v/1.2.5 + Archive: https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz + Integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== @types/node@26.6.4 | development | MIT Lockfile path: node_modules/@types/node Package: https://www.npmjs.com/package/@types/node/v/26.6.4 Archive: https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz Integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg== +@types/normalize-package-data@2.4.4 | development | MIT + Lockfile path: node_modules/@types/normalize-package-data + Package: https://www.npmjs.com/package/@types/normalize-package-data/v/2.4.4 + Archive: https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz + Integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== +@types/npm-package-arg@6.1.4 | development | MIT + Lockfile path: node_modules/@types/npm-package-arg + Package: https://www.npmjs.com/package/@types/npm-package-arg/v/6.1.4 + Archive: https://registry.npmjs.org/@types/npm-package-arg/-/npm-package-arg-6.1.4.tgz + Integrity: sha512-vDgdbMy2QXHnAruzlv68pUtXCjmqUk3WrBAsRboRovsOmxbfn/WiYCjmecyKjGztnMps5dWp4Uq2prp+Ilo17Q== @types/semver@7.7.1 | development | MIT Lockfile path: node_modules/@types/semver Package: https://www.npmjs.com/package/@types/semver/v/7.7.1 @@ -1535,6 +1655,16 @@ commander@13.1.0 | runtime | MIT Package: https://www.npmjs.com/package/@types/ws/v/8.18.1 Archive: https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz Integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== +@types/yargs@16.0.11 | development | MIT + Lockfile path: node_modules/@types/yargs + Package: https://www.npmjs.com/package/@types/yargs/v/16.0.11 + Archive: https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz + Integrity: sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g== +@types/yargs-parser@21.0.3 | development | MIT + Lockfile path: node_modules/@types/yargs-parser + Package: https://www.npmjs.com/package/@types/yargs-parser/v/21.0.3 + Archive: https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz + Integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== @typescript/typescript-aix-ppc64@7.0.2 | development, optional | Apache-2.0 Lockfile path: node_modules/@typescript/typescript-aix-ppc64 Package: https://www.npmjs.com/package/@typescript/typescript-aix-ppc64/v/7.0.2 @@ -1825,6 +1955,11 @@ abort-controller@3.0.0 | runtime | MIT Package: https://www.npmjs.com/package/abort-controller/v/3.0.0 Archive: https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz Integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== +agent-base@7.1.4 | development | MIT + Lockfile path: node_modules/agent-base + Package: https://www.npmjs.com/package/agent-base/v/7.1.4 + Archive: https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz + Integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== ajv@8.20.0 | runtime | MIT Lockfile path: node_modules/ajv Package: https://www.npmjs.com/package/ajv/v/8.20.0 @@ -1860,6 +1995,21 @@ ansi-styles@6.2.3 | development | MIT Package: https://www.npmjs.com/package/ansi-styles/v/6.2.3 Archive: https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz Integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== +argparse@2.0.1 | development | Python-2.0 + Lockfile path: node_modules/argparse + Package: https://www.npmjs.com/package/argparse/v/2.0.1 + Archive: https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz + Integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +array-ify@1.0.0 | development | MIT + Lockfile path: node_modules/array-ify + Package: https://www.npmjs.com/package/array-ify/v/1.0.0 + Archive: https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz + Integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng== +arrify@1.0.1 | development | MIT + Lockfile path: node_modules/arrify + Package: https://www.npmjs.com/package/arrify/v/1.0.1 + Archive: https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz + Integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== assertion-error@2.0.1 | development | MIT Lockfile path: node_modules/assertion-error Package: https://www.npmjs.com/package/assertion-error/v/2.0.1 @@ -1875,6 +2025,11 @@ async-lock@1.4.1 | runtime | MIT Package: https://www.npmjs.com/package/async-lock/v/1.4.1 Archive: https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz Integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ== +async-retry@1.3.3 | development | MIT + Lockfile path: node_modules/async-retry + Package: https://www.npmjs.com/package/async-retry/v/1.3.3 + Archive: https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz + Integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== atomic-sleep@1.0.0 | runtime | MIT Lockfile path: node_modules/atomic-sleep Package: https://www.npmjs.com/package/atomic-sleep/v/1.0.0 @@ -1900,6 +2055,11 @@ baseline-browser-mapping@2.11.28 | development | Apache-2.0 Package: https://www.npmjs.com/package/baseline-browser-mapping/v/2.11.28 Archive: https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz Integrity: sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA== +before-after-hook@2.2.3 | development | Apache-2.0 + Lockfile path: node_modules/before-after-hook + Package: https://www.npmjs.com/package/before-after-hook/v/2.2.3 + Archive: https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz + Integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== bindings@1.5.0 | runtime, optional | MIT Lockfile path: node_modules/bindings Package: https://www.npmjs.com/package/bindings/v/1.5.0 @@ -1925,6 +2085,11 @@ readable-stream@3.6.2 | runtime, optional | MIT Package: https://www.npmjs.com/package/readable-stream/v/3.6.2 Archive: https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz Integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== +boolbase@1.0.0 | development | ISC + Lockfile path: node_modules/boolbase + Package: https://www.npmjs.com/package/boolbase/v/1.0.0 + Archive: https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz + Integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== brace-expansion@5.0.12 | runtime | MIT Lockfile path: node_modules/brace-expansion Package: https://www.npmjs.com/package/brace-expansion/v/5.0.12 @@ -1955,6 +2120,16 @@ call-bound@1.0.4 | runtime | MIT Package: https://www.npmjs.com/package/call-bound/v/1.0.4 Archive: https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz Integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== +camelcase@5.3.1 | development | MIT + Lockfile path: node_modules/camelcase + Package: https://www.npmjs.com/package/camelcase/v/5.3.1 + Archive: https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz + Integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase-keys@6.2.2 | development | MIT + Lockfile path: node_modules/camelcase-keys + Package: https://www.npmjs.com/package/camelcase-keys/v/6.2.2 + Archive: https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz + Integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== caniuse-lite@1.0.30001815 | development | CC-BY-4.0 Lockfile path: node_modules/caniuse-lite Package: https://www.npmjs.com/package/caniuse-lite/v/1.0.30001815 @@ -2025,6 +2200,131 @@ cli-width@4.1.0 | development | ISC Package: https://www.npmjs.com/package/cli-width/v/4.1.0 Archive: https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz Integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== +cliui@8.0.1 | development | ISC + Lockfile path: node_modules/cliui + Package: https://www.npmjs.com/package/cliui/v/8.0.1 + Archive: https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz + Integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== +ansi-regex@5.0.1 | development | MIT + Lockfile path: node_modules/cliui/node_modules/ansi-regex + Package: https://www.npmjs.com/package/ansi-regex/v/5.0.1 + Archive: https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz + Integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@4.3.0 | development | MIT + Lockfile path: node_modules/cliui/node_modules/ansi-styles + Package: https://www.npmjs.com/package/ansi-styles/v/4.3.0 + Archive: https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz + Integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== +emoji-regex@8.0.0 | development | MIT + Lockfile path: node_modules/cliui/node_modules/emoji-regex + Package: https://www.npmjs.com/package/emoji-regex/v/8.0.0 + Archive: https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz + Integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +is-fullwidth-code-point@3.0.0 | development | MIT + Lockfile path: node_modules/cliui/node_modules/is-fullwidth-code-point + Package: https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 + Archive: https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz + Integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +string-width@4.2.3 | development | MIT + Lockfile path: node_modules/cliui/node_modules/string-width + Package: https://www.npmjs.com/package/string-width/v/4.2.3 + Archive: https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz + Integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== +strip-ansi@6.0.1 | development | MIT + Lockfile path: node_modules/cliui/node_modules/strip-ansi + Package: https://www.npmjs.com/package/strip-ansi/v/6.0.1 + Archive: https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz + Integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== +wrap-ansi@7.0.0 | development | MIT + Lockfile path: node_modules/cliui/node_modules/wrap-ansi + Package: https://www.npmjs.com/package/wrap-ansi/v/7.0.0 + Archive: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz + Integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== +code-suggester@5.0.1 | development | Apache-2.0 + Lockfile path: node_modules/code-suggester + Package: https://www.npmjs.com/package/code-suggester/v/5.0.1 + Archive: https://registry.npmjs.org/code-suggester/-/code-suggester-5.0.1.tgz + Integrity: sha512-8qJiiSCfkbPNWvjEFzdG1UW3axL+Bs0ldV1/TdlBKmF9I/a/WooSQZQCi9M44HoXbVTQesn/IQr6+nIWNnVIWA== +ansi-regex@5.0.1 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/ansi-regex + Package: https://www.npmjs.com/package/ansi-regex/v/5.0.1 + Archive: https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz + Integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@4.3.0 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/ansi-styles + Package: https://www.npmjs.com/package/ansi-styles/v/4.3.0 + Archive: https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz + Integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== +balanced-match@1.0.2 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/balanced-match + Package: https://www.npmjs.com/package/balanced-match/v/1.0.2 + Archive: https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz + Integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +brace-expansion@1.1.21 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/brace-expansion + Package: https://www.npmjs.com/package/brace-expansion/v/1.1.21 + Archive: https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz + Integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw== +cliui@7.0.4 | development | ISC + Lockfile path: node_modules/code-suggester/node_modules/cliui + Package: https://www.npmjs.com/package/cliui/v/7.0.4 + Archive: https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz + Integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== +diff@8.0.4 | development | BSD-3-Clause + Lockfile path: node_modules/code-suggester/node_modules/diff + Package: https://www.npmjs.com/package/diff/v/8.0.4 + Archive: https://registry.npmjs.org/diff/-/diff-8.0.4.tgz + Integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw== +emoji-regex@8.0.0 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/emoji-regex + Package: https://www.npmjs.com/package/emoji-regex/v/8.0.0 + Archive: https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz + Integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +glob@7.2.3 | development | ISC + Lockfile path: node_modules/code-suggester/node_modules/glob + Package: https://www.npmjs.com/package/glob/v/7.2.3 + Archive: https://registry.npmjs.org/glob/-/glob-7.2.3.tgz + Integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== +is-fullwidth-code-point@3.0.0 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/is-fullwidth-code-point + Package: https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 + Archive: https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz + Integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +minimatch@3.1.5 | development | ISC + Lockfile path: node_modules/code-suggester/node_modules/minimatch + Package: https://www.npmjs.com/package/minimatch/v/3.1.5 + Archive: https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz + Integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== +string-width@4.2.3 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/string-width + Package: https://www.npmjs.com/package/string-width/v/4.2.3 + Archive: https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz + Integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== +strip-ansi@6.0.1 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/strip-ansi + Package: https://www.npmjs.com/package/strip-ansi/v/6.0.1 + Archive: https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz + Integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== +wrap-ansi@7.0.0 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/wrap-ansi + Package: https://www.npmjs.com/package/wrap-ansi/v/7.0.0 + Archive: https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz + Integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== +yargs@16.2.2 | development | MIT + Lockfile path: node_modules/code-suggester/node_modules/yargs + Package: https://www.npmjs.com/package/yargs/v/16.2.2 + Archive: https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz + Integrity: sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w== +color-convert@2.0.1 | development | MIT + Lockfile path: node_modules/color-convert + Package: https://www.npmjs.com/package/color-convert/v/2.0.1 + Archive: https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz + Integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== +color-name@1.1.4 | development | MIT + Lockfile path: node_modules/color-name + Package: https://www.npmjs.com/package/color-name/v/1.1.4 + Archive: https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz + Integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== colorette@2.0.20 | development | MIT Lockfile path: node_modules/colorette Package: https://www.npmjs.com/package/colorette/v/2.0.20 @@ -2040,6 +2340,31 @@ commander@14.0.3 | development | MIT Package: https://www.npmjs.com/package/commander/v/14.0.3 Archive: https://registry.npmjs.org/commander/-/commander-14.0.3.tgz Integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw== +compare-func@2.0.0 | development | MIT + Lockfile path: node_modules/compare-func + Package: https://www.npmjs.com/package/compare-func/v/2.0.0 + Archive: https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz + Integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== +concat-map@0.0.1 | development | MIT + Lockfile path: node_modules/concat-map + Package: https://www.npmjs.com/package/concat-map/v/0.0.1 + Archive: https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz + Integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +conventional-changelog-conventionalcommits@6.1.0 | development | ISC + Lockfile path: node_modules/conventional-changelog-conventionalcommits + Package: https://www.npmjs.com/package/conventional-changelog-conventionalcommits/v/6.1.0 + Archive: https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-6.1.0.tgz + Integrity: sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw== +conventional-changelog-writer@6.0.1 | development | MIT + Lockfile path: node_modules/conventional-changelog-writer + Package: https://www.npmjs.com/package/conventional-changelog-writer/v/6.0.1 + Archive: https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-6.0.1.tgz + Integrity: sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ== +conventional-commits-filter@3.0.0 | development | MIT + Lockfile path: node_modules/conventional-commits-filter + Package: https://www.npmjs.com/package/conventional-commits-filter/v/3.0.0 + Archive: https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-3.0.0.tgz + Integrity: sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q== convert-source-map@2.0.0 | development | MIT Lockfile path: node_modules/convert-source-map Package: https://www.npmjs.com/package/convert-source-map/v/2.0.0 @@ -2060,11 +2385,46 @@ cross-spawn@7.0.6 | runtime | MIT Package: https://www.npmjs.com/package/cross-spawn/v/7.0.6 Archive: https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz Integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== +css-select@5.2.2 | development | BSD-2-Clause + Lockfile path: node_modules/css-select + Package: https://www.npmjs.com/package/css-select/v/5.2.2 + Archive: https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz + Integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw== +css-what@6.2.2 | development | BSD-2-Clause + Lockfile path: node_modules/css-what + Package: https://www.npmjs.com/package/css-what/v/6.2.2 + Archive: https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz + Integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== csstype@3.2.3 | development | MIT Lockfile path: node_modules/csstype Package: https://www.npmjs.com/package/csstype/v/3.2.3 Archive: https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz Integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== +dateformat@3.0.3 | development | MIT + Lockfile path: node_modules/dateformat + Package: https://www.npmjs.com/package/dateformat/v/3.0.3 + Archive: https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz + Integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +debug@4.4.3 | development | MIT + Lockfile path: node_modules/debug + Package: https://www.npmjs.com/package/debug/v/4.4.3 + Archive: https://registry.npmjs.org/debug/-/debug-4.4.3.tgz + Integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== +decamelize@1.2.0 | development | MIT + Lockfile path: node_modules/decamelize + Package: https://www.npmjs.com/package/decamelize/v/1.2.0 + Archive: https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz + Integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decamelize-keys@1.1.1 | development | MIT + Lockfile path: node_modules/decamelize-keys + Package: https://www.npmjs.com/package/decamelize-keys/v/1.1.1 + Archive: https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz + Integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg== +map-obj@1.0.1 | development | MIT + Lockfile path: node_modules/decamelize-keys/node_modules/map-obj + Package: https://www.npmjs.com/package/map-obj/v/1.0.1 + Archive: https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz + Integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== decompress-response@6.0.0 | runtime | MIT Lockfile path: node_modules/decompress-response Package: https://www.npmjs.com/package/decompress-response/v/6.0.0 @@ -2080,6 +2440,11 @@ define-data-property@1.1.4 | runtime | MIT Package: https://www.npmjs.com/package/define-data-property/v/1.1.4 Archive: https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz Integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== +deprecation@2.3.1 | development | ISC + Lockfile path: node_modules/deprecation + Package: https://www.npmjs.com/package/deprecation/v/2.3.1 + Archive: https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz + Integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== dequal@2.0.3 | development | MIT Lockfile path: node_modules/dequal Package: https://www.npmjs.com/package/dequal/v/2.0.3 @@ -2090,6 +2455,11 @@ des.js@1.1.0 | development | MIT Package: https://www.npmjs.com/package/des.js/v/1.1.0 Archive: https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz Integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== +detect-indent@6.1.0 | development | MIT + Lockfile path: node_modules/detect-indent + Package: https://www.npmjs.com/package/detect-indent/v/6.1.0 + Archive: https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz + Integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== detect-libc@2.1.2 | runtime | Apache-2.0 Lockfile path: node_modules/detect-libc Package: https://www.npmjs.com/package/detect-libc/v/2.1.2 @@ -2115,6 +2485,36 @@ diff3@0.0.3 | runtime | MIT Package: https://www.npmjs.com/package/diff3/v/0.0.3 Archive: https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz Integrity: sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g== +dom-serializer@2.0.0 | development | MIT + Lockfile path: node_modules/dom-serializer + Package: https://www.npmjs.com/package/dom-serializer/v/2.0.0 + Archive: https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz + Integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== +entities@4.5.0 | development | BSD-2-Clause + Lockfile path: node_modules/dom-serializer/node_modules/entities + Package: https://www.npmjs.com/package/entities/v/4.5.0 + Archive: https://registry.npmjs.org/entities/-/entities-4.5.0.tgz + Integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +domelementtype@2.3.0 | development | BSD-2-Clause + Lockfile path: node_modules/domelementtype + Package: https://www.npmjs.com/package/domelementtype/v/2.3.0 + Archive: https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz + Integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== +domhandler@5.0.3 | development | BSD-2-Clause + Lockfile path: node_modules/domhandler + Package: https://www.npmjs.com/package/domhandler/v/5.0.3 + Archive: https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz + Integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== +domutils@3.2.2 | development | BSD-2-Clause + Lockfile path: node_modules/domutils + Package: https://www.npmjs.com/package/domutils/v/3.2.2 + Archive: https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz + Integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== +dot-prop@5.3.0 | development | MIT + Lockfile path: node_modules/dot-prop + Package: https://www.npmjs.com/package/dot-prop/v/5.3.0 + Archive: https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz + Integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== dunder-proto@1.0.1 | runtime | MIT Lockfile path: node_modules/dunder-proto Package: https://www.npmjs.com/package/dunder-proto/v/1.0.1 @@ -2155,6 +2555,11 @@ environment@1.1.0 | development | MIT Package: https://www.npmjs.com/package/environment/v/1.1.0 Archive: https://registry.npmjs.org/environment/-/environment-1.1.0.tgz Integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== +error-ex@1.3.4 | development | MIT + Lockfile path: node_modules/error-ex + Package: https://www.npmjs.com/package/error-ex/v/1.3.4 + Archive: https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz + Integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== es-define-property@1.0.1 | runtime | MIT Lockfile path: node_modules/es-define-property Package: https://www.npmjs.com/package/es-define-property/v/1.0.1 @@ -2185,6 +2590,11 @@ escalade@3.2.0 | development | MIT Package: https://www.npmjs.com/package/escalade/v/3.2.0 Archive: https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz Integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== +escape-string-regexp@1.0.5 | development | MIT + Lockfile path: node_modules/escape-string-regexp + Package: https://www.npmjs.com/package/escape-string-regexp/v/1.0.5 + Archive: https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz + Integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== estree-walker@3.0.3 | development | MIT Lockfile path: node_modules/estree-walker Package: https://www.npmjs.com/package/estree-walker/v/3.0.3 @@ -2285,6 +2695,11 @@ file-uri-to-path@1.0.0 | runtime, optional | MIT Package: https://www.npmjs.com/package/file-uri-to-path/v/1.0.0 Archive: https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz Integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== +find-up@4.1.0 | development | MIT + Lockfile path: node_modules/find-up + Package: https://www.npmjs.com/package/find-up/v/4.1.0 + Archive: https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz + Integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== flru@1.0.2 | development | MIT Lockfile path: node_modules/flru Package: https://www.npmjs.com/package/flru/v/1.0.2 @@ -2315,6 +2730,11 @@ fs-constants@1.0.0 | runtime, optional | MIT Package: https://www.npmjs.com/package/fs-constants/v/1.0.0 Archive: https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz Integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs.realpath@1.0.0 | development | ISC + Lockfile path: node_modules/fs.realpath + Package: https://www.npmjs.com/package/fs.realpath/v/1.0.0 + Archive: https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz + Integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@2.3.3 | development, optional | MIT Lockfile path: node_modules/fsevents Package: https://www.npmjs.com/package/fsevents/v/2.3.3 @@ -2330,6 +2750,11 @@ gensync@1.0.0-beta.2 | development | MIT Package: https://www.npmjs.com/package/gensync/v/1.0.0-beta.2 Archive: https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz Integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +get-caller-file@2.0.5 | development | ISC + Lockfile path: node_modules/get-caller-file + Package: https://www.npmjs.com/package/get-caller-file/v/2.0.5 + Archive: https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz + Integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-east-asian-width@1.6.0 | development | MIT Lockfile path: node_modules/get-east-asian-width Package: https://www.npmjs.com/package/get-east-asian-width/v/1.6.0 @@ -2370,11 +2795,31 @@ gopd@1.2.0 | runtime | MIT Package: https://www.npmjs.com/package/gopd/v/1.2.0 Archive: https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz Integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== +handlebars@4.7.10 | development | MIT + Lockfile path: node_modules/handlebars + Package: https://www.npmjs.com/package/handlebars/v/4.7.10 + Archive: https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz + Integrity: sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg== +source-map@0.6.1 | development | BSD-3-Clause + Lockfile path: node_modules/handlebars/node_modules/source-map + Package: https://www.npmjs.com/package/source-map/v/0.6.1 + Archive: https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz + Integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== har-schema@2.0.0 | runtime | ISC Lockfile path: node_modules/har-schema Package: https://www.npmjs.com/package/har-schema/v/2.0.0 Archive: https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz Integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== +hard-rejection@2.1.0 | development | MIT + Lockfile path: node_modules/hard-rejection + Package: https://www.npmjs.com/package/hard-rejection/v/2.1.0 + Archive: https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz + Integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +has-flag@4.0.0 | development | MIT + Lockfile path: node_modules/has-flag + Package: https://www.npmjs.com/package/has-flag/v/4.0.0 + Archive: https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz + Integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@1.0.2 | runtime | MIT Lockfile path: node_modules/has-property-descriptors Package: https://www.npmjs.com/package/has-property-descriptors/v/1.0.2 @@ -2405,16 +2850,41 @@ hast-util-whitespace@3.0.0 | development | MIT Package: https://www.npmjs.com/package/hast-util-whitespace/v/3.0.0 Archive: https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz Integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw== +he@1.2.0 | development | MIT + Lockfile path: node_modules/he + Package: https://www.npmjs.com/package/he/v/1.2.0 + Archive: https://registry.npmjs.org/he/-/he-1.2.0.tgz + Integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== hookable@5.5.3 | development | MIT Lockfile path: node_modules/hookable Package: https://www.npmjs.com/package/hookable/v/5.5.3 Archive: https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz Integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== +hosted-git-info@4.1.0 | development | ISC + Lockfile path: node_modules/hosted-git-info + Package: https://www.npmjs.com/package/hosted-git-info/v/4.1.0 + Archive: https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz + Integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== +lru-cache@6.0.0 | development | ISC + Lockfile path: node_modules/hosted-git-info/node_modules/lru-cache + Package: https://www.npmjs.com/package/lru-cache/v/6.0.0 + Archive: https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz + Integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== html-void-elements@3.0.0 | development | MIT Lockfile path: node_modules/html-void-elements Package: https://www.npmjs.com/package/html-void-elements/v/3.0.0 Archive: https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz Integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== +http-proxy-agent@7.0.2 | development | MIT + Lockfile path: node_modules/http-proxy-agent + Package: https://www.npmjs.com/package/http-proxy-agent/v/7.0.2 + Archive: https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz + Integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== +https-proxy-agent@7.0.6 | development | MIT + Lockfile path: node_modules/https-proxy-agent + Package: https://www.npmjs.com/package/https-proxy-agent/v/7.0.6 + Archive: https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz + Integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== human-signals@8.0.1 | development | Apache-2.0 Lockfile path: node_modules/human-signals Package: https://www.npmjs.com/package/human-signals/v/8.0.1 @@ -2460,6 +2930,16 @@ incur@0.6.1 | runtime | MIT Package: https://www.npmjs.com/package/@modelcontextprotocol/server/v/2.0.0-alpha.4 Archive: https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-alpha.4.tgz Integrity: sha512-/KEo3ZJ50HlagHp0lz2vPgfBZFtXHu6zTBXT9XqPc+4O9i4+IbBVWKq/a9yAfv/ifp0u3+mRo8SUk5kMKzhN8A== +indent-string@4.0.0 | development | MIT + Lockfile path: node_modules/indent-string + Package: https://www.npmjs.com/package/indent-string/v/4.0.0 + Archive: https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz + Integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +inflight@1.0.6 | development | ISC + Lockfile path: node_modules/inflight + Package: https://www.npmjs.com/package/inflight/v/1.0.6 + Archive: https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz + Integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== inherits@2.0.4 | runtime | ISC Lockfile path: node_modules/inherits Package: https://www.npmjs.com/package/inherits/v/2.0.4 @@ -2470,16 +2950,31 @@ ini@1.3.8 | runtime, optional | ISC Package: https://www.npmjs.com/package/ini/v/1.3.8 Archive: https://registry.npmjs.org/ini/-/ini-1.3.8.tgz Integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +is-arrayish@0.2.1 | development | MIT + Lockfile path: node_modules/is-arrayish + Package: https://www.npmjs.com/package/is-arrayish/v/0.2.1 + Archive: https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz + Integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== is-callable@1.2.7 | runtime | MIT Lockfile path: node_modules/is-callable Package: https://www.npmjs.com/package/is-callable/v/1.2.7 Archive: https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz Integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== +is-core-module@2.17.0 | development | MIT + Lockfile path: node_modules/is-core-module + Package: https://www.npmjs.com/package/is-core-module/v/2.17.0 + Archive: https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz + Integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA== is-fullwidth-code-point@5.1.0 | development | MIT Lockfile path: node_modules/is-fullwidth-code-point Package: https://www.npmjs.com/package/is-fullwidth-code-point/v/5.1.0 Archive: https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz Integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ== +is-obj@2.0.0 | development | MIT + Lockfile path: node_modules/is-obj + Package: https://www.npmjs.com/package/is-obj/v/2.0.0 + Archive: https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz + Integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== is-plain-obj@4.1.0 | development | MIT Lockfile path: node_modules/is-plain-obj Package: https://www.npmjs.com/package/is-plain-obj/v/4.1.0 @@ -2540,6 +3035,11 @@ js-tokens@10.0.0 | development | MIT Package: https://www.npmjs.com/package/js-tokens/v/10.0.0 Archive: https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz Integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q== +js-yaml@4.3.2 | development | MIT + Lockfile path: node_modules/js-yaml + Package: https://www.npmjs.com/package/js-yaml/v/4.3.2 + Archive: https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz + Integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA== jscpd@5.0.12 | development | MIT Lockfile path: node_modules/jscpd Package: https://www.npmjs.com/package/jscpd/v/5.0.12 @@ -2575,11 +3075,21 @@ jscpd-windows-x64-msvc@5.0.12 | development, optional | MIT Package: https://www.npmjs.com/package/jscpd-windows-x64-msvc/v/5.0.12 Archive: https://registry.npmjs.org/jscpd-windows-x64-msvc/-/jscpd-windows-x64-msvc-5.0.12.tgz Integrity: sha512-01x1klKjvfzI6Of5tI9u72x7TIQZQLLG6kMdsEPJKl/BXbskdknrfCepeTXRBbdFPKs25jfcd0klZ4OdDvww5w== +jsep@1.6.0 | development | MIT + Lockfile path: node_modules/jsep + Package: https://www.npmjs.com/package/jsep/v/1.6.0 + Archive: https://registry.npmjs.org/jsep/-/jsep-1.6.0.tgz + Integrity: sha512-Y8Tr54CX7B5YGAwo7/n8eumidEX3Opx98RyyQVnlHqUaZcPA8uUzL2v1rshr9ypW9Q6hNjfJtg3sL+8kaNE7TA== jsesc@3.1.0 | development | MIT Lockfile path: node_modules/jsesc Package: https://www.npmjs.com/package/jsesc/v/3.1.0 Archive: https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz Integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== +json-parse-even-better-errors@2.3.1 | development | MIT + Lockfile path: node_modules/json-parse-even-better-errors + Package: https://www.npmjs.com/package/json-parse-even-better-errors/v/2.3.1 + Archive: https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz + Integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== json-rpc-2.0@1.8.1 | development | MIT Lockfile path: node_modules/json-rpc-2.0 Package: https://www.npmjs.com/package/json-rpc-2.0/v/1.8.1 @@ -2590,6 +3100,11 @@ json-schema-traverse@1.0.0 | runtime | MIT Package: https://www.npmjs.com/package/json-schema-traverse/v/1.0.0 Archive: https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz Integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== +json-stringify-safe@5.0.1 | development | ISC + Lockfile path: node_modules/json-stringify-safe + Package: https://www.npmjs.com/package/json-stringify-safe/v/5.0.1 + Archive: https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz + Integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== json5@2.2.3 | development | MIT Lockfile path: node_modules/json5 Package: https://www.npmjs.com/package/json5/v/2.2.3 @@ -2600,6 +3115,16 @@ jsonc-parser@3.3.1 | runtime | MIT Package: https://www.npmjs.com/package/jsonc-parser/v/3.3.1 Archive: https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz Integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== +jsonpath-plus@10.4.0 | development | MIT + Lockfile path: node_modules/jsonpath-plus + Package: https://www.npmjs.com/package/jsonpath-plus/v/10.4.0 + Archive: https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz + Integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA== +kind-of@6.0.3 | development | MIT + Lockfile path: node_modules/kind-of + Package: https://www.npmjs.com/package/kind-of/v/6.0.3 + Archive: https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz + Integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== knip@6.39.0 | development | ISC Lockfile path: node_modules/knip Package: https://www.npmjs.com/package/knip/v/6.39.0 @@ -2670,6 +3195,11 @@ lightningcss-win32-x64-msvc@1.33.0 | development, optional | MPL-2.0 Package: https://www.npmjs.com/package/lightningcss-win32-x64-msvc/v/1.33.0 Archive: https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz Integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA== +lines-and-columns@1.2.4 | development | MIT + Lockfile path: node_modules/lines-and-columns + Package: https://www.npmjs.com/package/lines-and-columns/v/1.2.4 + Archive: https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz + Integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== lint-staged@16.4.0 | development | MIT Lockfile path: node_modules/lint-staged Package: https://www.npmjs.com/package/lint-staged/v/16.4.0 @@ -2680,11 +3210,21 @@ listr2@9.0.5 | development | MIT Package: https://www.npmjs.com/package/listr2/v/9.0.5 Archive: https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz Integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g== +locate-path@5.0.0 | development | MIT + Lockfile path: node_modules/locate-path + Package: https://www.npmjs.com/package/locate-path/v/5.0.0 + Archive: https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz + Integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== lodash.groupby@4.6.0 | development | MIT Lockfile path: node_modules/lodash.groupby Package: https://www.npmjs.com/package/lodash.groupby/v/4.6.0 Archive: https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz Integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw== +lodash.ismatch@4.4.0 | development | MIT + Lockfile path: node_modules/lodash.ismatch + Package: https://www.npmjs.com/package/lodash.ismatch/v/4.4.0 + Archive: https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz + Integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g== log-update@6.1.0 | development | MIT Lockfile path: node_modules/log-update Package: https://www.npmjs.com/package/log-update/v/6.1.0 @@ -2735,6 +3275,11 @@ magicast@0.5.5 | development | MIT Package: https://www.npmjs.com/package/@babel/types/v/7.29.8 Archive: https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz Integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg== +map-obj@4.3.0 | development | MIT + Lockfile path: node_modules/map-obj + Package: https://www.npmjs.com/package/map-obj/v/4.3.0 + Archive: https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz + Integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== mark.js@8.11.1 | development | MIT Lockfile path: node_modules/mark.js Package: https://www.npmjs.com/package/mark.js/v/8.11.1 @@ -2750,6 +3295,16 @@ mdast-util-to-hast@13.2.1 | development | MIT Package: https://www.npmjs.com/package/mdast-util-to-hast/v/13.2.1 Archive: https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz Integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== +meow@8.1.2 | development | MIT + Lockfile path: node_modules/meow + Package: https://www.npmjs.com/package/meow/v/8.1.2 + Archive: https://registry.npmjs.org/meow/-/meow-8.1.2.tgz + Integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== +type-fest@0.18.1 | development | (MIT OR CC0-1.0) + Lockfile path: node_modules/meow/node_modules/type-fest + Package: https://www.npmjs.com/package/type-fest/v/0.18.1 + Archive: https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz + Integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== micromark-util-character@2.1.1 | development | MIT Lockfile path: node_modules/micromark-util-character Package: https://www.npmjs.com/package/micromark-util-character/v/2.1.1 @@ -2785,6 +3340,11 @@ mimic-response@3.1.0 | runtime | MIT Package: https://www.npmjs.com/package/mimic-response/v/3.1.0 Archive: https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz Integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== +min-indent@1.0.1 | development | MIT + Lockfile path: node_modules/min-indent + Package: https://www.npmjs.com/package/min-indent/v/1.0.1 + Archive: https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz + Integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== minimalistic-assert@1.0.1 | development | ISC Lockfile path: node_modules/minimalistic-assert Package: https://www.npmjs.com/package/minimalistic-assert/v/1.0.1 @@ -2800,6 +3360,16 @@ minimist@1.2.8 | runtime | MIT Package: https://www.npmjs.com/package/minimist/v/1.2.8 Archive: https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz Integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minimist-options@4.1.0 | development | MIT + Lockfile path: node_modules/minimist-options + Package: https://www.npmjs.com/package/minimist-options/v/4.1.0 + Archive: https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz + Integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== +is-plain-obj@1.1.0 | development | MIT + Lockfile path: node_modules/minimist-options/node_modules/is-plain-obj + Package: https://www.npmjs.com/package/is-plain-obj/v/1.1.0 + Archive: https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz + Integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== minimisted@2.0.1 | runtime | MIT Lockfile path: node_modules/minimisted Package: https://www.npmjs.com/package/minimisted/v/2.0.1 @@ -2825,6 +3395,16 @@ mkdirp-classic@0.5.3 | runtime, optional | MIT Package: https://www.npmjs.com/package/mkdirp-classic/v/0.5.3 Archive: https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz Integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== +modify-values@1.0.1 | development | MIT + Lockfile path: node_modules/modify-values + Package: https://www.npmjs.com/package/modify-values/v/1.0.1 + Archive: https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz + Integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +ms@2.1.3 | development | MIT + Lockfile path: node_modules/ms + Package: https://www.npmjs.com/package/ms/v/2.1.3 + Archive: https://registry.npmjs.org/ms/-/ms-2.1.3.tgz + Integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== mutation-server-protocol@0.4.1 | development | Apache-2.0 Lockfile path: node_modules/mutation-server-protocol Package: https://www.npmjs.com/package/mutation-server-protocol/v/0.4.1 @@ -2860,16 +3440,31 @@ napi-build-utils@2.0.0 | runtime, optional | MIT Package: https://www.npmjs.com/package/napi-build-utils/v/2.0.0 Archive: https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz Integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA== +neo-async@2.6.2 | development | MIT + Lockfile path: node_modules/neo-async + Package: https://www.npmjs.com/package/neo-async/v/2.6.2 + Archive: https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz + Integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== node-abi@3.98.0 | runtime, optional | MIT Lockfile path: node_modules/node-abi Package: https://www.npmjs.com/package/node-abi/v/3.98.0 Archive: https://registry.npmjs.org/node-abi/-/node-abi-3.98.0.tgz Integrity: sha512-Llh8noNw7s15m+rmIrZnZQbe4gLyasL9loT74rC92PkmwWKeEedKvc7o6pjp8B3MhrYT/Cl846NOYZbz1wmddw== +node-html-parser@6.1.13 | development | MIT + Lockfile path: node_modules/node-html-parser + Package: https://www.npmjs.com/package/node-html-parser/v/6.1.13 + Archive: https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz + Integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg== node-releases@2.0.58 | development | MIT Lockfile path: node_modules/node-releases Package: https://www.npmjs.com/package/node-releases/v/2.0.58 Archive: https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz Integrity: sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg== +normalize-package-data@3.0.3 | development | BSD-2-Clause + Lockfile path: node_modules/normalize-package-data + Package: https://www.npmjs.com/package/normalize-package-data/v/3.0.3 + Archive: https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz + Integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== npm-run-path@6.0.0 | development | MIT Lockfile path: node_modules/npm-run-path Package: https://www.npmjs.com/package/npm-run-path/v/6.0.0 @@ -2880,6 +3475,11 @@ path-key@4.0.0 | development | MIT Package: https://www.npmjs.com/package/path-key/v/4.0.0 Archive: https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz Integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== +nth-check@2.1.1 | development | BSD-2-Clause + Lockfile path: node_modules/nth-check + Package: https://www.npmjs.com/package/nth-check/v/2.1.1 + Archive: https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz + Integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== object-inspect@1.13.4 | development | MIT Lockfile path: node_modules/object-inspect Package: https://www.npmjs.com/package/object-inspect/v/1.13.4 @@ -2940,6 +3540,21 @@ oxlint@1.87.0 | development | MIT Package: https://www.npmjs.com/package/oxlint/v/1.87.0 Archive: https://registry.npmjs.org/oxlint/-/oxlint-1.87.0.tgz Integrity: sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg== +p-limit@2.3.0 | development | MIT + Lockfile path: node_modules/p-limit + Package: https://www.npmjs.com/package/p-limit/v/2.3.0 + Archive: https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz + Integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== +p-locate@4.1.0 | development | MIT + Lockfile path: node_modules/p-locate + Package: https://www.npmjs.com/package/p-locate/v/4.1.0 + Archive: https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz + Integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== +p-try@2.2.0 | development | MIT + Lockfile path: node_modules/p-try + Package: https://www.npmjs.com/package/p-try/v/2.2.0 + Archive: https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz + Integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== package-manager-detector@1.9.0 | development | MIT Lockfile path: node_modules/package-manager-detector Package: https://www.npmjs.com/package/package-manager-detector/v/1.9.0 @@ -2950,6 +3565,36 @@ pako@1.0.11 | runtime | (MIT AND Zlib) Package: https://www.npmjs.com/package/pako/v/1.0.11 Archive: https://registry.npmjs.org/pako/-/pako-1.0.11.tgz Integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== +parse-diff@0.11.1 | development | MIT + Lockfile path: node_modules/parse-diff + Package: https://www.npmjs.com/package/parse-diff/v/0.11.1 + Archive: https://registry.npmjs.org/parse-diff/-/parse-diff-0.11.1.tgz + Integrity: sha512-Oq4j8LAOPOcssanQkIjxosjATBIEJhCxMCxPhMu+Ci4wdNmAEdx0O+a7gzbR2PyKXgKPvRLIN5g224+dJAsKHA== +parse-github-repo-url@1.4.1 | development | MIT + Lockfile path: node_modules/parse-github-repo-url + Package: https://www.npmjs.com/package/parse-github-repo-url/v/1.4.1 + Archive: https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz + Integrity: sha512-bSWyzBKqcSL4RrncTpGsEKoJ7H8a4L3++ifTAbTFeMHyq2wRV+42DGmQcHIrJIvdcacjIOxEuKH/w4tthF17gg== +parse-json@5.2.0 | development | MIT + Lockfile path: node_modules/parse-json + Package: https://www.npmjs.com/package/parse-json/v/5.2.0 + Archive: https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz + Integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== +@babel/code-frame@7.29.7 | development | MIT + Lockfile path: node_modules/parse-json/node_modules/@babel/code-frame + Package: https://www.npmjs.com/package/@babel/code-frame/v/7.29.7 + Archive: https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz + Integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== +@babel/helper-validator-identifier@7.29.7 | development | MIT + Lockfile path: node_modules/parse-json/node_modules/@babel/helper-validator-identifier + Package: https://www.npmjs.com/package/@babel/helper-validator-identifier/v/7.29.7 + Archive: https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz + Integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== +js-tokens@4.0.0 | development | MIT + Lockfile path: node_modules/parse-json/node_modules/js-tokens + Package: https://www.npmjs.com/package/js-tokens/v/4.0.0 + Archive: https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz + Integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== parse-ms@4.0.0 | development | MIT Lockfile path: node_modules/parse-ms Package: https://www.npmjs.com/package/parse-ms/v/4.0.0 @@ -2965,11 +3610,26 @@ entities@8.1.0 | runtime | BSD-2-Clause Package: https://www.npmjs.com/package/entities/v/8.1.0 Archive: https://registry.npmjs.org/entities/-/entities-8.1.0.tgz Integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA== +path-exists@4.0.0 | development | MIT + Lockfile path: node_modules/path-exists + Package: https://www.npmjs.com/package/path-exists/v/4.0.0 + Archive: https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz + Integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== +path-is-absolute@1.0.1 | development | MIT + Lockfile path: node_modules/path-is-absolute + Package: https://www.npmjs.com/package/path-is-absolute/v/1.0.1 + Archive: https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz + Integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@3.1.1 | runtime | MIT Lockfile path: node_modules/path-key Package: https://www.npmjs.com/package/path-key/v/3.1.1 Archive: https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz Integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-parse@1.0.7 | development | MIT + Lockfile path: node_modules/path-parse + Package: https://www.npmjs.com/package/path-parse/v/1.0.7 + Archive: https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz + Integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-scurry@2.0.2 | runtime | BlueOak-1.0.0 Lockfile path: node_modules/path-scurry Package: https://www.npmjs.com/package/path-scurry/v/2.0.2 @@ -3090,6 +3750,11 @@ quick-format-unescaped@4.0.4 | runtime | MIT Package: https://www.npmjs.com/package/quick-format-unescaped/v/4.0.4 Archive: https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz Integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== +quick-lru@4.0.1 | development | MIT + Lockfile path: node_modules/quick-lru + Package: https://www.npmjs.com/package/quick-lru/v/4.0.1 + Archive: https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz + Integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== rc@1.2.8 | runtime, optional | (BSD-2-Clause OR MIT OR Apache-2.0) Lockfile path: node_modules/rc Package: https://www.npmjs.com/package/rc/v/1.2.8 @@ -3100,6 +3765,41 @@ strip-json-comments@2.0.1 | runtime, optional | MIT Package: https://www.npmjs.com/package/strip-json-comments/v/2.0.1 Archive: https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz Integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== +read-pkg@5.2.0 | development | MIT + Lockfile path: node_modules/read-pkg + Package: https://www.npmjs.com/package/read-pkg/v/5.2.0 + Archive: https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz + Integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== +read-pkg-up@7.0.1 | development | MIT + Lockfile path: node_modules/read-pkg-up + Package: https://www.npmjs.com/package/read-pkg-up/v/7.0.1 + Archive: https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz + Integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== +type-fest@0.8.1 | development | (MIT OR CC0-1.0) + Lockfile path: node_modules/read-pkg-up/node_modules/type-fest + Package: https://www.npmjs.com/package/type-fest/v/0.8.1 + Archive: https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz + Integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +hosted-git-info@2.8.9 | development | ISC + Lockfile path: node_modules/read-pkg/node_modules/hosted-git-info + Package: https://www.npmjs.com/package/hosted-git-info/v/2.8.9 + Archive: https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz + Integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== +normalize-package-data@2.5.0 | development | BSD-2-Clause + Lockfile path: node_modules/read-pkg/node_modules/normalize-package-data + Package: https://www.npmjs.com/package/normalize-package-data/v/2.5.0 + Archive: https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz + Integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== +semver@5.7.2 | development | ISC + Lockfile path: node_modules/read-pkg/node_modules/semver + Package: https://www.npmjs.com/package/semver/v/5.7.2 + Archive: https://registry.npmjs.org/semver/-/semver-5.7.2.tgz + Integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== +type-fest@0.6.0 | development | (MIT OR CC0-1.0) + Lockfile path: node_modules/read-pkg/node_modules/type-fest + Package: https://www.npmjs.com/package/type-fest/v/0.6.0 + Archive: https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz + Integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== readable-stream@4.7.0 | runtime | MIT Lockfile path: node_modules/readable-stream Package: https://www.npmjs.com/package/readable-stream/v/4.7.0 @@ -3110,6 +3810,11 @@ real-require@1.0.0 | runtime | MIT Package: https://www.npmjs.com/package/real-require/v/1.0.0 Archive: https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz Integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g== +redent@3.0.0 | development | MIT + Lockfile path: node_modules/redent + Package: https://www.npmjs.com/package/redent/v/3.0.0 + Archive: https://registry.npmjs.org/redent/-/redent-3.0.0.tgz + Integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== regex@6.1.0 | development | MIT Lockfile path: node_modules/regex Package: https://www.npmjs.com/package/regex/v/6.1.0 @@ -3125,11 +3830,76 @@ regex-utilities@2.3.0 | development | MIT Package: https://www.npmjs.com/package/regex-utilities/v/2.3.0 Archive: https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz Integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== +release-please@17.3.0 | development | Apache-2.0 + Lockfile path: node_modules/release-please + Package: https://www.npmjs.com/package/release-please/v/17.3.0 + Archive: https://registry.npmjs.org/release-please/-/release-please-17.3.0.tgz + Integrity: sha512-dB7HsFUpAvU1Wj9RGCINUz8Zi2qQDZiGbDEEVnJ7baNfJmk5XdYhuUFL6RcuDGjReGhp1gzY8Tc2g7vHlM4zpg== +@types/unist@2.0.11 | development | MIT + Lockfile path: node_modules/release-please/node_modules/@types/unist + Package: https://www.npmjs.com/package/@types/unist/v/2.0.11 + Archive: https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz + Integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== +@xmldom/xmldom@0.8.15 | development | MIT + Lockfile path: node_modules/release-please/node_modules/@xmldom/xmldom + Package: https://www.npmjs.com/package/@xmldom/xmldom/v/0.8.15 + Archive: https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz + Integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA== +ansi-styles@4.3.0 | development | MIT + Lockfile path: node_modules/release-please/node_modules/ansi-styles + Package: https://www.npmjs.com/package/ansi-styles/v/4.3.0 + Archive: https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz + Integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== +chalk@4.1.2 | development | MIT + Lockfile path: node_modules/release-please/node_modules/chalk + Package: https://www.npmjs.com/package/chalk/v/4.1.2 + Archive: https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz + Integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== +diff@8.0.4 | development | BSD-3-Clause + Lockfile path: node_modules/release-please/node_modules/diff + Package: https://www.npmjs.com/package/diff/v/8.0.4 + Archive: https://registry.npmjs.org/diff/-/diff-8.0.4.tgz + Integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw== +figures@3.2.0 | development | MIT + Lockfile path: node_modules/release-please/node_modules/figures + Package: https://www.npmjs.com/package/figures/v/3.2.0 + Archive: https://registry.npmjs.org/figures/-/figures-3.2.0.tgz + Integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== +typescript@4.9.5 | development | Apache-2.0 + Lockfile path: node_modules/release-please/node_modules/typescript + Package: https://www.npmjs.com/package/typescript/v/4.9.5 + Archive: https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz + Integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +unist-util-is@4.1.0 | development | MIT + Lockfile path: node_modules/release-please/node_modules/unist-util-is + Package: https://www.npmjs.com/package/unist-util-is/v/4.1.0 + Archive: https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz + Integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg== +unist-util-visit@2.0.3 | development | MIT + Lockfile path: node_modules/release-please/node_modules/unist-util-visit + Package: https://www.npmjs.com/package/unist-util-visit/v/2.0.3 + Archive: https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz + Integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q== +unist-util-visit-parents@3.1.1 | development | MIT + Lockfile path: node_modules/release-please/node_modules/unist-util-visit-parents + Package: https://www.npmjs.com/package/unist-util-visit-parents/v/3.1.1 + Archive: https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz + Integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg== +require-directory@2.1.1 | development | MIT + Lockfile path: node_modules/require-directory + Package: https://www.npmjs.com/package/require-directory/v/2.1.1 + Archive: https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz + Integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-from-string@2.0.2 | runtime | MIT Lockfile path: node_modules/require-from-string Package: https://www.npmjs.com/package/require-from-string/v/2.0.2 Archive: https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz Integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== +resolve@1.22.13 | development | MIT + Lockfile path: node_modules/resolve + Package: https://www.npmjs.com/package/resolve/v/1.22.13 + Archive: https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz + Integrity: sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA== resolve-pkg-maps@1.0.0 | development | MIT Lockfile path: node_modules/resolve-pkg-maps Package: https://www.npmjs.com/package/resolve-pkg-maps/v/1.0.0 @@ -3140,6 +3910,11 @@ restore-cursor@5.1.0 | development | MIT Package: https://www.npmjs.com/package/restore-cursor/v/5.1.0 Archive: https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz Integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA== +retry@0.13.1 | development | MIT + Lockfile path: node_modules/retry + Package: https://www.npmjs.com/package/retry/v/0.13.1 + Archive: https://registry.npmjs.org/retry/-/retry-0.13.1.tgz + Integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== rfdc@1.4.1 | development | MIT Lockfile path: node_modules/rfdc Package: https://www.npmjs.com/package/rfdc/v/1.4.1 @@ -3285,11 +4060,36 @@ space-separated-tokens@2.0.2 | development | MIT Package: https://www.npmjs.com/package/space-separated-tokens/v/2.0.2 Archive: https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz Integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== +spdx-correct@3.2.0 | development | Apache-2.0 + Lockfile path: node_modules/spdx-correct + Package: https://www.npmjs.com/package/spdx-correct/v/3.2.0 + Archive: https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz + Integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== +spdx-exceptions@2.5.0 | development | CC-BY-3.0 + Lockfile path: node_modules/spdx-exceptions + Package: https://www.npmjs.com/package/spdx-exceptions/v/2.5.0 + Archive: https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz + Integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== +spdx-expression-parse@3.0.1 | development | MIT + Lockfile path: node_modules/spdx-expression-parse + Package: https://www.npmjs.com/package/spdx-expression-parse/v/3.0.1 + Archive: https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz + Integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== +spdx-license-ids@3.0.24 | development | CC0-1.0 + Lockfile path: node_modules/spdx-license-ids + Package: https://www.npmjs.com/package/spdx-license-ids/v/3.0.24 + Archive: https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz + Integrity: sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ== speakingurl@14.0.1 | development | BSD-3-Clause Lockfile path: node_modules/speakingurl Package: https://www.npmjs.com/package/speakingurl/v/14.0.1 Archive: https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz Integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== +split@1.0.1 | development | MIT + Lockfile path: node_modules/split + Package: https://www.npmjs.com/package/split/v/1.0.1 + Archive: https://registry.npmjs.org/split/-/split-1.0.1.tgz + Integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== split2@4.2.0 | runtime | ISC Lockfile path: node_modules/split2 Package: https://www.npmjs.com/package/split2/v/4.2.0 @@ -3340,6 +4140,11 @@ strip-final-newline@4.0.0 | development | MIT Package: https://www.npmjs.com/package/strip-final-newline/v/4.0.0 Archive: https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz Integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw== +strip-indent@3.0.0 | development | MIT + Lockfile path: node_modules/strip-indent + Package: https://www.npmjs.com/package/strip-indent/v/3.0.0 + Archive: https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz + Integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== strip-json-comments@5.0.3 | development | MIT Lockfile path: node_modules/strip-json-comments Package: https://www.npmjs.com/package/strip-json-comments/v/5.0.3 @@ -3350,6 +4155,16 @@ superjson@2.2.6 | development | MIT Package: https://www.npmjs.com/package/superjson/v/2.2.6 Archive: https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz Integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA== +supports-color@7.2.0 | development | MIT + Lockfile path: node_modules/supports-color + Package: https://www.npmjs.com/package/supports-color/v/7.2.0 + Archive: https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz + Integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== +supports-preserve-symlinks-flag@1.0.0 | development | MIT + Lockfile path: node_modules/supports-preserve-symlinks-flag + Package: https://www.npmjs.com/package/supports-preserve-symlinks-flag/v/1.0.0 + Archive: https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz + Integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== tabbable@6.5.0 | development | MIT Lockfile path: node_modules/tabbable Package: https://www.npmjs.com/package/tabbable/v/6.5.0 @@ -3375,6 +4190,11 @@ thread-stream@4.2.0 | runtime | MIT Package: https://www.npmjs.com/package/thread-stream/v/4.2.0 Archive: https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz Integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ== +through@2.3.8 | development | MIT + Lockfile path: node_modules/through + Package: https://www.npmjs.com/package/through/v/2.3.8 + Archive: https://registry.npmjs.org/through/-/through-2.3.8.tgz + Integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== tinybench@6.2.0 | development | MIT Lockfile path: node_modules/tinybench Package: https://www.npmjs.com/package/tinybench/v/6.2.0 @@ -3420,6 +4240,11 @@ trim-lines@3.0.1 | development | MIT Package: https://www.npmjs.com/package/trim-lines/v/3.0.1 Archive: https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz Integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== +trim-newlines@3.0.1 | development | MIT + Lockfile path: node_modules/trim-newlines + Package: https://www.npmjs.com/package/trim-newlines/v/3.0.1 + Archive: https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz + Integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== tslib@2.8.1 | development | 0BSD Lockfile path: node_modules/tslib Package: https://www.npmjs.com/package/tslib/v/2.8.1 @@ -3450,6 +4275,11 @@ turbo@2.11.7 | development | MIT Package: https://www.npmjs.com/package/turbo/v/2.11.7 Archive: https://registry.npmjs.org/turbo/-/turbo-2.11.7.tgz Integrity: sha512-RhXHbqfvY6o3xt/zSHCwSNwLrZlXiqHzewi4eAj9PtZ1T1CqgMJ2yIDb7SKPTG9IikTm925mfzL6hldQVdyNWw== +type-fest@3.13.1 | development | (MIT OR CC0-1.0) + Lockfile path: node_modules/type-fest + Package: https://www.npmjs.com/package/type-fest/v/3.13.1 + Archive: https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz + Integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g== typed-array-buffer@1.0.3 | runtime | MIT Lockfile path: node_modules/typed-array-buffer Package: https://www.npmjs.com/package/typed-array-buffer/v/1.0.3 @@ -3480,6 +4310,11 @@ typescript@6.0.3 | development | Apache-2.0 Package: https://www.npmjs.com/package/typescript/v/6.0.3 Archive: https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz Integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw== +uglify-js@3.19.3 | development, optional | BSD-2-Clause + Lockfile path: node_modules/uglify-js + Package: https://www.npmjs.com/package/uglify-js/v/3.19.3 + Archive: https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz + Integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== unbash@4.0.12 | development | ISC Lockfile path: node_modules/unbash Package: https://www.npmjs.com/package/unbash/v/4.0.12 @@ -3525,6 +4360,11 @@ unist-util-visit-parents@6.0.2 | development | MIT Package: https://www.npmjs.com/package/unist-util-visit-parents/v/6.0.2 Archive: https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz Integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ== +universal-user-agent@6.0.1 | development | ISC + Lockfile path: node_modules/universal-user-agent + Package: https://www.npmjs.com/package/universal-user-agent/v/6.0.1 + Archive: https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz + Integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ== update-browserslist-db@1.3.4 | development | MIT Lockfile path: node_modules/update-browserslist-db Package: https://www.npmjs.com/package/update-browserslist-db/v/1.3.4 @@ -3535,6 +4375,11 @@ util-deprecate@1.0.2 | runtime, optional | MIT Package: https://www.npmjs.com/package/util-deprecate/v/1.0.2 Archive: https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz Integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== +validate-npm-package-license@3.0.4 | development | Apache-2.0 + Lockfile path: node_modules/validate-npm-package-license + Package: https://www.npmjs.com/package/validate-npm-package-license/v/3.0.4 + Archive: https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz + Integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== verkit@0.5.0 | development | MIT Lockfile path: node_modules/verkit Package: https://www.npmjs.com/package/verkit/v/0.5.0 @@ -3825,6 +4670,11 @@ why-is-node-running@3.2.1 | development | MIT Package: https://www.npmjs.com/package/why-is-node-running/v/3.2.1 Archive: https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz Integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ== +wordwrap@1.0.0 | development | MIT + Lockfile path: node_modules/wordwrap + Package: https://www.npmjs.com/package/wordwrap/v/1.0.0 + Archive: https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz + Integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== wrap-ansi@9.0.2 | development | MIT Lockfile path: node_modules/wrap-ansi Package: https://www.npmjs.com/package/wrap-ansi/v/9.0.2 @@ -3860,11 +4710,66 @@ xmlchars@2.2.0 | runtime | MIT Package: https://www.npmjs.com/package/xmlchars/v/2.2.0 Archive: https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz Integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +xpath@0.0.34 | development | MIT + Lockfile path: node_modules/xpath + Package: https://www.npmjs.com/package/xpath/v/0.0.34 + Archive: https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz + Integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA== +y18n@5.0.8 | development | ISC + Lockfile path: node_modules/y18n + Package: https://www.npmjs.com/package/y18n/v/5.0.8 + Archive: https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz + Integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@4.0.0 | development | ISC + Lockfile path: node_modules/yallist + Package: https://www.npmjs.com/package/yallist/v/4.0.0 + Archive: https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz + Integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@2.9.0 | runtime | ISC Lockfile path: node_modules/yaml Package: https://www.npmjs.com/package/yaml/v/2.9.0 Archive: https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz Integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA== +yargs@17.7.3 | development | MIT + Lockfile path: node_modules/yargs + Package: https://www.npmjs.com/package/yargs/v/17.7.3 + Archive: https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz + Integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g== +yargs-parser@20.2.9 | development | ISC + Lockfile path: node_modules/yargs-parser + Package: https://www.npmjs.com/package/yargs-parser/v/20.2.9 + Archive: https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz + Integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +ansi-regex@5.0.1 | development | MIT + Lockfile path: node_modules/yargs/node_modules/ansi-regex + Package: https://www.npmjs.com/package/ansi-regex/v/5.0.1 + Archive: https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz + Integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +emoji-regex@8.0.0 | development | MIT + Lockfile path: node_modules/yargs/node_modules/emoji-regex + Package: https://www.npmjs.com/package/emoji-regex/v/8.0.0 + Archive: https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz + Integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +is-fullwidth-code-point@3.0.0 | development | MIT + Lockfile path: node_modules/yargs/node_modules/is-fullwidth-code-point + Package: https://www.npmjs.com/package/is-fullwidth-code-point/v/3.0.0 + Archive: https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz + Integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +string-width@4.2.3 | development | MIT + Lockfile path: node_modules/yargs/node_modules/string-width + Package: https://www.npmjs.com/package/string-width/v/4.2.3 + Archive: https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz + Integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== +strip-ansi@6.0.1 | development | MIT + Lockfile path: node_modules/yargs/node_modules/strip-ansi + Package: https://www.npmjs.com/package/strip-ansi/v/6.0.1 + Archive: https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz + Integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== +yargs-parser@21.1.1 | development | ISC + Lockfile path: node_modules/yargs/node_modules/yargs-parser + Package: https://www.npmjs.com/package/yargs-parser/v/21.1.1 + Archive: https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz + Integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yoctocolors@2.2.0 | development | MIT Lockfile path: node_modules/yoctocolors Package: https://www.npmjs.com/package/yoctocolors/v/2.2.0 diff --git a/licenses/pefile-2024.8.26-LICENSE.txt b/licenses/pefile-2024.8.26-LICENSE.txt new file mode 100644 index 0000000000..0866dd7f8f --- /dev/null +++ b/licenses/pefile-2024.8.26-LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2004-2024 Ero Carrera + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/licenses/upstream-sources.json b/licenses/upstream-sources.json index 2fbf6a8855..c42bb91821 100644 --- a/licenses/upstream-sources.json +++ b/licenses/upstream-sources.json @@ -73,5 +73,10 @@ "file": "licenses/protobuf-es-v2.16.0-LICENSE.txt", "source": "https://raw.githubusercontent.com/bufbuild/protobuf-es/b3b232f7f466eda27c66e8f63c82f930578b6cc6/LICENSE", "sha256": "c04b4216f1cd4c5a4f7fb2f2a1b0ae70d847e9e0cac7c9dee9bf8cc03177c449" + }, + { + "file": "licenses/pefile-2024.8.26-LICENSE.txt", + "source": "https://raw.githubusercontent.com/erocarrera/pefile/4b3b1e2e568a88d4f1897d694d684f23d9e270c4/LICENSE", + "sha256": "b44409c067c6da52bfb54bb6624fb11a7b52157c3a13ae1400232f8196e86ad3" } ] diff --git a/package-lock.json b/package-lock.json index 98ad57b8ae..170aee203d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ "lint-staged": "16.4.0", "oxfmt": "0.72.0", "oxlint": "1.87.0", + "release-please": "17.3.0", "rolldown": "1.2.12", "tsrs": "0.18.0-ts7.1.0-dev.20260929", "tsx": "4.23.15", @@ -1133,6 +1134,66 @@ "node": ">= 20.12.0" } }, + "node_modules/@conventional-commits/parser": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@conventional-commits/parser/-/parser-0.4.1.tgz", + "integrity": "sha512-H2ZmUVt6q+KBccXfMBhbBF14NlANeqHTXL4qCL6QGbMzrc4HDXyzWuxPxPNbz71f/5UkR5DrycP5VO9u7crahg==", + "dev": true, + "license": "ISC", + "dependencies": { + "unist-util-visit": "^2.0.3", + "unist-util-visit-parents": "^3.1.1" + } + }, + "node_modules/@conventional-commits/parser/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@conventional-commits/parser/node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@conventional-commits/parser/node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@conventional-commits/parser/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/@docsearch/css": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", @@ -1709,6 +1770,27 @@ "vitest": "^4.1.0 || ^5.0.0" } }, + "node_modules/@google-automations/git-file-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@google-automations/git-file-utils/-/git-file-utils-3.2.1.tgz", + "integrity": "sha512-LYOjAnGu/xtEkYYh6W3/1RmeK9HB/fV+xMe7nHc+WiDYrnxAJWBfcyUucoJtYS8XAhVUzNWrQvYDPRbxe67DNQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@octokit/rest": "^20.1.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": ">= 22" + } + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "dev": true, + "license": "ISC" + }, "node_modules/@iconify-json/simple-icons": { "version": "1.2.99", "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.99.tgz", @@ -2106,6 +2188,32 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.4.0.tgz", + "integrity": "sha512-nw2Px23Eirx0PFTVoPrb3FEq9Wkmg6vxcDtt38MtBCf1G0tjLCB9cK+3seOXyiw+6+aalgYhvJabQbWwMVKlEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, + "node_modules/@jsep-plugin/regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.1.0.tgz", + "integrity": "sha512-2Vkbda+XRjK6VTI6Lrh0sNBuMT8mZ51EhqMvO+V2rSdmqjs7NAGssxPZfkL23EFzYfSrvxyd1ZjOdqPQMM50og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@lydell/node-pty": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.1.0.tgz", @@ -2289,6 +2397,173 @@ "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.7.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.8.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", + "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, "node_modules/@oxc-parser/binding-android-arm-eabi": { "version": "0.150.0", "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.150.0.tgz", @@ -4696,6 +4971,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.6.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", @@ -4706,6 +4988,20 @@ "undici-types": "~8.9.0" } }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/npm-package-arg": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/npm-package-arg/-/npm-package-arg-6.1.4.tgz", + "integrity": "sha512-vDgdbMy2QXHnAruzlv68pUtXCjmqUk3WrBAsRboRovsOmxbfn/WiYCjmecyKjGztnMps5dWp4Uq2prp+Ilo17Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -4747,6 +5043,23 @@ "@types/node": "*" } }, + "node_modules/@types/yargs": { + "version": "16.0.11", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.11.tgz", + "integrity": "sha512-sbtvk8wDN+JvEdabmZExoW/HNr1cB7D/j4LT08rMiuikfA7m/JNJg7ATQcgzs34zHnoScDkY0ZRSl29Fkmk36g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript/typescript-aix-ppc64": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", @@ -5604,6 +5917,16 @@ "node": ">=6.5" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -5715,6 +6038,30 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -5743,6 +6090,16 @@ "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", "license": "MIT" }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -5809,6 +6166,13 @@ "node": ">=6.0.0" } }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -5881,6 +6245,13 @@ "node": ">= 6" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", @@ -5998,6 +6369,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001815", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz", @@ -6159,6 +6558,335 @@ "node": ">= 12" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/code-suggester": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/code-suggester/-/code-suggester-5.0.1.tgz", + "integrity": "sha512-8qJiiSCfkbPNWvjEFzdG1UW3axL+Bs0ldV1/TdlBKmF9I/a/WooSQZQCi9M44HoXbVTQesn/IQr6+nIWNnVIWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@octokit/rest": "^20.1.2", + "@types/yargs": "^16.0.0", + "async-retry": "^1.3.1", + "diff": "^8.0.3", + "glob": "^7.1.6", + "parse-diff": "^0.11.0", + "yargs": "^16.0.0" + }, + "bin": { + "code-suggester": "build/src/bin/code-suggester.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/code-suggester/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/code-suggester/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/code-suggester/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/code-suggester/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/code-suggester/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/code-suggester/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/code-suggester/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/code-suggester/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/code-suggester/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/code-suggester/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/code-suggester/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/code-suggester/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/code-suggester/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/code-suggester/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -6187,6 +6915,73 @@ "node": ">=20" } }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-6.1.0.tgz", + "integrity": "sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-6.0.1.tgz", + "integrity": "sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-commits-filter": "^3.0.0", + "dateformat": "^3.0.3", + "handlebars": "^4.7.7", + "json-stringify-safe": "^5.0.1", + "meow": "^8.1.2", + "semver": "^7.0.0", + "split": "^1.0.1" + }, + "bin": { + "conventional-changelog-writer": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/conventional-commits-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-3.0.0.tgz", + "integrity": "sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6233,6 +7028,36 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -6240,6 +7065,71 @@ "dev": true, "license": "MIT" }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -6282,6 +7172,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -6303,6 +7200,16 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -6349,6 +7256,91 @@ "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", "license": "MIT" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6430,6 +7422,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -6519,6 +7521,16 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -6754,6 +7766,20 @@ "license": "MIT", "optional": true }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/flru": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", @@ -6829,6 +7855,13 @@ "license": "MIT", "optional": true }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6863,6 +7896,16 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-east-asian-width": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", @@ -6979,6 +8022,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/handlebars": { + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.8", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -6988,6 +8063,26 @@ "node": ">=4" } }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -7077,6 +8172,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", @@ -7084,6 +8189,32 @@ "dev": true, "license": "MIT" }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", @@ -7095,6 +8226,34 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", @@ -7221,6 +8380,28 @@ "node": ">=20" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -7234,6 +8415,13 @@ "license": "ISC", "optional": true }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -7246,6 +8434,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", @@ -7262,6 +8466,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -7395,6 +8609,29 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jscpd": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-5.0.12.tgz", @@ -7500,6 +8737,16 @@ "win32" ] }, + "node_modules/jsep": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.6.0.tgz", + "integrity": "sha512-Y8Tr54CX7B5YGAwo7/n8eumidEX3Opx98RyyQVnlHqUaZcPA8uUzL2v1rshr9ypW9Q6hNjfJtg3sL+8kaNE7TA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.16.0" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7513,6 +8760,13 @@ "node": ">=6" } }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-rpc-2.0": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.8.1.tgz", @@ -7526,6 +8780,13 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7545,6 +8806,35 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, + "node_modules/jsonpath-plus": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", + "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.3.0", + "@jsep-plugin/regex": "^1.0.4", + "jsep": "^1.4.0" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/knip": { "version": "6.39.0", "resolved": "https://registry.npmjs.org/knip/-/knip-6.39.0.tgz", @@ -7873,6 +9163,13 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/lint-staged": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", @@ -7915,6 +9212,19 @@ "node": ">=20.0.0" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", @@ -7922,6 +9232,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "dev": true, + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -8046,6 +9363,19 @@ "node": ">=6.9.0" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mark.js": { "version": "8.11.1", "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", @@ -8084,6 +9414,45 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -8203,6 +9572,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -8234,6 +9613,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/minimisted": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", @@ -8273,6 +9677,23 @@ "license": "MIT", "optional": true }, + "node_modules/modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/mutation-server-protocol": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz", @@ -8346,6 +9767,13 @@ "license": "MIT", "optional": true }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-abi": { "version": "3.98.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.98.0.tgz", @@ -8359,6 +9787,17 @@ "node": ">=10" } }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node_modules/node-releases": { "version": "2.0.58", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz", @@ -8369,6 +9808,22 @@ "node": ">=18" } }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -8399,6 +9854,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -8669,6 +10137,45 @@ } } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-manager-detector": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", @@ -8682,6 +10189,71 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/parse-diff": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/parse-diff/-/parse-diff-0.11.1.tgz", + "integrity": "sha512-Oq4j8LAOPOcssanQkIjxosjATBIEJhCxMCxPhMu+Ci4wdNmAEdx0O+a7gzbR2PyKXgKPvRLIN5g224+dJAsKHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-github-repo-url": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", + "integrity": "sha512-bSWyzBKqcSL4RrncTpGsEKoJ7H8a4L3++ifTAbTFeMHyq2wRV+42DGmQcHIrJIvdcacjIOxEuKH/w4tthF17gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/parse-json/node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/parse-json/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/parse-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", @@ -8719,6 +10291,26 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -8728,6 +10320,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -9049,6 +10648,16 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -9075,6 +10684,90 @@ "node": ">=0.10.0" } }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -9097,6 +10790,20 @@ "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", "license": "MIT" }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", @@ -9124,6 +10831,194 @@ "dev": true, "license": "MIT" }, + "node_modules/release-please": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/release-please/-/release-please-17.3.0.tgz", + "integrity": "sha512-dB7HsFUpAvU1Wj9RGCINUz8Zi2qQDZiGbDEEVnJ7baNfJmk5XdYhuUFL6RcuDGjReGhp1gzY8Tc2g7vHlM4zpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@conventional-commits/parser": "^0.4.1", + "@google-automations/git-file-utils": "^3.0.0", + "@iarna/toml": "^3.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/rest": "^20.1.1", + "@types/npm-package-arg": "^6.1.0", + "@xmldom/xmldom": "^0.8.4", + "chalk": "^4.0.0", + "code-suggester": "^5.0.0", + "conventional-changelog-conventionalcommits": "^6.0.0", + "conventional-changelog-writer": "^6.0.0", + "conventional-commits-filter": "^3.0.0", + "detect-indent": "^6.1.0", + "diff": "^8.0.3", + "figures": "^3.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.0.0", + "jsonpath-plus": "^10.0.0", + "node-html-parser": "^6.0.0", + "parse-github-repo-url": "^1.4.1", + "semver": "^7.5.3", + "type-fest": "^3.0.0", + "typescript": "^4.6.4", + "unist-util-visit": "^2.0.3", + "unist-util-visit-parents": "^3.1.1", + "xpath": "^0.0.34", + "yaml": "^2.2.2", + "yargs": "^17.0.0" + }, + "bin": { + "release-please": "build/src/bin/release-please.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/release-please/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/release-please/node_modules/@xmldom/xmldom": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", + "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/release-please/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/release-please/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/release-please/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/release-please/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/release-please/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/release-please/node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/release-please/node_modules/unist-util-visit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", + "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0", + "unist-util-visit-parents": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/release-please/node_modules/unist-util-visit-parents": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", + "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -9133,6 +11028,28 @@ "node": ">=0.10.0" } }, + "node_modules/resolve": { + "version": "1.22.13", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", + "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.17.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -9160,6 +11077,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -9608,6 +11535,42 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.24.tgz", + "integrity": "sha512-cLS9TtWkIQFyLkJ3/5aFQAOHOSKTlOs/7WDut/XPSdjom1fJhUdkepeJAKXa9Y05+FabHd0sti+Et559vDtkpQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/speakingurl": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", @@ -9618,6 +11581,19 @@ "node": ">=0.10.0" } }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -9735,6 +11711,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", @@ -9761,6 +11750,32 @@ "node": ">=16" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tabbable": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", @@ -9825,6 +11840,13 @@ "node": ">=20" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", @@ -9923,6 +11945,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -10010,6 +12042,19 @@ "@turbo/windows-arm64": "2.11.7" } }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -10116,6 +12161,20 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbash": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.12.tgz", @@ -10226,6 +12285,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, "node_modules/update-browserslist-db": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", @@ -10264,6 +12330,17 @@ "license": "MIT", "optional": true }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/verkit": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", @@ -11488,6 +13565,13 @@ "node": ">=20.11" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -11579,6 +13663,33 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/xpath": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", + "integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -11594,6 +13705,100 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yoctocolors": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", diff --git a/package.json b/package.json index 4720dbd50e..ab439eb950 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "bridge/android/ReaJadxBridge.java", "bridge/mitmproxy/capture.py", "scripts/rea.mjs", + "scripts/lib/cli-process-environment.mjs", "scripts/rea-android-workspace-guardian.mjs", "scripts/electron-active-hook.cjs", "scripts/electron-active-hook-boundaries.cjs", @@ -397,6 +398,7 @@ "lint-staged": "16.4.0", "oxfmt": "0.72.0", "oxlint": "1.87.0", + "release-please": "17.3.0", "rolldown": "1.2.12", "tsrs": "0.18.0-ts7.1.0-dev.20260929", "tsx": "4.23.15", diff --git a/patches/release-please+17.3.0.patch b/patches/release-please+17.3.0.patch new file mode 100644 index 0000000000..6766660fed --- /dev/null +++ b/patches/release-please+17.3.0.patch @@ -0,0 +1,71 @@ +--- a/build/src/manifest.js ++++ b/build/src/manifest.js +@@ -261,9 +261,19 @@ + this.logger.info('Collecting commits since all latest releases'); + const commits = []; + this.logger.debug(`commit search depth: ${this.commitSearchDepth}`); ++ // The root receives every commit without path splitting. Node's strategy, ++ // default changelog and extra-file updaters do not inspect commit.files. ++ // Keep the general path for exclusions, plugins and other strategies. ++ const rootConfig = this.repositoryConfig[exports.ROOT_PROJECT_PATH]; ++ const rootOnlyNode = Object.keys(this.repositoryConfig).length === 1 && ++ rootConfig?.releaseType === 'node' && ++ (!rootConfig.changelogType || rootConfig.changelogType === 'default') && ++ (!rootConfig.versioning || ['default', 'always-bump-minor'].includes(rootConfig.versioning)) && ++ !rootConfig.excludePaths?.length && ++ this.plugins.length === 0; + const commitGenerator = this.github.mergeCommitIterator(this.targetBranch, { + maxResults: this.commitSearchDepth, +- backfillFiles: true, ++ backfillFiles: !rootOnlyNode, + batchSize: this.commitBatchSize, + }); + const releaseShas = new Set(Object.values(releaseShasByPath)); +@@ -272,6 +282,7 @@ + // sha => release pull request + const releasePullRequestsBySha = {}; + let releaseCommitsFound = 0; ++ let historyBoundaryFound = false; + for await (const commit of commitGenerator) { + if (releaseShas.has(commit.sha)) { + if (commit.pullRequest) { +@@ -284,14 +295,17 @@ + } + if (this.lastReleaseSha && this.lastReleaseSha === commit.sha) { + this.logger.info(`Using configured lastReleaseSha ${this.lastReleaseSha} as last commit.`); ++ historyBoundaryFound = true; + break; + } + else if (needsBootstrap && commit.sha === this.bootstrapSha) { + this.logger.info(`Needed bootstrapping, found configured bootstrapSha ${this.bootstrapSha}`); ++ historyBoundaryFound = true; + break; + } + else if (!needsBootstrap && releaseCommitsFound >= expectedShas) { + // found enough commits ++ historyBoundaryFound = true; + break; + } + commits.push({ +@@ -301,16 +315,18 @@ + pullRequest: commit.pullRequest, + }); + } ++ if (releaseCommitsFound < expectedShas && !historyBoundaryFound) { ++ throw new Error(`Incomplete release history: expected ${expectedShas} release boundaries, found ${releaseCommitsFound}. Refusing a partial release proposal.`); ++ } + if (releaseCommitsFound < expectedShas) { + this.logger.warn(`Expected ${expectedShas} commits, only found ${releaseCommitsFound}`); + } + // split commits by path + this.logger.info(`Splitting ${commits.length} commits by path`); +- const cs = new commit_split_1.CommitSplit({ ++ const splitCommits = rootOnlyNode ? {} : new commit_split_1.CommitSplit({ + includeEmpty: true, + packagePaths: Object.keys(this.repositoryConfig), +- }); +- const splitCommits = cs.split(commits); ++ }).split(commits); + // limit paths to ones since the last release + let commitsPerPath = {}; + for (const path in this.repositoryConfig) { diff --git a/scripts/ci/plan.mjs b/scripts/ci/plan.mjs index fb9260bde4..7865e1157f 100644 --- a/scripts/ci/plan.mjs +++ b/scripts/ci/plan.mjs @@ -28,7 +28,6 @@ const FIXTURE_SOURCES = [ "tests/conformance/swift/**", "tests/conformance/versions/**", ]; -const READINESS_SOURCES = ["tests/conformance/readiness/**"]; // These reporters affect CI output only; they do not select a host or provider // verification lane when changed on their own. const PORTABLE_CI_REPORTERS = new Set([ @@ -281,7 +280,6 @@ if (tier === "reused") { selected.fixtures = true; selected.readiness = true; } - if (matchesAny(file, READINESS_SOURCES)) selected.readiness = true; if (file.startsWith("tests/")) continue; if ( /^(src\/process\/|src\/filesystem\/|src\/contracts\/tool|src\/server\/createServer|src\/cli\.ts|scripts\/ci\/)/.test( diff --git a/scripts/ci/scopes.mjs b/scripts/ci/scopes.mjs index f29cda9704..367a317901 100644 --- a/scripts/ci/scopes.mjs +++ b/scripts/ci/scopes.mjs @@ -2,6 +2,10 @@ // Shared build inputs are classified once in plan.mjs rather than repeated in // every workflow. export const SCOPES = { + readiness: [ + "tests/conformance/readiness/**", + "scripts/verify-reconstruction-readiness.mjs", + ], reverse_tools: [ ".github/workflows/mutation.yml", "src/application/reverse/**", @@ -141,6 +145,7 @@ export const SCOPES = { "scripts/prepack.mjs", "scripts/prepare.mjs", "scripts/rea.mjs", + "scripts/lib/cli-process-environment.mjs", "install.sh", "src/config/**", "src/application/Setup*.ts", @@ -373,6 +378,7 @@ export const SCOPES = { "src/contracts/toolContracts.ts", "src/contracts/toolEffects.ts", "scripts/rea.mjs", + "scripts/lib/cli-process-environment.mjs", "src/process/**", "src/artifacts/SafeOutputTree.ts", "scripts/verify/javascript/recovery.mjs", diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs index 85fa303e2e..52f3177125 100644 --- a/scripts/lib/catalog-builders.mjs +++ b/scripts/lib/catalog-builders.mjs @@ -260,6 +260,13 @@ export const providerCatalog = (sources) => { ({ name }) => name === "inspect_pe_certificates", ), }, + { + identity: sources.artifactProviders.PE_IMPORTS_PROVIDER, + contracts: + sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( + ({ name }) => name === "inspect_pe_imports", + ), + }, { identity: sources.artifactProviders.ELF_NOTES_PROVIDER, contracts: diff --git a/scripts/lib/cli-process-environment.mjs b/scripts/lib/cli-process-environment.mjs new file mode 100644 index 0000000000..8876318c24 --- /dev/null +++ b/scripts/lib/cli-process-environment.mjs @@ -0,0 +1,26 @@ +import { join } from "node:path"; + +/** Prepare the selected process environment before importing CLI or MCP dependencies. */ +export const prepareCliProcessEnvironment = ( + environment, + platform, + packageVersion, +) => { + environment.REA_PACKAGE_VERSION = packageVersion; + // Windows home lookup throws for an empty USERPROFILE. Dependencies resolve + // home during import, before REA's own resolver can apply its account fallback. + if (platform === "win32" && environment.USERPROFILE === "") { + delete environment.USERPROFILE; + } + + // Playwright's registry assumes a desktop host while choosing its cache. + // Termux needs an explicit path before the registry initializes. + if ( + platform === "android" && + environment.PLAYWRIGHT_BROWSERS_PATH === undefined + ) { + const cacheRoot = + environment.XDG_CACHE_HOME ?? join(environment.HOME ?? ".", ".cache"); + environment.PLAYWRIGHT_BROWSERS_PATH = join(cacheRoot, "ms-playwright"); + } +}; diff --git a/scripts/lib/real-hopper-assertions.d.mts b/scripts/lib/real-hopper-assertions.d.mts new file mode 100644 index 0000000000..b0e3afb179 --- /dev/null +++ b/scripts/lib/real-hopper-assertions.d.mts @@ -0,0 +1,35 @@ +/** Extract one real procedure address from the complete inline inventory. */ +export function firstProcedureAddress(input: unknown): string; + +/** Reject empty and success-shaped embedded decompilation failures. */ +export function requirePseudocode(input: unknown, operation: string): string; + +/** Validate a complete Hopper function dossier semantically. */ +export function requireFunctionDossier( + input: unknown, + expectedAddress: string, +): Record; + +/** Positive semantic facts required from one source-owned Hopper fixture. */ +export interface FunctionDossierOracle { + readonly procedure_address: string; + readonly callee_address?: string; + readonly caller_address?: string; + readonly referenced_string?: string; + readonly referenced_name?: string; + readonly comment?: string; + readonly require_cfg_successor?: boolean; + readonly require_assembly?: boolean; +} + +/** Reject a structurally valid dossier that omits fixture-proven semantics. */ +export function requireFunctionDossierOracle( + input: unknown, + oracle: FunctionDossierOracle, +): Record; + +/** Require every relationship to be an actual hexadecimal address. */ +export function requireAddressArray( + input: unknown, + operation: string, +): string[]; diff --git a/src/application/RealHopperAssertions.ts b/scripts/lib/real-hopper-assertions.mjs similarity index 79% rename from src/application/RealHopperAssertions.ts rename to scripts/lib/real-hopper-assertions.mjs index 5f2be5a7fc..db49b6abde 100644 --- a/src/application/RealHopperAssertions.ts +++ b/scripts/lib/real-hopper-assertions.mjs @@ -3,7 +3,7 @@ import { z } from "zod"; const addressSchema = z.string().regex(/^0x[0-9a-f]+$/iu); const identitySchema = z.object({ address: addressSchema, name: z.string() }); /** Extract one real procedure address from the complete inline inventory. */ -export const firstProcedureAddress = (input: unknown): string => { +export const firstProcedureAddress = (input) => { const first = z .array(z.object({ address: addressSchema })) .min(1) @@ -13,10 +13,7 @@ export const firstProcedureAddress = (input: unknown): string => { }; /** Reject empty and success-shaped embedded decompilation failures. */ -export const requirePseudocode = ( - input: unknown, - operation: string, -): string => { +export const requirePseudocode = (input, operation) => { if (typeof input !== "string" || input.trim().length === 0) throw new TypeError(`${operation} returned empty pseudocode`); if (input === "No output" || input.startsWith("Error:")) @@ -25,10 +22,7 @@ export const requirePseudocode = ( }; /** Validate a complete Hopper function dossier semantically. */ -export const requireFunctionDossier = ( - input: unknown, - expectedAddress: string, -): Record => { +export const requireFunctionDossier = (input, expectedAddress) => { const value = objectValue( input, "analyze_function returned no function dossier", @@ -40,29 +34,14 @@ export const requireFunctionDossier = ( throw new TypeError("analyze_function omitted the procedure name"); requirePseudocode(value.pseudocode, "analyze_function"); for (const field of COLLECTION_FIELDS) list(value, field); - for (const field of ["callers", "callees"] as const) + for (const field of ["callers", "callees"]) z.array(identitySchema).parse(list(value, field)); validateBlocks(list(value, "basic_blocks")); return value; }; -/** Positive semantic facts required from one source-owned Hopper fixture. */ -export interface FunctionDossierOracle { - readonly procedure_address: string; - readonly callee_address?: string; - readonly caller_address?: string; - readonly referenced_string?: string; - readonly referenced_name?: string; - readonly comment?: string; - readonly require_cfg_successor?: boolean; - readonly require_assembly?: boolean; -} - /** Reject a structurally valid dossier that omits fixture-proven semantics. */ -export const requireFunctionDossierOracle = ( - input: unknown, - oracle: FunctionDossierOracle, -): Record => { +export const requireFunctionDossierOracle = (input, oracle) => { const value = requireFunctionDossier(input, oracle.procedure_address); if (oracle.callee_address !== undefined) { const callees = z.array(identitySchema).parse(list(value, "callees")); @@ -158,12 +137,7 @@ const referenceEdgeSchema = z.object({ target_address: addressSchema, }); -const requireReferencedValue = ( - dossier: Record, - field: "referenced_strings" | "referenced_names", - expected: string, - normalize: (value: string) => string, -): void => { +const requireReferencedValue = (dossier, field, expected, normalize) => { const values = z.array(referencedValueSchema).parse(list(dossier, field)); const references = z .array(referenceEdgeSchema) @@ -183,10 +157,7 @@ const requireReferencedValue = ( }; /** Require every relationship to be an actual hexadecimal address. */ -export const requireAddressArray = ( - input: unknown, - operation: string, -): string[] => { +export const requireAddressArray = (input, operation) => { const parsed = z.array(addressSchema).parse(input); if (parsed.some((address) => address.length === 0)) throw new TypeError(`${operation} returned an invalid address list`); @@ -203,21 +174,17 @@ const COLLECTION_FIELDS = [ "referenced_strings", "referenced_names", "basic_blocks", -] as const; +]; -const list = (value: Record, field: string): unknown[] => - z.array(z.unknown()).parse(value[field]); +const list = (value, field) => z.array(z.unknown()).parse(value[field]); -const validateBlocks = (items: readonly unknown[]): void => { +const validateBlocks = (items) => { for (const block of items) if (!Array.isArray(objectValue(block, "Invalid basic block").successors)) throw new TypeError("analyze_function omitted CFG successor evidence"); }; -const objectValue = ( - input: unknown, - message: string, -): Record => { +const objectValue = (input, message) => { const parsed = z.record(z.string(), z.unknown()).safeParse(input); if (!parsed.success) throw new TypeError(message); return parsed.data; diff --git a/scripts/lib/real-hopper-semantic.mjs b/scripts/lib/real-hopper-semantic.mjs index 1f520a690f..d171a48da6 100644 --- a/scripts/lib/real-hopper-semantic.mjs +++ b/scripts/lib/real-hopper-semantic.mjs @@ -1,17 +1,6 @@ import assert from "node:assert/strict"; -/** - * `requireFunctionDossierOracle` is only used inside `verifyRealHopperFixture`, - * which runs against the real Hopper bridge after `build:cached`. Loading it - * lazily keeps this module importable from `test:fast` (which docs promise to - * run "without build" — see docs/testing.md § test scripts) so that mock-only - * callers such as `requireCurrentDocument` and `resolveFixtureProcedure` do - * not require `dist/`. - */ -const loadFunctionDossierOracle = () => - import("../../dist/application/RealHopperAssertions.js").then( - (module) => module.requireFunctionDossierOracle, - ); +import { requireFunctionDossierOracle } from "./real-hopper-assertions.mjs"; /** Require truthful tri-state permissions from real Hopper memory regions. */ export const requireTruthfulMemoryRegions = (segments) => { @@ -99,7 +88,6 @@ export async function verifyRealHopperFixture({ oracle, normalizedResult, }) { - const requireFunctionDossierOracle = await loadFunctionDossierOracle(); const main = await resolveFixtureProcedure( client, options, diff --git a/scripts/lib/release-notes.mjs b/scripts/lib/release-notes.mjs index d6c640c15b..4223b6ad83 100644 --- a/scripts/lib/release-notes.mjs +++ b/scripts/lib/release-notes.mjs @@ -53,7 +53,7 @@ export const releaseInventorySchema = z.object({ pullRequests: z.array(pullRequest), }); -export async function readReleaseHistory(run, base, target) { +export async function requireCompleteReleaseHistory(run) { if ( (await run("git", ["rev-parse", "--is-shallow-repository"])).trim() !== "false" @@ -61,6 +61,10 @@ export async function readReleaseHistory(run, base, target) { throw new Error( "Release inventory requires complete Git history; fetch --unshallow first", ); +} + +export async function readReleaseHistory(run, base, target) { + await requireCompleteReleaseHistory(run); const resolve = async (ref) => sha.parse( ( diff --git a/scripts/lib/release-proposal.d.mts b/scripts/lib/release-proposal.d.mts new file mode 100644 index 0000000000..bd92aaba19 --- /dev/null +++ b/scripts/lib/release-proposal.d.mts @@ -0,0 +1,6 @@ +export function applyReleasePleasePatch(directory?: string): Promise; + +export function releaseProposalSearchDepth( + run: (command: string, args: string[]) => Promise, + sourceSha: string, +): Promise; diff --git a/scripts/lib/release-proposal.mjs b/scripts/lib/release-proposal.mjs new file mode 100644 index 0000000000..1db7cf973c --- /dev/null +++ b/scripts/lib/release-proposal.mjs @@ -0,0 +1,61 @@ +import { execFile } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { dirname, relative } from "node:path"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; +import { requireCompleteReleaseHistory } from "./release-notes.mjs"; + +const execute = promisify(execFile); +const require = createRequire(import.meta.url); +const project = fileURLToPath(new URL("../../", import.meta.url)); +const patch = fileURLToPath( + new URL("../../patches/release-please+17.3.0.patch", import.meta.url), +); + +export async function applyReleasePleasePatch( + directory = dirname(require.resolve("release-please/package.json")), +) { + const metadata = JSON.parse( + await readFile(`${directory}/package.json`, "utf8"), + ); + if (metadata.version !== "17.3.0") { + throw new Error( + "The release proposal patch requires release-please 17.3.0", + ); + } + const args = ["--directory", relative(project, directory), patch]; + const check = async (reverse) => { + try { + await execute( + "git", + ["apply", "--check", ...(reverse ? ["--reverse"] : []), ...args], + { cwd: project }, + ); + return true; + } catch (error) { + if (error.code === 1) return false; + throw error; + } + }; + if (await check(false)) { + await execute("git", ["apply", ...args], { cwd: project }); + } else if (!(await check(true))) { + throw new Error( + "The release proposal dependency patch does not match the installed source", + ); + } +} + +export async function releaseProposalSearchDepth(run, sourceSha) { + if (!/^[a-f0-9]{40}$/u.test(sourceSha ?? "")) { + throw new Error("Expected an exact release proposal source SHA"); + } + await requireCompleteReleaseHistory(run); + const text = (await run("git", ["rev-list", "--count", sourceSha])).trim(); + const count = Number(text); + if (!/^[1-9][0-9]*$/u.test(text) || !Number.isSafeInteger(count)) { + throw new Error("Git returned an invalid release history size"); + } + return count; +} diff --git a/scripts/prepack.mjs b/scripts/prepack.mjs index 4279aab88f..f3c8061927 100644 --- a/scripts/prepack.mjs +++ b/scripts/prepack.mjs @@ -45,6 +45,7 @@ if (await pathExists(join(root, "src"))) { run(process.execPath, ["scripts/generate-skill-metadata.mjs", "--check"]); } else { for (const path of [ + "scripts/lib/cli-process-environment.mjs", "dist/main.js", "dist/mcp/main.js", "dist/cli-runtime/cli.js", diff --git a/scripts/rea.mjs b/scripts/rea.mjs index fa956d3293..27f334994a 100755 --- a/scripts/rea.mjs +++ b/scripts/rea.mjs @@ -1,9 +1,11 @@ #!/usr/bin/env node import { access } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { prepareCliProcessEnvironment } from "./lib/cli-process-environment.mjs"; + // Route production MCP before importing Incur. Incur owns registration helpers // such as `mcp add`, while the MCP runtime serves the tool catalog over stdio // or, with `--http`, over streamable HTTP. @@ -11,7 +13,11 @@ const args = process.argv.slice(2); const { default: packageJson } = await import("../package.json", { with: { type: "json" }, }); -process.env.REA_PACKAGE_VERSION = packageJson.version; +prepareCliProcessEnvironment( + process.env, + process.platform, + packageJson.version, +); const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const builtinArgumentRuntime = "dist/cli-runtime/cliBuiltinArguments.js"; const builtinArguments = (await compiledRuntimeExists([builtinArgumentRuntime])) @@ -36,26 +42,6 @@ const isMcpHttpMode = const SIGPIPE_EXIT_STATUS = 141; // Set once the reader of CLI output has closed the pipe. let stdoutClosed = false; -// Windows home lookup throws for an empty USERPROFILE. Dependencies resolve -// home during import, before REA's own resolver can apply its account fallback. -if (process.platform === "win32" && process.env.USERPROFILE === "") { - delete process.env.USERPROFILE; -} - -// Playwright's registry assumes one of its three desktop host platforms when -// it chooses the browser cache directory. Termux reports `android`, although -// its Node runtime and filesystem are POSIX-compatible. Keep the cache path -// explicit so REA can start on Termux. Browser launch still requires a -// caller-supplied Android-compatible Chromium executable. -if ( - process.platform === "android" && - process.env.PLAYWRIGHT_BROWSERS_PATH === undefined -) { - const cacheRoot = - process.env.XDG_CACHE_HOME ?? join(process.env.HOME ?? ".", ".cache"); - process.env.PLAYWRIGHT_BROWSERS_PATH = join(cacheRoot, "ms-playwright"); -} - const runtimeFiles = [ builtinArgumentRuntime, ...(isMcpMode || isMcpHttpMode diff --git a/scripts/release-npm-tag.mjs b/scripts/release-npm-tag.mjs index ad85b9798c..b6f9e1ac7c 100644 --- a/scripts/release-npm-tag.mjs +++ b/scripts/release-npm-tag.mjs @@ -1,15 +1,7 @@ import semver from "semver"; -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; export function releaseNpmTag(version) { if (version === undefined || semver.valid(version) !== version) throw new Error("Expected an exact SemVer version."); return semver.prerelease(version) === null ? "latest" : "next"; } - -if ( - process.argv[1] !== undefined && - resolve(process.argv[1]) === fileURLToPath(import.meta.url) -) - process.stdout.write(releaseNpmTag(process.argv[2])); diff --git a/scripts/release-proposal.mjs b/scripts/release-proposal.mjs new file mode 100644 index 0000000000..3f85424ef3 --- /dev/null +++ b/scripts/release-proposal.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +import { execFile } from "node:child_process"; +import { appendFile } from "node:fs/promises"; +import { parseArgs, promisify } from "node:util"; +import { + applyReleasePleasePatch, + releaseProposalSearchDepth, +} from "./lib/release-proposal.mjs"; + +const execute = promisify(execFile); + +async function main() { + const { values } = parseArgs({ + options: { + "target-branch": { type: "string" }, + "source-sha": { type: "string" }, + }, + }); + const repository = process.env.GITHUB_REPOSITORY; + const token = process.env.GH_TOKEN; + if ( + !repository || + !/^[\w.-]+\/[\w.-]+$/u.test(repository) || + !token || + !values["target-branch"] + ) { + throw new Error("Expected GITHUB_REPOSITORY, GH_TOKEN and --target-branch"); + } + const depth = await releaseProposalSearchDepth( + async (command, args) => + (await execute(command, args, { encoding: "utf8" })).stdout, + values["source-sha"], + ); + // Patch before import: only proposal generation uses this maintained upstream fix. + // Publication continues through the pinned release-please action. + await applyReleasePleasePatch(); + const { GitHub, Manifest } = await import("release-please"); + const [owner, repo] = repository.split("/"); + const branch = values["target-branch"]; + const github = await GitHub.create({ + owner, + repo, + token, + defaultBranch: branch, + }); + const manifest = await Manifest.fromManifest( + github, + branch, + undefined, + undefined, + { + commitSearchDepth: depth, + }, + ); + const prs = (await manifest.createPullRequests()).filter(Boolean); + if (process.env.GITHUB_OUTPUT) { + await appendFile( + process.env.GITHUB_OUTPUT, + `prs_created=${prs.length > 0}\n${prs.length ? `pr=${JSON.stringify(prs[0])}\n` : ""}`, + ); + } +} + +await main().catch((error) => { + process.stderr.write( + `Release proposal failed: ${error instanceof Error ? error.message : String(error)}\n`, + ); + process.exitCode = 1; +}); diff --git a/scripts/verify-agent-experience.mjs b/scripts/verify-agent-experience.mjs index fbb5b47ccb..ffac2db067 100644 --- a/scripts/verify-agent-experience.mjs +++ b/scripts/verify-agent-experience.mjs @@ -11,26 +11,44 @@ import { import { homedir } from "node:os"; import { delimiter, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { tsImport } from "tsx/esm/api"; -import { evaluateCodexEvents } from "../dist/evaluation/CodexAgentEval.js"; -import { - MISSING_TARGET_SCENARIO, - nativeProviderScenario, -} from "../dist/evaluation/AgentEvaluationScenarios.js"; -import { - createAgentEvaluationFixtures, - agentFixtureClaims, - agentFixtureAnswerInstructions, -} from "../dist/evaluation/AgentEvaluationFixtures.js"; -import { - agentEvaluationPassed, - summarizeFactualCorrectness, -} from "../dist/evaluation/AgentEvaluationReport.js"; import { PRODUCT_IDENTITY } from "../dist/identity.js"; import { createAgentEvaluationCommands } from "./lib/agent-evaluation-commands.mjs"; import { closeVerifierResources } from "./lib/verifier-cleanup.mjs"; import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs"; +const { evaluateCodexEvents } = await tsImport( + new URL("../tests/evaluation/model-evals/CodexAgentEval.ts", import.meta.url) + .href, + import.meta.url, +); +const { MISSING_TARGET_SCENARIO, nativeProviderScenario } = await tsImport( + new URL( + "../tests/evaluation/model-evals/AgentEvaluationScenarios.ts", + import.meta.url, + ).href, + import.meta.url, +); +const { + createAgentEvaluationFixtures, + agentFixtureClaims, + agentFixtureAnswerInstructions, +} = await tsImport( + new URL( + "../tests/evaluation/model-evals/AgentEvaluationFixtures.ts", + import.meta.url, + ).href, + import.meta.url, +); +const { agentEvaluationPassed, summarizeFactualCorrectness } = await tsImport( + new URL( + "../tests/evaluation/model-evals/AgentEvaluationReport.ts", + import.meta.url, + ).href, + import.meta.url, +); + const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const verifierRun = createVerifierRun(); const timeoutMs = Number(process.env.REA_AGENT_EVAL_TIMEOUT_MS ?? 480_000); diff --git a/scripts/verify-package-pack.mjs b/scripts/verify-package-pack.mjs index 9111f67887..993061f657 100644 --- a/scripts/verify-package-pack.mjs +++ b/scripts/verify-package-pack.mjs @@ -37,6 +37,8 @@ export async function verifyPackagePack({ root, workspace }) { throw new Error("package did not expose both rea command entry points"); if (packedManifest.mcpName !== "io.github.morluto/rea") throw new Error("package did not retain its MCP Registry ownership marker"); + if (!packedFiles.includes("package/scripts/lib/cli-process-environment.mjs")) + throw new Error("package omitted the CLI process environment owner"); if (packedFiles.some((path) => path.startsWith("package/.agents/"))) throw new Error( "package included authored skill sources instead of only the generated bundle", diff --git a/scripts/verify-real-hopper-deadlines.mjs b/scripts/verify-real-hopper-deadlines.mjs index 31b2d856fc..af2213ec6f 100644 --- a/scripts/verify-real-hopper-deadlines.mjs +++ b/scripts/verify-real-hopper-deadlines.mjs @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { firstProcedureAddress } from "../dist/application/RealHopperAssertions.js"; +import { firstProcedureAddress } from "./lib/real-hopper-assertions.mjs"; import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js"; import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js"; diff --git a/scripts/verify-real-hopper.mjs b/scripts/verify-real-hopper.mjs index 17ddde21c8..5f31565b4a 100644 --- a/scripts/verify-real-hopper.mjs +++ b/scripts/verify-real-hopper.mjs @@ -10,7 +10,7 @@ import { requireAddressArray, requireFunctionDossier, requirePseudocode, -} from "../dist/application/RealHopperAssertions.js"; +} from "./lib/real-hopper-assertions.mjs"; import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js"; import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js"; import { REA_WORKFLOW_PROVIDER } from "../dist/domain/investigationProviders.js"; diff --git a/scripts/verify-reconstruction-readiness.mjs b/scripts/verify-reconstruction-readiness.mjs index afcb27812e..fb8c03af45 100644 --- a/scripts/verify-reconstruction-readiness.mjs +++ b/scripts/verify-reconstruction-readiness.mjs @@ -6,14 +6,32 @@ import { promisify } from "node:util"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { tsImport } from "tsx/esm/api"; import { toolContract } from "../dist/contracts/toolContracts.js"; -import { RECONSTRUCTION_READINESS_EXAMPLE } from "../dist/contracts/reconstructionReadinessExample.js"; import { parseEvidence } from "../dist/domain/evidence.js"; import { createEvidenceBundle } from "../dist/domain/evidenceBundle.js"; -import { createReconstructionReadinessReport } from "../dist/domain/reconstructionReadiness.js"; -import { reconstructionReadinessReportSchema } from "../dist/domain/reconstructionReadinessSchemas.js"; -import { PACKAGE_METADATA } from "../dist/generatedPackageMetadata.js"; +import { closeVerifierResources } from "./lib/verifier-cleanup.mjs"; + +const [ + { RECONSTRUCTION_READINESS_EXAMPLE }, + { createReconstructionReadinessReport }, + { reconstructionReadinessReportSchema }, +] = await Promise.all( + [ + "reconstructionReadinessExample", + "reconstructionReadiness", + "reconstructionReadinessSchemas", + ].map((name) => + tsImport( + new URL( + `../tests/conformance/readiness/report/${name}.ts`, + import.meta.url, + ).href, + import.meta.url, + ), + ), +); const execute = promisify(execFile); const root = process.cwd(); @@ -50,6 +68,8 @@ const client = new Client({ capabilities: { elicitation: {} }, }); +let verificationReport; +let primaryError; try { await client.connect(transport); const catalog = await client.listTools(); @@ -61,6 +81,7 @@ try { { name: "binary_session", arguments: {} }, { toolDefinition: sessionTool }, ); + assert.notEqual(sessionReply.isError, true); const sessionStatus = toolContract("binary_session").outputSchema.parse( sessionReply.structuredContent, ).result; @@ -77,76 +98,93 @@ try { "CLI/MCP Electron analysis Evidence identity drifted", ); - const input = structuredClone(RECONSTRUCTION_READINESS_EXAMPLE); - input.identity.cli_version = PACKAGE_METADATA.version; - input.identity.server_version = PACKAGE_METADATA.version; - input.identity.catalog_digest = - sessionStatus.server_identity.catalog.digests.combined_sha256; - const records = [ - ...input.evidence_bundle.records, - nativeEvidence, - javascriptCliEvidence, - electronCliEvidence, - ].map(parseEvidence); - input.evidence_bundle = createEvidenceBundle(records); - setFixtureDigest(input, "native", nativeEvidence); - setFixtureDigest(input, "javascript-cli", javascriptCliEvidence); - setFixtureDigest(input, "electron", electronCliEvidence); - linkCheck(input, "discover-classify", "artifact-inventory", nativeEvidence); - linkCheck(input, "static-analysis", "native-routed", nativeEvidence); - linkCheck( - input, - "static-analysis", - "javascript-routed", - javascriptCliEvidence, - ); - linkCheck( - input, - "reactive-scenarios", - "electron-correlated", - electronCliEvidence, - ); - - const readinessReport = reconstructionReadinessReportSchema.parse( - createReconstructionReadinessReport(input), - ); - assert.equal(readinessReport.status, "pass"); - if (process.env.REA_READINESS_REPORT_PATH !== undefined) - await writeFile( - resolve(root, process.env.REA_READINESS_REPORT_PATH), - `${JSON.stringify(readinessReport, null, 2)}\n`, - ); - - const tampered = structuredClone(input); - tampered.replay.expected_source_digest = readinessReport.source_digest; - tampered.identity.server_version = "tampered"; - const tamperedReport = reconstructionReadinessReportSchema.parse( - createReconstructionReadinessReport(tampered), - ); - assert.equal(tamperedReport.status, "fail"); - assert.ok( - tamperedReport.findings.some( - ({ code }) => code === "replay-source-digest-mismatch", - ), - ); - - process.stdout.write( - `${JSON.stringify({ - status: readinessReport.status, - report_id: readinessReport.report_id, - report_digest: readinessReport.report_digest, - stages: readinessReport.summary.stages, - findings: readinessReport.summary.findings, - native_evidence_id: nativeEvidence.evidence_id, - javascript_evidence_id: javascriptCliEvidence.evidence_id, - electron_evidence_id: electronCliEvidence.evidence_id, - provider: "local-stdio", - })}\n`, - ); + verificationReport = { + schema: "ReconstructionReadinessVerificationReport", + schema_version: 1, + status: "pass", + observed_journeys: { + evidence_kind: "observed", + scope: "static-cli-mcp", + status: "pass", + server_identity: sessionStatus.server_identity, + evidence_bundles: { + cli: createEvidenceBundle([ + nativeEvidence, + javascriptCliEvidence, + electronCliEvidence, + ]), + mcp: createEvidenceBundle([javascriptMcp, electronMcp]), + }, + journeys: [ + { + journey_id: "native-artifact-cli", + operation: "inspect-artifact", + surfaces: ["cli"], + status: "pass", + evidence_ids: [nativeEvidence.evidence_id], + checks: ["cli-completed", "evidence-authenticated"], + }, + ...[ + ["javascript-static-cli-mcp", javascriptCliEvidence], + ["electron-static-cli-mcp", electronCliEvidence], + ].map(([journeyId, evidence]) => ({ + journey_id: journeyId, + operation: "analyze-javascript-application", + surfaces: ["cli", "mcp"], + status: "pass", + evidence_ids: [evidence.evidence_id], + checks: [ + "cli-completed", + "mcp-completed", + "evidence-authenticated", + "evidence-identity-parity", + ], + })), + ], + limitations: [ + "Native MCP parity was not exercised.", + "Fixture application runtime behavior was not executed.", + "Authority recovery, cancellation cleanup, behavioral comparison, and reconstruction closure were not exercised.", + ], + }, + evaluator_conformance: verifySyntheticEvaluator(), + }; +} catch (cause) { + primaryError = cause; + throw cause; } finally { - await client.close(); + await closeVerifierResources( + [() => client.close(), () => transport.close()], + primaryError, + ); } +if (process.env.REA_READINESS_REPORT_PATH !== undefined) + await writeFile( + resolve(root, process.env.REA_READINESS_REPORT_PATH), + `${JSON.stringify(verificationReport, null, 2)}\n`, + ); +process.stdout.write( + `${JSON.stringify({ + schema: verificationReport.schema, + schema_version: verificationReport.schema_version, + status: verificationReport.status, + observed_journeys: { + status: verificationReport.observed_journeys.status, + scope: verificationReport.observed_journeys.scope, + journeys: verificationReport.observed_journeys.journeys, + limitations: verificationReport.observed_journeys.limitations, + }, + evaluator_conformance: { + status: verificationReport.evaluator_conformance.status, + evidence_kind: verificationReport.evaluator_conformance.evidence_kind, + checks: verificationReport.evaluator_conformance.checks, + report_digest: + verificationReport.evaluator_conformance.baseline_report.report_digest, + }, + })}\n`, +); + async function runCli(arguments_) { const { stdout } = await execute( process.execPath, @@ -166,32 +204,50 @@ async function analyzeMcp(client_, inputPath) { arguments: { input_path: inputPath }, }); assert.notEqual(response.isError, true); - const projected = requireObject(response.structuredContent); - return { evidence_id: projected.evidence_id }; -} - -function requireObject(value) { - if (typeof value !== "object" || value === null || Array.isArray(value)) - throw new TypeError("Expected an object"); - return value; -} - -function setFixtureDigest(input, kind, evidence) { - const fixture = input.fixtures.find( - ({ kind: fixtureKind }) => fixtureKind === kind, + toolContract("analyze_javascript_application").outputSchema.parse( + response.structuredContent, ); - if (fixture === undefined || evidence.subject === null) - throw new TypeError(`Missing ${kind} fixture identity`); - fixture.artifact_sha256 = evidence.subject.digest.sha256; + return parseEvidence(response.structuredContent); } -function linkCheck(input, stageId, checkId, evidence) { - const stage = input.stages.find(({ stage_id: id }) => id === stageId); - const check = stage?.checks.find(({ check_id: id }) => id === checkId); - if (stage === undefined || check === undefined) - throw new TypeError(`Missing readiness check ${stageId}/${checkId}`); - stage.evidence_ids = [ - ...new Set([...stage.evidence_ids, evidence.evidence_id]), - ].sort(); - check.evidence_ids = [evidence.evidence_id]; +function verifySyntheticEvaluator() { + const input = structuredClone(RECONSTRUCTION_READINESS_EXAMPLE); + const evaluate = (value) => + reconstructionReadinessReportSchema.parse( + createReconstructionReadinessReport(value), + ); + const baseline = evaluate(input); + assert.equal(baseline.status, "pass"); + assert.deepEqual(evaluate(structuredClone(input)), baseline); + + const tampered = structuredClone(input); + const record = tampered.evidence_bundle.records[0]; + assert.ok(record, "Synthetic conformance requires Evidence"); + record.operation += "_tampered"; + assert.throws( + () => evaluate(tampered), + /Evidence semantic identifier does not match its record/u, + ); + + const stale = structuredClone(input); + stale.replay.expected_source_digest = baseline.source_digest; + stale.identity.server_version += "-changed"; + const staleReport = evaluate(stale); + assert.equal(staleReport.status, "fail"); + assert.ok( + staleReport.findings.some( + ({ code }) => code === "replay-source-digest-mismatch", + ), + ); + return { + evidence_kind: "synthetic", + status: "pass", + checks: [ + "baseline-accepted", + "deterministic-evaluation", + "tampered-evidence-rejected", + "stale-source-rejected", + ].map((checkId) => ({ check_id: checkId, status: "pass" })), + baseline_report: baseline, + }; } diff --git a/scripts/verify/apple/dispatch.mjs b/scripts/verify/apple/dispatch.mjs index 14a34b2a6f..6f6dd4bf34 100644 --- a/scripts/verify/apple/dispatch.mjs +++ b/scripts/verify/apple/dispatch.mjs @@ -100,6 +100,12 @@ const checkObjc = (metadata, mode) => { const protocol = metadata.objc_protocols.find( ({ name }) => name === "ReaDispatchProtocol", ); + const protocolProperty = protocol?.properties.find( + ({ name }) => name === "fixtureName", + ); + assert.equal(protocolProperty?.name, "fixtureName"); + assert.equal(protocolProperty?.type_encoding, '@"NSString"'); + assert.equal(protocolProperty?.atomicity, "nonatomic"); assert.equal(protocol?.methods[0]?.is_required, true); assert.equal(protocol?.methods[0]?.is_optional, false); assert.equal(protocol?.optional_methods[0]?.is_required, false); @@ -161,6 +167,31 @@ const checkObjc = (metadata, mode) => { const checkSwift = (metadata, mode) => { assert.equal(fixupCoverage(metadata)?.status, "complete"); + for (const context of ["ReaNestedA", "ReaNestedB"]) { + const type = `${context}.Implementation`; + const protocol = `${context}.Requirement`; + assert.ok( + metadata.swift_conformances.some( + (item) => + item.type_name === type && + item.protocol_name === protocol && + item.module === "ReaWitnessFixture" && + item.decode.status === "decoded", + ), + `Nested Swift conformance ${type}: ${protocol} missing (${mode.mode})`, + ); + assert.ok( + metadata.swift_dispatch_slots.some( + (item) => + item.table_kind === "witness_table" && + item.owner === `${type}: ${protocol}` && + item.implementation_address !== null && + item.decode.status === "decoded", + ), + `Nested Swift witness ${type}: ${protocol} missing (${mode.mode})`, + ); + } + if ( !metadata.swift_dispatch_slots.some( (slot) => @@ -204,6 +235,10 @@ const EXPECTED_SWIFT_DECLS = [ ["struct", "ReaDeclFixtureOuter.Inner", false], ["struct", "ReaDeclFixtureGeneric", true], ["actor", "ReaDeclFixtureActor", false], + ["enum", "ReaNestedA", false], + ["struct", "ReaNestedA.Implementation", false], + ["enum", "ReaNestedB", false], + ["struct", "ReaNestedB.Implementation", false], ]; /** Stable declaration facts compared across the symbols and stripped builds. */ @@ -336,6 +371,26 @@ try { await checkSwiftSymbolAddresses(swiftPath, declarations); unstrippedDeclarations = declarations; } else assert.deepEqual(declarations, unstrippedDeclarations); + if (mode.mode === "chained" && variant === "stripped") { + const cli = await artifactCliEvidence( + "inspect-native-dispatch-metadata", + swiftPath, + ); + const cliResult = nativeDispatchMetadataResultSchema.parse( + cli.normalized_result, + ); + assert.equal(cliResult.target_sha256, target.sha256); + checkSwift(cliResult.result, mode); + await withArtifactMcp(swiftPath, async (client) => { + const mcpResult = nativeDispatchMetadataResultSchema.parse( + await artifactMcpResult(client, "inspect_native_dispatch_metadata"), + ); + assert.equal(mcpResult.target_sha256, target.sha256); + checkSwift(mcpResult.result, mode); + assert.deepEqual(mcpResult.result, cliResult.result); + }); + } + results.push({ variant: `swift-${mode.mode}-${variant}`, target_sha256: target.sha256, diff --git a/src/apktool/ApktoolWorkspace.lifecycle.test.ts b/src/apktool/ApktoolWorkspace.lifecycle.test.ts index b2c6dd50f8..69cbffa247 100644 --- a/src/apktool/ApktoolWorkspace.lifecycle.test.ts +++ b/src/apktool/ApktoolWorkspace.lifecycle.test.ts @@ -452,3 +452,48 @@ it("classifies source state changes during snapshot copying as artifact changes" await commands.close(); } }); + +it.each(["EACCES", "EPERM", "EIO", "ENOENT"] as const)( + "preserves the %s acquisition failure classification before snapshot admission", + async (code) => { + const root = await PrivateRuntimeRoot.create({ + prefix: "rea-apktool-admission-test-", + }); + const commands = new ApktoolCommandCleanup(); + const workspace = new ApktoolWorkspace(root, commands); + const input = join(root.path, "source.apk"); + await fs.writeFile(input, "authored-apk"); + const failure = Object.assign(new Error("authored source open failure"), { + code, + }); + vi.mocked(fs.open).mockImplementation(async (path, flags, mode) => { + if (String(path) === input) throw failure; + return await actual.open(path, flags, mode); + }); + try { + const result: unknown = await workspace + .snapshotInput(input, "decode_android_resources") + .catch((cause: unknown) => cause); + if (code === "EIO") expect(result).toBe(failure); + else + expect(result).toMatchObject({ + _tag: + code === "ENOENT" + ? "AnalysisInputError" + : "AnalysisAccessDeniedError", + operation: "decode_android_resources", + cause: failure, + ...(code === "ENOENT" ? {} : { path: input, systemCode: code }), + }); + expect(workspace.snapshot).toBeUndefined(); + await expect(fs.stat(join(root.path, "input.apk"))).rejects.toMatchObject( + { + code: "ENOENT", + }, + ); + } finally { + await workspace.close(); + await commands.close(); + } + }, +); diff --git a/src/apktool/ApktoolWorkspace.ts b/src/apktool/ApktoolWorkspace.ts index 186420d60f..111e8c08e8 100644 --- a/src/apktool/ApktoolWorkspace.ts +++ b/src/apktool/ApktoolWorkspace.ts @@ -10,16 +10,21 @@ import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js"; import { assertArtifactReadActive, + artifactSelectedPathErrno, mapArtifactFileFailure, withArtifactFileHandle, withArtifactRegularFile, } from "../artifacts/ArtifactFileOperation.js"; import { AnalysisInputError, + AnalysisAccessDeniedError, AnalysisArtifactChangedError, AnalysisProtocolError, } from "../domain/analysisErrorCore.js"; -import { sameRegularFileState } from "../filesystem/RegularFile.js"; +import { + NonRegularFileReadError, + sameRegularFileState, +} from "../filesystem/RegularFile.js"; import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js"; import type { ApktoolCommandCleanup } from "./ApktoolCommandCleanup.js"; @@ -132,13 +137,29 @@ export class ApktoolWorkspace { "follow", ); } catch (cause) { - throw mapArtifactFileFailure(cause, (physical) => - admitted || - (physical instanceof ArtifactReaderFailure && - physical.reason === "cancelled") - ? physical - : new AnalysisInputError(operation, { cause: physical }), - ); + throw mapArtifactFileFailure(cause, (physical) => { + if (admitted) return physical; + if ( + physical instanceof Error && + "code" in physical && + (physical.code === "EACCES" || physical.code === "EPERM") + ) + return new AnalysisAccessDeniedError(operation, path, physical.code, { + cause: physical, + }); + if ( + physical instanceof NonRegularFileReadError || + artifactSelectedPathErrno(physical) !== undefined + ) + return new AnalysisInputError(operation, { cause: physical }, [ + { + path: ["path"], + reason: "invalid_value", + message: "APK input must resolve to an accessible regular file", + }, + ]); + return physical; + }); } } diff --git a/src/application/BinaryTargetResolver.ts b/src/application/BinaryTargetResolver.ts index b907c3fdf8..7a79229eaa 100644 --- a/src/application/BinaryTargetResolver.ts +++ b/src/application/BinaryTargetResolver.ts @@ -75,11 +75,17 @@ import { type ExecutableMetadata, } from "../domain/binaryTarget.js"; import { + classifyFatContainerIdentity, readFatContainerHeader, readMachoHeaderPrefixFacts, type FatSliceHeaderEvidence, type MachoHeaderPrefixFacts, } from "../domain/apple/machoContainer.js"; +import { + isJvmClassFile, + MAX_JVM_CLASS_IDENTITY_BYTES, + JVM_CLASS_IDENTITY_LIMIT_REASON, +} from "../domain/apple/jvmClassFile.js"; import { MachoLoadCommandCursor, readMachoLoadCommandTableHeader, @@ -865,12 +871,32 @@ const readExecutableMetadata = async ( signal, }); if (thin !== null) return thin; + const hasJvmMagic = bytes.length >= 4 && bytes.readUInt32BE(0) === 0xcafebabe; + if ( + hasJvmMagic && + fileSize > bytes.length && + fileSize <= MAX_JVM_CLASS_IDENTITY_BYTES + ) { + const complete = await readHandleRange(handle, 0, fileSize, signal); + if (complete.byteLength === fileSize && isJvmClassFile(complete)) + return err("unsupported binary format"); + } const fatEvidence = await readFatSliceHeaderEvidence( handle, bytes, fileSize, signal, ); + if ( + hasJvmMagic && + fileSize > MAX_JVM_CLASS_IDENTITY_BYTES && + fatEvidence !== undefined && + classifyFatContainerIdentity( + fatEvidence.header, + (offset) => fatEvidence.sliceHeadersByOffset.get(offset)?.magic, + ).state === "malformed" + ) + return err(JVM_CLASS_IDENTITY_LIMIT_REASON); const parsed = parseExecutableHeader( bytes, hostArchitecture, diff --git a/src/application/BrowserObservationService.test.ts b/src/application/BrowserObservationService.test.ts index b6e33a0e23..d06c0c06ca 100644 --- a/src/application/BrowserObservationService.test.ts +++ b/src/application/BrowserObservationService.test.ts @@ -17,6 +17,7 @@ import { import { BrowserObservationError } from "../domain/browserObservationError.js"; import { err, ok } from "../domain/result.js"; import { passiveNetworkCapture } from "../../tests/fixtures/passiveNetworkCapture.js"; +import { analyzeWebBundleFixture } from "../../tests/support/browser/analyzeWebBundleFixture.js"; import { inspectWebPage } from "./BrowserObservationService.js"; import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js"; import { inspectWebPageInputSchema } from "../domain/browserObservation.js"; @@ -163,11 +164,9 @@ it("retains captured page Evidence when bundle analysis fails after capture", as }); it("retains completed bundle facts as bundle Evidence after cancellation", async () => { - const { analyzeCapturedWebBundle } = - await import("../domain/webBundleAnalyzer.js"); const { AnalysisCancelledError } = await import("../domain/analysisErrorCore.js"); - const bundle = analyzeCapturedWebBundle(passiveNetworkCapture()); + const bundle = analyzeWebBundleFixture(passiveNetworkCapture()); const reason = { requested: "stop after completed script" }; const failure = new AnalysisCancelledError("analyze_web_bundle", { cause: reason, diff --git a/src/application/CapabilityInventory.ts b/src/application/CapabilityInventory.ts index 31abe683e3..7c5e65eb1b 100644 --- a/src/application/CapabilityInventory.ts +++ b/src/application/CapabilityInventory.ts @@ -529,7 +529,7 @@ const workflowAvailabilityFor = ({ }; if (name === "inspect_pe_resources" || name === "inspect_native_exports") return { reason: "available", remediation: null }; - if (name === "inspect_pe_certificates") + if (name === "inspect_pe_certificates" || name === "inspect_pe_imports") return { reason: "available", remediation: null }; if (name === "inspect_elf_notes") return { reason: "available", remediation: null }; diff --git a/src/application/ClientRegistration.ts b/src/application/ClientRegistration.ts index 0d9e4d6841..ec38c6d7c1 100644 --- a/src/application/ClientRegistration.ts +++ b/src/application/ClientRegistration.ts @@ -21,22 +21,36 @@ const tomlTimeoutSchema = z.union([ z.bigint().positive(), ]); -/** Common launcher facts, without assigning a shared meaning to client policy. */ -const registrationSchema = z +const launcherSchema = z .object({ command: z.string().min(1), args: z.array(z.string()).default([]), - startup_timeout_sec: tomlTimeoutSchema.optional(), - tool_timeout_sec: tomlTimeoutSchema.optional(), - startupTimeoutMs: z.number().positive().optional(), - toolTimeoutMs: z.number().positive().optional(), - type: z.string().optional(), - transport: z.string().optional(), - serverName: z.string().optional(), - disabled: z.boolean().optional(), - enabled: z.boolean().optional(), }) .passthrough(); +const opencodeLauncherSchema = z + .object({ + type: z.literal("local"), + command: z.array(z.string()).min(1), + }) + .passthrough(); +const normalizeOpencodeLauncher = ( + entry: z.output, +): z.output => { + const [command = "", ...args] = entry.command; + return { ...entry, command, args }; +}; + +const registrationSchema = launcherSchema.extend({ + startup_timeout_sec: tomlTimeoutSchema.optional(), + tool_timeout_sec: tomlTimeoutSchema.optional(), + startupTimeoutMs: z.number().positive().optional(), + toolTimeoutMs: z.number().positive().optional(), + type: z.string().optional(), + transport: z.string().optional(), + serverName: z.string().optional(), + disabled: z.boolean().optional(), + enabled: z.boolean().optional(), +}); export type ClientRegistration = z.output; @@ -76,6 +90,22 @@ const registrationError = (error: z.ZodError): ConfigurationError => })), }); +/** Launcher identity remains usable when unrelated readiness settings are invalid. */ +export const parseClientRegistrationLauncher = ( + value: unknown, + client: SetupClient, +): Result<{ command: string; args: string[] }, ConfigurationError> => { + const parsed = + client.format === "opencode" + ? opencodeLauncherSchema + .transform(normalizeOpencodeLauncher) + .safeParse(value) + : launcherSchema.safeParse(value); + return parsed.success + ? ok({ command: parsed.data.command, args: parsed.data.args }) + : err(registrationError(parsed.error)); +}; + /** Policy belongs to the client dialect, and is validated before refresh writes. */ const validateClientPolicy = ( value: unknown, @@ -109,17 +139,14 @@ export const parseClientRegistration = ( : err(registrationError(parsed.error)); } if (client.format === "opencode") { - const entry = z - .object({ - type: z.literal("local"), - command: z.array(z.string()).min(1), + const entry = opencodeLauncherSchema + .extend({ environment: z.record(z.string(), z.string()).optional(), }) - .passthrough() + .transform(normalizeOpencodeLauncher) .safeParse(value); if (!entry.success) return err(registrationError(entry.error)); - const [command = "", ...args] = entry.data.command; - value = { ...entry.data, command, args }; + value = entry.data; } const parsed = registrationSchema.safeParse(value); if (!parsed.success) return err(registrationError(parsed.error)); @@ -182,7 +209,7 @@ export const refreshClientRegistration = ( const existing = source.success ? source.data : {}; const policy = validateClientPolicy(existing, client); if (!policy.ok) return policy; - const prior = parseClientRegistration(existing, client); + const prior = parseClientRegistrationLauncher(existing, client); const priorCommand = prior.ok ? [prior.value.command, ...prior.value.args] : undefined; diff --git a/src/application/RegularFileRead.ts b/src/application/RegularFileRead.ts index 25aa256afd..7921353ac4 100644 --- a/src/application/RegularFileRead.ts +++ b/src/application/RegularFileRead.ts @@ -179,17 +179,23 @@ export const retryRegularFileCleanupFailures = async ( const failureMessage = (cause: unknown): string => cause instanceof Error ? cause.message : String(cause); -/** Admit one regular-file handle, retain it through the consumer, and always close it. */ -export const withRegularFile = async ( +/** Completed stable read facts survive cancellation during descriptor finalization. */ +export interface RegularFileCompletion { + readonly value: Value; + readonly cancellation?: { readonly reason: unknown } | undefined; +} + +/** Admit one regular-file handle, retaining completion through close and cleanup retry. */ +export const withRegularFileCompletion = async ( path: string, read: (handle: FileHandle, stats: Stats) => Promise, options: RegularFileReadOptions = {}, -): Promise => { +): Promise> => { const { signal, resources } = options; let admitted: FileHandle | undefined; - const operation = async () => { + const operation = async (): Promise> => { try { - return await withOwnedRegularFile( + const value = await withOwnedRegularFile( path, { ...(signal === undefined ? {} : { signal }), @@ -214,6 +220,7 @@ export const withRegularFile = async ( return value; }, ); + return { value }; } catch (cause: unknown) { // Caller reasons are opaque, including objects shaped like file failures. // They do not transfer ownership of any descriptor to this operation. @@ -253,8 +260,16 @@ export const withRegularFile = async ( throw cause.operationFailure; throw retry.outcome.cause; } - return retry.outcome.value; + return { + value: retry.outcome.value as Value, + cancellation: regularFileCancellation(cause), + }; } + if (cause instanceof RegularFileCancelled) + return { + value: cause.outcome.value as Value, + cancellation: { reason: cause.reason }, + }; const cancellation = regularFileCancellation(cause); if (cancellation !== undefined) { if (options.cancellationFailure !== undefined) @@ -266,15 +281,34 @@ export const withRegularFile = async ( throw cause; } }; - const value = await (resources === undefined + const completion = await (resources === undefined ? operation() : resources.run(operation)); - // A retained close may have recovered after the descriptor owner's outcome - // was captured. Include that retry and the scope's finalization in cancellation. - if (signal?.aborted === true && options.cancellationFailure !== undefined) - throw options.cancellationFailure(signal.reason); - signal?.throwIfAborted(); - return value; + // Include successful cleanup retries and scope finalization in cancellation. + return completion.cancellation !== undefined || signal?.aborted !== true + ? completion + : { ...completion, cancellation: { reason: signal.reason } }; +}; + +/** Read consumers without completion facts retain their existing cancellation contract. */ +export const withRegularFile = async ( + path: string, + read: (handle: FileHandle, stats: Stats) => Promise, + options: RegularFileReadOptions = {}, +): Promise => { + const completion = await withRegularFileCompletion(path, read, options); + const cancellation = + completion.cancellation ?? + (options.signal?.aborted === true + ? { reason: options.signal.reason } + : undefined); + if (cancellation !== undefined) { + const { reason } = cancellation; + if (options.cancellationFailure !== undefined) + throw options.cancellationFailure(reason); + throw reason; + } + return completion.value; }; /** Admit a regular file without waiting for a pipe, then read its verified handle. */ diff --git a/src/application/Uninstall.ts b/src/application/Uninstall.ts index a950313657..9db087b5d7 100644 --- a/src/application/Uninstall.ts +++ b/src/application/Uninstall.ts @@ -22,7 +22,7 @@ import { defaultKunaCacheDirectory } from "../config/kunaCacheDirectory.js"; import { linuxHopperReleaseCacheDirectory } from "./LinuxHopperCacheDirectory.js"; import { ClientConfigurationPublication } from "./ClientConfigurationPublication.js"; -import { z } from "zod"; +import { parseClientRegistrationLauncher } from "./ClientRegistration.js"; import { PRODUCT_IDENTITY } from "../identity.js"; import { @@ -734,21 +734,9 @@ const resolveUninstallConfigPath = async ( /** Ownership rule for persistent MCP registrations created by REA setup. */ const isOwnedRegistration = (value: unknown, client: SetupClient): boolean => { - if (client.format === "opencode") { - const parsed = z - .object({ type: z.literal("local"), command: z.array(z.string()).min(1) }) - .passthrough() - .safeParse(value); - if (!parsed.success) return false; - const command = parsed.data.command; - return ( - classifyClientRegistrationCommand(command, CURRENT_ENTRYPOINT_PATH) === - "owned" - ); - } - const parsed = registrationSchema.safeParse(value); - if (!parsed.success) return false; - const { command, args } = parsed.data; + const parsed = parseClientRegistrationLauncher(value, client); + if (!parsed.ok) return false; + const { command, args } = parsed.value; return ( classifyClientRegistrationCommand( [command, ...args], @@ -1183,8 +1171,5 @@ const uninstallStatusPriority = (status: UninstallItem["status"]): number => { } } }; -const registrationSchema = z - .object({ command: z.string(), args: z.array(z.string()) }) - .passthrough(); const isMissing = (cause: unknown): boolean => cause instanceof Error && "code" in cause && cause.code === "ENOENT"; diff --git a/src/application/binaryDiagnostics/BinaryDiagnosticsCancellation.test.ts b/src/application/binaryDiagnostics/BinaryDiagnosticsCancellation.test.ts new file mode 100644 index 0000000000..c020e0474b --- /dev/null +++ b/src/application/binaryDiagnostics/BinaryDiagnosticsCancellation.test.ts @@ -0,0 +1,49 @@ +import { expect, it } from "vitest"; +import { ElfNotesService } from "./ElfNotesService.js"; +import { PeCertificatesService } from "./PeCertificatesService.js"; +import { PeImportsService } from "./PeImportsService.js"; +import { + ELF_NOTES_PROVIDER, + PE_CERTIFICATES_PROVIDER, + PE_IMPORTS_PROVIDER, +} from "../../domain/investigationProviders.js"; + +const inspect = (): never => { + throw new Error("An already cancelled request must not reach its provider"); +}; + +it.each([ + [ + "PE imports", + () => new PeImportsService({ identity: PE_IMPORTS_PROVIDER, inspect }), + ], + [ + "PE certificates", + () => + new PeCertificatesService({ + identity: PE_CERTIFICATES_PROVIDER, + inspect, + }), + ], + [ + "ELF notes", + () => new ElfNotesService({ identity: ELF_NOTES_PROVIDER, inspect }), + ], +] as const)( + "preserves an early cancellation reason for %s before validation", + async (_label, create) => { + const service = create(); + try { + for (const reason of [null, { caller: "selected task" }]) { + const result = await service.inspect(null, { + signal: AbortSignal.abort(reason), + }); + if (result.ok) throw new Error("Expected early cancellation"); + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(reason); + } + } finally { + await service.close(); + } + }, +); diff --git a/src/application/binaryDiagnostics/ByteEntropyService.test.ts b/src/application/binaryDiagnostics/ByteEntropyService.test.ts index 15300d51ab..c0f2a5fd45 100644 --- a/src/application/binaryDiagnostics/ByteEntropyService.test.ts +++ b/src/application/binaryDiagnostics/ByteEntropyService.test.ts @@ -38,6 +38,70 @@ it("keeps whole-file identity and entropy while selecting unaligned blocks and a expect(result.value.confidence).toBe("observed"); }); +it.each([256, 4096, 65536])( + "keeps %i-byte blocks relative to an unaligned window across short physical reads", + async (blockSize) => { + const root = await createTestTempDirectory("rea-entropy-short-reads-"); + const path = join(root, "bytes"); + const bytes = Buffer.from( + Array.from({ length: 3 * 65536 + 256 }, (_, i) => i % 256), + ); + await writeFile(path, bytes); + let reads = 0; + const service = new ByteEntropyService({ + stat, + open: async (...args) => { + const file = await open(...args); + const read = file.read.bind(file); + Object.defineProperty(file, "read", { + value: async ( + buffer: Buffer, + offset: number, + length: number, + position: number, + ) => { + reads += 1; + return read(buffer, offset, Math.min(length, 3571), position); + }, + }); + return file; + }, + }); + try { + const result = await service.inspect({ + path, + offset: 3, + length: 2 * 65536 + 1, + block_size: blockSize, + }); + if (!result.ok) throw result.error; + const report = byteEntropySchema.parse(result.value.normalized_result); + const fullBlocks = (2 * 65536) / blockSize; + expect(report.blocks).toEqual([ + ...Array.from({ length: fullBlocks }, (_, i) => ({ + offset: 3 + i * blockSize, + length: blockSize, + entropy: 8, + })), + { offset: 3 + 2 * 65536, length: 1, entropy: 0 }, + ]); + expect(report.runs).toEqual([ + { offset: 3, length: 2 * 65536, classification: "high" }, + { offset: 3 + 2 * 65536, length: 1, classification: "low" }, + ]); + expect(report.file_entropy).toBe(8); + expect(report.artifact.sha256).toBe( + createHash("sha256").update(bytes).digest("hex"), + ); + // Even with 3,571-byte short reads, finer report resolution does not + // require hundreds of asynchronous reads of this 193 KiB file. + expect(reads).toBeLessThan(100); + } finally { + await service.close(); + } + }, +); + it("rejects missing files, non-files and windows beyond EOF, but accepts empty windows", async () => { const root = await createTestTempDirectory("rea-entropy-input-"); const path = join(root, "empty"); diff --git a/src/application/binaryDiagnostics/ByteEntropyService.ts b/src/application/binaryDiagnostics/ByteEntropyService.ts index 08dd3c853e..b565feb4b9 100644 --- a/src/application/binaryDiagnostics/ByteEntropyService.ts +++ b/src/application/binaryDiagnostics/ByteEntropyService.ts @@ -241,7 +241,10 @@ export class ByteEntropyService { ); const end = input.offset + length; const blocks: ByteEntropy["blocks"] = []; - const buffer = Buffer.alloc(input.block_size); + // Physical reads must not shrink with report resolution, including + // outside the selected window. Every admitted power-of-two block + // size divides this buffer, so only the window's final block is short. + const buffer = Buffer.alloc(65_536); const counts = new Float64Array(256); const hash = createHash("sha256"); let position = 0; @@ -250,7 +253,7 @@ export class ByteEntropyService { const selected = position >= input.offset && position < end; const boundary = position < input.offset ? input.offset : selected ? end : size; - const wanted = Math.min(input.block_size, boundary - position); + const wanted = Math.min(buffer.length, boundary - position); let filled = 0; while (filled < wanted) { assertArtifactReadActive(options?.signal); @@ -267,11 +270,18 @@ export class ByteEntropyService { hash.update(bytes); for (const byte of bytes) counts[byte] = (counts[byte] ?? 0) + 1; if (selected) - blocks.push({ - offset: position, - length: filled, - entropy: computeBlockEntropy(bytes), - }); + for ( + let offset = 0; + offset < filled; + offset += input.block_size + ) { + const block = bytes.subarray(offset, offset + input.block_size); + blocks.push({ + offset: position + offset, + length: block.length, + entropy: computeBlockEntropy(block), + }); + } position += filled; } if ( diff --git a/src/application/binaryDiagnostics/ElfNotesService.ts b/src/application/binaryDiagnostics/ElfNotesService.ts index e34f4effb3..4bb19dbcf1 100644 --- a/src/application/binaryDiagnostics/ElfNotesService.ts +++ b/src/application/binaryDiagnostics/ElfNotesService.ts @@ -34,7 +34,9 @@ export class ElfNotesService { options?: ExecutionOptions, ): Promise> { if (options?.signal?.aborted) - return err(new AnalysisCancelledError(OPERATION)); + return err( + new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), + ); const input = inspectElfNotesInputSchema.safeParse(rawInput); if (!input.success) return err( diff --git a/src/application/binaryDiagnostics/PeCertificatesService.ts b/src/application/binaryDiagnostics/PeCertificatesService.ts index 0ce7aa9e18..2a37994e4c 100644 --- a/src/application/binaryDiagnostics/PeCertificatesService.ts +++ b/src/application/binaryDiagnostics/PeCertificatesService.ts @@ -37,7 +37,9 @@ export class PeCertificatesService { options?: ExecutionOptions, ): Promise> { if (options?.signal?.aborted) - return err(new AnalysisCancelledError(OPERATION)); + return err( + new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), + ); const input = inspectPeCertificatesInputSchema.safeParse(rawInput); if (!input.success) return err( diff --git a/src/application/binaryDiagnostics/PeImportsPort.ts b/src/application/binaryDiagnostics/PeImportsPort.ts new file mode 100644 index 0000000000..229c04d45a --- /dev/null +++ b/src/application/binaryDiagnostics/PeImportsPort.ts @@ -0,0 +1,20 @@ +import type { + ExecutionOptions, + ProviderIdentity, +} from "../AnalysisProvider.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import type { + InspectPeImportsInput, + PeImports, +} from "../../domain/native/peImports.js"; +import type { Result } from "../../domain/result.js"; + +/** Stable read and PE import-table decoding, independent of a disassembler session. */ +export interface PeImportsPort { + readonly identity: ProviderIdentity; + close?(): Promise; + inspect( + input: InspectPeImportsInput, + options?: ExecutionOptions, + ): Promise>; +} diff --git a/src/application/binaryDiagnostics/PeImportsService.test.ts b/src/application/binaryDiagnostics/PeImportsService.test.ts new file mode 100644 index 0000000000..0b8a777333 --- /dev/null +++ b/src/application/binaryDiagnostics/PeImportsService.test.ts @@ -0,0 +1,242 @@ +import { expect, it } from "vitest"; +import { PeImportsService } from "./PeImportsService.js"; +import { PE_IMPORTS_PROVIDER } from "../../domain/investigationProviders.js"; +import type { PeImports } from "../../domain/native/peImports.js"; +import { peImportsSchema } from "../../domain/native/peImports.js"; +import { ok } from "../../domain/result.js"; +import { + AnalysisCancelledError, + AnalysisInputError, + AnalysisOutputError, +} from "../../domain/analysisErrorCore.js"; + +const identity = PE_IMPORTS_PROVIDER; +const report: PeImports = { + artifact: { + path: "/artifacts/app.exe", + sha256: "a".repeat(64), + bytes: 512, + }, + format: "pe32-plus", + machine: 0x8664, + bits: 64, + architecture: "amd64", + import_directory: null, + delay_import_directory: null, + imports: [], + delay_imports: [], + imphash: { + algorithm: "pefile", + md5: null, + reason: "IMAGE_DIRECTORY_ENTRY_IMPORT is absent.", + }, + coverage: { + status: "complete", + reason: null, + examined_dlls: 0, + imports: 0, + delay_imports: 0, + }, + issues: [], + limitations: ["IMAGE_DIRECTORY_ENTRY_IMPORT is absent."], +}; + +it("rejects a relative path and extra arguments before reading", async () => { + let calls = 0; + const service = new PeImportsService({ + identity, + inspect: () => { + calls += 1; + return Promise.resolve(ok(report)); + }, + }); + const relative = await service.inspect({ path: "app.exe" }); + const extra = await service.inspect({ + path: report.artifact.path, + extra: true, + }); + expect(relative.ok).toBe(false); + expect(extra.ok).toBe(false); + if (!relative.ok) expect(relative.error).toBeInstanceOf(AnalysisInputError); + if (!extra.ok) expect(extra.error).toBeInstanceOf(AnalysisInputError); + expect(calls).toBe(0); +}); + +it("records the selected PE and an absent import table", async () => { + const result = await new PeImportsService({ + identity, + inspect: () => Promise.resolve(ok(report)), + }).inspect({ path: report.artifact.path }); + expect(result.ok).toBe(true); + if (!result.ok) throw result.error; + expect(result.value).toMatchObject({ + confidence: "observed", + subject: { format: "pe", architecture: "x86_64" }, + normalized_result: report, + }); +}); + +it.each(["imports", "delay_imports", "examined_dlls"] as const)( + "rejects contradictory complete %s coverage", + async (field) => { + const result = await new PeImportsService({ + identity, + inspect: () => + Promise.resolve( + ok({ + ...report, + coverage: { ...report.coverage, [field]: 1 }, + }), + ), + }).inspect({ path: report.artifact.path }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError); + }, +); + +it.each(["import_directory", "delay_import_directory"] as const)( + "accepts unexamined %s declarations only with partial coverage", + async (field) => { + for (const status of ["complete", "partial"] as const) { + const value: PeImports = { + ...report, + [field]: { rva: 0xfffffff0, size: 40, locations: [] }, + coverage: { + ...report.coverage, + status, + reason: + status === "partial" + ? "First descriptor cannot be examined." + : null, + }, + issues: + status === "partial" + ? [ + { + code: "malformed_entry", + location: "0xfffffff0", + message: "First descriptor cannot be examined.", + }, + ] + : [], + }; + const result = await new PeImportsService({ + identity, + inspect: () => Promise.resolve(ok(value)), + }).inspect({ path: report.artifact.path }); + expect(result.ok).toBe(status === "partial"); + if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError); + } + }, +); + +it("discards an observation if the caller cancels while the provider completes", async () => { + const controller = new AbortController(); + const service = new PeImportsService({ + identity, + inspect: () => { + controller.abort(); + return Promise.resolve(ok(report)); + }, + }); + const result = await service.inspect( + { path: report.artifact.path }, + { signal: controller.signal }, + ); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError); +}); + +it("publishes every consumed directory span as its own Evidence location", async () => { + const split: PeImports = { + ...report, + import_directory: { + rva: 0x1000, + size: 40, + locations: [ + { offset: 0x100, bytes: 20 }, + { offset: 0x1e0, bytes: 20 }, + ], + }, + imports: [ + { + index: 0, + name: "KERNEL32.dll", + name_rva: 0x1100, + original_first_thunk_rva: 0x1120, + first_thunk_rva: 0x1128, + time_date_stamp: 0, + forwarder_chain: 0, + location: { offset: 0x100, bytes: 20 }, + functions: [], + }, + ], + coverage: { ...report.coverage, examined_dlls: 1 }, + imphash: { ...report.imphash, reason: "The descriptor has no functions." }, + limitations: ["Loaded import state remains unobserved."], + }; + const result = await new PeImportsService({ + identity, + inspect: () => Promise.resolve(ok(split)), + }).inspect({ path: report.artifact.path }); + if (!result.ok) throw result.error; + expect(result.value.locations).toEqual([ + { kind: "artifact-path", path: report.artifact.path }, + { kind: "file-offset-range", start: 0x100, end: 0x114 }, + { kind: "file-offset-range", start: 0x1e0, end: 0x1f4 }, + ]); + const outside = await new PeImportsService({ + identity, + inspect: () => + Promise.resolve( + ok({ + ...split, + import_directory: { + rva: 0x1000, + size: 40, + locations: [ + { offset: 0x100, bytes: 20 }, + { offset: 0x1f8, bytes: 20 }, + ], + }, + }), + ), + }).inspect({ path: report.artifact.path }); + expect(outside.ok).toBe(false); + if (!outside.ok) expect(outside.error).toBeInstanceOf(AnalysisOutputError); +}); + +it("retains examined unsupported descriptors without inventing DLL rows", async () => { + const partial: PeImports = { + ...report, + delay_import_directory: { + rva: 0x1000, + size: 64, + locations: [{ offset: 0x100, bytes: 64 }], + }, + coverage: { + ...report.coverage, + status: "partial", + reason: "An unsupported descriptor was examined.", + examined_dlls: 1, + }, + issues: [ + { + code: "unsupported_form", + location: "0x1000", + message: "Delay descriptor uses virtual addresses.", + }, + ], + }; + const result = await new PeImportsService({ + identity, + inspect: () => Promise.resolve(ok(partial)), + }).inspect({ path: report.artifact.path }); + if (!result.ok) throw result.error; + const normalized = peImportsSchema.parse(result.value.normalized_result); + expect(normalized.coverage).toMatchObject({ + status: "partial", + examined_dlls: 1, + }); + expect(normalized.delay_imports).toEqual([]); +}); diff --git a/src/application/binaryDiagnostics/PeImportsService.ts b/src/application/binaryDiagnostics/PeImportsService.ts new file mode 100644 index 0000000000..eaad73f967 --- /dev/null +++ b/src/application/binaryDiagnostics/PeImportsService.ts @@ -0,0 +1,177 @@ +import { isAbsolute } from "node:path"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; +import type { PeImportsPort } from "./PeImportsPort.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + AnalysisCancelledError, + AnalysisInputError, + AnalysisOutputError, +} from "../../domain/analysisErrorCore.js"; +import { createEvidence, type Evidence } from "../../domain/evidence.js"; +import { + inspectPeImportsInputSchema, + peImportsSchema, + type PeImports, +} from "../../domain/native/peImports.js"; +import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; +import { + decodePeMachine, + peOptionalHeaderMagic, + type PeArchitecture, +} from "../../domain/peMachine.js"; +import { err, type Result } from "../../domain/result.js"; +import { completeProviderEvidence } from "../ProviderOperationExecution.js"; + +const OPERATION = "inspect_pe_imports"; + +/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */ +export class PeImportsService { + constructor(readonly provider: PeImportsPort) {} + + close(): Promise { + return this.provider.close?.() ?? Promise.resolve(); + } + + async inspect( + rawInput: unknown, + options?: ExecutionOptions, + ): Promise> { + if (options?.signal?.aborted) + return err( + new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), + ); + const input = inspectPeImportsInputSchema.safeParse(rawInput); + if (!input.success) + return err( + analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { + cause: input.error, + }), + ); + if (!isAbsolute(input.data.path)) + return err( + new AnalysisInputError(OPERATION, undefined, [ + { + path: ["path"], + reason: "invalid_format", + message: "Expected an absolute filesystem path on this host.", + }, + ]), + ); + const result = await this.provider.inspect(input.data, options); + if (!result.ok) return result; + const report = peImportsSchema.safeParse(result.value); + if ( + !report.success || + report.data.artifact.path !== input.data.path || + !validReport(report.data, input.data) + ) + return err( + new AnalysisOutputError( + OPERATION, + "PE import provider returned malformed output or changed artifact identity.", + ), + ); + const value = report.data; + const architecture = subjectArchitecture(value); + const locations = [ + { kind: "artifact-path" as const, path: value.artifact.path }, + ...fileRange(value.import_directory), + ...fileRange(value.delay_import_directory), + ]; + return completeProviderEvidence( + createEvidence( + { + path: value.artifact.path, + format: "pe", + sha256: value.artifact.sha256, + ...(architecture === undefined ? {} : { architecture }), + }, + this.provider.identity, + { + operation: OPERATION, + parameters: input.data, + result: value, + confidence: "observed", + limitations: value.limitations, + locations, + }, + ), + options?.signal, + ); + } +} + +const subjectArchitecture = (report: PeImports): PeArchitecture | undefined => { + const machine = decodePeMachine(report.machine); + if (machine === undefined || machine.nativeOsOverride) return undefined; + const magic = report.format === "pe32-plus" ? 0x20b : 0x10b; + return magic === peOptionalHeaderMagic(machine.architecture) + ? machine.architecture + : undefined; +}; + +const fileRange = ( + directory: PeImports["import_directory"], +): readonly { + kind: "file-offset-range"; + start: number; + end: number; +}[] => + directory === null + ? [] + : directory.locations.map(({ offset, bytes }) => ({ + kind: "file-offset-range" as const, + start: offset, + end: offset + bytes, + })); + +const validReport = ( + report: PeImports, + input: { + readonly max_dlls: number; + readonly max_functions: number; + readonly max_file_bytes: number; + }, +): boolean => { + const inside = ({ offset, bytes }: { offset: number; bytes: number }) => + offset <= report.artifact.bytes - bytes; + const dllCount = report.imports.length + report.delay_imports.length; + if ( + report.artifact.bytes > input.max_file_bytes || + report.coverage.examined_dlls > input.max_dlls || + dllCount > report.coverage.examined_dlls || + (report.coverage.status === "complete" && + (dllCount !== report.coverage.examined_dlls || report.issues.length > 0)) + ) + return false; + const importCount = report.imports.reduce( + (count, dll) => count + dll.functions.length, + 0, + ); + const delayImportCount = report.delay_imports.reduce( + (count, dll) => count + dll.functions.length, + 0, + ); + if ( + importCount !== report.coverage.imports || + delayImportCount !== report.coverage.delay_imports || + importCount + delayImportCount > input.max_functions + ) + return false; + for (const directory of [ + report.import_directory, + report.delay_import_directory, + ]) { + if ( + directory !== null && + (!directory.locations.every(inside) || + (directory.locations.length === 0 && + report.coverage.status === "complete")) + ) + return false; + } + const descriptorsFit = ( + rows: readonly { location: { offset: number; bytes: number } }[], + ) => rows.every((row) => inside(row.location)); + return descriptorsFit(report.imports) && descriptorsFit(report.delay_imports); +}; diff --git a/src/application/environment/environmentCompare.test.ts b/src/application/environment/environmentCompare.test.ts index 7c2676ce4f..d1436ba915 100644 --- a/src/application/environment/environmentCompare.test.ts +++ b/src/application/environment/environmentCompare.test.ts @@ -686,6 +686,10 @@ describe("probe request chronology", () => { ["2026-10-20T09:00:00.0002Z", "2026-10-20T09:00:00.0001Z"], ["2026-10-20T09:00:00.0000000002Z", "2026-10-20T09:00:00.0000000001Z"], ["0001-01-01T00:00:00.0002Z", "0001-01-01T01:00:00.0001+01:00"], + ["2026-10-20 09:00:00.0002Z", "2026-10-20 09:00:00.0001Z"], + ["2026-10-20T10:00:00.0002+0100", "2026-10-20T09:00:00.0001Z"], + ["2026-10-20 10:00:00.0002+0100", "2026-10-20 09:00:00.0001+0000"], + ["2026-10-20T09:00:00.0002", "2026-10-20T09:00:00.0001"], ])( "replays an earlier delete before a later submillisecond read (%s)", (later, earlier) => { @@ -730,6 +734,7 @@ describe("probe request chronology", () => { it.each([ ["2026-10-20T09:00:00.000100Z", "2026-10-20T10:00:00.0001+01:00"], + ["2026-10-20T09:00:00.0001Z", "2026-10-20T09:00:00.000100"], ["Tue, 20 Oct 2026 09:00:00 GMT", "2026-10-20T09:00:00Z"], ])( "keeps ordinal order for equal instants and native-Date spelling compatibility (%s)", diff --git a/src/application/environment/environmentCompareProbe.ts b/src/application/environment/environmentCompareProbe.ts index da08ef12d4..cdfb7ce423 100644 --- a/src/application/environment/environmentCompareProbe.ts +++ b/src/application/environment/environmentCompareProbe.ts @@ -15,8 +15,7 @@ import type { JsonValue } from "../../domain/jsonValue.js"; import { compareOffsetDateTimeInstants, offsetDateTimeInstantFromEpochMilliseconds, - offsetDateTimeSchema, - parseOffsetDateTimeInstant, + readRecordedDateTime, type OffsetDateTimeInstant, } from "../../domain/offsetDateTime.js"; import type { WebNetworkCaptureRecord } from "../../domain/webNetworkCapture.js"; @@ -58,10 +57,16 @@ const startedInstant = ( ): OffsetDateTimeInstant | undefined => { const started = startedSchema.safeParse(record.reported); if (!started.success) return undefined; - const timestamp = offsetDateTimeSchema.safeParse( - started.data.startedDateTime, - ); - if (timestamp.success) return parseOffsetDateTimeInstant(timestamp.data); + const reading = readRecordedDateTime(started.data.startedDateTime); + if (reading !== undefined) { + const { spelling } = reading; + const offsetTimestamp = + spelling.separator === "T" && + (spelling.zone === "Z" || spelling.zone.includes(":")); + const clockTimestamp = + spelling.secondsPresent && spelling.fractionDigits <= 9; + if (offsetTimestamp || clockTimestamp) return reading.instant; + } // Preserve native-Date admission for legacy spellings, at its original precision. const ms = Date.parse(started.data.startedDateTime); return Number.isFinite(ms) @@ -153,16 +158,16 @@ export const probeRequests = ( const origin = probeOrigin(lines); for (const record of ordered) { const line = lines.get(record.ordinal); - const exchange = recordedExchange(record); const { ordinal } = record; if (line?.kind === "line" && line.origin !== origin) unobserved.push({ ordinal, reason: "other-origin" }); - else if (line?.kind !== "line" || exchange === undefined) + else if (line?.kind !== "line") unobserved.push({ ordinal, reason: line?.kind === "excluded" ? line.reason : "request-unavailable", }); else { + const exchange = recordedExchange(record, line); const request = { method: line.method, path: exchange.path, diff --git a/src/application/environment/environmentSource.test.ts b/src/application/environment/environmentSource.test.ts index ad94c40f73..af8e925a11 100644 --- a/src/application/environment/environmentSource.test.ts +++ b/src/application/environment/environmentSource.test.ts @@ -7,6 +7,7 @@ import { timedCaptureEvidence, } from "../../../tests/fixtures/analysisView.js"; import { scenarioCaptureEvidence } from "../../../tests/fixtures/browserScenarioCapture.js"; +import { readRecordedRequestLine } from "../../domain/analysisView/webNetworkRouteTemplates.js"; import { environmentServeFixture, environmentServeSource, @@ -67,8 +68,10 @@ it.each([ redactions: [], limitations: [], }; - const exchange = recordedExchange(record); - if (exchange === undefined) throw new Error("Expected a recorded exchange"); + const line = readRecordedRequestLine(record.reported, true, () => true); + if (line?.kind !== "line") + throw new Error("Expected a recorded request line"); + const exchange = recordedExchange(record, line); expect(exchange.bodyText).toBe(expected); expect(replayedBody(exchange)).toEqual( expected === null ? { kind: "none" } : { kind: "text", text: expected }, @@ -84,7 +87,7 @@ it.each([ reason: "explicit-sensitive-value", scope: "property-name", }); - expect(recordedExchange(record)?.bodyText).toBeNull(); + expect(recordedExchange(record, line).bodyText).toBeNull(); } }, ); diff --git a/src/application/environment/environmentSource.ts b/src/application/environment/environmentSource.ts index 789ec7198f..0ebc4bb60a 100644 --- a/src/application/environment/environmentSource.ts +++ b/src/application/environment/environmentSource.ts @@ -16,6 +16,7 @@ import { isJsonLike } from "../../domain/analysisView/EnvironmentManifestAccumul import { mediaTypeEssence, readRecordedRequestLine, + type RecordedRequestLine, } from "../../domain/analysisView/webNetworkRouteTemplates.js"; import { retainedBrowserScenarioCaptureSchema } from "../../domain/browserScenarioCapture.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; @@ -173,21 +174,18 @@ const schemaInputError = ( })), ); -/** One capture record reduced to its request line and retained response body, or undefined without a request line. */ +/** Combine an already parsed request line with its record's retained response body. */ export const recordedExchange = ( record: WebNetworkCaptureRecord, -): RecordedExchange | undefined => { - const line = readRecordedRequestLine(record.reported, true, () => true); - if (line === null || line.kind !== "line") return undefined; - return { - ordinal: record.ordinal, - path: line.path, - query: line.query === undefined || line.query === "" ? null : line.query, - status: line.status, - contentType: line.rawMime === "" ? null : line.rawMime, - ...readRecordedResponseBody(record, REPLAY_BODY_LIMIT_BYTES), - }; -}; + line: Extract, +): RecordedExchange => ({ + ordinal: record.ordinal, + path: line.path, + query: line.query === undefined || line.query === "" ? null : line.query, + status: line.status, + contentType: line.rawMime === "" ? null : line.rawMime, + ...readRecordedResponseBody(record, REPLAY_BODY_LIMIT_BYTES), +}); /** Authenticate a view Evidence file and require one view kind; a refusal names the file role. */ export const parseViewEvidence = ( @@ -419,9 +417,10 @@ const indexExchanges = ( for (const ordinal of tool.record_ordinals) { if (exchanges.has(ordinal)) continue; const record = byOrdinal.get(ordinal); - const exchange = - record === undefined ? undefined : recordedExchange(record); - if (exchange !== undefined) exchanges.set(ordinal, exchange); + if (record === undefined) continue; + const line = readRecordedRequestLine(record.reported, true, () => true); + if (line?.kind === "line") + exchanges.set(ordinal, recordedExchange(record, line)); } return exchanges; }; diff --git a/src/application/managed/ManagedReconstructionService.test.ts b/src/application/managed/ManagedReconstructionService.test.ts index 1d75633e79..cf489058d4 100644 --- a/src/application/managed/ManagedReconstructionService.test.ts +++ b/src/application/managed/ManagedReconstructionService.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest"; import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../../contracts/managed/managedWorkflowExamples.js"; import { - importManagedReconstruction, managedReconstructionImportInputSchema, + resolveManagedReconstructionImport, } from "../../domain/managed/managedReconstruction.js"; import { managedMemberInspectionSchema } from "../../domain/managed/managedArtifact.js"; import { createEvidence } from "../../domain/evidence.js"; @@ -21,150 +21,191 @@ const exampleMethod = () => { return method; }; -describe("managed decompiler reconstruction import", () => { - it("reports UTF-8 byte totals while preserving UTF-16 text lengths", () => { - const text = 'return "é"; // 😀'; - const method = exampleMethod(); - const result = importManagedReconstructionEvidence({ - ...MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE, - methods: [ - { - ...method, - reconstruction: { ...method.reconstruction, text }, - }, - ], - }); - if (!result.ok) throw result.error; - - expect(result.value.normalized_result).toMatchObject({ - summary: { total_text_bytes: 20 }, - methods: [{ reconstruction: { text, text_length: 17 } }], - }); - }); - - it("rejects reconstruction text hash drift", () => { - expect(() => - importManagedReconstruction({ - ...exampleInput(), - methods: [ - { - ...exampleMethod(), - reconstruction: { - ...exampleMethod().reconstruction, - text_sha256: "0".repeat(64), - }, - }, - ], - }), - ).toThrow(AnalysisInputError); - }); - - it("rejects stale method locks", () => { - const staleInput = { - ...exampleInput(), - methods: [ - { - ...exampleMethod(), - normalized_il_sha256: "6".repeat(64), - }, - ], - }; - expect(() => importManagedReconstruction(staleInput)).toThrow( - AnalysisInputError, - ); - - const result = importManagedReconstructionEvidence(staleInput); - expect(result.ok).toBe(false); - if (!result.ok) - expect(result.error).toMatchObject({ - _tag: "AnalysisInputError", - issues: [ - { - path: ["methods", 0], - reason: "invalid_value", - message: expect.stringMatching(/does not match/u), - }, - ], - }); - }); - - it("rejects static Evidence with a token MVID but no module identity", () => { - const input = exampleInput(); - const members = managedMemberInspectionSchema.parse( - input.static_members.normalized_result, - ); - if (members.module === null || members.module.mvid === null) - throw new Error("Expected fixture module identity and MVID"); - const inconsistentEvidence = createEvidence( - undefined, - input.static_members.provider, +it("managed reconstruction reports UTF-8 byte totals while preserving UTF-16 text lengths", () => { + const text = 'return "é"; // 😀'; + const method = exampleMethod(); + const result = importManagedReconstructionEvidence({ + ...MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE, + methods: [ { - operation: "inspect_managed_members", - parameters: {}, - result: { ...members, module: null }, - rawResult: null, - limitations: members.limitations, + ...method, + reconstruction: { ...method.reconstruction, text }, }, - ); - - expect(() => - importManagedReconstruction({ - ...input, - static_members: inconsistentEvidence, - }), - ).toThrow(/token scope MVID/u); + ], }); + if (!result.ok) throw result.error; - it("rejects reconstruction locks for partial CIL without normalized identity", () => { - const input = exampleInput(); - const observed = managedMemberInspectionSchema.parse( - input.static_members.normalized_result, - ); - const method = observed.methods[0]; - if (method === undefined) throw new Error("missing observed method"); - const partial = { - ...observed, - methods: [ - { - ...method, - body: { - ...method.body, - status: "partial" as const, - normalized_il_sha256: null, - truncated_instructions: 1, - issue: "instruction limit reached", - }, - }, - ], - coverage: { state: "partial" as const, issues: [] }, - }; - const partialEvidence = createEvidence( - undefined, - input.static_members.provider, - { - operation: "inspect_managed_members", - parameters: {}, - result: partial, - rawResult: null, - limitations: partial.limitations, - }, - ); - - expect(() => - importManagedReconstruction({ - ...input, - static_members: partialEvidence, - methods: [ - { - ...exampleMethod(), - normalized_il_sha256: null, - }, - ], - }), - ).toThrow(AnalysisInputError); + expect(result.value.normalized_result).toMatchObject({ + summary: { total_text_bytes: 20 }, + methods: [{ reconstruction: { text, text_length: 17 } }], }); }); +it("managed reconstruction rejects text hash drift", () => { + const rejected = resolveManagedReconstructionImport({ + ...exampleInput(), + methods: [ + { + ...exampleMethod(), + reconstruction: { + ...exampleMethod().reconstruction, + text_sha256: "0".repeat(64), + }, + }, + ], + }); + expect(rejected).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + operation: "import_managed_reconstruction", + issues: [ + { + path: ["methods", 0, "reconstruction", "text_sha256"], + reason: "invalid_value", + message: expect.stringContaining("Reconstruction text hash mismatch"), + }, + ], + }, + }); + if (!rejected.ok) expect(rejected.error).toBeInstanceOf(AnalysisInputError); +}); + +it("managed reconstruction rejects stale method locks", () => { + const staleInput = { + ...exampleInput(), + methods: [ + { + ...exampleMethod(), + normalized_il_sha256: "6".repeat(64), + }, + ], + }; + const rejected = resolveManagedReconstructionImport(staleInput); + expect(rejected).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + operation: "import_managed_reconstruction", + issues: [ + { + path: ["methods", 0], + reason: "invalid_value", + message: expect.stringMatching(/does not match/u), + }, + ], + }, + }); + if (!rejected.ok) expect(rejected.error).toBeInstanceOf(AnalysisInputError); + + const result = importManagedReconstructionEvidence(staleInput); + expect(result.ok).toBe(false); + if (!result.ok) + expect(result.error).toMatchObject({ + _tag: "AnalysisInputError", + issues: [ + { + path: ["methods", 0], + reason: "invalid_value", + message: expect.stringMatching(/does not match/u), + }, + ], + }); +}); + +it("managed reconstruction rejects static Evidence with a token MVID but no module identity", () => { + const input = exampleInput(); + const members = managedMemberInspectionSchema.parse( + input.static_members.normalized_result, + ); + if (members.module === null || members.module.mvid === null) + throw new Error("Expected fixture module identity and MVID"); + const inconsistentEvidence = createEvidence( + undefined, + input.static_members.provider, + { + operation: "inspect_managed_members", + parameters: {}, + result: { ...members, module: null }, + rawResult: null, + limitations: members.limitations, + }, + ); + + const rejected = resolveManagedReconstructionImport({ + ...input, + static_members: inconsistentEvidence, + }); + expect(rejected).toMatchObject({ + ok: false, + error: { + _tag: "EvidenceIntegrityError", + message: `Managed Evidence inspect_managed_members (${inconsistentEvidence.evidence_id}) token scope MVID ${members.identity_scope.requires_mvid} does not match module MVID null`, + }, + }); +}); + +it("managed reconstruction rejects locks for partial CIL without normalized identity", () => { + const input = exampleInput(); + const observed = managedMemberInspectionSchema.parse( + input.static_members.normalized_result, + ); + const method = observed.methods[0]; + if (method === undefined) throw new Error("missing observed method"); + const partial = { + ...observed, + methods: [ + { + ...method, + body: { + ...method.body, + status: "partial" as const, + normalized_il_sha256: null, + truncated_instructions: 1, + issue: "instruction limit reached", + }, + }, + ], + coverage: { state: "partial" as const, issues: [] }, + }; + const partialEvidence = createEvidence( + undefined, + input.static_members.provider, + { + operation: "inspect_managed_members", + parameters: {}, + result: partial, + rawResult: null, + limitations: partial.limitations, + }, + ); + + const rejected = resolveManagedReconstructionImport({ + ...input, + static_members: partialEvidence, + methods: [ + { + ...exampleMethod(), + normalized_il_sha256: null, + }, + ], + }); + expect(rejected).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + operation: "import_managed_reconstruction", + issues: [ + { + path: ["methods", 0], + reason: "invalid_value", + message: expect.stringMatching(/does not match/u), + }, + ], + }, + }); + if (!rejected.ok) expect(rejected.error).toBeInstanceOf(AnalysisInputError); +}); describe("managed reconstruction input diagnostics", () => { it("lists schema issues for malformed raw input", () => { const result = importManagedReconstructionEvidence({}); diff --git a/src/artifacts/ArtifactOperationFailure.ts b/src/artifacts/ArtifactOperationFailure.ts index e484f8afb2..b67f74e048 100644 --- a/src/artifacts/ArtifactOperationFailure.ts +++ b/src/artifacts/ArtifactOperationFailure.ts @@ -37,5 +37,7 @@ export const translateArtifactFailure = ( } // Keep typed Evidence and other analysis failures instead of reducing them to I/O. if (cause instanceof AnalysisError) return cause; - return new ArtifactOperationError(operation, "io"); + return new ArtifactOperationError(operation, "io", undefined, undefined, { + cause, + }); }; diff --git a/src/artifacts/ZipArtifactReader.test.ts b/src/artifacts/ZipArtifactReader.test.ts index 517630419a..eb55c6b08f 100644 --- a/src/artifacts/ZipArtifactReader.test.ts +++ b/src/artifacts/ZipArtifactReader.test.ts @@ -1,4 +1,3 @@ -import { translateArtifactFailure } from "./ArtifactOperationFailure.js"; import { writeOrderedZip } from "../../tests/fixtures/artifactEntryOrder.js"; import { zipSymlinkMetadataFixture } from "../../tests/fixtures/zipSymlinkMetadata.js"; import { ArtifactReaderFailure } from "./ArtifactReader.js"; @@ -389,48 +388,3 @@ it.each([null, { caller: "opaque ZIP interruption" }])( } }, ); - -it("retains exact caller cancellation after a real ZIP member opens and leaves an uncancelled read intact", async () => { - const directory = await createTestTempDirectory( - "rea-zip-stream-cancellation-", - ); - const path = join(directory, "fixture.zip"); - const bytes = new Uint8Array(512 * 1024).fill(0x71); - const writer = new ZipWriter(new Uint8ArrayWriter()); - await writer.add("member.bin", new Uint8ArrayReader(bytes), { level: 0 }); - await writeFile(path, await writer.close()); - const reader = new ZipArtifactReader(path, "zip"); - const reason = { requested: "stop member" }; - const controller = new AbortController(); - try { - const entries = []; - for await (const entry of reader.entries()) entries.push(entry); - const entry = entries[0]; - if (entry === undefined) throw new Error("Expected member"); - const stream = await reader.open(entry, controller.signal); - const consumption = (async () => { - for await (const _chunk of stream) controller.abort(reason); - })(); - const failure: unknown = await consumption.catch((cause: unknown) => cause); - expect(failure).toBeInstanceOf(ArtifactReaderFailure); - if (!(failure instanceof ArtifactReaderFailure)) - throw new Error("Expected cancellation"); - expect(failure.reason).toBe("cancelled"); - expect(failure.cancellation?.reason).toBe(reason); - expect(translateArtifactFailure("extract_artifact", failure).cause).toBe( - reason, - ); - await expect( - Promise.resolve().then(() => reader.open(entry, controller.signal)), - ).rejects.toMatchObject({ - reason: "cancelled", - cancellation: { reason }, - }); - const chunks: Buffer[] = []; - for await (const chunk of await reader.open(entry)) - chunks.push(Buffer.from(chunk)); - expect(Buffer.concat(chunks)).toEqual(Buffer.from(bytes)); - } finally { - await reader.close(); - } -}); diff --git a/src/artifacts/apple/MachoLoadCommandReader.ts b/src/artifacts/apple/MachoLoadCommandReader.ts index 47c268131a..0825fcc481 100644 --- a/src/artifacts/apple/MachoLoadCommandReader.ts +++ b/src/artifacts/apple/MachoLoadCommandReader.ts @@ -1,4 +1,8 @@ import { MachoLoadCommandCursor } from "../../domain/apple/machoLoadCommands.js"; +import { + MAX_JVM_CLASS_IDENTITY_BYTES, + JVM_CLASS_IDENTITY_LIMIT_REASON, +} from "../../domain/apple/jvmClassFile.js"; import type { MachoDependency, MachoImageFacts, @@ -139,7 +143,7 @@ export const readMachoImage = async ( const view = viewOf(head); const magic = view.getUint32(0, false); let header: ReturnType; - if (magic === FAT_MAGIC && size <= MAX_LOAD_COMMAND_BYTES) { + if (magic === FAT_MAGIC && size <= MAX_JVM_CLASS_IDENTITY_BYTES) { const complete = await readAt(0, size); header = complete.byteLength === size @@ -177,10 +181,10 @@ export const readMachoImage = async ( // A large CAFEBABE input whose bounded prefix resembles an invalid // known-CPU FAT table remains unknown until its JVM identity can be // checked; do not turn the resource limit into a malformed claim. - if (magic === FAT_MAGIC && size > MAX_LOAD_COMMAND_BYTES) + if (magic === FAT_MAGIC && size > MAX_JVM_CLASS_IDENTITY_BYTES) throw new MachoFormatIssue( "unsupported", - "FAT/JVM identity is unresolved because the class identity scan exceeds the 16 MiB limit", + JVM_CLASS_IDENTITY_LIMIT_REASON, ); throw new MachoFormatIssue(identity.category, identity.reason); } diff --git a/src/artifacts/apple/machoContainer.test.ts b/src/artifacts/apple/machoContainer.test.ts index 235ba4d6e7..9d7e42129f 100644 --- a/src/artifacts/apple/machoContainer.test.ts +++ b/src/artifacts/apple/machoContainer.test.ts @@ -12,7 +12,9 @@ import { javaClassFile } from "../../../tests/fixtures/javaClassFile.js"; import { parseExecutableHeader } from "../../domain/binaryTarget.js"; import { classifyFatContainerIdentity, + findOverlappingFatSlices, readFatContainerHeader, + readFatSliceDeclarations, readMachoHeaderPrefixFacts, } from "../../domain/apple/machoContainer.js"; @@ -29,6 +31,94 @@ const classify = (bytes: Uint8Array) => { }); }; +describe("FAT declaration facts", () => { + it.each([ + { wide: false, littleEndian: false }, + { wide: false, littleEndian: true }, + { wide: true, littleEndian: false }, + { wide: true, littleEndian: true }, + ])( + "retains raw fields and reserved-field absence for wide=$wide littleEndian=$littleEndian", + async ({ wide, littleEndian }) => { + const arm64e = machoImage({ cpu: CPU.arm64e }); + const x86 = machoImage({ cpu: CPU.x86_64 }); + const bytes = fatImage( + [ + { cpu: CPU.arm64e, bytes: arm64e }, + { cpu: CPU.x86_64, bytes: x86 }, + ], + wide, + littleEndian, + ); + if (wide) + new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ).setUint32(8 + 32 + 28, 0x89abcdef, littleEndian); + const declarations = readFatSliceDeclarations( + bytes.subarray(8, 8 + 2 * (wide ? 32 : 20)), + { count: 2, wide, littleEndian }, + ); + expect(declarations).toEqual({ + status: "parsed", + slices: [ + { + index: 0, + offset: 4096, + size: arm64e.byteLength, + alignmentExponent: 12, + cpuType: CPU.arm64e.type, + cpuSubtype: CPU.arm64e.subtype, + reserved: wide ? 0 : null, + }, + { + index: 1, + offset: 8192, + size: x86.byteLength, + alignmentExponent: 12, + cpuType: CPU.x86_64.type, + cpuSubtype: CPU.x86_64.subtype, + reserved: wide ? 0x89abcdef : null, + }, + ], + }); + // Physical facts do not impose Hopper's reserved-field admission policy. + expect(classify(bytes)).toEqual({ state: "mach-o" }); + expect( + await readMachoImage(readerOf(bytes), bytes.byteLength), + ).toMatchObject({ + status: "parsed", + }); + }, + ); + + it("finds overlap in physical order while retaining declarations in table order", () => { + const bytes = fatImage([ + { cpu: CPU.arm64, bytes: machoImage({ cpu: CPU.arm64 }) }, + { cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) }, + ]); + const header = readFatContainerHeader(bytes, bytes.byteLength); + if (header.state !== "parsed") throw new Error("expected parsed FAT table"); + const first = { ...header.slices[0]!, offset: 128, size: 64 }; + const second = { ...header.slices[1]!, offset: 96, size: 64 }; + const tableOrder = [first, second]; + const overlap = findOverlappingFatSlices(tableOrder); + expect(overlap).toEqual([second, first]); + expect(overlap?.[0]).toBe(second); + expect(overlap?.[1]).toBe(first); + expect(tableOrder).toEqual([first, second]); + expect( + findOverlappingFatSlices([first, { ...second, size: 32 }]), + ).toBeNull(); + expect( + findOverlappingFatSlices([first, { ...second, size: 16 }]), + ).toBeNull(); + expect(findOverlappingFatSlices([first])).toBeNull(); + expect(findOverlappingFatSlices([])).toBeNull(); + }); +}); + describe("FAT Mach-O identity", () => { it("does not treat known CPU declarations as proof when slices are ar archives", async () => { const archive = arArchive(); diff --git a/src/cli/binaryDiagnosticsCommands.ts b/src/cli/binaryDiagnosticsCommands.ts index 91f0f6803d..fb5ecbe6b5 100644 --- a/src/cli/binaryDiagnosticsCommands.ts +++ b/src/cli/binaryDiagnosticsCommands.ts @@ -7,6 +7,7 @@ import { createElfNotesService, createPeResourcesService, createPeCertificatesService, + createPeImportsService, createNativeExportsService, } from "../composition/binaryDiagnostics.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; @@ -18,6 +19,7 @@ import { inspectBinaryLayoutInputSchema } from "../domain/native/binaryLayout.js import { inspectRecordedCrashInputSchema } from "../domain/native/recordedCrash.js"; import { inspectPeResourcesInputSchema } from "../domain/native/peResources.js"; import { inspectPeCertificatesInputSchema } from "../domain/native/peCertificates.js"; +import { inspectPeImportsInputSchema } from "../domain/native/peImports.js"; import { inspectNativeExportsInputSchema } from "../domain/native/nativeExports.js"; import { inspectElfNotesInputSchema } from "../domain/native/elfNotes.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; @@ -143,6 +145,41 @@ export const registerBinaryDiagnosticsCommands = ( ); }), }); + cli.command(CLI_COMMANDS.inspectPeImports, { + description: + "Inspect PE import and delay-load table identities without executing the image", + args: z.object({ path: z.string().describe("Local PE32/PE32+ image") }), + options: z.object({ + maxFileBytes: inspectPeImportsInputSchema.shape.max_file_bytes, + maxDlls: inspectPeImportsInputSchema.shape.max_dlls, + maxFunctions: inspectPeImportsInputSchema.shape.max_functions, + }), + run: ({ args, options }) => + withCommandCancellation(async (signal) => { + const pe = createPeImportsService(); + return await logCliCommand( + logger, + CLI_COMMANDS.inspectPeImports, + async () => + finishAnalysisCommand( + () => + pe.inspect( + { + path: absoluteCallerPath(args.path), + max_file_bytes: options.maxFileBytes, + max_dlls: options.maxDlls, + max_functions: options.maxFunctions, + }, + { signal }, + ), + pe, + "rea-pe-imports", + "inspect_pe_imports", + signal, + ), + ); + }), + }); cli.command(CLI_COMMANDS.inspectPeResources, { description: "Inspect PE resource identities, payload ranges, hashes and icon-group relationships", diff --git a/src/cli/coreBinaryCommands.ts b/src/cli/coreBinaryCommands.ts index 9268ecb436..9146bd59f6 100644 --- a/src/cli/coreBinaryCommands.ts +++ b/src/cli/coreBinaryCommands.ts @@ -18,7 +18,10 @@ import { snapshotSelectionOption, } from "./options.js"; import type { CliInstance } from "./types.js"; -import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnalysis.js"; +import { + cliJavaScriptSourcePaths, + runCliJavaScriptApplicationAnalysis, +} from "./javascriptApplicationAnalysis.js"; import { withCommandCancellation } from "./commandCancellation.js"; import { analysisEvidenceDeliveryOptions, @@ -98,6 +101,7 @@ const registerOverviewCommands = ( options: overviewOptions.extend({ integrityPolicy: laneIntegrityPolicy, sourcePath: javascriptApplicationOptions.shape.sourcePath, + sourcePathsJson: javascriptApplicationOptions.shape.sourcePathsJson, }), run: async ({ args, options }) => { // Route JavaScript targets exactly like analyze-javascript-application, @@ -105,23 +109,32 @@ const registerOverviewCommands = ( // code is not replaced by the logged failure status. if (await routesToJavaScriptAnalysis(args.path, options)) return withCommandCancellation((signal) => - logCliCommand(logger, "analyze", () => - runCliJavaScriptApplicationAnalysis( + logCliCommand(logger, "analyze", async () => { + const sourcePaths = await cliJavaScriptSourcePaths( + options.sourcePath, + options.sourcePathsJson, + signal, + ); + if (!sourcePaths.ok) return sourcePaths.error; + return runCliJavaScriptApplicationAnalysis( { input_path: absoluteCallerPath(args.path), integrity_policy: options.integrityPolicy ?? defaultIntegrityPolicy, - ...(options.sourcePath === undefined + ...(sourcePaths.value === undefined ? {} - : { source_paths: options.sourcePath }), + : { source_paths: sourcePaths.value }), }, environment, signal, - ), - ), + ); + }), ); return logCliCommand(logger, "analyze", () => { - if (options.sourcePath !== undefined) + if ( + options.sourcePath !== undefined || + options.sourcePathsJson !== undefined + ) throw new AnalysisInputError( "analyze_javascript_application", undefined, diff --git a/src/cli/electronCommands.ts b/src/cli/electronCommands.ts index d395fcb864..4dd452e852 100644 --- a/src/cli/electronCommands.ts +++ b/src/cli/electronCommands.ts @@ -22,7 +22,10 @@ import { javascriptApplicationOptions, selectedCommandTimeout, } from "../cliObservationOptions.js"; -import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnalysis.js"; +import { + cliJavaScriptSourcePaths, + runCliJavaScriptApplicationAnalysis, +} from "./javascriptApplicationAnalysis.js"; import { withCommandCancellation } from "./commandCancellation.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; @@ -322,6 +325,12 @@ const registerJavaScriptApplicationCommand = ( options, ); if (!delivery.ok) return projectAnalysisError(delivery.error); + const sourcePaths = await cliJavaScriptSourcePaths( + options.sourcePath, + options.sourcePathsJson, + signal, + ); + if (!sourcePaths.ok) return sourcePaths.error; return runCliJavaScriptApplicationAnalysis( { // An empty path names no file; leave it for input validation. @@ -347,9 +356,9 @@ const registerJavaScriptApplicationCommand = ( max_semantic_unknowns_per_file: options.maxSemanticUnknownsPerFile, }), - ...(options.sourcePath === undefined + ...(sourcePaths.value === undefined ? {} - : { source_paths: options.sourcePath }), + : { source_paths: sourcePaths.value }), ...(options.maxHeapMb === undefined ? {} : { max_heap_mb: options.maxHeapMb }), diff --git a/src/cli/erlangCommands.ts b/src/cli/erlangCommands.ts index dbdc21823d..cea8894f1a 100644 --- a/src/cli/erlangCommands.ts +++ b/src/cli/erlangCommands.ts @@ -1,23 +1,29 @@ import { z } from "incur"; +import { localPathStringSchema } from "../domain/localPath.js"; +import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { createBeamService } from "../composition/erlang.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; -import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; -import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { inspectErlangBeamInputSchema } from "../domain/erlang/beam.js"; -import { localPathStringSchema } from "../domain/localPath.js"; +import { + analysisEvidenceDeliveryOptions, + deliverCliAnalysisEvidence, + parseCliAnalysisEvidenceDelivery, +} from "./analysisEvidenceDelivery.js"; +import { erlangBeamDeliverySchema } from "../domain/analysisView/erlangBeamView.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; -/** Resolve caller paths and inspect BEAM IFF tables through the shared workflow. */ +/** Inspect operator-selected BEAM files through the same shared workflow as MCP. */ export const registerErlangCommands = ( cli: CliInstance, logger: Logger, ): void => { cli.command(CLI_COMMANDS.inspectErlangBeam, { description: - "Inspect Erlang BEAM atoms, exports, imports and the Code header without executing bytecode", + "Inspect Erlang BEAM atoms, exports, imports and Code header without executing bytecode", args: z.object({ path: localPathStringSchema.describe("Local BEAM file"), }), @@ -25,31 +31,46 @@ export const registerErlangCommands = ( maxFileBytes: inspectErlangBeamInputSchema.shape.max_file_bytes, maxChunks: inspectErlangBeamInputSchema.shape.max_chunks, maxAtoms: inspectErlangBeamInputSchema.shape.max_atoms, + ...analysisEvidenceDeliveryOptions, + view: z + .string() + .optional() + .describe( + "JSON beam view; requires --detail summary and --evidence_output", + ), }), run: ({ args, options }) => - withCommandCancellation(async (signal) => { - const service = createBeamService(); - return await logCliCommand( - logger, - CLI_COMMANDS.inspectErlangBeam, - async () => - finishAnalysisCommand( - () => - service.inspect( - { - path: absoluteCallerPath(args.path), - max_file_bytes: options.maxFileBytes, - max_chunks: options.maxChunks, - max_atoms: options.maxAtoms, - }, - { signal }, - ), - service, - "rea-erlang-beam", - "inspect_erlang_beam", - signal, - ), - ); - }), + withCommandCancellation((signal) => + logCliCommand(logger, CLI_COMMANDS.inspectErlangBeam, async () => { + const delivery = parseCliAnalysisEvidenceDelivery( + "inspect_erlang_beam", + options, + erlangBeamDeliverySchema, + ); + if (!delivery.ok) return projectAnalysisError(delivery.error); + const service = createBeamService(); + return deliverCliAnalysisEvidence({ + run: () => + service.inspect( + { + path: absoluteCallerPath(args.path), + max_file_bytes: options.maxFileBytes, + max_chunks: options.maxChunks, + max_atoms: options.maxAtoms, + }, + { signal }, + ), + service, + providerId: "rea-erlang-beam", + operation: "inspect_erlang_beam", + signal, + detail: delivery.value.detail, + evidenceOutput: delivery.value.evidenceOutput, + ...(delivery.value.view === undefined + ? {} + : { view: delivery.value.view }), + }); + }), + ), }); }; diff --git a/src/cli/javascriptApplicationAnalysis.ts b/src/cli/javascriptApplicationAnalysis.ts index c0f717f0bf..df2b80e813 100644 --- a/src/cli/javascriptApplicationAnalysis.ts +++ b/src/cli/javascriptApplicationAnalysis.ts @@ -8,6 +8,46 @@ import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup. import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; +import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; +import { javaScriptSourcePathsSchema } from "../domain/javascript/javascriptSourceSelection.js"; +import { parseCliJsonInput } from "../cliJsonInput.js"; + +const OPERATION = "analyze_javascript_application"; + +/** + * Combine repeated `--source-path` values with `--source-paths-json`, an inline + * JSON array or a JSON file. A file keeps large selections below the host + * command-line limit; entries remain exact application-relative paths and are + * not resolved against the working directory. + */ +export const cliJavaScriptSourcePaths = async ( + sourcePath: readonly string[] | undefined, + sourcePathsJson: string | undefined, + signal?: AbortSignal, +): Promise< + | { readonly ok: true; readonly value: readonly string[] | undefined } + | { readonly ok: false; readonly error: JsonValue } +> => { + if (sourcePathsJson === undefined) return { ok: true, value: sourcePath }; + const input = await parseCliJsonInput(sourcePathsJson, OPERATION, signal); + if (!input.ok) return input; + const parsed = javaScriptSourcePathsSchema.safeParse(input.value); + if (!parsed.success) + return { + ok: false, + error: cliError( + analysisInputErrorFromIssues( + OPERATION, + parsed.error.issues.map((issue) => ({ + ...issue, + path: ["source_paths_json", ...issue.path], + })), + { source_paths_json: input.value }, + ), + ), + }; + return { ok: true, value: [...(sourcePath ?? []), ...parsed.data] }; +}; /** * One scope for every JavaScript analysis in this process. A healthy worker diff --git a/src/cliCommandNames.ts b/src/cliCommandNames.ts index b9ac7075bf..ae3f188fd1 100644 --- a/src/cliCommandNames.ts +++ b/src/cliCommandNames.ts @@ -71,6 +71,7 @@ export const CLI_COMMANDS = Object.freeze({ inspectErlangBeam: "inspect-erlang-beam", inspectPeResources: "inspect-pe-resources", inspectPeCertificates: "inspect-pe-certificates", + inspectPeImports: "inspect-pe-imports", inspectNativeExports: "inspect-native-exports", inspectElfNotes: "inspect-elf-notes", inspectAnalysisView: "inspect-analysis-view", diff --git a/src/cliObservationOptions.ts b/src/cliObservationOptions.ts index e78df7403e..ad15af9146 100644 --- a/src/cliObservationOptions.ts +++ b/src/cliObservationOptions.ts @@ -88,6 +88,13 @@ export const javascriptApplicationOptions = z.object({ .describe( "Exact inventoried file path relative to the application root; repeat to select several. No implicit dependencies.", ), + sourcePathsJson: z + .string() + .min(1) + .optional() + .describe( + "Inline JSON array or JSON file path of exact inventoried file paths relative to the application root, selected after any --source-path values. Use a file for selections that exceed the host command-line limit.", + ), maxHeapMb: javaScriptAnalysisResourceControlsSchema.shape.max_heap_mb, analysisTimeoutMs: javaScriptAnalysisResourceControlsSchema.shape.analysis_timeout_ms, diff --git a/src/composition/binaryDiagnostics.ts b/src/composition/binaryDiagnostics.ts index b0b562bf41..df34d4ee53 100644 --- a/src/composition/binaryDiagnostics.ts +++ b/src/composition/binaryDiagnostics.ts @@ -8,6 +8,8 @@ import { PeResourcesService } from "../application/binaryDiagnostics/PeResources import { PeResourcesProvider } from "../native/pe/PeResourcesProvider.js"; import { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js"; import { PeCertificatesProvider } from "../native/pe/PeCertificatesProvider.js"; +import { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.js"; +import { PeImportsProvider } from "../native/pe/PeImportsProvider.js"; import { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js"; import { ElfNotesProvider } from "../native/elf/ElfNotesProvider.js"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; @@ -24,6 +26,10 @@ export const createPeResourcesService = (): PeResourcesService => export const createPeCertificatesService = (): PeCertificatesService => new PeCertificatesService(new PeCertificatesProvider()); +/** Portable PE import-table inspection with no engine or catalog verification. */ +export const createPeImportsService = (): PeImportsService => + new PeImportsService(new PeImportsProvider()); + /** Portable PE/ELF export tables with no engine, toolchain or target acquisition. */ export const createNativeExportsService = (): NativeExportsService => new NativeExportsService(new NativeExportsProvider()); diff --git a/src/contracts/analysisViewToolContracts.ts b/src/contracts/analysisViewToolContracts.ts index 5ecaa90a57..211148ba18 100644 --- a/src/contracts/analysisViewToolContracts.ts +++ b/src/contracts/analysisViewToolContracts.ts @@ -17,7 +17,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [ ...toolContractMetadata("inspect_analysis_view"), kind: "application", description: - 'Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Inspect selected views of supported completed Evidence without repeating analysis. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized reads that are not entities and have one initial request line (item-shaped paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state (a list also learns the filters, search, sort and paging its recordings verify, and answers new values of them from the state), and every other read tool is a recorded view with a reason. It also reports a write plan per state-changing tool (create, update, delete or document, built from its first successful call) and write_clock; a plan is served only when replaying the capture shows a later read confirming its effect and none contradicting it. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. The environment-coverage view, view {kind: "environment-coverage", template_rules: [...]}, summarizes that state without its entities: reads.state and reads.recorded count read tools answered from the state and replayed (recorded_reasons per view reason), writes.served lists the served write tools, and writes.withheld lists each other write tool with its reason, plan record, contradicting read and a hint about what to record next; it refuses what the environment-state view refuses. For a retained record use source: {kind: "retained-evidence", evidence_id: ""}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: }. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: ""}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. The file may also be an Evidence bundle written by CLI --evidence_output or export_evidence_bundle; select evidence_id when it contains more than one record. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Typed module references use view {kind: "javascript", collection: "modules", projection: "references", where: {path: "main.js", kind: "javascript-module"}, offset: 0, limit: 64}; where matches exact retained strings (null selects unavailable paths). Rows include canonical identity, retained source-range count and observation support states. Source coverage and selection remain explicit; exhausting matching rows does not prove runtime loading or complete analysis. For exact file/native facts and processing gaps, use view {kind: "javascript-facts", where: {paths: ["package.json"]}, property_keys: ["name", "operation", "analysis_status"]}. Rows page original observations with canonical identities and Evidence; missing properties remain omitted. Paths match recorded artifact-path/source-range locations; optional kinds filter nodes. To discover field names without values, opt into include_available_property_keys: true with property_keys: []. Canonical identity and Evidence are separate from properties; keep each node_id/observation_id pair together when composing results. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions. Occurrences accept exact logical_paths and occurrence_ids; nodes accept artifact_ids. Batch filters intersect (omitted means all available, [] means none); fields selects columns while retaining identity links. source_complete describes retained graph membership; source_coverage describes producer coverage, including unknown encrypted PCK directories. Page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. Mach-O views use kind macho and facet entrypoints/architectures/build_metadata/load_commands/dependencies/imports/exports/segments/provenance; source_exhaustive false means unreturned rows may exist. Interface Builder views use kind interface-builder and collection documents/nodes/edges/coverage; nodes and edges accept an exact document relative_path. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Managed artifact summaries preserve complete identity/classification, metadata status, module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed pages use kind managed and collection references/resources/attributes, retaining source coverage and build-local token scope; page exhaustion does not establish complete metadata. Managed member summaries keep identity, selection with unmatched names, coverage and per-collection counts; managed-members pages use collection types/exported_types/fields/methods/member_refs/call_edges/field_accesses/user_strings and carry the source selection and coverage. Android package summaries retain all declarations, engine identity and counts without decoded manifest text. Android manifest pages use kind android, facet manifest, offset and limit in UTF-16 code units; source_status preserves partial decoded coverage even when the retained text page is exhausted. Dart AOT summary preserves target/ABI/library identity, snapshot headers and pool lower bounds. String pages use kind dart-aot, collection package_uris/dart_uris/class_like_tokens; source coverage remains partial when the producer says partial. Protobuf views use kind protobuf, collection bodies/frames/fields, with record_ordinals, body_pointers and fields; field pages also select frame_indices, field_ids, field_numbers or schema_names. Byte identity, offset basis and body status remain present. Text and schema_value are opt-in page columns. APK signing views use kind apk-signature and collection signers/digests/lineage, with ids, signer_ids, schemes, fields and paging. Identity links remain when fields is empty. Declared identities and digest matches do not establish signature verification or authenticity. Model views use kind model-artifact and collection metadata (keys), tensors (names/types) or types (derived totals), with indices, fields and paging. Header SHA-256 scopes declarations; full-file identity remains unknown unless full hashing was selected. Arrays retain typed eight-element previews with explicit omissions. IL2CPP metadata views use kind il2cpp-metadata and collection sections/images/types/methods/fields/properties/events/parameters/string_literals/code_gen_modules/method_addresses/return_types/field_types/parameter_types. Select indices, fields and pages; sections/images/types/methods/fields/properties/events/parameters accept names, types also accept image_indices/namespaces/declaring_types, and methods/members accept declaring_types. Parameters and parameter_types accept method_indices; code_gen_modules accept image_indices; method_addresses accept addresses/states, preserving every folded alternative. Row index is scoped to the metadata digest; native pages add binary_source. Uncollected native tables have null counts and remain unavailable. Desktop directory views use kind desktop-application and collection files/omissions/candidates/components/next_probes, with fields and pagination; files and omissions accept exact paths or their stable IDs, and candidates accept families. Source coverage and stable row IDs remain available. Native inventory Evidence from list_names, list/search_procedures or list/search_strings uses view: {kind: "native-inventory", offset: 0, limit: 64, fields: ["value"]}. Rows keep source_index and address even with selected fields. Summary counts describe retained rows; provider discovery coverage remains unknown. Native function Evidence accepts kind summary for identity, availability and facet counts, or kind native for selected facts: view: {kind: "native", facet: "procedure"}. Facets select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.', + 'Use javascript-routes collections to query route patterns, source-linked occurrences, source status, unknowns or summary counts from retained JavaScript application or web-bundle Evidence. Inspect selected views of supported completed Evidence without repeating analysis. Choose a view compatible with the parent operation described below. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Native export tables use kind native-exports, collection exports/issues, exact names or original indices, selected fields and matching-row pagination; parent Evidence plus source_index identifies each row. Source coverage stays separate from page coverage. BEAM views use kind beam, collections atoms/exports/imports/lambdas/chunks, exact names/indices/fields and matching-row pages; import modules intersects function names. source_index identifies original parent-and-collection rows, preserving every arity and raw atom index. beam.counts preserves null missing tables versus decoded empty tables; runtime absence remains unknown. PYC views use kind python-pyc, collection code_objects, names/indices/fields and paging; source_index scopes every same-name alternative to the parent, nested_code keeps code-constant references, and pyc.coverage preserves source coverage. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized reads that are not entities and have one initial request line (item-shaped paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state (a list also learns the filters, search, sort and paging its recordings verify, and answers new values of them from the state), and every other read tool is a recorded view with a reason. It also reports a write plan per state-changing tool (create, update, delete or document, built from its first successful call) and write_clock; a plan is served only when replaying the capture shows a later read confirming its effect and none contradicting it. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. The environment-coverage view, view {kind: "environment-coverage", template_rules: [...]}, summarizes that state without its entities: reads.state and reads.recorded count read tools answered from the state and replayed (recorded_reasons per view reason), writes.served lists the served write tools, and writes.withheld lists each other write tool with its reason, plan record, contradicting read and a hint about what to record next; it refuses what the environment-state view refuses. For a retained record use source: {kind: "retained-evidence", evidence_id: ""}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: }. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: ""}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. The file may also be an Evidence bundle written by CLI --evidence_output or export_evidence_bundle; select evidence_id when it contains more than one record. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Typed module references use view {kind: "javascript", collection: "modules", projection: "references", where: {path: "main.js", kind: "javascript-module"}, offset: 0, limit: 64}; where matches exact retained strings (null selects unavailable paths). Rows include canonical identity, retained source-range count and observation support states. Source coverage and selection remain explicit; exhausting matching rows does not prove runtime loading or complete analysis. For exact file/native facts and processing gaps, use view {kind: "javascript-facts", where: {paths: ["package.json"]}, property_keys: ["name", "operation", "analysis_status"]}. Rows page original observations with canonical identities and Evidence; missing properties remain omitted. Paths match recorded artifact-path/source-range locations; optional kinds filter nodes. To discover field names without values, opt into include_available_property_keys: true with property_keys: []. Canonical identity and Evidence are separate from properties; keep each node_id/observation_id pair together when composing results. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions. Occurrences accept exact logical_paths and occurrence_ids; nodes accept artifact_ids. Batch filters intersect (omitted means all available, [] means none); fields selects columns while retaining identity links. source_complete describes retained graph membership; source_coverage describes producer coverage, including unknown encrypted PCK directories. Page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. Mach-O views use kind macho and facet entrypoints/architectures/build_metadata/load_commands/dependencies/imports/exports/segments/provenance; source_exhaustive false means unreturned rows may exist. Interface Builder views use kind interface-builder and collection documents/nodes/edges/coverage; nodes and edges accept an exact document relative_path. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Managed artifact summaries preserve complete identity/classification, metadata status, module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed pages use kind managed and collection references/resources/attributes, retaining source coverage and build-local token scope; page exhaustion does not establish complete metadata. Managed member summaries keep identity, selection with unmatched names, coverage and per-collection counts; managed-members pages use collection types/exported_types/fields/methods/member_refs/call_edges/field_accesses/user_strings and carry the source selection and coverage. Android package summaries retain all declarations, engine identity and counts without decoded manifest text. Android manifest pages use kind android, facet manifest, offset and limit in UTF-16 code units; source_status preserves partial decoded coverage even when the retained text page is exhausted. Dart AOT summary preserves target/ABI/library identity, snapshot headers and pool lower bounds. String pages use kind dart-aot, collection package_uris/dart_uris/class_like_tokens; source coverage remains partial when the producer says partial. Protobuf views use kind protobuf, collection bodies/frames/fields, with record_ordinals, body_pointers and fields; field pages also select frame_indices, field_ids, field_numbers or schema_names. Byte identity, offset basis and body status remain present. Text and schema_value are opt-in page columns. APK signing views use kind apk-signature and collection signers/digests/lineage, with ids, signer_ids, schemes, fields and paging. Identity links remain when fields is empty. Declared identities and digest matches do not establish signature verification or authenticity. Model views use kind model-artifact and collection metadata (keys), tensors (names/types) or types (derived totals), with indices, fields and paging. Header SHA-256 scopes declarations; full-file identity remains unknown unless full hashing was selected. Arrays retain typed eight-element previews with explicit omissions. IL2CPP metadata views use kind il2cpp-metadata and collection sections/images/types/methods/fields/properties/events/parameters/string_literals/code_gen_modules/method_addresses/return_types/field_types/parameter_types. Select indices, fields and pages; sections/images/types/methods/fields/properties/events/parameters accept names, types also accept image_indices/namespaces/declaring_types, and methods/members accept declaring_types. Parameters and parameter_types accept method_indices; code_gen_modules accept image_indices; method_addresses accept addresses/states, preserving every folded alternative. Row index is scoped to the metadata digest; native pages add binary_source. Uncollected native tables have null counts and remain unavailable. Desktop directory views use kind desktop-application and collection files/omissions/candidates/components/next_probes, with fields and pagination; files and omissions accept exact paths or their stable IDs, and candidates accept families. Source coverage and stable row IDs remain available. Native inventory Evidence from list_names, list/search_procedures or list/search_strings uses view: {kind: "native-inventory", offset: 0, limit: 64, fields: ["value"]}. Rows keep source_index and address even with selected fields. Summary counts describe retained rows; provider discovery coverage remains unknown. Native function Evidence accepts kind summary for identity, availability and facet counts, or kind native for selected facts: view: {kind: "native", facet: "procedure"}. Facets select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.', inputSchema: inspectAnalysisViewInputSchema, outputSchema: evidenceResultOf(analysisViewResultSchema), examples: [ diff --git a/src/contracts/erlang/beamToolContracts.ts b/src/contracts/erlang/beamToolContracts.ts index 2112f46534..6cd77f7be0 100644 --- a/src/contracts/erlang/beamToolContracts.ts +++ b/src/contracts/erlang/beamToolContracts.ts @@ -1,12 +1,29 @@ import { - erlangBeamSchema, inspectErlangBeamInputSchema, + erlangBeamSchema, } from "../../domain/erlang/beam.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; -import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; +import { analysisDetailOutputSchemaOf } from "../analysisDetail.js"; +import { z } from "zod"; +import { + erlangBeamSummaryViewResultSchema, + erlangBeamTableViewResultSchema, +} from "../../domain/analysisView/analysisView.js"; +import { + erlangBeamCompleteDeliveryShape, + erlangBeamSummaryDeliveryShape, +} from "../../domain/analysisView/erlangBeamView.js"; -/** Static BEAM IFF inspection is independent of active native-analysis providers. */ +/** Delivery selection does not change provider inputs or complete Evidence identity. */ +export const inspectErlangBeamRequestSchema = z + .discriminatedUnion("detail", [ + inspectErlangBeamInputSchema.extend(erlangBeamCompleteDeliveryShape), + inspectErlangBeamInputSchema.extend(erlangBeamSummaryDeliveryShape), + ]) + .meta({ "x-rea-root-discriminator": "detail" }); + +/** Static BEAM inspection is independent of active native-analysis providers. */ export const ERLANG_BEAM_TOOL_CONTRACTS = [ { name: "inspect_erlang_beam", @@ -14,12 +31,31 @@ export const ERLANG_BEAM_TOOL_CONTRACTS = [ kind: "artifact-provider", requiresActiveTarget: true, description: - "Inspect one explicit local Erlang BEAM IFF container without executing bytecode or installing Erlang. Returns FOR1 form bounds, chunk names, payload SHA-256, Atom/AtU8 names, export/import/lambda tables with resolved atom names when present, and the Code chunk instruction-set header. Missing optional tables stay null rather than empty-everywhere. Reads a stable regular file; malformed FOR1/BEAM headers, truncated chunks and exhausted budgets return no partial success. Portable on supported Node hosts; no BEAM emulator is required.", - inputSchema: inspectErlangBeamInputSchema, - outputSchema: evidenceResultOf(erlangBeamSchema), + "Inspect one explicit local Erlang BEAM IFF container without executing bytecode or installing Erlang. Returns form/chunk byte ranges and payload SHA-256, atoms, export/import/lambda tables with resolved atom names, and the Code header. Complete output is the default. detail summary retains complete Evidence and returns module/header/counts; include a beam view for selected atoms/exports/imports/lambdas/chunks in the first call, then page retained or offline facts with inspect_analysis_view. Exact names/indices/fields select facts; import modules intersects function names. Same-name arities survive under parent-and-collection-scoped source_index; atom and lambda indexes retain their original meaning. Missing tables stay null in counts; unresolved names stay null and runtime absence remains unknown. Reads a stable regular file; malformed/truncated input and exhausted parser budgets return no partial success. Portable on supported Node hosts; no BEAM emulator required.", + inputSchema: inspectErlangBeamRequestSchema, + outputSchema: analysisDetailOutputSchemaOf( + erlangBeamSchema, + z.union([ + erlangBeamSummaryViewResultSchema, + erlangBeamTableViewResultSchema, + ]), + ), examples: [ { - title: "Inspect BEAM atoms, exports and the Code header", + title: "Return selected exports and retain the complete BEAM report", + input: { + path: "/artifacts/app.beam", + detail: "summary", + view: { + kind: "beam", + collection: "exports", + names: ["module_info"], + fields: ["function_index", "function", "arity", "label"], + }, + }, + }, + { + title: "Inspect the complete BEAM structure", input: { path: "/artifacts/app.beam" }, }, ], diff --git a/src/contracts/erlangBeamDeliveryContracts.test.ts b/src/contracts/erlangBeamDeliveryContracts.test.ts new file mode 100644 index 0000000000..4a8f4a670e --- /dev/null +++ b/src/contracts/erlangBeamDeliveryContracts.test.ts @@ -0,0 +1,88 @@ +import { Ajv2020 } from "ajv/dist/2020.js"; +import { Ajv } from "ajv"; +import { expect, it } from "vitest"; +import { toolContract } from "./toolContracts.js"; +import { toolInputSchemaWithMetadata } from "./toolSchemaMetadata.js"; + +const view = { + kind: "beam", + collection: "exports", + fields: ["function"], +}; + +it.each(["draft-2020-12", "draft-07"] as const)( + "advertises BEAM delivery combinations in %s", + (target) => { + const contract = toolContract("inspect_erlang_beam"); + const projected = toolInputSchemaWithMetadata(contract)[ + "~standard" + ].jsonSchema.input({ target }); + const ajv = + target === "draft-2020-12" + ? new Ajv2020({ strict: false, validateFormats: false }) + : new Ajv({ strict: false, validateFormats: false }); + expect(ajv.validateSchema(projected)).toBe(true); + expect(projected).toMatchObject({ type: "object" }); + for (const key of ["anyOf", "oneOf", "allOf", "x-rea-root-discriminator"]) + expect(projected).not.toHaveProperty(key); + const validate = ajv.compile(projected); + for (const [delivery, valid] of [ + [{}, true], + [{ detail: "complete" }, true], + [{ detail: "summary" }, true], + [{ detail: "summary", view }, true], + [ + { + detail: "summary", + view: { ...view, names: [], fields: [], indices: [] }, + }, + true, + ], + [{ view }, false], + [{ detail: "complete", view }, false], + [{ detail: "summary", view: { ...view, fields: ["missing"] } }, false], + [{ detail: "summary", view: { ...view, kind: "native-exports" } }, false], + [{ detail: "summary", view, unexpected: true }, false], + [{ detail: "summary", view: { ...view, fields: ["name"] } }, false], + [{ detail: "summary", view: { ...view, modules: ["erlang"] } }, false], + [ + { + detail: "summary", + view: { ...view, collection: "imports", modules: ["erlang"] }, + }, + true, + ], + [ + { + detail: "summary", + view: { ...view, collection: "chunks", fields: ["payload"] }, + }, + true, + ], + [ + { + detail: "summary", + view: { ...view, collection: "atoms", fields: ["index", "name"] }, + }, + true, + ], + [ + { + detail: "summary", + view: { ...view, collection: "lambdas", fields: ["free_vars"] }, + }, + true, + ], + ] as const) { + const input = { path: "/tmp/module.beam", ...delivery }; + expect( + contract.inputSchema.safeParse(input).success, + JSON.stringify(input), + ).toBe(valid); + expect( + validate(input), + JSON.stringify({ input, errors: validate.errors }), + ).toBe(valid); + } + }, +); diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts index 65d938465b..646b16f0b4 100644 --- a/src/contracts/native/binaryDiagnosticsToolContracts.ts +++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts @@ -32,6 +32,10 @@ import { inspectPeCertificatesInputSchema, peCertificatesSchema, } from "../../domain/native/peCertificates.js"; +import { + inspectPeImportsInputSchema, + peImportsSchema, +} from "../../domain/native/peImports.js"; import { inspectElfNotesInputSchema, elfNotesSchema, @@ -109,6 +113,21 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [ }, ], }, + { + name: "inspect_pe_imports", + ...toolContractMetadata("inspect_pe_imports"), + kind: "native-provider", + description: + "Inspect one explicit local PE32/PE32+ image's IMAGE_DIRECTORY_ENTRY_IMPORT and delay-load tables without executing it or opening a disassembler. Returns DLL names, named and ordinal functions, ILT/IAT RVAs, delay-load descriptors, and the pefile import hash of the standard import table, withheld with a reason when that table is partial or pefile would hash a different row sequence. A missing import directory is observed absence of that table, not proof the image never imported APIs through another table or overlay. Bound snapshots, loaded IAT contents and export-forward resolution remain unobserved. Reads a stable regular file; malformed PE headers and exhausted DLL or function budgets return no partial success. Unreadable names or thunks and descriptors missing a required table are reported as partial coverage with issues. Portable on supported Node hosts; no Windows SDK or engine installation required.", + inputSchema: inspectPeImportsInputSchema, + outputSchema: evidenceResultOf(peImportsSchema), + examples: [ + { + title: "Inspect PE import and delay-load identities", + input: { path: "/artifacts/application.exe" }, + }, + ], + }, { name: "inspect_pe_resources", ...toolContractMetadata("inspect_pe_resources"), diff --git a/src/contracts/native/nativeToolContracts.ts b/src/contracts/native/nativeToolContracts.ts index ff5d16aa0a..dc64d451dd 100644 --- a/src/contracts/native/nativeToolContracts.ts +++ b/src/contracts/native/nativeToolContracts.ts @@ -115,7 +115,7 @@ export const NATIVE_TOOL_CONTRACTS = [ ), native( "inspect_plist", - "Requires an open binary or bundle-resource session; use open_binary or open_bundle_resources first. Parse its Info.plist by default, or select a local plist with path within that session. Supplying path does not open a session. Returns normalized JSON rather than plutil text. native_command_timeout_ms is each file or plutil command's wall clock (omitted means 60000 ms). The 67108864-byte plist input and decode allowances and the 128-level nesting allowance have no runtime override. The 4096-byte markup probe is a probe window, not a document cap, and has no runtime override.", + "Requires an open binary or bundle-resource session; use open_binary or open_bundle_resources first. Parse its Info.plist by default, or select a local plist with path within that session. Supplying path does not open a session. Returns normalized JSON rather than plutil text. native_command_timeout_ms is each file or plutil command's wall clock (omitted means 60000 ms). The 67108864-byte plist input and decode allowances and the 998-level nesting allowance have no runtime override. XML sources also require native plutil validation, which can refuse shallower inputs with its own diagnostics. The 4096-byte markup probe is a probe window, not a document cap, and has no runtime override.", z.object({ path: localPathStringSchema .optional() diff --git a/src/contracts/toolEffects.test.ts b/src/contracts/toolEffects.test.ts index d4d701f8cb..2761ed5cd3 100644 --- a/src/contracts/toolEffects.test.ts +++ b/src/contracts/toolEffects.test.ts @@ -32,6 +32,7 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly> = { inspect_desktop_application: "states that directory triage performs no target execution", inspect_pe_certificates: "states that it does not execute the image", + inspect_pe_imports: "states that it does not execute the image", inspect_go_binary: "reads embedded metadata without executing the target", inspect_rust_binary: "reads an embedded dependency list without executing the target", diff --git a/src/contracts/toolEffects.ts b/src/contracts/toolEffects.ts index 4ae4149a96..fc5ee54690 100644 --- a/src/contracts/toolEffects.ts +++ b/src/contracts/toolEffects.ts @@ -258,6 +258,7 @@ export const TOOL_EFFECTS: Readonly> = { inspect_byte_entropy: evidence, inspect_pe_resources: evidence, inspect_pe_certificates: evidence, + inspect_pe_imports: evidence, inspect_native_exports: evidence, inspect_elf_notes: evidence, inspect_analysis_view: evidence, diff --git a/src/domain/analysisView/EnvironmentStateAccumulator.ts b/src/domain/analysisView/EnvironmentStateAccumulator.ts index 60e474179a..1b2d6a6dc3 100644 --- a/src/domain/analysisView/EnvironmentStateAccumulator.ts +++ b/src/domain/analysisView/EnvironmentStateAccumulator.ts @@ -1,12 +1,13 @@ import { z } from "zod"; -import { - compareClockInstants, - readClock, -} from "./environmentStateWriteValues.js"; import type { ScenarioResponseBodyRetention } from "./scenarioCaptureRecords.js"; import type { AnalysisError } from "../analysisErrorBase.js"; import { parseExactJson } from "../exactJsonNumber.js"; import { MAX_JSON_DEPTH, type JsonValue } from "../jsonValue.js"; +import { + compareOffsetDateTimeInstants, + readRecordedDateTime, + type OffsetDateTimeInstant, +} from "../offsetDateTime.js"; import { ok, type Result } from "../result.js"; import { WEB_NETWORK_CAPTURE_LIMITS, @@ -110,8 +111,8 @@ interface RecordEntry { /** Null for a write whose URL was redacted: its target is unknown, so it bounds every type. */ readonly origin: string | null; readonly contentType: string; - readonly timed: boolean; - readonly subMillisecond: number; + /** Exact ordering fact; request interval coordinates remain milliseconds. */ + readonly startInstant: OffsetDateTimeInstant | undefined; readonly record: WebNetworkCaptureRecord; } @@ -159,20 +160,40 @@ export class EnvironmentStateAccumulator extends EnvironmentManifestAccumulator const line = readRecordedRequestLine(record.reported, true, this.scratch); if (line === null) return this.overBudget(); const timing = timingSchema.safeParse(record.reported); - const clock = timing.success - ? readClock(timing.data.startedDateTime) + // Admit normalization and fraction parsing before materializing their strings. + if ( + timing.success && + !this.scratch( + RECORD_SCRATCH_BYTES + + textBytes(timing.data.startedDateTime.length + 6) * 3, + ) + ) + return this.overBudget(); + const startInstant = timing.success + ? readRecordedDateTime(timing.data.startedDateTime)?.instant : undefined; - const start = clock?.ms ?? record.ordinal; - const timed = timing.success && clock !== undefined; + // Keep the established millisecond interval policy independently of sorting. + const start = + startInstant === undefined + ? record.ordinal + : startInstant.epochSecond * 1000 + + Number(startInstant.fraction.slice(0, 3).padEnd(3, "0")); + // A fraction substring may retain its normalized timestamp's backing store, + // including trailing zeroes removed from the exact comparison value. + const instantBytes = + timing.success && startInstant !== undefined + ? ENTRY_BYTES + textBytes(timing.data.startedDateTime.length + 6) + : 0; if (line.kind === "excluded") return line.reason === "url-redacted" ? this.#redactedWrite( record, - timing.success && timed + timing.success && startInstant !== undefined ? { start, end: start + timing.data.time, - subMillisecond: clock.subMillisecond, + startInstant, + instantBytes, } : undefined, ) @@ -181,6 +202,7 @@ export class EnvironmentStateAccumulator extends EnvironmentManifestAccumulator if ( !this.retain( ENTRY_BYTES * 8 + + instantBytes + textBytes( line.method.length + line.origin.length + @@ -198,13 +220,15 @@ export class EnvironmentStateAccumulator extends EnvironmentManifestAccumulator path: line.path, query: line.query === "" ? undefined : line.query, status: line.status, - start: timed ? start : record.ordinal, - end: timed ? start + timing.data.time : record.ordinal, + start, + end: + timing.success && startInstant !== undefined + ? start + timing.data.time + : record.ordinal, }, origin: line.origin, contentType: line.rawMime, - timed, - subMillisecond: clock?.subMillisecond ?? 0, + startInstant, record, }); return ok(undefined); @@ -220,7 +244,8 @@ export class EnvironmentStateAccumulator extends EnvironmentManifestAccumulator | { readonly start: number; readonly end: number; - readonly subMillisecond: number; + readonly startInstant: OffsetDateTimeInstant; + readonly instantBytes: number; } | undefined, ): Result { @@ -237,14 +262,19 @@ export class EnvironmentStateAccumulator extends EnvironmentManifestAccumulator end: timing?.end ?? record.ordinal, }; if (!isStateWrite(facts)) return ok(undefined); - if (!this.retain(ENTRY_BYTES * 8 + textBytes(facts.method.length))) + if ( + !this.retain( + ENTRY_BYTES * 8 + + textBytes(facts.method.length) + + (timing?.instantBytes ?? 0), + ) + ) return this.overBudget(); this.#entries.set(record.ordinal, { facts, origin: null, contentType: "", - timed: timing !== undefined, - subMillisecond: timing?.subMillisecond ?? 0, + startInstant: timing?.startInstant, record, }); return ok(undefined); @@ -316,16 +346,17 @@ export class EnvironmentStateAccumulator extends EnvironmentManifestAccumulator (entry) => entry.origin === null, ).length; const timeline: StateTimeline = - entries.length > 0 && entries.every((entry) => entry.timed) + entries.length > 0 && + entries.every((entry) => entry.startInstant !== undefined) ? "recorded-start-time" : "record-order"; const records = entries .sort((left, right) => timeline === "record-order" ? left.facts.ordinal - right.facts.ordinal - : compareClockInstants( - { ms: left.facts.start, subMillisecond: left.subMillisecond }, - { ms: right.facts.start, subMillisecond: right.subMillisecond }, + : compareOffsetDateTimeInstants( + left.startInstant!, + right.startInstant!, ) || left.facts.ordinal - right.facts.ordinal, ) .map((entry) => entry.facts) diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index f394b88367..4f8d983671 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -8,6 +8,12 @@ import { apkSignatureSourceCoverageSchema, } from "./apkSignatureView.js"; import { byteEntropySchema } from "../native/byteEntropy.js"; +import { + erlangBeamViewRequestSchema, + erlangBeamViewPageSchema, + erlangBeamSummarySchema, + projectErlangBeamView, +} from "./erlangBeamView.js"; import { pythonPycViewRequestSchema, pythonPycViewItemsSchema, @@ -345,6 +351,7 @@ const nativeViewSchema = z.strictObject({ /** Caller-selected projection of already completed analysis Evidence. */ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [ + erlangBeamViewRequestSchema, pythonPycViewRequestSchema, nativeExportsViewRequestSchema, javaScriptRoutesViewRequestSchema, @@ -471,6 +478,7 @@ const viewResultBase = { parent_evidence_id: prefixedDigestSchema("ev"), parent_operation: z.enum([ "inspect_python_pyc", + "inspect_erlang_beam", "inspect_byte_entropy", "inspect_native_exports", "inspect_binary_layout", @@ -526,6 +534,20 @@ export const nativeExportsSummaryViewResultSchema = z.strictObject({ summary: nativeExportsSummarySchema, }); +export const erlangBeamSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_erlang_beam"), + summary: erlangBeamSummarySchema, +}); +export const erlangBeamTableViewResultSchema = z.strictObject({ + ...viewResultBase, + kind: z.literal("beam"), + parent_operation: z.literal("inspect_erlang_beam"), + view: erlangBeamViewRequestSchema, + beam: erlangBeamSummarySchema, + page: erlangBeamViewPageSchema, +}); + export const pythonPycSummaryViewResultSchema = z.strictObject({ ...summaryViewResultBase, parent_operation: z.literal("inspect_python_pyc"), @@ -961,6 +983,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [ view: environmentCoverageViewRequestSchema, }), z.discriminatedUnion("parent_operation", [ + erlangBeamSummaryViewResultSchema, pythonPycSummaryViewResultSchema, nativeExportsSummaryViewResultSchema, byteEntropySummaryViewResultSchema, @@ -992,6 +1015,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [ nativeFunctionSummaryViewResultSchema, sourceToBundleSummaryViewResultSchema, ]), + erlangBeamTableViewResultSchema, pythonPycCodeObjectsViewResultSchema, z.strictObject({ ...viewResultBase, @@ -1260,6 +1284,7 @@ const parentArtifactPath = (parent: AnalysisViewParent): string => { /** Named source owners for views that need no extra decoder or accumulator. */ const retainedViewProjectors = new Map([ + ["inspect_erlang_beam", projectErlangBeamView], ["inspect_native_exports", projectNativeExportsView], ["inspect_python_pyc", projectPythonPycView], ["frida_session_status", projectFridaSessionStatusView], diff --git a/src/domain/analysisView/environmentStateView.test.ts b/src/domain/analysisView/environmentStateView.test.ts index f1b5e7342d..0b160e70d5 100644 --- a/src/domain/analysisView/environmentStateView.test.ts +++ b/src/domain/analysisView/environmentStateView.test.ts @@ -13,6 +13,7 @@ import { } from "../../../tests/fixtures/browserScenarioCapture.js"; import type { Evidence } from "../evidence.js"; import type { JsonValue } from "../jsonValue.js"; +import { webNetworkCaptureSchema } from "../webNetworkCapture.js"; import { projectAnalysisView, type AnalysisViewParent, @@ -967,6 +968,66 @@ it("chooses the earliest related write in timeline order, across ancestor and de expect(state.entities).toMatchObject([{ fields: { title: "initial" } }]); }); +it.each<{ + spelling: string; + starts: readonly [string, string, string]; +}>([ + { + spelling: "minute-only offset timestamps", + starts: ["2026-10-20T09:00Z", "2026-10-20T09:02Z", "2026-10-20T09:01Z"], + }, + { + spelling: "ten-digit fractional timestamps", + starts: [ + "2026-10-20T09:00:00.0000000001Z", + "2026-10-20T09:00:00.0000000003Z", + "2026-10-20T09:00:00.0000000002Z", + ], + }, +])( + "keeps the pre-write document when $spelling put the write before an earlier ordinal", + ({ starts }) => { + const entries = starts.map((startedDateTime, ordinal) => ({ + startedDateTime, + // Zero-duration requests isolate start ordering from interval precision. + time: 0, + request: { + method: ordinal === 2 ? "PATCH" : "GET", + url: "http://app.example/api/settings", + ...(ordinal === 2 + ? { + postData: { + mimeType: "application/json", + text: '{"theme":"dark"}', + }, + } + : {}), + }, + response: { + status: 200, + content: { + mimeType: "application/json", + text: ordinal === 0 ? '{"theme":"light"}' : '{"theme":"dark"}', + }, + }, + })); + const capture = webNetworkCaptureSchema.parse( + environmentManifestCaptureFixture(entries), + ); + expect(capture.records.map((record) => record.reported)).toEqual(entries); + const state = stateOf(parentOf(analysisViewCaptureEvidence(capture))); + expect(state.documents).toEqual([ + { + tool: "get_settings", + path: "/api/settings", + value: { theme: "light" }, + record_ordinal: 0, + }, + ]); + expect(state.timeline).toBe("recorded-start-time"); + }, +); + it("bounds a parameterized namespace by concrete writes before publishing new items", () => { const result = projectAnalysisView( captureOf([ diff --git a/src/domain/analysisView/environmentStateWriteValues.ts b/src/domain/analysisView/environmentStateWriteValues.ts index c9eb0eb3e2..65a2008923 100644 --- a/src/domain/analysisView/environmentStateWriteValues.ts +++ b/src/domain/analysisView/environmentStateWriteValues.ts @@ -1,9 +1,6 @@ import { createHash } from "node:crypto"; import type { JsonValue } from "../jsonValue.js"; -import { - offsetDateTimeSchema, - parseOffsetDateTimeInstant, -} from "../offsetDateTime.js"; +import { readRecordedDateTime } from "../offsetDateTime.js"; /** How a recorded clock value is spelled: ISO-8601 with its separator, fraction digits and zone, or an epoch number. */ export type ClockFormat = @@ -22,14 +19,6 @@ interface ClockReading { readonly format: ClockFormat; } -/** Compare recorded instants without rounding away their submillisecond digits. */ -export const compareClockInstants = ( - left: Pick, - right: Pick, -): number => left.ms - right.ms || left.subMillisecond - right.subMillisecond; - -const ISO_INSTANT = - /^\d{4}-\d{2}-\d{2}([T ])\d{2}:\d{2}:\d{2}(?:\.(\d{1,9}))?(Z|[+-]\d{2}:?\d{2})?$/u; const MINUTE_MS = 60_000; /** Minutes east of UTC that a zone suffix names; none and `Z` are UTC. */ @@ -57,30 +46,23 @@ const zoneMinutesForFormat = (zone: string): number => { /** The instant of an ISO-8601 date-time and how it is spelled, or undefined when it is not one. */ const readIsoClock = (value: string): ClockReading | undefined => { - const match = ISO_INSTANT.exec(value); - if (match === null) return undefined; - const [, separator, fraction = "", zone = ""] = match; - // Clock spelling also permits a space, compact offsets and implicit UTC. - // Normalize those spellings before the shared owner validates the calendar. - const offset = - zone === "" - ? "Z" - : zone === "Z" || zone.includes(":") - ? zone - : `${zone.slice(0, 3)}:${zone.slice(3)}`; - const timestamp = offsetDateTimeSchema.safeParse( - `${value.slice(0, 10)}T${value.slice(11, 19)}${fraction === "" ? "" : `.${fraction}`}${offset}`, - ); - if (!timestamp.success) return undefined; - const instant = parseOffsetDateTimeInstant(timestamp.data); + const reading = readRecordedDateTime(value); + if ( + reading === undefined || + !reading.spelling.secondsPresent || + reading.spelling.fractionDigits > 9 + ) + return undefined; + const { instant, spelling } = reading; return { - ms: instant.epochSecond * 1000 + Number(`${fraction}000`.slice(0, 3)), - subMillisecond: Number(fraction.padEnd(9, "0").slice(3)), + ms: + instant.epochSecond * 1000 + Number(`${instant.fraction}000`.slice(0, 3)), + subMillisecond: Number(instant.fraction.padEnd(9, "0").slice(3)), format: { kind: "iso", - separator: separator as "T" | " ", - fraction: fraction.length, - zone, + separator: spelling.separator, + fraction: spelling.fractionDigits, + zone: spelling.zone, }, }; }; diff --git a/src/domain/analysisView/erlangBeamView.ts b/src/domain/analysisView/erlangBeamView.ts new file mode 100644 index 0000000000..e2e25c2939 --- /dev/null +++ b/src/domain/analysisView/erlangBeamView.ts @@ -0,0 +1,236 @@ +import { z } from "zod"; +import { erlangBeamSchema, type ErlangBeam } from "../erlang/beam.js"; +import type { AnalysisError } from "../analysisErrorBase.js"; +import { err, ok, type Result } from "../result.js"; +import type { + AnalysisViewParent, + AnalysisViewRequest, + UnsignedAnalysisView, +} from "./analysisView.js"; +import { analysisViewInputError } from "./analysisViewErrors.js"; +import { createAnalysisViewResultReader } from "./analysisViewParent.js"; +import { + boundedViewMessages, + completeWithinViewCoverage, + pageViewCoverage, +} from "./analysisViewCoverage.js"; +import { selectViewRows } from "./selectViewRows.js"; + +const atom = erlangBeamSchema.shape.atoms.unwrap().element; +const exported = erlangBeamSchema.shape.exports.unwrap().element; +const imported = erlangBeamSchema.shape.imports.unwrap().element; +const lambda = erlangBeamSchema.shape.lambdas.unwrap().element; +const chunk = erlangBeamSchema.shape.chunks.element; +const page = { + kind: z.literal("beam"), + names: z.array(z.string()).optional(), + indices: z.array(z.int().nonnegative()).optional(), + offset: z.int().nonnegative().default(0), + limit: z.int().positive().default(64), +}; + +/** Names select exact retained names; function names retain every arity and row. */ +export const erlangBeamViewRequestSchema = z.discriminatedUnion("collection", [ + z.strictObject({ + ...page, + collection: z.literal("atoms"), + fields: z.array(atom.keyof()).optional(), + }), + z.strictObject({ + ...page, + collection: z.literal("exports"), + fields: z.array(exported.keyof()).optional(), + }), + z.strictObject({ + ...page, + collection: z.literal("imports"), + modules: z.array(z.string()).optional(), + fields: z.array(imported.keyof()).optional(), + }), + z.strictObject({ + ...page, + collection: z.literal("lambdas"), + fields: z.array(lambda.keyof()).optional(), + }), + z.strictObject({ + ...page, + collection: z.literal("chunks"), + fields: z.array(chunk.keyof()).optional(), + }), +]); +const index = { source_index: z.int().nonnegative() }; +export const erlangBeamViewPageSchema = z.discriminatedUnion("collection", [ + z.strictObject({ + collection: z.literal("atoms"), + items: z.array(atom.partial().extend(index)), + }), + z.strictObject({ + collection: z.literal("exports"), + items: z.array(exported.partial().extend(index)), + }), + z.strictObject({ + collection: z.literal("imports"), + items: z.array(imported.partial().extend(index)), + }), + z.strictObject({ + collection: z.literal("lambdas"), + items: z.array(lambda.partial().extend(index)), + }), + z.strictObject({ + collection: z.literal("chunks"), + items: z.array(chunk.partial().extend(index)), + }), +]); +const count = z.int().nonnegative(); +/** Null counts preserve unavailable tables separately from decoded empty tables. */ +export const erlangBeamSummarySchema = erlangBeamSchema + .omit({ + chunks: true, + atoms: true, + exports: true, + imports: true, + lambdas: true, + limitations: true, + }) + .extend({ + counts: z.strictObject({ + chunks: count, + atoms: count.nullable(), + exports: count.nullable(), + imports: count.nullable(), + lambdas: count.nullable(), + }), + limitation_count: count, + }); +export const erlangBeamCompleteDeliveryShape = { + detail: z.literal("complete").optional().default("complete"), + view: z.never().exactOptional(), +}; +export const erlangBeamSummaryDeliveryShape = { + detail: z.literal("summary"), + view: erlangBeamViewRequestSchema.exactOptional(), +}; +export const erlangBeamDeliverySchema = z.discriminatedUnion("detail", [ + z.strictObject(erlangBeamCompleteDeliveryShape), + z.strictObject(erlangBeamSummaryDeliveryShape), +]); +const readReport = createAnalysisViewResultReader(erlangBeamSchema); +type Table = "atoms" | "exports" | "imports" | "lambdas" | "chunks"; +type Row = NonNullable[number]; +type BeamView = z.infer; + +/** Select already parsed rows; original atom/lambda indexes are never repurposed. */ +const selectRows = (rows: readonly Row[] | null, view: BeamView) => { + const names = view.names === undefined ? undefined : new Set(view.names); + const indices = + view.indices === undefined ? undefined : new Set(view.indices); + const modules = + view.collection === "imports" && view.modules !== undefined + ? new Set(view.modules) + : undefined; + const fields = + view.fields === undefined ? undefined : new Set(view.fields); + const project = (row: Row, source_index: number) => ({ + ...Object.fromEntries( + Object.entries(row).filter( + ([key]) => fields === undefined || fields.has(key), + ), + ), + source_index, + }); + const source = rows ?? []; + if (names === undefined && indices === undefined && modules === undefined) + return { + rows: source + .slice(view.offset, view.offset + view.limit) + .map((row, i) => project(row, view.offset + i)), + total: source.length, + }; + return selectViewRows( + source, + view, + (row, i) => { + const name = "name" in row ? row.name : row.function; + return ( + (indices === undefined || indices.has(i)) && + (names === undefined || (name !== null && names.has(name))) && + (modules === undefined || + ("module" in row && row.module !== null && modules.has(row.module))) + ); + }, + project, + ); +}; + +/** Project retained BEAM facts without reopening files or reinterpreting chunks. */ +export const projectErlangBeamView = ( + parent: AnalysisViewParent, + view: AnalysisViewRequest, +): Result => { + if (view.kind !== "summary" && view.kind !== "beam") + return err( + analysisViewInputError([ + { + path: ["view"], + reason: "invalid_value", + message: "BEAM Evidence supports summary and beam views.", + }, + ]), + ); + const parsed = readReport(parent); + if (!parsed.ok) return parsed; + const { atoms, exports, imports, lambdas, chunks, limitations, ...identity } = + parsed.value; + const summary = { + ...identity, + counts: { + atoms: atoms?.length ?? null, + exports: exports?.length ?? null, + imports: imports?.length ?? null, + lambdas: lambdas?.length ?? null, + chunks: chunks.length, + }, + limitation_count: limitations.length, + }; + const shared = { + parent_evidence_id: parent.evidenceId, + parent_operation: "inspect_erlang_beam" as const, + parent_digest: parent.evidenceId.slice(3), + artifact: { + path: identity.artifact.path, + sha256: identity.artifact.sha256, + }, + limitations: boundedViewMessages( + [...parent.limitations, ...limitations], + parent.evidenceId, + "limitations", + ), + unknowns: (["atoms", "exports", "imports", "lambdas"] as const) + .filter((key) => summary.counts[key] === null) + .map( + (key) => + `No ${key} table was decoded; runtime absence is not established.`, + ), + }; + if (view.kind === "summary") + return ok({ + ...shared, + kind: "summary", + view, + summary, + coverage: completeWithinViewCoverage(chunks.length, chunks.length), + }); + const selected = selectRows(parsed.value[view.collection], view); + return ok({ + ...shared, + kind: "beam", + view, + beam: summary, + page: { collection: view.collection, items: selected.rows }, + coverage: pageViewCoverage( + view.offset, + selected.rows.length, + selected.total, + ), + }); +}; diff --git a/src/domain/apple/jvmClassFile.ts b/src/domain/apple/jvmClassFile.ts index adcc8f9424..40f52e2cd1 100644 --- a/src/domain/apple/jvmClassFile.ts +++ b/src/domain/apple/jvmClassFile.ts @@ -1,3 +1,9 @@ +/** Maximum complete class-file buffer retained for FAT/JVM disambiguation. */ +export const MAX_JVM_CLASS_IDENTITY_BYTES = 16 * 1024 * 1024; + +export const JVM_CLASS_IDENTITY_LIMIT_REASON = + "FAT/JVM identity is unresolved because the class identity scan exceeds the 16 MiB limit"; + /** Class-file identity used to disambiguate JVM CAFEBABE from Mach-O FAT. */ class ClassFileCursor { private offset = 0; diff --git a/src/domain/apple/machoContainer.ts b/src/domain/apple/machoContainer.ts index f4f1afaec1..2cee12db0a 100644 --- a/src/domain/apple/machoContainer.ts +++ b/src/domain/apple/machoContainer.ts @@ -12,6 +12,8 @@ export interface FatSliceDeclaration { readonly alignmentExponent: number; readonly cpuType: number; readonly cpuSubtype: number; + /** FAT64's raw reserved field; FAT32 has no corresponding field. */ + readonly reserved: number | null; } export interface FatSliceRangeIssue { @@ -273,11 +275,30 @@ export const readFatSliceDeclarations = ( alignmentExponent: view.getUint32(base + (wide ? 24 : 16), littleEndian), cpuType: view.getUint32(base, littleEndian), cpuSubtype: view.getUint32(base + 4, littleEndian), + reserved: wide ? view.getUint32(base + 28, littleEndian) : null, }); } return { status: "parsed", slices }; }; +/** Find an overlapping pair without changing the declarations' table order. */ +export const findOverlappingFatSlices = ( + declarations: readonly FatSliceDeclaration[], +): readonly [FatSliceDeclaration, FatSliceDeclaration] | null => { + const ranges = [...declarations].sort((a, b) => a.offset - b.offset); + for (let index = 1; index < ranges.length; index++) { + const previous = ranges[index - 1]; + const current = ranges[index]; + if ( + previous !== undefined && + current !== undefined && + current.offset < previous.offset + previous.size + ) + return [previous, current]; + } + return null; +}; + /** Validate byte-range and table/header identity facts shared by Mach-O readers. */ export const validateFatSlice = ( declaration: FatSliceDeclaration, diff --git a/src/domain/investigationProviders.ts b/src/domain/investigationProviders.ts index ebd415e05c..a0e3c23126 100644 --- a/src/domain/investigationProviders.ts +++ b/src/domain/investigationProviders.ts @@ -33,6 +33,13 @@ export const PE_CERTIFICATES_PROVIDER = Object.freeze({ version: "1", } as const); +/** Provider identity for portable, execution-free PE import-table decoding. */ +export const PE_IMPORTS_PROVIDER = Object.freeze({ + id: "rea-pe-imports", + name: "REA PE import parser", + version: "1", +} as const); + /** Provider identity for portable, execution-free ELF note-table decoding. */ export const ELF_NOTES_PROVIDER = Object.freeze({ id: "rea-elf-notes", diff --git a/src/domain/javascript/javascriptAstFingerprint.ts b/src/domain/javascript/javascriptAstFingerprint.ts index 6b76eb124a..9efcc803a7 100644 --- a/src/domain/javascript/javascriptAstFingerprint.ts +++ b/src/domain/javascript/javascriptAstFingerprint.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; +import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; @@ -60,7 +61,25 @@ export const collectJavaScriptExports = ( return (reference: t.Node | null): boolean => { if (!t.isIdentifier(reference)) return false; if (roles === undefined) return reference.name === fallback; - return id !== undefined && bindingId(reference) === id; + if (id === undefined || bindings === undefined) return false; + const binding = resolveSemanticBindingState( + bindings, + reference, + reference.name, + ); + if (binding?.bindingId !== id) return false; + // The calling convention owns the parameter's entry value, not every + // later value assigned to the same lexical binding. + const read = semanticBindingAtRead( + binding, + reference, + bindings.parentsByNode, + ); + return ( + read.status === "ordered" && + read.binding.initializers.length === 0 && + read.binding.referenceInitializers.length === 0 + ); }; }; const isExportsAlias = identity("exports", "exports"); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.initialization_boundaries.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.initialization_boundaries.test.ts index 216ef23e90..748a83477a 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.initialization_boundaries.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.initialization_boundaries.test.ts @@ -123,6 +123,38 @@ it("keeps ordinary arguments local while arrows retain the outer binding", () => ).toBe(false); }); +it.each([ + ["object", "{ handler }", "{ handler: state }"], + ["array", "[handler]", "[state]"], +])( + "retains mutations through catch-shadowed %s var aliases", + (_shape, pattern, value) => { + for (const shadowed of [true, false]) { + const ir = analyze(` + exports.read = function read() { + const state = { value: 1 }; + try { throw 0; } catch (${shadowed ? "handler" : "error"}) { + var ${pattern} = ${value}; + handler.value = 2; + } + return state.value; + }; + `); + expect(ir.coverage.status).toBe("complete"); + expect(onlyCallable(ir, "read").returnSites[0]?.value).toEqual({ + status: "unknown", + reason: "Own property value is unknown-coverage.", + }); + if (shadowed) + expect( + ir.bindings.find( + ({ name, kind }) => name === "handler" && kind === "variable", + )?.value.status, + ).toBe("unknown"); + } + }, +); + it("does not resolve Promise sources through a future initializer", () => { const early = analyze( ` diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index 5c6d86bf87..51c6b3fe78 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -1156,11 +1156,7 @@ function* collectDefinitionsSteps( parentScope.kind === "block" ) legacyBlockFunctions.push({ node, scope: parentScope }); - if ( - t.isVariableDeclarator(node) && - node.init != null && - t.isIdentifier(node.id) - ) { + if (t.isVariableDeclarator(node) && node.init != null) { const owner = semanticVariableScope(parentScope, parent, state); if (owner !== parentScope) displacedVarInitializers.push({ node, scope: parentScope, owner }); @@ -1233,30 +1229,68 @@ function* collectDefinitionsSteps( // A var declaration is instantiated in its variable environment, while its // initializer assigns through the lexical environment at the source site. for (const { node, scope, owner } of displacedVarInitializers) { - if (!t.isIdentifier(node.id) || node.init == null) continue; - const selected = resolveSemanticBindingFromScope( - scope, - node.id.name, - state, - ); - const declared = owner.bindings.get(node.id.name); - if ( - selected === undefined || - selected === declared || - declared === undefined - ) - continue; - for (let index = declared.initializers.length - 1; index >= 0; index--) - if (declared.initializers[index]?.node === node.init) - declared.initializers.splice(index, 1); - addAssignment( - node.id, - node.init, - scope, - state, - state.conditionallySkippedExpressions.has(node.init), - node, - ); + for (const identifier of semanticWriteTargets(node.id)) { + if (!t.isIdentifier(identifier)) continue; + const selected = resolveSemanticBindingFromScope( + scope, + identifier.name, + state, + ); + const declared = owner.bindings.get(identifier.name); + if ( + selected === undefined || + selected === declared || + declared === undefined + ) + continue; + // Move the facts already parsed for this slot, including destructuring + // projections, defaults and rest copies, without reinterpreting its RHS. + const belongsToDeclaration = (value: t.Node): boolean => { + let candidate: t.Node | undefined = value; + while (candidate !== undefined && candidate !== node) + candidate = state.parentsByNode.get(candidate); + return candidate === node; + }; + for (let index = 0; index < declared.initializers.length; index++) { + const initializer = declared.initializers[index]; + if ( + initializer !== undefined && + belongsToDeclaration(initializer.node) + ) { + declared.initializers.splice(index, 1); + index--; + selected.initializers.push(initializer); + } + } + for ( + let index = 0; + index < declared.referenceInitializers.length; + index++ + ) { + const initializer = declared.referenceInitializers[index]; + if ( + initializer !== undefined && + belongsToDeclaration(initializer.node) + ) { + declared.referenceInitializers.splice(index, 1); + index--; + selected.referenceInitializers.push({ + ...initializer, + assignment: true, + }); + } + } + addAssignment( + identifier, + null, + scope, + state, + node.init !== null && + node.init !== undefined && + state.conditionallySkippedExpressions.has(node.init), + node, + ); + } } for (const { node, ancestors } of thrownValues) { let child: t.Node = node; diff --git a/src/domain/managed/managedNativeVerification.ts b/src/domain/managed/managedNativeVerification.ts index 3174d36e48..88126c13a0 100644 --- a/src/domain/managed/managedNativeVerification.ts +++ b/src/domain/managed/managedNativeVerification.ts @@ -1,7 +1,6 @@ import { digestCanonicalValue } from "../canonicalDigest.js"; import { managedNativeVerificationResultSchema, - type ManagedNativeVerificationInput, type ManagedNativeVerificationResult, } from "./managedNativeVerificationSchemas.js"; import { @@ -9,10 +8,7 @@ import { collectNativeSymbols, verifyPinvoke, } from "./managedNativeVerificationMatch.js"; -import { - readManagedNativeVerificationSource, - type ManagedNativeVerificationSource, -} from "./managedNativeVerificationSource.js"; +import type { ManagedNativeVerificationSource } from "./managedNativeVerificationSource.js"; /** Pure verification over authenticated, parsed source facts. */ export const verifyManagedNativeVerificationSource = ( @@ -56,12 +52,3 @@ export const managedNativeVerificationHasUnknowns = ( result.summary.unresolved > 0 || result.summary.contradicted > 0 || result.summary.native_body_unresolved > 0; - -/** Raw domain API retained for CLI and existing callers. */ -export const verifyManagedNativeBoundaries = ( - input: ManagedNativeVerificationInput, -): ManagedNativeVerificationResult => { - const source = readManagedNativeVerificationSource(input); - if (!source.ok) throw source.error; - return verifyManagedNativeVerificationSource(source.value); -}; diff --git a/src/domain/managed/managedNativeVerificationSchemas.ts b/src/domain/managed/managedNativeVerificationSchemas.ts index 8d44f8e924..9f3859ef24 100644 --- a/src/domain/managed/managedNativeVerificationSchemas.ts +++ b/src/domain/managed/managedNativeVerificationSchemas.ts @@ -127,10 +127,6 @@ export const managedNativeVerificationInputSchema = z } }); -export type ManagedNativeVerificationInput = z.infer< - typeof managedNativeVerificationInputSchema ->; - const verificationCoverageSchema = z.strictObject({ status: z.enum(["complete-within-inputs", "partial"]), }); diff --git a/src/domain/managed/managedReconstruction.ts b/src/domain/managed/managedReconstruction.ts index 35690625eb..1ca256b8a7 100644 --- a/src/domain/managed/managedReconstruction.ts +++ b/src/domain/managed/managedReconstruction.ts @@ -330,12 +330,3 @@ export const executeManagedReconstructionImport = ( )}`, }); }; - -/** Authenticate decompiler reconstruction against static IL Evidence. */ -export const importManagedReconstruction = ( - input: ManagedReconstructionImportInput, -): ManagedReconstructionImportResult => { - const resolved = resolveManagedReconstructionImport(input); - if (!resolved.ok) throw resolved.error; - return executeManagedReconstructionImport(resolved.value); -}; diff --git a/src/domain/native/peImports.test.ts b/src/domain/native/peImports.test.ts new file mode 100644 index 0000000000..ffdbc7786e --- /dev/null +++ b/src/domain/native/peImports.test.ts @@ -0,0 +1,44 @@ +import { expect, it } from "vitest"; +import { inspectPeImportsInputSchema, peImportsSchema } from "./peImports.js"; + +it("rejects extra caller arguments and admits an empty import inventory", () => { + expect( + inspectPeImportsInputSchema.safeParse({ + path: "/artifacts/app.exe", + extra: true, + }).success, + ).toBe(false); + const report = peImportsSchema.parse({ + artifact: { + path: "/artifacts/app.exe", + sha256: "a".repeat(64), + bytes: 512, + }, + format: "pe32-plus", + machine: 0x8664, + bits: 64, + architecture: "amd64", + import_directory: null, + delay_import_directory: null, + imports: [], + delay_imports: [], + imphash: { + algorithm: "pefile", + md5: null, + reason: "IMAGE_DIRECTORY_ENTRY_IMPORT is absent.", + }, + coverage: { + status: "complete", + reason: null, + examined_dlls: 0, + imports: 0, + delay_imports: 0, + }, + issues: [], + limitations: [ + "IMAGE_DIRECTORY_ENTRY_IMPORT is absent. That is not proof the image never imported APIs through another table or overlay.", + ], + }); + expect(report.imports).toEqual([]); + expect(report.imphash.md5).toBeNull(); +}); diff --git a/src/domain/native/peImports.ts b/src/domain/native/peImports.ts new file mode 100644 index 0000000000..ab0c11713b --- /dev/null +++ b/src/domain/native/peImports.ts @@ -0,0 +1,153 @@ +import { z } from "zod"; + +import { digestSchema } from "../digests.js"; +import { absoluteLocalPathStringSchema } from "../localPath.js"; + +const uint = z.number().int().min(0).max(0xffffffff); +const range = z.strictObject({ offset: uint, bytes: uint }); +const md5Schema = z.string().regex(/^[a-f0-9]{32}$/u); + +/** Hard ceiling on the caller-selected input read budget. */ +export const MAX_INSPECT_PE_IMPORT_FILE_BYTES = 536870912; +/** Hard ceiling on import and delay-load descriptors examined together. */ +export const MAX_INSPECT_PE_IMPORT_DLLS = 4096; +/** Hard ceiling on named and ordinal functions across every table. */ +export const MAX_INSPECT_PE_IMPORT_FUNCTIONS = 262144; +/** Fixed ceiling on decoded DLL and function name text retained in one report. */ +export const MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES = 16 * 1024 * 1024; + +/** Complete bounded inspection of PE import and delay-load tables. */ +export const inspectPeImportsInputSchema = z.strictObject({ + path: absoluteLocalPathStringSchema.describe( + "Absolute filesystem path to a PE image", + ), + max_file_bytes: z + .number() + .int() + .min(64) + .max(MAX_INSPECT_PE_IMPORT_FILE_BYTES) + .default(67108864) + .describe("Maximum stable artifact bytes to read (default 64 MiB)"), + max_dlls: z + .number() + .int() + .min(1) + .max(MAX_INSPECT_PE_IMPORT_DLLS) + .default(256) + .describe( + "Maximum import and delay-load descriptors to examine; exceeding the budget fails without partial success", + ), + max_functions: z + .number() + .int() + .min(1) + .max(MAX_INSPECT_PE_IMPORT_FUNCTIONS) + .default(8192) + .describe( + "Maximum imported functions to examine across every table; exceeding the budget fails without partial success", + ), +}); +export type InspectPeImportsInput = z.infer; + +const peImportFunctionSchema = z.strictObject({ + hint: uint.nullable(), + name: z.string().min(1).nullable(), + ordinal: z.number().int().min(0).max(0xffff).nullable(), + ilt_rva: uint.nullable(), + iat_rva: uint, + thunk_hex: z.string().regex(/^[0-9a-f]+$/u), +}); + +const peImportDllSchema = z.strictObject({ + index: uint, + name: z.string().min(1).nullable(), + name_rva: uint, + original_first_thunk_rva: uint, + first_thunk_rva: uint, + time_date_stamp: uint, + forwarder_chain: uint, + location: range, + functions: z.array(peImportFunctionSchema), +}); + +const peDelayImportDllSchema = z.strictObject({ + index: uint, + name: z.string().min(1).nullable(), + name_rva: uint, + attributes: uint, + import_address_table_rva: uint, + import_name_table_rva: uint, + bound_import_rva: uint, + unload_information_table_rva: uint, + module_handle_rva: uint, + time_date_stamp: uint, + location: range, + functions: z.array(peImportFunctionSchema), +}); + +/** + * `locations` are the file spans the parser actually consumed for the + * descriptor array and its terminator, in RVA order. Consecutive descriptors + * that map to discontiguous file bytes produce separate spans. + * No bytes are consumed when the first slot cannot be examined; the declared + * directory remains present and coverage is partial. + */ +const directorySchema = z + .strictObject({ + rva: uint, + size: uint, + locations: z.array(range), + }) + .nullable(); + +const peImportIssueSchema = z.strictObject({ + code: z.enum([ + "malformed_entry", + "descriptor_unreadable", + "name_unreadable", + "thunk_unreadable", + "unsupported_form", + ]), + location: z.string().min(1).nullable(), + message: z.string().min(1), +}); + +const coverageSchema = z.strictObject({ + status: z.enum(["complete", "partial"]), + reason: z.string().min(1).nullable(), + examined_dlls: uint, + imports: uint, + delay_imports: uint, +}); + +const imphashSchema = z.strictObject({ + algorithm: z.literal("pefile"), + md5: md5Schema.nullable(), + reason: z.string().min(1).nullable(), +}); + +/** Static import inventory of one PE32/PE32+ image, read without execution. */ +export const peImportsSchema = z.strictObject({ + artifact: z.strictObject({ + path: z.string().min(1), + sha256: digestSchema, + bytes: uint, + }), + format: z.enum(["pe32", "pe32-plus"]), + machine: uint, + bits: z.union([z.literal(32), z.literal(64)]), + architecture: z.string().min(1), + import_directory: directorySchema, + delay_import_directory: directorySchema, + imports: z.array(peImportDllSchema), + delay_imports: z.array(peDelayImportDllSchema), + imphash: imphashSchema, + coverage: coverageSchema, + issues: z.array(peImportIssueSchema), + limitations: z.array(z.string().min(1)), +}); +export type PeImports = z.infer; +export type PeImportDll = z.infer; +export type PeDelayImportDll = z.infer; +export type PeImportFunction = z.infer; +export type PeImportIssue = z.infer; diff --git a/src/domain/offsetDateTime.test.ts b/src/domain/offsetDateTime.test.ts index 6d334109f2..783710a63c 100644 --- a/src/domain/offsetDateTime.test.ts +++ b/src/domain/offsetDateTime.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from "vitest"; import { z } from "zod"; -import { offsetDateTimeSchema } from "./offsetDateTime.js"; +import { + offsetDateTimeSchema, + readRecordedDateTime, +} from "./offsetDateTime.js"; /** The validator this schema replaces; it remains the oracle for the accepted set. */ const replaced = z.string().datetime({ offset: true }); @@ -78,6 +81,137 @@ describe("offset datetime schema", () => { }); }); +describe("recorded datetime facts", () => { + const epochSecond = Date.UTC(2026, 9, 20, 9) / 1000; + + it.each<{ + text: string; + expected: NonNullable>; + }>([ + { + text: "2026-10-20T09:00:00.000100Z", + expected: { + instant: { epochSecond, fraction: "0001" }, + spelling: { + separator: "T", + secondsPresent: true, + fractionDigits: 6, + zone: "Z", + }, + }, + }, + { + text: "2026-10-20 10:00:00.000100+0100", + expected: { + instant: { epochSecond, fraction: "0001" }, + spelling: { + separator: " ", + secondsPresent: true, + fractionDigits: 6, + zone: "+0100", + }, + }, + }, + { + text: "2026-10-20T09:00:00.0000000002", + expected: { + instant: { epochSecond, fraction: "0000000002" }, + spelling: { + separator: "T", + secondsPresent: true, + fractionDigits: 10, + zone: "", + }, + }, + }, + { + text: "2026-10-20T08:30:00.1234567890123456789000-00:30", + expected: { + instant: { epochSecond, fraction: "1234567890123456789" }, + spelling: { + separator: "T", + secondsPresent: true, + fractionDigits: 22, + zone: "-00:30", + }, + }, + }, + { + text: "2026-10-20T09:00-00:00", + expected: { + instant: { epochSecond, fraction: "" }, + spelling: { + separator: "T", + secondsPresent: false, + fractionDigits: 0, + zone: "-00:00", + }, + }, + }, + { + text: "2026-10-20 10:00+0100", + expected: { + instant: { epochSecond, fraction: "" }, + spelling: { + separator: " ", + secondsPresent: false, + fractionDigits: 0, + zone: "+0100", + }, + }, + }, + { + text: "0001-01-01T01:00:00.0000000002+0100", + expected: { + instant: { epochSecond: -62_135_596_800, fraction: "0000000002" }, + spelling: { + separator: "T", + secondsPresent: true, + fractionDigits: 10, + zone: "+0100", + }, + }, + }, + { + text: "0000-02-29 00:00:00.000Z", + expected: { + instant: { epochSecond: -62_162_121_600, fraction: "" }, + spelling: { + separator: " ", + secondsPresent: true, + fractionDigits: 3, + zone: "Z", + }, + }, + }, + ])( + "preserves the exact instant and source spelling of $text", + ({ text, expected }) => { + expect(readRecordedDateTime(text)).toEqual(expected); + }, + ); + + it.each([ + "0001-02-29T00:00:00Z", + "1900-02-29 00:00:00", + "2026-04-31 00:00:00+0100", + "2026-13-01T00:00:00Z", + "2026-10-20T24:00:00Z", + "2026-10-20T09:60:00Z", + "2026-10-20T09:00:60Z", + "2026-10-20T09:00:00+24:00", + "2026-10-20 09:00:00+0060", + "2026-10-20T09:00:00+01", + "2026-10-20T09:00.1Z", + "2026-10-20t09:00:00Z", + "2026-10-20T09:00:00z", + "2026-10-20", + "Tue, 20 Oct 2026 09:00:00 GMT", + ])("does not invent ISO facts for %s", (text) => { + expect(readRecordedDateTime(text)).toBeUndefined(); + }); +}); + const findPattern = (value: unknown): string | undefined => { if (value === null || typeof value !== "object") return undefined; if ("pattern" in value && typeof value.pattern === "string") diff --git a/src/domain/offsetDateTime.ts b/src/domain/offsetDateTime.ts index b7d38550f2..801aeb3ca5 100644 --- a/src/domain/offsetDateTime.ts +++ b/src/domain/offsetDateTime.ts @@ -50,6 +50,47 @@ export const parseOffsetDateTimeInstant = ( }; }; +/** Parse recorded ISO spelling once, retaining its exact instant and formatting facts. */ +export const readRecordedDateTime = ( + value: string, +): + | { + readonly instant: OffsetDateTimeInstant; + readonly spelling: { + readonly separator: "T" | " "; + readonly secondsPresent: boolean; + readonly fractionDigits: number; + readonly zone: string; + }; + } + | undefined => { + const match = + /^(\d{4}-\d{2}-\d{2})([T ])(\d{2}:\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?$/u.exec( + value, + ); + if (match === null) return undefined; + const [, date, separator, time, seconds, fraction = "", zone = ""] = match; + const offset = + zone === "" + ? "Z" + : zone === "Z" || zone.includes(":") + ? zone + : `${zone.slice(0, 3)}:${zone.slice(3)}`; + const normalized = offsetDateTimeSchema.safeParse( + `${date}T${time}${seconds === undefined ? "" : `:${seconds}`}${fraction === "" ? "" : `.${fraction}`}${offset}`, + ); + if (!normalized.success) return undefined; + return { + instant: parseOffsetDateTimeInstant(normalized.data), + spelling: { + separator: separator as "T" | " ", + secondsPresent: seconds !== undefined, + fractionDigits: fraction.length, + zone, + }, + }; +}; + /** Convert a Date.now-style integer into the same exact comparison shape. */ export const offsetDateTimeInstantFromEpochMilliseconds = ( epochMilliseconds: number, diff --git a/src/domain/webBundleAnalyzer.test.ts b/src/domain/webBundleAnalyzer.test.ts index cbd1cf6424..bd7f9cb6ce 100644 --- a/src/domain/webBundleAnalyzer.test.ts +++ b/src/domain/webBundleAnalyzer.test.ts @@ -6,10 +6,8 @@ import { describe, expect, it } from "vitest"; import { createWebPageInspection } from "../../tests/support/browser/webPageInspection.js"; import { analyzeJavaScriptStaticSource } from "./javascript/javascriptStaticAnalysis.js"; -import { - analyzeCapturedWebBundle, - analyzeCapturedWebScript, -} from "./webBundleAnalyzer.js"; +import { analyzeCapturedWebScript } from "./webBundleAnalyzer.js"; +import { analyzeWebBundleFixture } from "../../tests/support/browser/analyzeWebBundleFixture.js"; import { buildCapturedWebBundleAnalysis } from "./webBundleAnalyzerResult.js"; import { webBundleAnalysisSchema } from "./webBundleAnalysis.js"; import { isIncludedScript } from "./webBundleAnalyzerState.js"; @@ -19,7 +17,7 @@ import { createWebTextArtifact } from "./webContentArtifact.js"; const origin = "https://app.example.test"; it("retains standalone GraphQL Source operations in captured scripts", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` const source = { body: "query GetUser { user { id } }", @@ -57,7 +55,7 @@ it.each([ if (good === undefined) throw new Error("Missing valid script fixture"); captured.scripts.items.push({ ...good, script_key: "valid-script" }); captured.scripts.total += 1; - const result = analyzeCapturedWebBundle(captured); + const result = analyzeWebBundleFixture(captured); expect(result.completeness).toMatchObject({ status: "partial", parsed_scripts: parsed, @@ -98,7 +96,7 @@ it.each([ "shares Map receiver certainty with application analysis: %s", (source, candidate) => { const input = source + " fetch('/network');"; - const result = analyzeCapturedWebBundle(inspection(input)); + const result = analyzeWebBundleFixture(inspection(input)); expect(result.observations.endpoints.map(({ value }) => value)).toEqual([ "/network", ]); @@ -140,7 +138,7 @@ it("discloses widened returned-reference effects only for the affected script", if (independent === undefined) throw new Error("Missing independent script"); captured.scripts.items.push({ ...independent, script_key: "independent" }); captured.scripts.total += 1; - const result = analyzeCapturedWebBundle(captured); + const result = analyzeWebBundleFixture(captured); expect(result.completeness).toMatchObject({ status: "partial", parsed_scripts: 2, @@ -225,7 +223,7 @@ it.each([ ])( "preserves WebMCP facts through $label", ({ source, name, description, properties, coverage }) => { - const result = analyzeCapturedWebBundle(inspection(source)); + const result = analyzeWebBundleFixture(inspection(source)); expect(result.observations.webmcp_declarations).toEqual([ expect.objectContaining({ @@ -270,7 +268,7 @@ it.each([ ])( "preserves rightmost uncertainty for $label", ({ source, name, description, properties, coverage }) => { - const result = analyzeCapturedWebBundle(inspection(source)); + const result = analyzeWebBundleFixture(inspection(source)); expect(result.observations.webmcp_declarations).toEqual([ expect.objectContaining({ @@ -375,7 +373,7 @@ it.each([ ])( "preserves WebMCP schema ownership for $label", ({ source, name, properties, coverage }) => { - const result = analyzeCapturedWebBundle(inspection(source)); + const result = analyzeWebBundleFixture(inspection(source)); expect(result.observations.webmcp_declarations).toEqual([ expect.objectContaining({ @@ -408,7 +406,7 @@ describe("web bundle analyzer", () => { }); const __webpack_require__ = () => createApp(routes, lazy); `; - const result = analyzeCapturedWebBundle(inspection(source)); + const result = analyzeWebBundleFixture(inspection(source)); expect(result.observations.chunks.edges).toEqual( expect.arrayContaining([ @@ -451,14 +449,14 @@ describe("web bundle analyzer", () => { it("reports parser gaps without imposing a finding cap", () => { const malformed = inspection("function broken( {"); - const failed = analyzeCapturedWebBundle(malformed); + const failed = analyzeWebBundleFixture(malformed); expect(failed.completeness).toMatchObject({ status: "partial", parse_failures: 1, }); expect(failed.unknowns[0]?.dimension).toBe("javascript_ast"); - const analyzed = analyzeCapturedWebBundle( + const analyzed = analyzeWebBundleFixture( inspection("fetch('/one'); fetch('/two'); fetch('/three');"), ); expect(analyzed.observations.endpoints).toHaveLength(3); @@ -467,7 +465,7 @@ describe("web bundle analyzer", () => { it("analyzes AST nodes after the former fixed node ceiling", () => { const source = `${";".repeat(250_001)}fetch('/last');`; - const result = analyzeCapturedWebBundle(inspection(source)); + const result = analyzeWebBundleFixture(inspection(source)); expect(result.observations.endpoints).toContainEqual( expect.objectContaining({ value: "/last" }), @@ -481,7 +479,7 @@ describe("web bundle analyzer", () => { const name = "tool".repeat(100); const description = "untrusted declaration ".repeat(150); const propertyName = "property".repeat(100); - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` import "${specifier}"; document.modelContext.registerTool({ @@ -505,7 +503,7 @@ describe("web bundle analyzer", () => { describe("web bundle source-map coverage", () => { it("reports unavailable source maps as partial without dropping requested maps", () => { - const analysis = analyzeCapturedWebBundle(inspection("export {};"), { + const analysis = analyzeWebBundleFixture(inspection("export {};"), { status: "unavailable", requested: 1, processed: 1, @@ -534,7 +532,7 @@ describe("web bundle source-map coverage", () => { }); it("keeps map-less captured source complete when no maps were requested", () => { - const analysis = analyzeCapturedWebBundle(inspection("export {};"), { + const analysis = analyzeWebBundleFixture(inspection("export {};"), { status: "not_requested", requested: 0, processed: 0, @@ -546,7 +544,7 @@ describe("web bundle source-map coverage", () => { }); it("rejects inconsistent source-map coverage counts", () => { - const result = analyzeCapturedWebBundle(inspection("export {};")); + const result = analyzeWebBundleFixture(inspection("export {};")); expect( webBundleAnalysisSchema.safeParse({ ...result, @@ -564,7 +562,7 @@ describe("web bundle source-map coverage", () => { describe("web bundle static-analysis parity", () => { it("recognizes endpoints through nested dynamic receivers like static analysis", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection('window[recv].fetch("/nested-fetch");'), ); expect(result.observations.endpoints).toContainEqual( @@ -573,7 +571,7 @@ describe("web bundle static-analysis parity", () => { }); it("reads open endpoints only from XMLHttpRequest method and URL pairs", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` xhr.open("GET", "/xhr-get"); xhr.open("post", "https://xhr.example.test/submit"); @@ -632,7 +630,7 @@ describe("web bundle API classification parity", () => { indexedDB.open("GET", "/storage-named-xhr"); `; expect( - analyzeCapturedWebBundle(inspection(source)).observations.endpoints.map( + analyzeWebBundleFixture(inspection(source)).observations.endpoints.map( ({ value }) => value, ), ).toEqual(["/storage-named-xhr"]); @@ -649,7 +647,7 @@ describe("web bundle API classification parity", () => { }); it("uses lexical receiver facts for overloaded open calls", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` const popup = window; popup.open(url, "/preview"); @@ -676,7 +674,7 @@ describe("web bundle API classification parity", () => { const xhr = new XMLHttpRequest(); xhr?.open(method, "/api"); `; - const bundle = analyzeCapturedWebBundle(inspection(source)); + const bundle = analyzeWebBundleFixture(inspection(source)); expect(bundle.observations.endpoints.map(({ value }) => value)).toEqual([ "/api", ]); @@ -690,7 +688,7 @@ describe("web bundle API classification parity", () => { }); it("keeps HTTP-method-named reads of provable keyed collections out of endpoints", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` axios.get("https://api.example.test/users"); api.get("users"); @@ -728,7 +726,7 @@ describe("web bundle API classification parity", () => { }); it("keeps undici header-store reads out of endpoint observations", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` response.headersList.get("location", true); request.headersList.delete("authorization"); @@ -829,7 +827,7 @@ describe("web bundle explicit request reference forms", () => { "POST", "X", ].sort(); - const web = analyzeCapturedWebBundle(inspection(source)); + const web = analyzeWebBundleFixture(inspection(source)); expect(web.observations.endpoints.map(({ value }) => value).sort()).toEqual( expected, ); @@ -869,7 +867,7 @@ describe("web bundle endpoint candidate classification", () => { xhr.open(method, "login"); xhr.open("GET", "ping"); `; - const result = analyzeCapturedWebBundle(inspection(source)); + const result = analyzeWebBundleFixture(inspection(source)); expect( result.observations.endpoints.map(({ value }) => value).sort(), ).toEqual(["login", "ping"]); @@ -919,7 +917,7 @@ describe("web bundle endpoint candidate classification", () => { const first = captured.scripts.items[0]; if (first === undefined) throw new Error("Missing captured script fixture"); const secondKey = `scr_${"2".repeat(64)}`; - const result = analyzeCapturedWebBundle({ + const result = analyzeWebBundleFixture({ ...captured, scripts: { total: 2, @@ -1011,7 +1009,7 @@ describe("web bundle configuration and navigation parity", () => { "shadowed", "users", ]; - const web = analyzeCapturedWebBundle(inspection(source)); + const web = analyzeWebBundleFixture(inspection(source)); expect(web.observations.endpoints).toEqual([]); expect( web.inferences @@ -1036,7 +1034,7 @@ describe("web bundle configuration and navigation parity", () => { api.get("users"); `; const analyze = (source: string) => { - const web = analyzeCapturedWebBundle(inspection(source)); + const web = analyzeWebBundleFixture(inspection(source)); const staticAnalysis = analyzeJavaScriptStaticSource(source); return { web: { @@ -1090,7 +1088,7 @@ describe("web bundle configuration and navigation parity", () => { }); it("recognizes loadURL endpoints like static analysis", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection('window.win.loadURL("https://embedded.test/app");'), ); expect(result.observations.endpoints).toContainEqual( @@ -1102,17 +1100,17 @@ describe("web bundle configuration and navigation parity", () => { describe("web bundle artifact metadata", () => { it("retains only a validated HTTP target origin", () => { expect( - analyzeCapturedWebBundle(inspection("export {};")).capture.target_origin, + analyzeWebBundleFixture(inspection("export {};")).capture.target_origin, ).toBe(origin); const nonHttp = inspection("export {};"); nonHttp.target.url = "ftp://host/app"; nonHttp.target.origin = "ftp://host"; - expect(analyzeCapturedWebBundle(nonHttp).capture.target_origin).toBeNull(); + expect(analyzeWebBundleFixture(nonHttp).capture.target_origin).toBeNull(); }); it("preserves long provider media types in artifact summaries", () => { - const result = analyzeCapturedWebBundle(inspection("export {};")); + const result = analyzeWebBundleFixture(inspection("export {};")); const mediaType = `application/${"x".repeat(300)}`; const artifact = result.capture.source_artifacts[0] ?? { sha256: "0".repeat(64), @@ -1137,7 +1135,7 @@ describe("web bundle artifact metadata", () => { describe("deep captured web bundle syntax", () => { it("retains evidence after a parser-admitted deep property chain", () => { const source = `const value = root${".next".repeat(12_000)};\nimport "./last.js";\nfetch("/after");`; - const result = analyzeCapturedWebBundle(inspection(source)); + const result = analyzeWebBundleFixture(inspection(source)); expect(result.completeness).toMatchObject({ status: "complete", parsed_scripts: 1, @@ -1530,7 +1528,7 @@ it("rejects a failed parse that claims visited nodes or derived facts", () => { }); it("recognizes expression-free template literals as route properties", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` const routes = [ { path: \`score-board\`, component: ScoreBoard }, @@ -1547,7 +1545,7 @@ it("recognizes expression-free template literals as route properties", () => { }); it("uses the shared parser for decorated captured scripts", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(`@sealed class Widget {} fetch('/decorated');`), ); @@ -1562,7 +1560,7 @@ it("uses the shared parser for decorated captured scripts", () => { }); it("keeps bare module locations unresolved without inventing URL-relative paths", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` import "package-name"; import("@scope/package"); @@ -1590,7 +1588,7 @@ it("keeps bare module locations unresolved without inventing URL-relative paths" }); it("retains each literal importScripts argument in source order", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection( 'importScripts("first.js", dynamicValue, "second.js", "first.js");', ), @@ -1604,7 +1602,7 @@ it("retains each literal importScripts argument in source order", () => { describe("web metadata key semantics", () => { it("keeps computed metadata keys and overwrite order conservative", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` const path = "description"; const routes = [{ [path]: "/not-a-route" }, { ["path"]: "/real" }]; @@ -1649,7 +1647,7 @@ describe("web metadata key semantics", () => { }); it("projects only exact input schema property names", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` document.modelContext.registerTool({ name: "literal", @@ -1730,7 +1728,7 @@ describe("web metadata key semantics", () => { }); it("keeps missing coverage absent when parsing historical observations", () => { - const analyzed = analyzeCapturedWebBundle( + const analyzed = analyzeWebBundleFixture( inspection( `document.modelContext.registerTool({ name: "legacy", inputSchema: { properties: {} } });`, ), @@ -1757,7 +1755,7 @@ describe("web metadata key semantics", () => { }); it("retains WebMCP declarations through optional member and call chaining", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` document.modelContext?.registerTool({ name: "optional member" }); document.modelContext.registerTool?.({ name: "optional call" }); @@ -1777,7 +1775,7 @@ it("retains WebMCP declarations through optional member and call chaining", () = }); it("reads expression-free template dynamic imports as chunk edges", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` import(\`./lazy.js\`); import("./quoted.js"); @@ -1799,7 +1797,7 @@ it("reads expression-free template dynamic imports as chunk edges", () => { }); it("does not infer call names from dynamic computed identifiers", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` const fetch = "render"; const require = "render"; @@ -1816,7 +1814,7 @@ it("does not infer call names from dynamic computed identifiers", () => { }); it("retains direct and literal computed call names", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` window.fetch("/direct"); window["fetch"]("/literal"); @@ -1841,7 +1839,7 @@ it("retains direct and literal computed call names", () => { }); it("requires WebMCP receiver and method names to occupy exact member segments", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` navigator.modelContext.registerTool({ name: "navigator" }); local.modelContext.registerTool({ name: "exact nested" }); @@ -1862,7 +1860,7 @@ it("requires WebMCP receiver and method names to occupy exact member segments", }); it("recognizes tRPC procedures and named GraphQL operations in captured scripts", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` const n = createTRPCClient({ links: [httpBatchLink({ url: "/api/trpc" })] }); e.invoice.list.useQuery({ competencia: "2026-09" }); @@ -1892,7 +1890,7 @@ it("keeps RPC lexical identity and bundled factory recognition in captured scrip "\nquery " + longOperation + " { users { id } }"; - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` function first() { const c = createTRPCClient({}); } function unrelated(c) { c.users.query("select 1"); } @@ -1915,7 +1913,7 @@ it("keeps RPC lexical identity and bundled factory recognition in captured scrip }); it("retains distinct WebMCP descriptions and names while deduplicating equivalent properties", () => { - const result = analyzeCapturedWebBundle( + const result = analyzeWebBundleFixture( inspection(` document.modelContext.registerTool({ name: "lookup", description: "first", inputSchema: { properties: { alpha: {} } } }); document.modelContext.registerTool({ name: "lookup", description: "second", inputSchema: { properties: { alpha: {} } } }); @@ -2008,7 +2006,7 @@ it("keeps known capture omissions in compact route summaries and queryable gap f count: null, }, ); - const report = analyzeCapturedWebBundle(captured), + const report = analyzeWebBundleFixture(captured), parent = { evidenceId: `ev_${"a".repeat(64)}`, operation: "analyze_web_bundle", diff --git a/src/domain/webBundleAnalyzer.ts b/src/domain/webBundleAnalyzer.ts index 8d3b9bd087..8e2b8b06ce 100644 --- a/src/domain/webBundleAnalyzer.ts +++ b/src/domain/webBundleAnalyzer.ts @@ -1,12 +1,8 @@ -import type { WebPageInspection } from "./browserObservationSchemas.js"; import { analyzeScript } from "./webBundleAnalyzerInspection.js"; import { createWebBundleAnalysisAccumulator, - isIncludedScript, type IncludedScript, } from "./webBundleAnalyzerState.js"; -import { buildCapturedWebBundleAnalysis } from "./webBundleAnalyzerResult.js"; -import type { WebBundleAnalysis } from "./webBundleAnalysis.js"; import { webBundleScriptAnalysisSchema, type WebBundleScriptAnalysis, @@ -36,21 +32,3 @@ export const analyzeCapturedWebScript = ( inferences: accumulator.inferences, }); }; - -/** Analyze selected captured JavaScript source without execution. */ -export const analyzeCapturedWebBundle = ( - inspection: WebPageInspection, - sourceMaps: WebBundleAnalysis["observations"]["source_maps"] = { - status: "not_requested", - requested: 0, - processed: 0, - items: [], - }, -): WebBundleAnalysis => { - const sourceScripts = inspection.scripts.items.filter(isIncludedScript); - return buildCapturedWebBundleAnalysis( - inspection, - sourceMaps, - sourceScripts.map(analyzeCapturedWebScript), - ); -}; diff --git a/src/dotnet/ManagedSnapshot.ts b/src/dotnet/ManagedSnapshot.ts index 90c134e766..0b33ebce6f 100644 --- a/src/dotnet/ManagedSnapshot.ts +++ b/src/dotnet/ManagedSnapshot.ts @@ -10,7 +10,7 @@ import { } from "../filesystem/OwnedRegularFile.js"; import { bindRegularFileCleanup, - withRegularFile, + withRegularFileCompletion, type RegularFileReadOptions, } from "../application/RegularFileRead.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; @@ -138,15 +138,29 @@ export const hashManagedSource = async ( options, ); -const readManagedFile = ( +const readManagedFile = ( path: string, operation: string, read: (handle: FileHandle, metadata: Stats) => Promise, options: RegularFileReadOptions, ): Promise => - withRegularFile(path, read, options).catch((cause: unknown) => { - throw normalizeManagedReadFailure(cause, path, operation, options.signal); - }); + withRegularFileCompletion(path, read, options) + .then(({ value, cancellation }) => { + if (cancellation !== undefined) + throw new AnalysisCancelledError(operation, { + cause: cancellation.reason, + partialObservation: { + kind: "file-snapshot-hash", + path, + bytes: value.byteLength, + sha256: value.sha256, + }, + }); + return value; + }) + .catch((cause: unknown) => { + throw normalizeManagedReadFailure(cause, path, operation, options.signal); + }); /** Keep cleanup ownership and the original typed read failure at this boundary. */ const normalizeManagedReadFailure = ( diff --git a/src/hopper/HopperFileOffset.ts b/src/hopper/HopperFileOffset.ts index 99cc014974..885d33e311 100644 --- a/src/hopper/HopperFileOffset.ts +++ b/src/hopper/HopperFileOffset.ts @@ -25,10 +25,12 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import type { JsonValue } from "../domain/jsonValue.js"; import type { FileOffsetPartialObservation } from "../domain/native/fileOffsetPartialObservation.js"; import { + findOverlappingFatSlices, readFatSliceDeclarations, readMachoHeaderPrefixFacts, type FatSliceDeclaration, validateFatSlice, + validateFatSliceIdentity, } from "../domain/apple/machoContainer.js"; import { err, ok, type Result } from "../domain/result.js"; @@ -351,17 +353,11 @@ function fatLayout(prefix: Buffer): Result { const table = prefix.subarray(8, tableEnd); const parsed = readFatSliceDeclarations(table, { count, wide, littleEndian }); if (parsed.status !== "parsed") return err(parsed.reason); - if (wide) { - for (const slice of parsed.slices) - if ( - (littleEndian - ? table.readUInt32LE(slice.index * 32 + 28) - : table.readUInt32BE(slice.index * 32 + 28)) !== 0 - ) - return err( - `FAT64 architecture ${slice.index} has a nonzero reserved field`, - ); - } + for (const slice of parsed.slices) + if (slice.reserved !== null && slice.reserved !== 0) + return err( + `FAT64 architecture ${slice.index} has a nonzero reserved field`, + ); return ok({ slices: parsed.slices, tableEnd }); } @@ -436,17 +432,8 @@ function originalOffset( "Hopper did not expose the loaded Mach-O header needed to identify its FAT slice", ); const matches = []; - const ranges = [...layout.value.slices].sort((a, b) => a.offset - b.offset); - for (let index = 1; index < ranges.length; index++) { - const previous = ranges[index - 1]; - const current = ranges[index]; - if ( - previous !== undefined && - current !== undefined && - current.offset < previous.offset + previous.size - ) - return err("Original FAT slices overlap"); - } + if (findOverlappingFatSlices(layout.value.slices) !== null) + return err("Original FAT slices overlap"); for (const slice of layout.value.slices) { const reason = validateFatSlice(slice, size, layout.value.tableEnd); if (reason !== null) return err(reason); @@ -460,12 +447,7 @@ function originalOffset( return err( `FAT architecture ${slice.index} does not contain a Mach-O image`, ); - const identityError = validateFatSlice( - slice, - size, - layout.value.tableEnd, - embeddedIdentity, - ); + const identityError = validateFatSliceIdentity(slice, embeddedIdentity); if (identityError !== null) return err(identityError); if ( slice.cpuType === loadedIdentity.cpuType && diff --git a/src/hopper/HopperMachOImage.ts b/src/hopper/HopperMachOImage.ts index ad2c9e3bf4..e41d62953b 100644 --- a/src/hopper/HopperMachOImage.ts +++ b/src/hopper/HopperMachOImage.ts @@ -14,9 +14,11 @@ import { BinaryTargetError } from "../domain/configurationErrors.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { + findOverlappingFatSlices, readFatSliceDeclarations, readMachoHeaderPrefixFacts, validateFatSlice, + validateFatSliceIdentity, type FatSliceDeclaration, } from "../domain/apple/machoContainer.js"; import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js"; @@ -366,7 +368,6 @@ const inspectHopperMachOImage = async ( const architecture = target.architecture; const cpuTypes = { x86: 7, x86_64: 0x1000007, arm: 12, arm64: 0x100000c }; const matches = []; - const ranges = []; for (const slice of parsed.slices) { if (signal?.aborted) return err( @@ -374,15 +375,10 @@ const inspectHopperMachOImage = async ( ); const rangeError = validateFatSlice(slice, size, tableEnd); if (rangeError !== null) return invalid(rangeError); - if (wide) { - const reserved = littleEndian - ? table.readUInt32LE(slice.index * 32 + 28) - : table.readUInt32BE(slice.index * 32 + 28); - if (reserved !== 0) - return invalid( - `FAT64 architecture ${slice.index} has a nonzero reserved field`, - ); - } + if (slice.reserved !== null && slice.reserved !== 0) + return invalid( + `FAT64 architecture ${slice.index} has a nonzero reserved field`, + ); if (slice.size < 12) return invalid( `FAT architecture ${slice.index} has a truncated Mach-O header`, @@ -395,22 +391,12 @@ const inspectHopperMachOImage = async ( return invalid( `FAT architecture ${slice.index} does not contain a Mach-O image`, ); - const identityError = validateFatSlice(slice, size, tableEnd, identity); + const identityError = validateFatSliceIdentity(slice, identity); if (identityError !== null) return invalid(identityError); - ranges.push(slice); if (slice.cpuType === cpuTypes[architecture]) matches.push(slice); } - ranges.sort((a, b) => a.offset - b.offset); - for (let index = 1; index < ranges.length; index++) { - const previous = ranges[index - 1]; - const current = ranges[index]; - if ( - previous !== undefined && - current !== undefined && - current.offset < previous.offset + previous.size - ) - return invalid("FAT slices overlap"); - } + if (findOverlappingFatSlices(parsed.slices) !== null) + return invalid("FAT slices overlap"); const selected = selectHopperSlice(matches, architecture); if (selected === undefined) return invalid( diff --git a/src/native/AppleDispatchMetadata.failure.test.ts b/src/native/AppleDispatchMetadata.failure.test.ts index 11d187ac97..8c9a36141a 100644 --- a/src/native/AppleDispatchMetadata.failure.test.ts +++ b/src/native/AppleDispatchMetadata.failure.test.ts @@ -2,19 +2,18 @@ import { expect, it } from "vitest"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { appleDispatchMetadataFailure } from "./AppleDispatchMetadata.js"; +// The metadata target is already bound, so losing it is a change to that artifact rather than a new input error. it.each(["integrity", "path"] as const)( - "keeps %s source-read refusals distinct from provider execution failures", + "reports a %s source-read refusal as a change to the bound target, not a provider failure", (reason) => { const cause = new ArtifactReaderFailure(reason, "source boundary refused"); expect(appleDispatchMetadataFailure(cause, "/selected")._tag).toBe( - reason === "integrity" - ? "AnalysisArtifactChangedError" - : "AnalysisInputError", + "AnalysisArtifactChangedError", ); }, ); it.each(["EACCES", "EPERM", "ENOENT"])( - "preserves the filesystem outcome %s for a selected metadata target", + "preserves the filesystem outcome %s for a bound metadata target", (code) => { const physical = Object.assign(new Error("selected read refused"), { code, @@ -23,7 +22,9 @@ it.each(["EACCES", "EPERM", "ENOENT"])( cause: physical, }); expect(appleDispatchMetadataFailure(cause, "/selected")._tag).toBe( - code === "ENOENT" ? "AnalysisInputError" : "AnalysisAccessDeniedError", + code === "ENOENT" + ? "AnalysisArtifactChangedError" + : "AnalysisAccessDeniedError", ); }, ); diff --git a/src/native/AppleDispatchMetadata.test.ts b/src/native/AppleDispatchMetadata.test.ts index 8fe3a34950..b7f00de5b0 100644 --- a/src/native/AppleDispatchMetadata.test.ts +++ b/src/native/AppleDispatchMetadata.test.ts @@ -336,8 +336,13 @@ const swiftExternalSwiftBindingImage = ( bytes.writeBigUInt64LE(0n, 0xb00); // Bound slot has no in-image pointer. bytes.writeUInt32LE(16, 0xc00); bytes.writeInt32LE(0x1f8, 0xc08); + bytes.writeInt32LE(0xc40 - 0xc04, 0xc04); + bytes.writeUInt32LE(0, 0xc40); + bytes.writeInt32LE(0xe40 - 0xc48, 0xc48); + bytes.write("FixtureModule\0", 0xe40); if (boundField !== "protocol") { bytes.writeUInt32LE(3, 0xd00); + bytes.writeInt32LE(0xc40 - 0xd04, 0xd04); bytes.writeInt32LE(0xf8, 0xd08); bytes.write("Equatable\0", 0xe00); } else { @@ -399,6 +404,7 @@ const swiftBoundWitnessSlotImage = () => { bytes.writeInt32LE(0xec0 - 0xc48, 0xc48); bytes.write("FixtureModule\0", 0xec0); bytes.writeUInt32LE(3, 0xd00); + bytes.writeInt32LE(0xc40 - 0xd04, 0xd04); bytes.writeInt32LE(0x148, 0xd08); bytes.writeUInt32LE(0, 0xd0c); bytes.writeUInt32LE(1, 0xd10); @@ -740,6 +746,10 @@ describe("Swift conformance type-reference kinds", () => { bytes.writeInt32LE(0xdc, 0x224); // Indirect class cell at 0x300. bytes.writeUInt32LE(3 << 3, 0x22c); bytes.writeUInt32LE(3, 0x260); // Protocol context descriptor. + bytes.writeInt32LE(0x320 - 0x264, 0x264); // Parent module context. + bytes.writeUInt32LE(0, 0x320); + bytes.writeInt32LE(0x340 - 0x328, 0x328); + bytes.write("FixtureModule\0", 0x340); bytes.writeInt32LE(0x18, 0x268); bytes.write("FixtureProtocol\0", 0x280); bytes.writeBigUInt64LE(target, 0x300); @@ -2379,3 +2389,36 @@ it("retains the enclosing Swift type and module in a conformance owner", () => { }); expect(result.swift_dispatch_slots[0]?.owner).toBe("Outer.Widget: Equatable"); }); + +it.each([false, true])( + "preserves a nested protocol identity and reports unread parent context (%s)", + (unreadParent) => { + const bytes = swiftBoundWitnessSlotImage(); + // This identity regression uses a local executable witness, not an import. + bytes.writeUInt32LE(0, 280); + bytes.writeUInt32LE(0, 284); + bytes.writeBigUInt64LE(base + 0xb00n, 0xe08); + bytes.writeInt32LE(0xc20 - 0xd04, 0xd04); + bytes.writeUInt32LE(17, 0xc20); + bytes.writeInt32LE((unreadParent ? 0x5000 : 0xc40) - 0xc24, 0xc24); + bytes.writeInt32LE(0xea0 - 0xc28, 0xc28); + bytes.write("Outer\0", 0xea0); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + expect(result.swift_conformances[0]).toMatchObject({ + type_name: "Widget", + protocol_name: unreadParent ? "Equatable" : "Outer.Equatable", + module: "FixtureModule", + decode: { status: unreadParent ? "partial" : "decoded" }, + }); + expect(result.swift_dispatch_slots[0]).toMatchObject({ + owner: unreadParent ? "Widget: Equatable" : "Widget: Outer.Equatable", + implementation_address: "0x100000b00", + decode: { status: "decoded" }, + }); + expect( + result.coverage.find( + ({ facet }) => facet === "swift_conformances_static_witness_slots", + )?.status, + ).toBe(unreadParent ? "partial" : "complete"); + }, +); diff --git a/src/native/AppleDispatchMetadata.ts b/src/native/AppleDispatchMetadata.ts index 6e3dc53c0a..ce249c1ca5 100644 --- a/src/native/AppleDispatchMetadata.ts +++ b/src/native/AppleDispatchMetadata.ts @@ -14,7 +14,6 @@ import { } from "../domain/binaryTargetTypes.js"; import { AnalysisCancelledError, - AnalysisInputError, AnalysisArtifactChangedError, AnalysisAccessDeniedError, AnalysisCapabilityUnavailableError, @@ -473,9 +472,9 @@ const appleDispatchMetadataPrimaryFailure = ( cause, }); if (cause instanceof ArtifactReaderFailure && cause.reason === "path") - return new AnalysisInputError(OPERATION, { cause }, [ - { path: ["path"], reason: "invalid_value", message: cause.message }, - ]); + return new AnalysisArtifactChangedError(OPERATION, path, cause.message, { + cause, + }); } const readCause = origin === "owned-or-classified" && @@ -491,14 +490,15 @@ const appleDispatchMetadataPrimaryFailure = ( return new AnalysisAccessDeniedError(OPERATION, path, readCause.code, { cause, }); + // This path already belongs to the bound target; loss of readability is a + // change to that artifact, rather than a new caller input-selection error. if (isSelectedRegularFileError(readCause)) - return new AnalysisInputError(OPERATION, { cause }, [ - { - path: ["path"], - reason: "invalid_value", - message: `Selected metadata target could not be read (${String(readCause.code)}): ${path}`, - }, - ]); + return new AnalysisArtifactChangedError( + OPERATION, + path, + `Bound metadata target could not be read (${readCause.code}): ${path}`, + { cause }, + ); return new ProviderAdapterError(IDENTITY.id, OPERATION, { cause, diagnostics: { diff --git a/src/native/AppleMachoSelection.test.ts b/src/native/AppleMachoSelection.test.ts index 69c54725e9..bb4caafafd 100644 --- a/src/native/AppleMachoSelection.test.ts +++ b/src/native/AppleMachoSelection.test.ts @@ -70,6 +70,25 @@ const registerThinCpuFamilyControls = (): void => { }); }; +it.each([false, true])( + "retains the shared FAT declaration without imposing Hopper policy (wide=%s)", + (wide) => { + const thin = machoImage({ cpu: CPU.arm64e }); + const fat = Buffer.from(fatImage([{ cpu: CPU.arm64e, bytes: thin }], wide)); + if (wide) fat.writeUInt32BE(0x89abcdef, 8 + 28); + expect(selectMachoSlice(fat, "arm64").fatDeclaration).toEqual({ + index: 0, + offset: 4096, + size: thin.byteLength, + cpuType: CPU.arm64e.type, + cpuSubtype: CPU.arm64e.subtype, + alignmentExponent: 12, + reserved: wide ? 0x89abcdef : null, + tableEnd: 8 + (wide ? 32 : 20), + }); + }, +); + describe("native Mach-O slice selection", () => { it("rejects misaligned load commands", () => { expect(() => diff --git a/src/native/AppleMachoSelection.ts b/src/native/AppleMachoSelection.ts index 5ca972f526..a81399a963 100644 --- a/src/native/AppleMachoSelection.ts +++ b/src/native/AppleMachoSelection.ts @@ -11,6 +11,7 @@ import { machoCpuSubtypeBase } from "../domain/apple/machoCpuIdentity.js"; import { readFatSliceDeclarations, validateFatSlice, + validateFatSliceIdentity, type FatSliceDeclaration, } from "../domain/apple/machoContainer.js"; @@ -34,15 +35,7 @@ export interface MachoSlice { slice: number; sliceEnd: number; /** FAT table identity, absent for a thin image. */ - fatDeclaration?: { - offset: number; - size: number; - cpuType: number; - cpuSubtype: number; - alignmentExponent: number; - index: number; - tableEnd: number; - }; + fatDeclaration?: FatSliceDeclaration & { readonly tableEnd: number }; } /** Parsed little-endian 64-bit Mach-O layout for the selected architecture slice. */ @@ -193,7 +186,7 @@ export const parseMachoLayout = ( const cpuSubtype = readHeaderUInt32(slice + 8); const fat = selection.fatDeclaration; if (fat !== undefined) { - const issue = validateFatSlice(fat, bytes.length, fat.tableEnd, { + const issue = validateFatSliceIdentity(fat, { cpuType, cpuSubtype, }); diff --git a/src/native/AppleSwiftDispatchFacets.ts b/src/native/AppleSwiftDispatchFacets.ts index 31d0609aaa..8d223a5b97 100644 --- a/src/native/AppleSwiftDispatchFacets.ts +++ b/src/native/AppleSwiftDispatchFacets.ts @@ -1,4 +1,7 @@ -import type { SwiftContextReader } from "./AppleSwiftNominalTypes.js"; +import { + createSwiftContextReader, + type SwiftContextReader, +} from "./AppleSwiftNominalTypes.js"; import type { ObjcSwiftMetadata } from "../domain/native/objcSwiftMetadata.js"; import type { PointerResolution } from "./AppleMachoFixups.js"; import type { Segment, Section } from "./AppleMachoSelection.js"; @@ -109,13 +112,10 @@ interface SwiftProtocolFacts { const readSwiftProtocolFacts = (input: { readonly target: bigint | null; - readonly relative: ( - field: bigint, - kind: SwiftRelativeField, - ) => SwiftRelativeValue; + readonly contextReader: SwiftContextReader; readonly read: SwiftDispatchReaders; }): SwiftProtocolFacts => { - const { target, relative, read } = input; + const { target, read } = input; if (target === null) return { name: null, @@ -136,24 +136,14 @@ const readSwiftProtocolFacts = (input: { readable: false, reason: "Unsupported Swift protocol descriptor kind", }; - let nameAddress: bigint; try { - nameAddress = requiredSwiftRelativeTarget( - relative, - target + 8n, - "protocolName", - unresolvedSwiftRelativeReason, - ); - } catch (cause) { - const reason = expectedSwiftMetadataReadFailure(cause); - if (reason === null) throw cause; - return { name: null, readable: false, reason }; - } - try { - const name = read.string(nameAddress); - if (name.length === 0) - throw new RangeError("Swift protocol descriptor name is empty"); - return { name, readable: true, reason: null }; + const name = input.contextReader.protocolName(target); + const context = input.contextReader.contextOf(target); + return { + name: context.path ?? name, + readable: true, + reason: context.reason, + }; } catch (cause) { const reason = expectedSwiftMetadataReadFailure(cause); if (reason === null) throw cause; @@ -326,6 +316,8 @@ export const decodeSwiftDispatchFacets = (input: { result: ObjcSwiftMetadata; }): FacetDecodeFacts => { const { sections, segments, readers: read, relative, budget, result } = input; + const contextReader = + input.contextReader ?? createSwiftContextReader({ ...read, relative }); const issues = [] as ReturnType[]; let swiftExamined = 0; let decodedRecords = 0; @@ -358,7 +350,7 @@ export const decodeSwiftDispatchFacets = (input: { : null; const protocolFacts = readSwiftProtocolFacts({ target: protocol, - relative, + contextReader, read, }); const protocolName = protocolFacts.name; @@ -373,9 +365,7 @@ export const decodeSwiftDispatchFacets = (input: { descriptor, flags, read, - ...(input.contextReader === undefined - ? {} - : { contextReader: input.contextReader }), + contextReader, relative, unresolvedReason: unresolvedSwiftRelativeReason, }); @@ -410,17 +400,18 @@ export const decodeSwiftDispatchFacets = (input: { descriptor, ); } - const conformanceReason = witnessReason ?? typeReference.reason; + const conformanceReason = + witnessReason ?? typeReference.reason ?? protocolFacts.reason; const conformanceDecode = conformanceReason === null ? { status: "decoded" as const, reason: null } : { status: "partial" as const, reason: conformanceReason }; - if (witnessReason === null && typeReference.reason !== null) + if (witnessReason === null && conformanceReason !== null) issues.push( issue( "swift_conformances_static_witness_slots", "conformance_identity_partial", - typeReference.reason, + conformanceReason, hex(descriptor), ), ); diff --git a/src/native/AppleSwiftNominalTypes.ts b/src/native/AppleSwiftNominalTypes.ts index 49135b614b..b2156aa96b 100644 --- a/src/native/AppleSwiftNominalTypes.ts +++ b/src/native/AppleSwiftNominalTypes.ts @@ -54,6 +54,7 @@ const ENUM = 18; const MODULE = 0; const EXTENSION = 1; const ANONYMOUS = 2; +const PROTOCOL = 3; const GENERIC = 0x80; const CLASS_IS_ACTOR = 0x80; /** Swift nesting is shallow; a longer chain is malformed or cyclic. */ @@ -102,15 +103,20 @@ export const createSwiftContextReader = ( const contexts = new Map(); const names = new Map(); - const typeName = (descriptor: bigint): string => { + const namedContext = ( + descriptor: bigint, + field: "typeName" | "protocolName", + ): string => { const known = names.get(descriptor); if (known !== undefined) return known; - const value = read.relative(descriptor + 8n, "typeName"); + const value = read.relative(descriptor + 8n, field); if (value.kind !== "target") - throw new RangeError(unresolvedSwiftRelativeReason(value, "typeName")); + throw new RangeError(unresolvedSwiftRelativeReason(value, field)); const name = read.string(value.target); if (name.length === 0) - throw new RangeError("Swift type descriptor name is empty"); + throw new RangeError( + `Swift ${field === "protocolName" ? "protocol" : "type"} descriptor name is empty`, + ); names.set(descriptor, name); return name; }; @@ -168,10 +174,14 @@ export const createSwiftContextReader = ( walkLimited: false, }; } + case PROTOCOL: case CLASS: case STRUCT: case ENUM: { - const name = typeName(context); + const name = namedContext( + context, + kind === PROTOCOL ? "protocolName" : "typeName", + ); const parent = parentOf(context, walk); return child(parent, { path: @@ -220,7 +230,9 @@ export const createSwiftContextReader = ( }; return { - typeName, + typeName: (descriptor: bigint) => namedContext(descriptor, "typeName"), + protocolName: (descriptor: bigint) => + namedContext(descriptor, "protocolName"), contextOf: (descriptor: bigint) => contextOf(descriptor, 0), }; }; diff --git a/src/native/AppleSwiftVtables.test.ts b/src/native/AppleSwiftVtables.test.ts index 68465e35f8..2351c2451c 100644 --- a/src/native/AppleSwiftVtables.test.ts +++ b/src/native/AppleSwiftVtables.test.ts @@ -226,6 +226,7 @@ it("returns a located issue for an emitted unresolved witness slot", () => { const values = new Map([ [0x20cn, 0], // simple, unconditional conformance [0x500n, 3], // protocol descriptor kind + [0x800n, 0], // module context [0x50cn, 0], // signature count [0x510n, 1], // witness requirement count [0x2b0n, 16], // nominal type descriptor kind @@ -279,6 +280,8 @@ it("returns a located issue for an emitted unresolved witness slot", () => { [0x100n, 0x200n], [0x200n, 0x500n], [0x508n, 0x600n], + [0x504n, 0x800n], + [0x808n, 0x820n], [0x204n, 0x2b0n], [0x2b8n, 0x700n], [0x208n, 0x380n], diff --git a/src/native/CommandRunner.ts b/src/native/CommandRunner.ts index 696cc08f33..3c7590efe8 100644 --- a/src/native/CommandRunner.ts +++ b/src/native/CommandRunner.ts @@ -223,18 +223,16 @@ export class XcrunCommandRunner implements NativeCommandRunner { this.launcher, ); if (!captured.ok) return err(this.#retainCleanupOwner(captured.error)); - return captured.ok - ? ok({ - ...captured.value, - tool, - executable: resolved.value.path, - executableSha256: resolved.value.sha256, - toolVersion: null, - versionReason: - "Tool exposes no uniform stable version flag; executable SHA-256 is a prelaunch file sample and does not prove atomic OS image binding.", - arguments: [...arguments_], - }) - : captured; + return ok({ + ...captured.value, + tool, + executable: resolved.value.path, + executableSha256: resolved.value.sha256, + toolVersion: null, + versionReason: + "Tool exposes no uniform stable version flag; executable SHA-256 is a prelaunch file sample and does not prove atomic OS image binding.", + arguments: [...arguments_], + }); } async #retryCleanup(): Promise { diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts index 76aa576078..5a3b3efee1 100644 --- a/src/native/NativeMacOSProvider.ts +++ b/src/native/NativeMacOSProvider.ts @@ -210,7 +210,7 @@ export class NativeMacOSProvider implements AnalysisProvider { class NativeMacOSClient implements AnalysisClient { readonly #pendingNativeUiRuntimes = new Set(); - readonly #pendingCommands = new Set>(); + readonly #pendingCommandOperations = new Set>(); readonly #signatureResources = new ArtifactResourceScope(); readonly #plistResources = new ArtifactResourceScope(); readonly #callResources = new ArtifactResourceScope(); @@ -351,12 +351,12 @@ class NativeMacOSClient implements AnalysisClient { ) { return this.#observeNativeUi(operation, parameters, options?.signal); } + const pending = Promise.resolve().then(() => + this.#dispatchCommand(operation, parameters, options?.signal), + ); + this.#pendingCommandOperations.add(pending); try { - const observation = await this.#dispatch( - operation, - parameters, - options?.signal, - ); + const observation = await pending; return observation.ok ? ok( createAnalysisExecution(observation.value.result, IDENTITY, { @@ -367,12 +367,14 @@ class NativeMacOSClient implements AnalysisClient { : err(resolvedAnalysisError(observation.error)); } catch (cause: unknown) { return err(nativeOutputFailure(operation, cause)); + } finally { + this.#pendingCommandOperations.delete(pending); } } async close(): Promise> { this.#closed = true; - await Promise.allSettled(this.#pendingCommands); + await Promise.allSettled(this.#pendingCommandOperations); const uiCleanupFailures = await this.#serializeNativeUi(() => this.#retryNativeUiCleanup(), ); @@ -570,33 +572,6 @@ class NativeMacOSClient implements AnalysisClient { return new NativeUiCleanupError(failures, operation, partialObservation); } - #dispatch( - operation: NativeToolName, - parameters: Readonly>, - signal?: AbortSignal, - ): Promise> { - switch (operation) { - case "observe_native_ui": - case "capture_native_ui_scenario": - case "observe_native_calls": - return Promise.resolve( - err( - new AnalysisCapabilityUnavailableError( - IDENTITY.id, - operation, - "UI observation uses its explicit authority boundary", - ), - ), - ); - case "inspect_macho": - case "inspect_signature": - case "inspect_plist": - case "list_architectures": - case "demangle_swift": - return this.#dispatchCommand(operation, parameters, signal); - } - } - #dispatchCommand( operation: | "inspect_macho" @@ -1090,8 +1065,31 @@ class NativeMacOSClient implements AnalysisClient { const source = await readNativePlistBytes(fileInput); if (!source.ok) return source; let parsed: ReturnType; + const provenance = [ + invocation(classified, fileInput.selection.path, "$PLIST"), + ]; if (format === "xml") { parsed = parsePlistXmlSource(source.value); + if (parsed.ok) { + const validated = await this.#run( + "inspect_plist", + "plutil", + ["-lint", "--", fileInput.selection.path], + { + signal: fileInput.signal, + acceptNonZero: true, + timeoutMs: fileInput.timeoutMs, + }, + ); + if (!validated.ok) return validated; + if (validated.value.exitCode !== 0) + return err( + nativePlistDecodeFailure(fileInput.selection, validated.value), + ); + provenance.push( + invocation(validated.value, fileInput.selection.path, "$PLIST"), + ); + } if ( !parsed.ok && parsed.error instanceof XmlPlistTextDecodeError && @@ -1125,9 +1123,7 @@ class NativeMacOSClient implements AnalysisClient { format, path: fileInput.selection.path, resourceRoot: this.bundleResourceRoot, - provenance: [ - invocation(classified, fileInput.selection.path, "$PLIST"), - ], + provenance, }), ); } @@ -1168,24 +1164,7 @@ class NativeMacOSClient implements AnalysisClient { if (!converted.ok) return converted; xml = converted.value; if (xml.exitCode !== 0) { - const capture = failureCapture(xml, false); - return err( - unreadablePlist( - fileInput.selection, - `plutil could not decode it as a property list (${plutilDiagnostic(capture, path)})`, - { - capturedOutput: { - stdout: capture.stdout, - stderr: capture.stderr, - truncated: capture.truncated, - stdout_bytes: capture.stdoutBytes, - stderr_bytes: capture.stderrBytes, - exit_code: capture.exitCode, - signal: capture.signal, - }, - }, - ), - ); + return err(nativePlistDecodeFailure(fileInput.selection, xml)); } parsed = jsonAttempt === undefined @@ -1249,27 +1228,13 @@ class NativeMacOSClient implements AnalysisClient { | undefined; }, ): Promise> { - if (this.#closed) - return err( - new AnalysisCapabilityUnavailableError( - IDENTITY.id, - operation, - "Native provider client is closed", - ), - ); - const command = this.runner.run(tool, arguments_, { + // Commands borrow their enclosing operation's admission through finalization. + const captured = await this.runner.run(tool, arguments_, { ...(options.signal === undefined ? {} : { signal: options.signal }), timeoutMs: options.timeoutMs, acceptNonZero: options.acceptNonZero ?? options.acceptNonZeroIf !== undefined, }); - this.#pendingCommands.add(command); - let captured: Awaited; - try { - captured = await command; - } finally { - this.#pendingCommands.delete(command); - } if (captured.ok) { const hasFailedExit = captured.value.exitCode !== 0 || captured.value.signal !== null; @@ -1560,7 +1525,29 @@ const parseEntitlements = ( }; }; -/** Failed plutil conversions can report on either stream; preserve both. */ +/** Native admission failures preserve the selected/default source and physical command output. */ +const nativePlistDecodeFailure = ( + selection: NativePlistSelection, + command: NativeCommandCapture, +): AnalysisError => { + const capture = failureCapture(command, false); + return unreadablePlist( + selection, + `plutil could not decode it as a property list (${plutilDiagnostic(capture, selection.path)})`, + { + capturedOutput: { + stdout: capture.stdout, + stderr: capture.stderr, + truncated: capture.truncated, + stdout_bytes: capture.stdoutBytes, + stderr_bytes: capture.stderrBytes, + exit_code: capture.exitCode, + signal: capture.signal, + }, + }, + ); +}; + const plutilDiagnostic = ( capture: Pick, path: string, diff --git a/src/native/metadata/BinaryContainer.test.ts b/src/native/metadata/BinaryContainer.test.ts index 1603d63e72..65cf2aaa1a 100644 --- a/src/native/metadata/BinaryContainer.test.ts +++ b/src/native/metadata/BinaryContainer.test.ts @@ -1,10 +1,95 @@ import { expect, it } from "vitest"; import { + BinaryFormatFailure, + BinaryMappingFailure, + createFileMappingIndex, mappedFileOffset, readMappedCString, type BinaryContainer, + type FileMapping, + type ZeroFillMapping, } from "./BinaryContainer.js"; +const outcome = (translate: () => number): number | string => { + try { + return translate(); + } catch (cause) { + if (cause instanceof BinaryMappingFailure) return cause.reason; + if (cause instanceof BinaryFormatFailure) return cause.message; + throw cause; + } +}; + +/** Deterministic xorshift so failures replay exactly. */ +const random = (seed: number) => { + let state = seed; + return (limit: number): number => { + state ^= state << 13; + state ^= state >>> 17; + state ^= state << 5; + return (state >>> 0) % limit; + }; +}; + +it("indexes exactly the spans mappedFileOffset accepts, rejects and translates", () => { + const next = random(0x5eed); + const seen = new Set(); + for (let round = 0; round < 150; round++) { + const mappings: FileMapping[] = Array.from({ length: 1 + next(6) }, () => ({ + address: 0x1000n + BigInt(next(96)), + offset: next(4) === 0 ? 0x400 : next(256), + size: next(48), + })); + // Some rounds alias one mapping at the same translation to cover consistent overlap. + if (next(3) === 0 && mappings[0] !== undefined) + mappings.push({ + address: mappings[0].address + 4n, + offset: mappings[0].offset + 4, + size: Math.max(0, mappings[0].size - 4), + }); + const zeroFills: ZeroFillMapping[] = Array.from( + { length: next(3) }, + () => ({ + address: 0x1000n + BigInt(next(160)), + size: BigInt(next(16)), + }), + ); + const lookup = createFileMappingIndex(mappings, zeroFills).offset; + const indexed: (number | string)[] = []; + const scanned: (number | string)[] = []; + for (let address = 0xff8n; address < 0x10a8n; address++) { + for (const size of [1, 2, 4, 9, 20]) { + indexed.push(outcome(() => lookup(address, size))); + scanned.push( + outcome(() => mappedFileOffset(mappings, address, size, zeroFills)), + ); + } + } + expect(indexed, `round ${String(round)}`).toEqual(scanned); + for (const value of scanned) + seen.add(typeof value === "number" ? "translated" : value); + } + expect([...seen].sort()).toEqual(["ambiguous", "translated", "unmapped"]); +}); + +it("maps adjacent ranges sharing one file translation", () => { + const lookup = createFileMappingIndex([ + { address: 0x1000n, offset: 0x400, size: 0x10 }, + { address: 0x1010n, offset: 0x410, size: 0x10 }, + ]).offset; + expect(lookup(0x1008n, 8)).toBe(0x408); + expect(lookup(0x100cn, 8)).toBe(0x40c); +}); + +it("rejects partially overlapping mappings with different file translations", () => { + const lookup = createFileMappingIndex([ + { address: 0x1000n, offset: 0x400, size: 0x20 }, + { address: 0x1018n, offset: 0x900, size: 0x10 }, + ]).offset; + expect(lookup(0x1000n, 0x10)).toBe(0x400); + expect(() => lookup(0x1010n, 0x10)).toThrow(/ambiguous/u); +}); + const mappings = [ { address: 100n, offset: 0, size: 4 }, { address: 104n, offset: 4, size: 4 }, @@ -32,21 +117,58 @@ it("refuses a zero-fill alias inside a span spanning multiple mappings", () => { mappedFileOffset(mappings, 102n, 4, [{ address: 104n, size: 1n }]), ).toThrow(/zero-fill/); }); -it("reads strings across discontiguous source ranges without examining bytes after NUL", () => { - const image: BinaryContainer = { - format: "pe", - architecture: "amd64", - bits: 64, - byte_order: "little", - pe_header_mapping: mappings[0]!, - mappings: [{ address: 104n, offset: 8, size: 4 }], - zero_fills: [{ address: 106n, size: 2n }], - sections: [], - }; - expect( - readMappedCString(Buffer.from("ABCDxxxxE\0xx"), image, 102n, 8).toString(), - ).toBe("CDE"); - expect(() => - mappedFileOffset([...mappings.slice(0, 1), ...image.mappings], 102n, 4), - ).toThrow(/ambiguous/); -}); +it.each(["scan", "index"] as const)( + "reads strings across discontiguous source ranges without examining bytes after NUL (%s)", + (mode) => { + const image: BinaryContainer = { + format: "pe", + architecture: "amd64", + bits: 64, + byte_order: "little", + pe_header_mapping: mappings[0]!, + mappings: [{ address: 104n, offset: 8, size: 4 }], + zero_fills: [{ address: 106n, size: 2n }], + sections: [], + }; + const bytes = Buffer.from("ABCDxxxxE\0xx"); + const result = + mode === "scan" + ? readMappedCString(bytes, image, 102n, 8) + : createFileMappingIndex( + [mappings[0]!, ...image.mappings], + image.zero_fills, + ).readCString(bytes, 102n, 8); + expect(result.toString()).toBe("CDE"); + expect(() => + mappedFileOffset([...mappings.slice(0, 1), ...image.mappings], 102n, 4), + ).toThrow(/ambiguous/); + }, +); + +it.each([0, -1, 0.5, Number.NaN, Number.POSITIVE_INFINITY])( + "preserves zero-span and invalid-size semantics for %s", + (size) => { + const index = createFileMappingIndex(mappings); + expect(outcome(() => index.offset(104n, size))).toEqual( + outcome(() => mappedFileOffset(mappings, 104n, size)), + ); + }, +); + +it.each([false, true])( + "keeps conflict classification before gap and zero-fill checks (%s)", + (conflicting) => { + const ranges = [ + { address: 100n, offset: 0, size: 2 }, + { address: 104n, offset: conflicting ? 8 : 4, size: 2 }, + ]; + const zeroFills = [{ address: 102n, size: 1n }]; + const index = createFileMappingIndex(ranges, zeroFills); + expect(outcome(() => index.offset(101n, 4))).toBe( + conflicting ? "ambiguous" : "unmapped", + ); + expect(outcome(() => index.offset(101n, 4))).toEqual( + outcome(() => mappedFileOffset(ranges, 101n, 4, zeroFills)), + ); + }, +); diff --git a/src/native/metadata/BinaryContainer.ts b/src/native/metadata/BinaryContainer.ts index e8fd6ff4d0..c7c92e4818 100644 --- a/src/native/metadata/BinaryContainer.ts +++ b/src/native/metadata/BinaryContainer.ts @@ -315,6 +315,207 @@ export const mappedFileOffset = ( return found; }; +/** Index of the last sorted value at or below `value`, or -1. */ +const lastAtOrBelow = (sorted: readonly bigint[], value: bigint): number => { + let low = 0; + let high = sorted.length; + while (low < high) { + const middle = (low + high) >>> 1; + if ((sorted[middle] as bigint) <= value) low = middle + 1; + else high = middle; + } + return low - 1; +}; + +/** Sorted starts with the furthest end reached by any span starting at or before each. */ +const prefixReach = ( + spans: readonly { readonly start: bigint; readonly end: bigint }[], +): { readonly starts: bigint[]; readonly reach: bigint[] } => { + const sorted = [...spans].sort((left, right) => + left.start < right.start ? -1 : left.start > right.start ? 1 : 0, + ); + const starts: bigint[] = []; + const reach: bigint[] = []; + let furthest = -1n; + for (const span of sorted) { + if (span.end > furthest) furthest = span.end; + starts.push(span.start); + reach.push(furthest); + } + return { starts, reach }; +}; + +/** + * Indexed `mappedFileOffset` for parsers that translate many spans in one + * image. Construction is O(n log n) over mappings; each lookup is O(log n), so + * hostile section counts cannot multiply the cost of every table entry. It + * accepts, rejects and translates exactly the spans `mappedFileOffset` does; + * a span that is both a conflicting file alias and a zero-fill overlap may be + * refused under either ambiguity message. + */ +export const createFileMappingIndex = ( + mappings: readonly FileMapping[], + zeroFills: readonly ZeroFillMapping[] = [], +): FileMappingIndex => { + const backed = mappings + .filter((mapping) => mapping.size > 0) + .map((mapping) => ({ + start: mapping.address, + end: mapping.address + BigInt(mapping.size), + translation: BigInt(mapping.offset) - mapping.address, + })); + const zeroFilled = prefixReach( + zeroFills + .filter((zeroFill) => zeroFill.size > 0n) + .map((zeroFill) => ({ + start: zeroFill.address, + end: zeroFill.address + zeroFill.size, + })), + ); + // Elementary intervals between every mapping boundary, each with the single + // file translation shared by all mappings covering it (or none/ambiguous). + const events = new Map(); + const event = (at: bigint) => { + let entry = events.get(at); + if (entry === undefined) { + entry = { add: [], remove: [] }; + events.set(at, entry); + } + return entry; + }; + for (const span of backed) { + event(span.start).add.push(span.translation); + event(span.end).remove.push(span.translation); + } + const boundaries = [...events.keys()].sort((left, right) => + left < right ? -1 : left > right ? 1 : 0, + ); + const active = new Map(); + // Undefined is a gap; null is an overlap with conflicting translations. + const translations: (bigint | null | undefined)[] = []; + for (const boundary of boundaries) { + const entry = events.get(boundary); + for (const translation of entry?.remove ?? []) { + const count = (active.get(translation) ?? 0) - 1; + if (count === 0) active.delete(translation); + else active.set(translation, count); + } + for (const translation of entry?.add ?? []) + active.set(translation, (active.get(translation) ?? 0) + 1); + translations.push( + active.size === 0 + ? undefined + : active.size === 1 + ? (active.keys().next().value as bigint) + : null, + ); + } + // runEnd[i]: end of the maximal run of intervals sharing interval i's translation. + const runEnd: bigint[] = boundaries.map(() => 0n); + for (let index = boundaries.length - 1; index >= 0; index--) { + const next = boundaries[index + 1]; + const translation = translations[index]; + runEnd[index] = + next === undefined + ? (boundaries[index] as bigint) + : translation !== null && + translation !== undefined && + translations[index + 1] === translation + ? (runEnd[index + 1] as bigint) + : next; + } + const ambiguousPrefix = [0]; + const previousMapped: number[] = []; + const nextMapped: number[] = []; + const translationRuns: number[] = []; + let previous = -1; + let run = 0; + let previousTranslation: bigint | undefined; + for (const [index, translation] of translations.entries()) { + ambiguousPrefix.push( + (ambiguousPrefix[index] ?? 0) + (translation === null ? 1 : 0), + ); + if (typeof translation === "bigint") { + if (translation !== previousTranslation) run += 1; + previousTranslation = translation; + previous = index; + } + previousMapped.push(previous); + translationRuns.push(run); + } + let next = translations.length; + for (let index = translations.length - 1; index >= 0; index--) { + if (typeof translations[index] === "bigint") next = index; + nextMapped[index] = next; + } + const offset = (address: bigint, size: number): number => { + if (!Number.isSafeInteger(size) || size <= 0) + return mappedFileOffset(mappings, address, size, zeroFills); + const end = address + BigInt(size); + const interval = lastAtOrBelow(boundaries, address); + const lower = Math.max(0, interval); + const upper = lastAtOrBelow(boundaries, end - 1n); + const first = nextMapped[lower] ?? translations.length; + const last = previousMapped[upper] ?? -1; + // Like the scan, conflicts anywhere in the requested span win over gaps. + if ( + (ambiguousPrefix[upper + 1] ?? 0) - (ambiguousPrefix[lower] ?? 0) > 0 || + (first <= upper && + last >= lower && + translationRuns[first] !== translationRuns[last]) + ) + throw new BinaryMappingFailure( + "ambiguous", + "Virtual span has ambiguous or noncontiguous file mappings", + ); + const translation = translations[interval]; + if (typeof translation !== "bigint" || (runEnd[interval] as bigint) < end) + throw new BinaryMappingFailure( + "unmapped", + "Virtual address or string range is not file mapped", + ); + const zeroFill = lastAtOrBelow(zeroFilled.starts, end - 1n); + if (zeroFill >= 0 && (zeroFilled.reach[zeroFill] as bigint) > address) + throw new BinaryMappingFailure( + "ambiguous", + "Virtual address has ambiguous file and zero-fill mappings", + ); + return Number(address + translation); + }; + const access: MappedStringAccess = { + offset, + mappingAt: (address) => { + const start = offset(address, 1); + const interval = lastAtOrBelow(boundaries, address); + return { + address, + offset: start, + size: Number((runEnd[interval] as bigint) - address), + }; + }, + }; + return { + offset, + readCString: (bytes, address, maximumBytes) => + readMappedString(bytes, access, address, maximumBytes), + }; +}; + +/** One immutable image's indexed mappings and bounded string access. */ +export interface FileMappingIndex { + readonly offset: (address: bigint, size: number) => number; + readonly readCString: ( + bytes: Buffer, + address: bigint, + maximumBytes: number, + ) => Buffer; +} + +interface MappedStringAccess { + readonly mappingAt: (address: bigint) => FileMapping | undefined; + readonly offset: (address: bigint, size: number) => number; +} + /** Read a bounded NUL-terminated string while retaining mapping/alias semantics. */ export const readMappedCString = ( bytes: Buffer, @@ -323,16 +524,36 @@ export const readMappedCString = ( maximumBytes: number, ): Buffer => { const mappings = imageFileMappings(image); + return readMappedString( + bytes, + { + mappingAt: (cursor) => + mappings.find( + (range) => + range.address <= cursor && + cursor < range.address + BigInt(range.size), + ), + offset: (cursor, size) => + mappedFileOffset(mappings, cursor, size, image.zero_fills), + }, + address, + maximumBytes, + ); +}; + +const readMappedString = ( + bytes: Buffer, + access: MappedStringAccess, + address: bigint, + maximumBytes: number, +): Buffer => { const chunks: Buffer[] = []; let length = 0; while (length <= maximumBytes) { const cursor = address + BigInt(length); - const mapping = mappings.find( - (range) => - range.address <= cursor && cursor < range.address + BigInt(range.size), - ); + const mapping = access.mappingAt(cursor); if (mapping === undefined) { - mappedFileOffset(mappings, cursor, 1, image.zero_fills); + access.offset(cursor, 1); throw new BinaryMappingFailure( "unmapped", "String range is not file mapped", @@ -346,7 +567,7 @@ export const readMappedCString = ( const chunk = bytes.subarray(offset, offset + size); const terminator = chunk.indexOf(0); const consumed = terminator < 0 ? size : terminator + 1; - mappedFileOffset(mappings, cursor, consumed, image.zero_fills); + access.offset(cursor, consumed); if (terminator >= 0) { const tail = chunk.subarray(0, terminator); if (chunks.length === 0) return tail; diff --git a/src/native/metadata/PeImage.ts b/src/native/metadata/PeImage.ts index 5c89dc9a20..dc807b2d28 100644 --- a/src/native/metadata/PeImage.ts +++ b/src/native/metadata/PeImage.ts @@ -61,8 +61,29 @@ const optionalHeaderMagic = ( return reader.u16(offset); }; +/** Declared PE fields retained by their parsing owner for format consumers. */ +export type PeImageSection = Extract< + ImageSection, + { readonly format: "pe" } +> & { + readonly raw_offset: number; +}; + +export type PeImage = BinaryContainer & { + readonly format: "pe"; + readonly image_base: bigint; + readonly pe_machine: number; + readonly pe_section_alignment: number; + readonly pe_file_alignment: number; + readonly pe_header_mapping: FileMapping; + readonly pe_data_directories: NonNullable< + BinaryContainer["pe_data_directories"] + >; + readonly sections: readonly PeImageSection[]; +}; + /** Read PE32/PE32+ native image mappings and section facts. */ -export const readPeImage = (bytes: Buffer): BinaryContainer => { +export const readPeImage = (bytes: Buffer): PeImage => { const reader = new BinaryReader(bytes, true); const pe = peHeaderOffset(reader); const optional = pe + 24; @@ -107,7 +128,7 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => { const mappings: FileMapping[] = []; const executableMappings: FileMapping[] = []; const zeroFills: ZeroFillMapping[] = []; - const sections: ImageSection[] = []; + const sections: PeImageSection[] = []; for (let index = 0; index < count; index++) { const entry = start + index * 40; const { @@ -167,6 +188,9 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => { return { format: "pe", image_base: imageBase, + pe_machine: machine, + pe_section_alignment: reader.u32(optional + 32), + pe_file_alignment: fileAlignment, pe_data_directories: dataDirectories, pe_header_mapping: headerMapping, architecture: names.get(machine) ?? `pe-machine-${machine}`, @@ -199,7 +223,7 @@ const peImageSection = ({ readonly virtualSize: number; readonly rawSize: number; readonly fileAlignment: number; -}): ImageSection => { +}): PeImageSection => { const range = declaredFileRange( reader.bytes.length, BigInt(offset), @@ -217,6 +241,7 @@ const peImageSection = ({ virtual_address: virtualAddress, virtual_size: virtualSize, raw_size: rawSize, + raw_offset: offset, file_alignment: fileAlignment, characteristics, ...range, diff --git a/src/native/pe/PeImportParser.test.ts b/src/native/pe/PeImportParser.test.ts new file mode 100644 index 0000000000..2db2f1cacc --- /dev/null +++ b/src/native/pe/PeImportParser.test.ts @@ -0,0 +1,650 @@ +import { createHash } from "node:crypto"; +import { expect, it } from "vitest"; +import { + inspectPeImportsInputSchema, + peImportsSchema, +} from "../../domain/native/peImports.js"; +import { + peImportFixtureOffset, + peImportImage, + peImportLeadingSectionImage, + peImportManySectionsImage, + peImportDirectoryAtRva, + peImportDirectoryMappingStops, + peImportNameAtRva, + peWithoutImportTableFixture, +} from "../../../tests/fixtures/binaryDiagnostics/peImports.js"; +import { parsePeImports, PeImportLimitFailure } from "./PeImportParser.js"; + +const input = inspectPeImportsInputSchema.parse({ + path: "/artifacts/app.exe", +}); + +const artifact = (bytes: Buffer) => ({ + path: "/artifacts/app.exe", + sha256: createHash("sha256").update(bytes).digest("hex"), + bytes: bytes.length, +}); + +it("reads named PE32+ imports and a pefile-compatible import hash", async () => { + const bytes = peImportImage({ bits: 64 }); + const report = await parsePeImports(bytes, artifact(bytes), input); + expect(report.format).toBe("pe32-plus"); + expect(report.bits).toBe(64); + expect(report.imports).toHaveLength(1); + expect(report.imports[0]?.name).toBe("KERNEL32.dll"); + expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([ + "GetProcAddress", + "ExitProcess", + ]); + expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f"); + expect(report.coverage.status).toBe("complete"); + expect(report.delay_imports).toEqual([]); +}); + +const parse = (bytes: Buffer) => parsePeImports(bytes, artifact(bytes), input); + +it.each([ + ["standard", 0], + ["standard", 1], + ["delay", 0], + ["delay", 1], +] as const)( + "retains %s rows before a descriptor mapping gap after %s rows", + async (kind, retained) => { + const report = await parse(peImportDirectoryMappingStops(kind, retained)); + const directory = + kind === "standard" + ? report.import_directory + : report.delay_import_directory; + const rows = kind === "standard" ? report.imports : report.delay_imports; + const width = kind === "standard" ? 20 : 32; + expect(rows).toHaveLength(retained); + expect(directory).toMatchObject({ + rva: 0x40000000, + locations: retained === 0 ? [] : [{ offset: 0x1000, bytes: width }], + }); + expect(report.coverage.status).toBe("partial"); + expect(report.issues).toContainEqual( + expect.objectContaining({ + code: "descriptor_unreadable", + location: `0x${(0x40000000 + retained * width).toString(16)}`, + }), + ); + expect(report.imphash.md5).toBe( + kind === "delay" ? "92cef6cc91a85825f3c8a0516517c66f" : null, + ); + expect(peImportsSchema.safeParse(report).success).toBe(true); + }, +); + +it.each([ + ["standard", 0xffffffec, 1, "0x100000000"], + ["standard", 0xfffffff0, 0, "0xfffffff0"], + ["delay", 0xffffffe0, 1, "0x100000000"], + ["delay", 0xfffffff0, 0, "0xfffffff0"], +] as const)( + "stops the %s descriptor walk at RVA %s", + async (kind, rva, retained, invalid) => { + const report = await parse(peImportDirectoryAtRva(kind, rva)); + const directory = + kind === "standard" + ? report.import_directory + : report.delay_import_directory; + const rows = kind === "standard" ? report.imports : report.delay_imports; + expect(rows).toHaveLength(retained); + expect(directory?.rva).toBe(rva); + expect(directory?.locations).toHaveLength(retained); + expect(report.coverage.status).toBe("partial"); + expect(report.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ code: "malformed_entry", location: invalid }), + ]), + ); + expect(peImportsSchema.safeParse(report).success).toBe(true); + expect(report.imphash.md5).toBe( + kind === "delay" ? "92cef6cc91a85825f3c8a0516517c66f" : null, + ); + }, +); + +it("does not consume a DLL name past the uint32 RVA end", async () => { + const report = await parse(peImportNameAtRva(0xfffffff8)); + expect(report.imports[0]?.name).toBeNull(); + expect(report.imports[0]?.functions).toHaveLength(2); + expect(report.coverage.status).toBe("partial"); + expect(report.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "name_unreadable", + location: "0xfffffff8", + }), + ]), + ); + expect(report.imphash.md5).toBeNull(); +}); + +it("accepts a DLL name whose NUL is the final uint32 RVA byte", async () => { + const report = await parse(peImportNameAtRva(0xfffffff3)); + expect(report.imports[0]?.name).toBe("KERNEL32.dll"); + expect(report.coverage.status).toBe("complete"); + expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f"); +}); + +it.each([ + [0x8000000100000001n, "72d9cd67504bb3b9f29c37a226bf4331"], + [0x8000000000010001n, null], + [0x8000000000000001n, "72d9cd67504bb3b9f29c37a226bf4331"], +] as const)( + "matches pefile 2024.8.26 for ordinal thunk %s", + async (thunk, expected) => { + const bytes = peImportImage(); + for (const rva of [0x1028, 0x1040]) + bytes.writeBigUInt64LE(thunk, peImportFixtureOffset(rva)); + const report = await parse(bytes); + expect(report.imports[0]?.functions[0]).toMatchObject({ + ordinal: 1, + thunk_hex: thunk.toString(16), + }); + expect(report.imphash.md5).toBe(expected); + }, +); + +it("withholds the hash beyond pefile's section parser capacity", async () => { + const report = await parse(peImportManySectionsImage(2049, 2)); + expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([ + "Function0000", + "Function0001", + ]); + expect(report.coverage.status).toBe("complete"); + expect(report.imphash.md5).toBeNull(); + expect(report.imphash.reason).toMatch(/mapping|compatibility/u); +}); + +it.each([false, true])( + "withholds the hash when pefile stops at an earlier section (suspicious: %s)", + async (suspicious) => { + const report = await parse(peImportLeadingSectionImage(suspicious)); + expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([ + "GetProcAddress", + "ExitProcess", + ]); + expect(report.coverage.status).toBe("complete"); + expect(report.imphash.md5).toBeNull(); + expect(report.imphash.reason).toMatch(/mapping|compatibility/u); + }, +); + +/** Default PE32+ KERNEL32 image with one descriptor field rewritten. */ +const withDescriptorField = ( + field: number, + value: number, + options: Parameters[0] = {}, +) => { + const bytes = peImportImage(options); + bytes.writeUInt32LE(value, peImportFixtureOffset(0x1000) + field); + return bytes; +}; + +it("hashes unmapped ordinals as pefile's ord fallback", async () => { + const bytes = peImportImage({ + bits: 32, + dlls: [ + { + name: "USER32.dll", + functions: [{ ordinal: 42 }], + }, + ], + }); + const report = await parsePeImports(bytes, artifact(bytes), input); + expect(report.format).toBe("pe32"); + expect(report.imports[0]?.functions[0]).toMatchObject({ + name: null, + ordinal: 42, + hint: null, + }); + // pefile 2024.8.26 get_imphash() over this image: MD5("user32.ord42"). + expect(report.imphash).toEqual({ + algorithm: "pefile", + md5: "f39f7ee67c122319a74d4270d3e5c36c", + reason: null, + }); +}); + +it("falls back to ord for DLL names that shadow object members", async () => { + const bytes = peImportImage({ + bits: 32, + dlls: [ + { name: "constructor", functions: [{ ordinal: 1 }] }, + { name: "__proto__", functions: [{ ordinal: 2 }] }, + ], + }); + // pefile: "constructor.ord1,__proto__.ord2". + expect((await parse(bytes)).imphash).toEqual({ + algorithm: "pefile", + md5: "8a4fd3988e11f8e29b11a7edc60886e6", + reason: null, + }); +}); + +it("uses pefile's complete ws2_32 and wsock32 ordinal tables", async () => { + const bytes = peImportImage({ + bits: 32, + dlls: [ + { + name: "WS2_32.dll", + functions: [{ ordinal: 23 }, { ordinal: 999 }, { name: "WSAStartup" }], + }, + { name: "wsock32.dll", functions: [{ ordinal: 1155 }, { ordinal: 1 }] }, + ], + }); + // pefile: "ws2_32.socket,ws2_32.ord999,ws2_32.wsastartup,wsock32.ord1155,wsock32.accept". + expect((await parse(bytes)).imphash.md5).toBe( + "79b3bdee998e474e75390fac55093cb0", + ); +}); + +it("withholds the import hash when a standard import name is unreadable", async () => { + const bytes = peImportImage(); + const lookup = (await parse(bytes)).imports[0]?.original_first_thunk_rva ?? 0; + bytes.writeBigUInt64LE(0x7fff0000n, peImportFixtureOffset(lookup + 8)); + const report = await parse(bytes); + expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([ + "GetProcAddress", + ]); + expect(report.issues).toMatchObject([ + { code: "name_unreadable", location: "0x7fff0000" }, + ]); + expect(report.coverage.status).toBe("partial"); + expect(report.imphash.md5).toBeNull(); + expect(report.imphash.reason).toMatch(/standard import table is incomplete/u); +}); + +it("withholds the import hash when a standard thunk is unreadable", async () => { + const bytes = withDescriptorField(0, 0x7fff0000); + const report = await parse(bytes); + expect(report.imports[0]?.functions).toEqual([]); + expect(report.issues[0]?.code).toBe("thunk_unreadable"); + expect(report.coverage.status).toBe("partial"); + expect(report.imphash.md5).toBeNull(); +}); + +it("reports a zero FirstThunk beside a populated lookup table as malformed", async () => { + const report = await parse(withDescriptorField(16, 0)); + expect(report.imports[0]).toMatchObject({ + name: "KERNEL32.dll", + first_thunk_rva: 0, + functions: [], + }); + expect(report.issues).toMatchObject([ + { code: "malformed_entry", message: expect.stringMatching(/FirstThunk/u) }, + ]); + expect(report.coverage.status).toBe("partial"); + expect(report.imphash.md5).toBeNull(); + expect(report.imphash.reason).toMatch(/incomplete/u); +}); + +it.each([ + ["import address table", 12], + ["import name table", 16], +])( + "reports a delay-load descriptor with a zero %s as malformed without withholding the standard hash", + async (table, field) => { + const bytes = peImportImage({ + delayDlls: [ + { name: "ADVAPI32.dll", functions: [{ name: "RegOpenKeyExW" }] }, + ], + }); + const location = (await parse(bytes)).delay_imports[0]?.location; + if (location === undefined) throw new Error("Expected a delay descriptor"); + bytes.writeUInt32LE(0, location.offset + field); + const report = await parse(bytes); + expect(report.delay_imports[0]?.functions).toEqual([]); + expect(report.issues).toMatchObject([ + { + code: "malformed_entry", + message: expect.stringContaining(`zero ${table} RVA`), + }, + ]); + expect(report.coverage.status).toBe("partial"); + expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f"); + }, +); + +it("reports each discontiguous file span a descriptor array consumes", async () => { + const options = { + delayDlls: [ + { name: "ADVAPI32.dll", functions: [{ name: "RegOpenKeyExW" }] }, + ], + }; + const whole = await parse(peImportImage(options)); + expect(whole.import_directory?.locations).toEqual([ + { offset: 0x400, bytes: 40 }, + ]); + expect(whole.delay_import_directory?.locations).toEqual([ + { offset: 0x428, bytes: 64 }, + ]); + // Second section bytes move to file offset 0x400 + 0x800 + 0x200. + const importSplit = await parse(peImportImage({ ...options, splitAt: 20 })); + expect(importSplit.import_directory?.locations).toEqual([ + { offset: 0x400, bytes: 20 }, + { offset: 0xe00, bytes: 20 }, + ]); + expect(importSplit.delay_import_directory?.locations).toEqual([ + { offset: 0xe14, bytes: 64 }, + ]); + const delaySplit = await parse(peImportImage({ ...options, splitAt: 72 })); + expect(delaySplit.import_directory?.locations).toEqual([ + { offset: 0x400, bytes: 40 }, + ]); + expect(delaySplit.delay_import_directory?.locations).toEqual([ + { offset: 0x428, bytes: 32 }, + { offset: 0xe00, bytes: 32 }, + ]); + for (const report of [importSplit, delaySplit]) { + expect(report.coverage.status).toBe("complete"); + expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([ + "GetProcAddress", + "ExitProcess", + ]); + expect(report.delay_imports[0]?.functions[0]?.name).toBe("RegOpenKeyExW"); + expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f"); + } +}); + +it.each([ + [ + "a function name pefile treats as invalid", + { name: "Get-Proc" }, + /invalid/u, + ], + ["ordinal 0", { ordinal: 0 }, /ordinal 0/u], + ["a name pefile truncates", { name: "A".repeat(512) }, /invalid/u], +] as const)( + "withholds the import hash for %s rather than diverge from pefile", + async (_label, entry, reason) => { + const report = await parse( + peImportImage({ + sectionBytes: 0x1000, + dlls: [{ name: "KERNEL32.dll", functions: [entry] }], + }), + ); + expect(report.coverage.status).toBe("complete"); + expect(report.imphash.md5).toBeNull(); + expect(report.imphash.reason).toMatch(reason); + }, +); + +it("reads delay-load descriptors separately from the import hash", async () => { + const bytes = peImportImage({ + delayDlls: [ + { + name: "ADVAPI32.dll", + functions: [{ name: "RegOpenKeyExW", hint: 3 }], + }, + ], + }); + const report = await parsePeImports(bytes, artifact(bytes), input); + expect(report.delay_imports).toHaveLength(1); + expect(report.delay_imports[0]?.name).toBe("ADVAPI32.dll"); + expect(report.delay_imports[0]?.functions[0]?.name).toBe("RegOpenKeyExW"); + expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f"); +}); + +it("reports an image without an import directory as importing nothing", async () => { + const bytes = peWithoutImportTableFixture(); + const report = await parsePeImports(bytes, artifact(bytes), input); + expect(report.import_directory).toBeNull(); + expect(report.imports).toEqual([]); + expect(report.coverage.status).toBe("complete"); + expect(report.limitations[0]).toContain( + "IMAGE_DIRECTORY_ENTRY_IMPORT is absent", + ); +}); + +it("fails closed when the DLL budget is exhausted", async () => { + const bytes = peImportImage({ + dlls: [ + { name: "A.dll", functions: [{ name: "a" }] }, + { name: "B.dll", functions: [{ name: "b" }] }, + ], + }); + await expect( + parsePeImports( + bytes, + artifact(bytes), + inspectPeImportsInputSchema.parse({ + path: "/artifacts/app.exe", + max_dlls: 1, + }), + ), + ).rejects.toThrow( + expect.objectContaining({ limit: "dlls", maximum_bytes: 1 }), + ); +}); + +it("fails closed when the function budget is exhausted", async () => { + const bytes = peImportImage({ + dlls: [ + { + name: "KERNEL32.dll", + functions: [{ name: "GetProcAddress" }, { name: "ExitProcess" }], + }, + ], + }); + await expect( + parsePeImports( + bytes, + artifact(bytes), + inspectPeImportsInputSchema.parse({ + path: "/artifacts/app.exe", + max_functions: 1, + }), + ), + ).rejects.toThrow( + expect.objectContaining({ limit: "functions", maximum_bytes: 1 }), + ); +}); + +it("fails closed with a structured tag when retained name text is exhausted", async () => { + const name = "A".repeat(4096); + const bytes = peImportImage({ + sectionBytes: 0x1200000, + dlls: [ + { + name: "KERNEL32.dll", + functions: Array.from({ length: 4097 }, () => ({ name })), + }, + ], + }); + const failure = parse(bytes); + await expect(failure).rejects.toThrow(PeImportLimitFailure); + await expect(failure).rejects.toThrow( + expect.objectContaining({ + limit: "retained_text", + maximum_bytes: 16 * 1024 * 1024, + }), + ); +}); + +it("computes a pefile import hash for oleaut32 ordinal names", async () => { + const bytes = peImportImage({ + dlls: [ + { + name: "OLEAUT32.dll", + functions: [{ ordinal: 2 }], + }, + ], + }); + const report = await parsePeImports(bytes, artifact(bytes), input); + expect(report.imports[0]?.functions[0]).toMatchObject({ + name: null, + ordinal: 2, + }); + expect(report.imphash.md5).toBe("3dc76082b322e4bf41120e4657c7f5c6"); +}); + +it("records delay-load virtual-address descriptors as unsupported", async () => { + const bytes = peImportImage({ + delayDlls: [ + { + name: "ADVAPI32.dll", + functions: [{ name: "RegOpenKeyExW" }], + }, + ], + delayAttributes: 0, + }); + const report = await parsePeImports(bytes, artifact(bytes), input); + expect(report.delay_imports).toEqual([]); + expect(report.coverage.status).toBe("partial"); + expect(report.issues[0]?.code).toBe("unsupported_form"); + expect(report.imphash.md5).toBe("92cef6cc91a85825f3c8a0516517c66f"); +}); + +it("withholds the import hash exactly where pefile's import symbol cap truncates", async () => { + const image = (sizes: readonly number[]) => + peImportImage({ + sectionBytes: 0x100000, + dlls: sizes.map((count, dll) => ({ + name: `D${String(dll)}.dll`, + functions: Array.from({ length: count }, (_, index) => ({ + name: `F${String(dll)}_${String(index)}`, + })), + })), + }); + // pefile 2024.8.26 hashes all 4096 rows here... + expect((await parse(image([4095, 1]))).imphash.md5).toBe( + "2217c72699a462a8ca9980afe6a852aa", + ); + // ...but drops D1.dll here because its 8192-thunk cap is reached first. + const capped = await parse(image([4096, 1])); + expect(capped.coverage.status).toBe("complete"); + expect(capped.imphash.md5).toBeNull(); + expect(capped.imphash.reason).toMatch(/descriptor 1 .*symbol cap/u); +}); + +it("yields during long table walks and stops when the caller cancels", async () => { + const bytes = peImportImage({ + sectionBytes: 0x20000, + dlls: [ + { + name: "KERNEL32.dll", + functions: Array.from({ length: 4000 }, (_, index) => ({ + name: `F${String(index)}`, + })), + }, + ], + }); + const controller = new AbortController(); + const pending = parsePeImports( + bytes, + artifact(bytes), + input, + controller.signal, + ); + let ticks = 0; + const tick = setImmediate(() => { + ticks += 1; + controller.abort(); + }); + await expect(pending).rejects.toMatchObject({ + name: "ArtifactReaderFailure", + reason: "cancelled", + }); + clearImmediate(tick); + expect(ticks).toBe(1); +}); + +it("withholds the import hash once pefile would discard a repetitive lookup table", async () => { + const repeated = async (repeats: number) => { + const bytes = peImportImage({ + dlls: [ + { + name: "KERNEL32.dll", + functions: Array.from({ length: repeats + 1 }, (_, index) => ({ + name: `F${String(index)}`, + })), + }, + ], + }); + const lookup = (await parse(bytes)).imports[0]?.original_first_thunk_rva; + if (lookup === undefined) throw new Error("Expected a lookup table"); + const first = bytes.readBigUInt64LE(peImportFixtureOffset(lookup)); + for (let index = 1; index <= repeats; index++) + bytes.writeBigUInt64LE(first, peImportFixtureOffset(lookup + index * 8)); + return parse(bytes); + }; + // pefile 2024.8.26 keeps 14 repeats and hashes the lookup rows... + expect((await repeated(14)).imphash.md5).toBe( + "a849c88f8733c2f2171c4130e925bca6", + ); + // ...but discards the table at 15 and hashes the import address table instead. + const discarded = await repeated(15); + expect(discarded.coverage.status).toBe("complete"); + expect(discarded.imphash.md5).toBeNull(); + expect(discarded.imphash.reason).toMatch(/repeats hint\/name addresses 15/u); +}); + +it.each([32, 64] as const)( + "retains valid rows before a PE%s IAT RVA overflows", + async (bits) => { + const width = bits / 8; + const bytes = withDescriptorField(16, 0x100000000 - width, { bits }); + const report = await parse(bytes); + expect(peImportsSchema.safeParse(report).success).toBe(true); + expect(report.imports[0]?.functions).toHaveLength(1); + expect(report.imports[0]?.functions[0]?.iat_rva).toBe(0x100000000 - width); + expect(report.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "malformed_entry", + location: "0x100000000", + }), + ]), + ); + expect(report.coverage.status).toBe("partial"); + expect(report.imphash.md5).toBeNull(); + }, +); + +it("preserves an invalid wide named thunk without rounding its source value", async () => { + const bytes = peImportImage(); + bytes.writeBigUInt64LE(0x20000000000001n, peImportFixtureOffset(0x1028)); + const report = await parse(bytes); + expect(report.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "malformed_entry", + location: "0x20000000000001", + }), + ]), + ); + expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([ + "ExitProcess", + ]); + expect(report.coverage.status).toBe("partial"); + expect(report.imphash.md5).toBeNull(); +}); + +it.each(["function", "DLL"])( + "retains a cross-section %s name but withholds an unproven pefile hash", + async (kind) => { + const count = kind === "function" ? 140 : 134; + const functions = Array.from({ length: count }, (_, i) => ({ + name: `Function${String(i).padStart(3, "0")}${kind === "DLL" && i === 0 ? "A".repeat(10) : ""}`, + })); + const bytes = peImportImage({ + sectionBytes: 0x4000, + splitAt: 0x1000, + dlls: [{ name: "KERNEL32.dll", functions }], + }); + const report = await parse(bytes); + expect(report.imports[0]?.name).toBe("KERNEL32.dll"); + expect(report.imports[0]?.functions.map((row) => row.name)).toEqual( + functions.map((row) => row.name), + ); + expect(report.coverage.status).toBe("complete"); + expect(report.imphash.md5).toBeNull(); + expect(report.imphash.reason).toMatch(/mapping|compatibility/u); + }, +); diff --git a/src/native/pe/PeImportParser.ts b/src/native/pe/PeImportParser.ts new file mode 100644 index 0000000000..9366274204 --- /dev/null +++ b/src/native/pe/PeImportParser.ts @@ -0,0 +1,574 @@ +import { createPefileReadCompatibility } from "./PefileReadCompatibility.js"; +import { setImmediate } from "node:timers/promises"; +import { assertArtifactReadActive } from "../../artifacts/ArtifactFileOperation.js"; +import { + BinaryFormatFailure, + BinaryMappingFailure, + BinaryReader, + BinaryResourceFailure, + createFileMappingIndex, + imageFileMappings, +} from "../metadata/BinaryContainer.js"; +import { readPeImage } from "../metadata/PeImage.js"; +import { + MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES, + type InspectPeImportsInput, + type PeDelayImportDll, + type PeImportDll, + type PeImportFunction, + type PeImportIssue, + type PeImports, +} from "../../domain/native/peImports.js"; +import { + IMPORT_DESCRIPTOR_BYTES, + PEFILE_MAX_IMPORT_SYMBOLS, + pefileImphash, +} from "./pefileImphash.js"; + +type Artifact = PeImports["artifact"]; + +const PE_IMPORT_DIRECTORY = 1; +const PE_DELAY_IMPORT_DIRECTORY = 13; +const DELAY_DESCRIPTOR_BYTES = 32; +const MAXIMUM_NAME_BYTES = 4096; +/** Table entries decoded between event-loop yields and cancellation checks. */ +const ENTRIES_PER_YIELD = 1024; +const ORDINAL_FLAG32 = 0x80000000n; +const ORDINAL_FLAG64 = 0x8000000000000000n; +const PE_RVA_END = 0x100000000; + +/** PE32+ keeps 32-bit RVAs even though preferred virtual addresses are 64-bit. */ +const fitsRva = (rva: number, size: number): boolean => + Number.isSafeInteger(rva) && + rva >= 0 && + Number.isSafeInteger(size) && + size > 0 && + rva <= PE_RVA_END - size; + +const requireRva = (rva: number, size: number): void => { + if (!fitsRva(rva, size)) + throw new BinaryFormatFailure( + "malformed", + `PE RVA ${hex(rva)} span of ${String(size)} bytes extends beyond the 32-bit RVA space`, + ); +}; + +const PE_LIMITATIONS = [ + "Rows reproduce the PE import and delay-load directories; they do not establish that an import is bound, forwarded or reachable at runtime.", + "Bound import snapshots, the import address table as loaded, and export-forward resolution in another DLL remain unobserved.", +]; + +/** The parser-owned allowance an image exhausted. */ +export type PeImportLimit = "dlls" | "functions" | "retained_text"; + +/** Structured parser allowance refusal; consumers branch on `limit`, never on prose. */ +export class PeImportLimitFailure extends BinaryResourceFailure { + constructor( + readonly limit: PeImportLimit, + readonly allowance: number, + message: string, + ) { + super("structure", allowance, message); + this.name = "PeImportLimitFailure"; + } +} + +const hex = (value: bigint | number): string => `0x${value.toString(16)}`; + +const dllBudget = (count: number, max: number): void => { + if (count > max) + throw new PeImportLimitFailure( + "dlls", + max, + `Import inventory declares ${String(count)} descriptors, above the ${String(max)}-entry DLL allowance`, + ); +}; + +const functionBudget = (count: number, max: number): void => { + if (count > max) + throw new PeImportLimitFailure( + "functions", + max, + `Import inventory declares ${String(count)} functions, above the ${String(max)}-entry function allowance`, + ); +}; + +const createRetention = () => { + let text = 0; + return (value: string): void => { + text += value.length; + if (text > MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES) + throw new PeImportLimitFailure( + "retained_text", + MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES, + `Import names exceed the fixed ${String(MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES)}-byte retained-text allowance`, + ); + }; +}; + +const failureMessage = (cause: unknown): string => { + if (cause instanceof BinaryFormatFailure) return cause.message; + throw cause; +}; + +/** Append one consumed file span, merging it into the previous span when contiguous. */ +const consume = ( + spans: { offset: number; bytes: number }[], + offset: number, + bytes: number, +): void => { + const last = spans.at(-1); + if (last !== undefined && last.offset + last.bytes === offset) + last.bytes += bytes; + else spans.push({ offset, bytes }); +}; + +const coverage = ( + issues: readonly PeImportIssue[], + examinedDlls: number, + imports: number, + delayImports: number, +): PeImports["coverage"] => + issues.length === 0 + ? { + status: "complete", + reason: null, + examined_dlls: examinedDlls, + imports, + delay_imports: delayImports, + } + : { + status: "partial", + reason: `${String(issues.length)} import table issue(s) left entries unreported; see issues`, + examined_dlls: examinedDlls, + imports, + delay_imports: delayImports, + }; + +/** + * Read PE import and delay-load directories without executing the image. + * Yields to the event loop and honors cancellation every + * `ENTRIES_PER_YIELD` descriptors or thunks. + */ +export const parsePeImports = async ( + bytes: Buffer, + artifact: Artifact, + input: InspectPeImportsInput, + signal?: AbortSignal, +): Promise => { + assertArtifactReadActive(signal); + const image = readPeImage(bytes); + const bits = image.bits; + const base = image.image_base; + const reader = new BinaryReader(bytes, true); + const lookup = createFileMappingIndex( + imageFileMappings(image), + image.zero_fills, + ); + const compatibleRead = createPefileReadCompatibility(image, bytes.length); + let mappingCompatibility = true; + const map = (rva: number, size: number): number => { + requireRva(rva, size); + const offset = lookup.offset(base + BigInt(rva), size); + mappingCompatibility &&= compatibleRead(rva, size, offset); + return offset; + }; + const format = bits === 64 ? ("pe32-plus" as const) : ("pe32" as const); + const result = { + artifact, + format, + machine: image.pe_machine, + bits, + architecture: image.architecture, + }; + const issues: PeImportIssue[] = []; + const admitRva = (rva: number, size: number, label: string): boolean => { + if (fitsRva(rva, size)) return true; + issues.push({ + code: "malformed_entry", + location: hex(rva), + message: `${label} span of ${String(size)} bytes extends beyond the 32-bit RVA space`, + }); + return false; + }; + const mapDescriptor = ( + rva: number, + size: number, + label: string, + ): number | null => { + if (!admitRva(rva, size, label)) return null; + try { + return map(rva, size); + } catch (cause) { + if (!(cause instanceof BinaryMappingFailure)) throw cause; + issues.push({ + code: "descriptor_unreadable", + location: hex(rva), + message: `${label}: ${cause.message}`, + }); + return null; + } + }; + const retain = createRetention(); + let examinedDlls = 0; + let examinedFunctions = 0; + let entries = 0; + const checkpoint = async (): Promise => { + entries += 1; + if (entries % ENTRIES_PER_YIELD !== 0) return; + await setImmediate(); + assertArtifactReadActive(signal); + }; + + const cString = (rva: number): string => { + requireRva(rva, 1); + const raw = lookup.readCString( + bytes, + base + BigInt(rva), + Math.min(MAXIMUM_NAME_BYTES, PE_RVA_END - rva - 1), + ); + if (raw.length === 0) + throw new BinaryFormatFailure( + "malformed", + `Import name at RVA ${hex(rva)} is empty`, + ); + // CString decoding may span physically discontiguous sections. A pefile + // hash is only justified when its single-section read sees the whole name. + if (mappingCompatibility) { + try { + const size = raw.length + 1; + const offset = lookup.offset(base + BigInt(rva), size); + mappingCompatibility = compatibleRead(rva, size, offset); + } catch (cause) { + if (!(cause instanceof BinaryFormatFailure)) throw cause; + mappingCompatibility = false; + } + } + return raw.toString("latin1"); + }; + + const readName = (rva: number, label: string): string | null => { + try { + const name = cString(rva); + retain(name); + return name; + } catch (cause) { + issues.push({ + code: "name_unreadable", + location: hex(rva), + message: `${label}: ${failureMessage(cause)}`, + }); + return null; + } + }; + + const readFunctions = async ( + nameTableRva: number, + addressTableRva: number, + ): Promise => { + const thunkWidth = bits === 64 ? 8 : 4; + const ordinalFlag = bits === 64 ? ORDINAL_FLAG64 : ORDINAL_FLAG32; + const functions: PeImportFunction[] = []; + for (let index = 0; ; index++) { + await checkpoint(); + const nameRva = nameTableRva + index * thunkWidth; + const iatRva = addressTableRva + index * thunkWidth; + if (!admitRva(nameRva, thunkWidth, "Import lookup table")) break; + let thunk: bigint; + try { + thunk = reader.word(map(nameRva, thunkWidth), bits); + } catch (cause) { + issues.push({ + code: "thunk_unreadable", + location: hex(nameRva), + message: failureMessage(cause), + }); + break; + } + if (thunk === 0n) break; + if (!admitRva(iatRva, thunkWidth, "Import address table")) break; + examinedFunctions += 1; + functionBudget(examinedFunctions, input.max_functions); + const thunkHex = thunk.toString(16); + if ((thunk & ordinalFlag) !== 0n) { + functions.push({ + hint: null, + name: null, + ordinal: Number(thunk & 0xffffn), + ilt_rva: nameTableRva === addressTableRva ? null : nameRva, + iat_rva: iatRva, + thunk_hex: thunkHex, + }); + continue; + } + if (thunk > 0x7fffffffn) { + issues.push({ + code: "malformed_entry", + location: `0x${thunkHex}`, + message: + "A named import thunk uses bits outside its 31-bit hint/name RVA", + }); + continue; + } + const hintNameRva = Number(thunk); + try { + const hintAt = map(hintNameRva, 3); + const hint = reader.u16(hintAt); + const name = cString(hintNameRva + 2); + retain(name); + functions.push({ + hint, + name, + ordinal: null, + ilt_rva: nameTableRva === addressTableRva ? null : nameRva, + iat_rva: iatRva, + thunk_hex: thunkHex, + }); + } catch (cause) { + issues.push({ + code: "name_unreadable", + location: hex(hintNameRva), + message: failureMessage(cause), + }); + } + } + return functions; + }; + + // pefile counts every ILT and IAT thunk iteration against one image-wide cap + // and silently truncates tables past it. Replay that count (reading IAT + // entries only until the cap) to know when its import hash covers a prefix. + let pefileThunkReads = 0; + let pefileTruncatedDescriptor: number | null = null; + const pefileTableWalk = (rva: number, entries: number | null): number => { + const width = bits === 64 ? 8 : 4; + let processed = 0; + for (let index = 0; ; index++) { + if (pefileThunkReads > PEFILE_MAX_IMPORT_SYMBOLS) break; + pefileThunkReads += 1; + if (entries !== null) { + if (index === entries) break; + } else { + let word: bigint; + try { + word = reader.word(map(rva + index * width, width), bits); + } catch (cause) { + if (!(cause instanceof BinaryFormatFailure)) throw cause; + break; + } + if (word === 0n) break; + } + processed += 1; + } + return processed; + }; + const replayPefileSymbolCap = ( + index: number, + originalFirstThunk: number, + firstThunk: number, + entries: number, + ): void => { + const table = + originalFirstThunk === 0 + ? pefileTableWalk(firstThunk, entries) + : pefileTableWalk(originalFirstThunk, entries); + if (originalFirstThunk !== 0) pefileTableWalk(firstThunk, null); + if (table < entries) pefileTruncatedDescriptor ??= index; + }; + + const missingTable = (descriptorRva: number, message: string): [] => { + issues.push({ + code: "malformed_entry", + location: hex(descriptorRva), + message, + }); + return []; + }; + + const importDirectory = image.pe_data_directories?.[PE_IMPORT_DIRECTORY]; + const delayDirectory = image.pe_data_directories?.[PE_DELAY_IMPORT_DIRECTORY]; + const imports: PeImportDll[] = []; + let importDirectoryResult: PeImports["import_directory"] = null; + if (importDirectory !== undefined && importDirectory.virtual_address !== 0) { + const spans: { offset: number; bytes: number }[] = []; + for (let index = 0; ; index++) { + await checkpoint(); + const descriptorRva = + importDirectory.virtual_address + index * IMPORT_DESCRIPTOR_BYTES; + const descriptor = mapDescriptor( + descriptorRva, + IMPORT_DESCRIPTOR_BYTES, + "Import descriptor", + ); + if (descriptor === null) break; + consume(spans, descriptor, IMPORT_DESCRIPTOR_BYTES); + const originalFirstThunk = reader.u32(descriptor); + const timeDateStamp = reader.u32(descriptor + 4); + const forwarderChain = reader.u32(descriptor + 8); + const nameRva = reader.u32(descriptor + 12); + const firstThunk = reader.u32(descriptor + 16); + if ( + originalFirstThunk === 0 && + timeDateStamp === 0 && + forwarderChain === 0 && + nameRva === 0 && + firstThunk === 0 + ) + break; + examinedDlls += 1; + dllBudget(examinedDlls, input.max_dlls); + const name = readName(nameRva, "Import descriptor DLL name"); + const functions = + firstThunk === 0 + ? missingTable( + descriptorRva, + originalFirstThunk === 0 + ? "Import descriptor declares neither a lookup table nor an import address table (FirstThunk is zero); its functions are unknown" + : `Import descriptor has a zero FirstThunk (import address table RVA); its lookup table at ${hex(originalFirstThunk)} is not decoded`, + ) + : await readFunctions( + originalFirstThunk === 0 ? firstThunk : originalFirstThunk, + firstThunk, + ); + if (firstThunk !== 0) + replayPefileSymbolCap( + index, + originalFirstThunk, + firstThunk, + functions.length, + ); + imports.push({ + index, + name, + name_rva: nameRva, + original_first_thunk_rva: originalFirstThunk, + first_thunk_rva: firstThunk, + time_date_stamp: timeDateStamp, + forwarder_chain: forwarderChain, + location: { + offset: descriptor, + bytes: IMPORT_DESCRIPTOR_BYTES, + }, + functions, + }); + } + importDirectoryResult = { + rva: importDirectory.virtual_address, + size: importDirectory.size, + locations: spans, + }; + } + const standardIssues = issues.length; + const standardMappingCompatibility = mappingCompatibility; + + const delayImports: PeDelayImportDll[] = []; + let delayDirectoryResult: PeImports["delay_import_directory"] = null; + if (delayDirectory !== undefined && delayDirectory.virtual_address !== 0) { + const spans: { offset: number; bytes: number }[] = []; + for (let index = 0; ; index++) { + await checkpoint(); + const descriptorRva = + delayDirectory.virtual_address + index * DELAY_DESCRIPTOR_BYTES; + const descriptor = mapDescriptor( + descriptorRva, + DELAY_DESCRIPTOR_BYTES, + "Delay-load descriptor", + ); + if (descriptor === null) break; + consume(spans, descriptor, DELAY_DESCRIPTOR_BYTES); + const attributes = reader.u32(descriptor); + const nameRva = reader.u32(descriptor + 4); + const moduleHandleRva = reader.u32(descriptor + 8); + const importAddressTableRva = reader.u32(descriptor + 12); + const importNameTableRva = reader.u32(descriptor + 16); + const boundImportRva = reader.u32(descriptor + 20); + const unloadInformationTableRva = reader.u32(descriptor + 24); + const timeDateStamp = reader.u32(descriptor + 28); + if ( + attributes === 0 && + nameRva === 0 && + moduleHandleRva === 0 && + importAddressTableRva === 0 && + importNameTableRva === 0 && + boundImportRva === 0 && + unloadInformationTableRva === 0 && + timeDateStamp === 0 + ) + break; + examinedDlls += 1; + dllBudget(examinedDlls, input.max_dlls); + if ((attributes & 1) === 0) { + issues.push({ + code: "unsupported_form", + location: hex(descriptorRva), + message: + "Delay-load descriptor uses virtual addresses rather than RVAs; those entries are not decoded", + }); + continue; + } + const name = readName(nameRva, "Delay-load DLL name"); + delayImports.push({ + index, + name, + name_rva: nameRva, + attributes, + import_address_table_rva: importAddressTableRva, + import_name_table_rva: importNameTableRva, + bound_import_rva: boundImportRva, + unload_information_table_rva: unloadInformationTableRva, + module_handle_rva: moduleHandleRva, + time_date_stamp: timeDateStamp, + location: { offset: descriptor, bytes: DELAY_DESCRIPTOR_BYTES }, + functions: + importAddressTableRva === 0 + ? missingTable( + descriptorRva, + "Delay-load descriptor has a zero import address table RVA; its functions are not decoded", + ) + : importNameTableRva === 0 + ? missingTable( + descriptorRva, + "Delay-load descriptor has a zero import name table RVA; its address table holds loader thunks rather than names, so its functions are not decoded", + ) + : await readFunctions(importNameTableRva, importAddressTableRva), + }); + } + delayDirectoryResult = { + rva: delayDirectory.virtual_address, + size: delayDirectory.size, + locations: spans, + }; + } + + const limitations = + importDirectoryResult === null + ? [ + "IMAGE_DIRECTORY_ENTRY_IMPORT is absent. That is not proof the image never imported APIs through another table or overlay.", + ...PE_LIMITATIONS, + ] + : PE_LIMITATIONS; + + return { + ...result, + import_directory: importDirectoryResult, + delay_import_directory: delayDirectoryResult, + imports, + delay_imports: delayImports, + imphash: pefileImphash(imports, { + standardIssues, + mappingCompatibility: standardMappingCompatibility, + truncatedDescriptor: pefileTruncatedDescriptor, + bits, + directoryRva: importDirectory?.virtual_address ?? 0, + fileBytes: bytes.length, + }), + coverage: coverage( + issues, + examinedDlls, + imports.reduce((count, dll) => count + dll.functions.length, 0), + delayImports.reduce((count, dll) => count + dll.functions.length, 0), + ), + issues, + limitations, + }; +}; diff --git a/src/native/pe/PeImportsProvider.ts b/src/native/pe/PeImportsProvider.ts new file mode 100644 index 0000000000..94d1b013bb --- /dev/null +++ b/src/native/pe/PeImportsProvider.ts @@ -0,0 +1,259 @@ +import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js"; +import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; +import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; +import { + artifactFilePrimaryFacts, + type ArtifactFilePrimaryFacts, + assertArtifactReadActive, + bindArtifactFileCleanup, + artifactFileCancellationCause, +} from "../../artifacts/ArtifactFileOperation.js"; +import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; +import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js"; +import type { PeImportsPort } from "../../application/binaryDiagnostics/PeImportsPort.js"; +import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; +import { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + AnalysisAccessDeniedError, + AnalysisArtifactChangedError, + AnalysisCancelledError, + AnalysisInputError, + AnalysisResourceConstraintError, + AnalysisUnsupportedTargetError, + type ResourceConstraintRecovery, +} from "../../domain/analysisErrorCore.js"; +import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js"; +import { + MAX_INSPECT_PE_IMPORT_DLLS, + MAX_INSPECT_PE_IMPORT_FILE_BYTES, + MAX_INSPECT_PE_IMPORT_FUNCTIONS, + MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES, + type InspectPeImportsInput, +} from "../../domain/native/peImports.js"; +import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; +import { ProviderCleanupError } from "../../domain/providerCleanupError.js"; +import { err, ok } from "../../domain/result.js"; +import { PE_IMPORTS_PROVIDER } from "../../domain/investigationProviders.js"; +import { + BinaryFormatFailure, + BinaryResourceFailure, +} from "../metadata/BinaryContainer.js"; +import { + parsePeImports, + PeImportLimitFailure, + type PeImportLimit, +} from "./PeImportParser.js"; + +const OPERATION = "inspect_pe_imports"; + +/** Portable import-table reader over one bounded, identity-checked regular file. */ +export class PeImportsProvider implements PeImportsPort { + readonly identity = PE_IMPORTS_PROVIDER; + readonly #resources = new ArtifactResourceScope(); + + async close(): Promise { + try { + await this.#resources.close(); + } catch (cause: unknown) { + if ( + !(cause instanceof ArtifactReaderFailure) || + cause.cleanup === undefined + ) + throw cause; + const error = new ProviderCleanupError( + PE_IMPORTS_PROVIDER.id, + cause.cleanup.resources, + { reason: cause.cleanup.reason }, + { cause, operation: OPERATION }, + ); + bindArtifactFileCleanup(cause, error); + throw error; + } + } + + async inspect(input: InspectPeImportsInput, options?: ExecutionOptions) { + try { + return await this.#resources.run(async () => { + const snapshot = await readStableArtifact( + input.path, + input.max_file_bytes, + options?.signal, + undefined, + this.#resources, + ); + assertArtifactReadActive(options?.signal); + const report = await parsePeImports( + snapshot.bytes, + { + path: input.path, + sha256: snapshot.sha256, + bytes: snapshot.bytes.length, + }, + input, + options?.signal, + ); + assertArtifactReadActive(options?.signal); + return ok(report); + }); + } catch (cause) { + return err(peImportsFailure(cause, input)); + } + } +} + +const fileSizeRemediation = (input: InspectPeImportsInput): string => + input.max_file_bytes >= MAX_INSPECT_PE_IMPORT_FILE_BYTES + ? `The image exceeds the ${String(MAX_INSPECT_PE_IMPORT_FILE_BYTES)}-byte input ceiling this operation accepts. This release has no runtime override. Select a smaller artifact.` + : `Raise max_file_bytes above ${String(input.max_file_bytes)} bytes or select a smaller artifact; the limit is the caller-selected input read budget.`; + +const dllRemediation = (input: InspectPeImportsInput): string => + input.max_dlls >= MAX_INSPECT_PE_IMPORT_DLLS + ? `The image exceeds the ${String(MAX_INSPECT_PE_IMPORT_DLLS)}-entry import-descriptor ceiling this operation accepts; select a smaller artifact.` + : `Raise max_dlls above ${String(input.max_dlls)} or select a smaller artifact; the limit is the caller-selected DLL budget.`; + +const functionRemediation = (input: InspectPeImportsInput): string => + input.max_functions >= MAX_INSPECT_PE_IMPORT_FUNCTIONS + ? `The image exceeds the ${String(MAX_INSPECT_PE_IMPORT_FUNCTIONS)}-entry imported-function ceiling this operation accepts; select a smaller artifact.` + : `Raise max_functions above ${String(input.max_functions)} or select a smaller artifact; the limit is the caller-selected function budget.`; + +/** Recovery for one exhausted allowance, chosen from the parser's structured tag. */ +const limitRecovery = ( + limit: PeImportLimit | null, + input: InspectPeImportsInput, +): ResourceConstraintRecovery => { + switch (limit) { + case "dlls": + return { retryable: false, remediationAction: dllRemediation(input) }; + case "functions": + return { + retryable: false, + remediationAction: functionRemediation(input), + }; + case "retained_text": + return fixedAllowanceOptions( + `The ${String(MAX_INSPECT_PE_IMPORT_RETAINED_TEXT_BYTES)}-byte PE import retained-text allowance (this release has no runtime override; max_file_bytes, max_dlls and max_functions do not raise it)`, + "bytes", + ); + case null: + return fixedAllowanceOptions( + "The PE structural decoding allowance", + "parser", + ); + } +}; + +const peImportsFailurePrimary = ( + facts: ArtifactFilePrimaryFacts, + input: InspectPeImportsInput, +): AnalysisError => { + const { cause, origin } = facts; + if (origin === "owned-or-classified" && cause instanceof AnalysisError) + return cause; + if ( + origin === "owned-or-classified" && + cause instanceof ArtifactReaderFailure && + cause.reason === "integrity" + ) + return new AnalysisArtifactChangedError( + OPERATION, + input.path, + cause.message, + { cause }, + ); + if ( + origin === "owned-or-classified" && + cause instanceof ArtifactReaderFailure && + cause.reason === "cancelled" + ) + return new AnalysisCancelledError(OPERATION, { + cause: artifactFileCancellationCause(cause), + }); + if (cause instanceof BinaryResourceFailure) { + const limit = cause instanceof PeImportLimitFailure ? cause.limit : null; + return new AnalysisResourceConstraintError( + OPERATION, + "capacity", + `Selected artifact ${input.path}: ${cause.message}`, + { boundary: cause.boundary, maximum: cause.maximum_bytes, limit }, + { cause, ...limitRecovery(limit, input) }, + ); + } + if (cause instanceof BinaryFormatFailure) + return cause.kind === "unsupported" + ? new AnalysisUnsupportedTargetError( + OPERATION, + input.path, + cause.message, + { cause }, + ) + : new AnalysisInputError(OPERATION, { cause }, [ + { + path: ["path"], + reason: "invalid_format", + message: `Selected artifact ${input.path}: ${cause.message}`, + }, + ]); + if ( + origin === "owned-or-classified" && + cause instanceof ArtifactReaderFailure + ) { + if (cause.reason === "limit") + return new AnalysisResourceConstraintError( + OPERATION, + "file-size", + cause.message, + { max_file_bytes: input.max_file_bytes }, + { + cause, + retryable: false, + remediationAction: fileSizeRemediation(input), + }, + ); + if (cause.reason === "path" || cause.reason === "format") + return new AnalysisInputError(OPERATION, { cause }, [ + { path: ["path"], reason: "invalid_format", message: cause.message }, + ]); + } + if (cause instanceof Error && "code" in cause) { + if (cause.code === "EACCES" || cause.code === "EPERM") + return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, { + cause, + }); + if (isSelectedRegularFileError(cause)) + return new AnalysisInputError(OPERATION, { cause }, [ + { + path: ["path"], + reason: "invalid_value", + message: `Selected image could not be read (${String(cause.code)}): ${input.path}.`, + }, + ]); + } + return new ProviderAdapterError(PE_IMPORTS_PROVIDER.id, OPERATION, { + cause, + diagnostics: { + path: input.path, + reason: cause instanceof Error ? cause.message : String(cause), + }, + }); +}; + +const peImportsFailure = ( + cause: unknown, + input: InspectPeImportsInput, +): AnalysisError => { + const primary = peImportsFailurePrimary( + artifactFilePrimaryFacts(cause), + input, + ); + if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined) + return primary; + const cleanup = new ProviderCleanupError( + PE_IMPORTS_PROVIDER.id, + cause.cleanup.resources, + { reason: cause.cleanup.reason }, + { operation: OPERATION, cause }, + ); + const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION); + bindArtifactFileCleanup(cause, error); + return error; +}; diff --git a/src/native/pe/PefileReadCompatibility.ts b/src/native/pe/PefileReadCompatibility.ts new file mode 100644 index 0000000000..bf4def80c7 --- /dev/null +++ b/src/native/pe/PefileReadCompatibility.ts @@ -0,0 +1,78 @@ +import type { PeImage } from "../metadata/PeImage.js"; + +/** + * Prove that a successful native read sees the same bytes as pefile 2024.8.26. + * pefile adjusts section addresses/raw pointers and clips each read at its + * selected section's raw end. Native mappings can legitimately span sections. + * Unsupported layout geometry leaves hash compatibility unknown, without + * discarding the native parser's observations. + */ +export const createPefileReadCompatibility = ( + image: PeImage, + fileBytes: number, +): ((rva: number, size: number, offset: number) => boolean) => { + // pefile's section loop stops at MAX_SECTIONS, even when later mappings + // are structurally readable. It may also stop at a null/suspicious header. + if (image.sections.length > 0x800) return () => false; + const sections = [...image.sections].sort( + (left, right) => left.virtual_address - right.virtual_address, + ); + const alignment = + image.pe_section_alignment < 0x1000 + ? image.pe_file_alignment + : image.pe_section_alignment; + let layoutAgrees = true; + let previousEnd = 0; + for (const section of sections) { + const start = section.virtual_address; + const emptyHeader = + section.name === "" && + start === 0 && + section.virtual_size === 0 && + section.raw_size === 0 && + section.raw_offset === 0 && + section.characteristics === 0; + const sectionWarnings = + Number(section.raw_offset + section.raw_size > fileBytes) + + Number(Math.floor(section.raw_offset / 512) * 512 > fileBytes) + + Number(section.virtual_size > 0x10000000) + + Number(start > 0x10000000) + + Number( + image.pe_file_alignment !== 0 && + section.raw_offset % image.pe_file_alignment !== 0, + ); + if ( + emptyHeader || + sectionWarnings >= 3 || + (alignment > 0 && start % alignment !== 0) || + section.raw_offset % 0x200 !== 0 || + start < previousEnd + ) + layoutAgrees = false; + previousEnd = start + Math.max(section.raw_size, section.virtual_size); + } + return (rva, size, offset) => { + if (!layoutAgrees) return false; + let low = 0; + let high = sections.length; + while (low < high) { + const middle = Math.floor((low + high) / 2); + if ((sections[middle]?.virtual_address ?? Infinity) <= rva) + low = middle + 1; + else high = middle; + } + const section = sections[low - 1]; + if ( + section === undefined || + rva >= + section.virtual_address + + Math.max(section.raw_size, section.virtual_size) + ) + return offset === rva; + const relative = rva - section.virtual_address; + return ( + offset === section.raw_offset + relative && + size <= section.raw_size - relative + ); + }; +}; diff --git a/src/native/pe/pefileImphash.ts b/src/native/pe/pefileImphash.ts new file mode 100644 index 0000000000..ca11f981f5 --- /dev/null +++ b/src/native/pe/pefileImphash.ts @@ -0,0 +1,200 @@ +import { createHash } from "node:crypto"; +import type { + PeImportDll, + PeImportFunction, + PeImports, +} from "../../domain/native/peImports.js"; +import { PEFILE_ORDINAL_NAMES } from "./pefileOrdinalNames.js"; + +/** IMAGE_IMPORT_DESCRIPTOR size. */ +export const IMPORT_DESCRIPTOR_BYTES = 20; + +/** pefile 2024.8.26 parsing constants that change which rows its import hash sees. */ +const PEFILE_MAX_NAME_BYTES = 0x200; +export const PEFILE_MAX_IMPORT_SYMBOLS = 0x2000; +const PEFILE_MAX_REPEATED_ADDRESSES = 15; +const PEFILE_MAX_ADDRESS_SPREAD = 128n * 1024n * 1024n; +const PEFILE_DLL_NAME = /^[A-Za-z0-9!#$%&'()\-@^_`{}~+,.;=[\]:\\/]+$/u; +const PEFILE_FUNCTION_NAME = /^[A-Za-z0-9._?@$()<>]+$/u; + +const stripImportExtension = (name: string): string => { + const lower = name.toLowerCase(); + for (const extension of [".dll", ".sys", ".ocx"]) { + if (lower.endsWith(extension)) return lower.slice(0, -extension.length); + } + return lower; +}; + +/** pefile `ordlookup.ordLookup(dll, ordinal, make_name=True)`. */ +const pefileOrdinalName = (dll: string, ordinal: number): string => { + const key = dll.toLowerCase(); + // Own keys only: "constructor" and "__proto__" are valid DLL names. + const names = Object.hasOwn(PEFILE_ORDINAL_NAMES, key) + ? PEFILE_ORDINAL_NAMES[key] + : undefined; + return names?.get(ordinal) ?? `ord${String(ordinal)}`; +}; + +const withheld = (reason: string): PeImports["imphash"] => ({ + algorithm: "pefile", + md5: null, + reason, +}); + +/** + * Why pefile would read a different row sequence from this complete + * descriptor than the one reported, or null when both agree. Mirrors + * `PE.get_import_table` and `PE.parse_imports` bounds and validity rules. + */ +const pefileTableDivergence = ( + dll: PeImportDll, + bits: 32 | 64, + directoryRva: number, + fileBytes: number, +): string | null => { + const label = `Standard import descriptor ${String(dll.index)}`; + if ( + dll.name === null || + dll.name.length >= PEFILE_MAX_NAME_BYTES || + !PEFILE_DLL_NAME.test(dll.name) + ) + return `${label} DLL name is unreadable or has a length or characters pefile replaces with *invalid*.`; + if (dll.functions.length === 0) + return `${label} lists no functions; pefile then falls back to the import address table or drops the descriptor.`; + const width = bits / 8; + const firstThunk = dll.first_thunk_rva; + const originalFirstThunk = dll.original_first_thunk_rva; + const tableRva = originalFirstThunk === 0 ? firstThunk : originalFirstThunk; + const next = directoryRva + (dll.index + 1) * IMPORT_DESCRIPTOR_BYTES; + const maxLength = + next > originalFirstThunk || next > firstThunk + ? Math.max(next - originalFirstThunk, next - firstThunk) + : fileBytes - dll.location.offset; + const named: bigint[] = []; + for (const [index, row] of dll.functions.entries()) { + if (index * width >= maxLength) + return `${label} lookup table runs past pefile's descriptor-relative bound.`; + const thunk = BigInt(`0x${row.thunk_hex}`); + const divergence = rowDivergence( + row, + thunk, + BigInt(tableRva), + BigInt(tableRva + index * width), + ); + if (divergence !== null) return `${label} ${divergence}`; + if (row.ordinal === null) named.push(thunk); + } + const addresses = addressDivergence(named); + return addresses === null ? null : `${label} ${addresses}`; +}; + +/** One lookup entry pefile would drop, truncate at or discard its table for. */ +const rowDivergence = ( + row: PeImportFunction, + thunk: bigint, + tableRva: bigint, + entryRva: bigint, +): string | null => { + if (thunk >= tableRva && thunk <= entryRva) + return "has a thunk that points into its own lookup table; pefile truncates there."; + if (row.ordinal !== null) { + // Pinned pefile 2024.8.26 masks only 31 bits for both widths. In PE32+ + // it ignores reserved bits 31–62; retain that compatibility behavior and + // the source thunk, without asserting that a loader accepts those bits. + if ((thunk & 0x7fffffffn) > 0xffffn) + return "has an ordinal thunk whose low 31-bit payload exceeds 16 bits; pefile discards the table."; + return row.ordinal === 0 ? "imports ordinal 0, which pefile omits." : null; + } + return row.name === null || + row.name.length >= PEFILE_MAX_NAME_BYTES || + !PEFILE_FUNCTION_NAME.test(row.name) + ? "has a function name with a length or characters pefile treats as invalid and omits." + : null; +}; + +/** pefile's repeated-address and address-spread table rejection heuristics. */ +const addressDivergence = (named: readonly bigint[]): string | null => { + const sets = { low: new Set(), high: new Set() }; + let repeated = 0; + for (const thunk of named) { + const set = thunk >= 1n << 32n ? sets.high : sets.low; + if (set.has(thunk)) repeated += 1; + set.add(thunk); + } + if (repeated >= PEFILE_MAX_REPEATED_ADDRESSES) + return `repeats hint/name addresses ${String(repeated)} times; pefile discards such tables.`; + for (const set of [sets.low, sets.high]) { + if (set.size === 0) continue; + const values = [...set]; + const low = values.reduce((left, right) => (right < left ? right : left)); + const high = values.reduce((left, right) => (right > left ? right : left)); + if (high - low > PEFILE_MAX_ADDRESS_SPREAD) + return "hint/name addresses spread beyond 128 MiB; pefile discards such tables."; + } + return null; +}; + +/** Parser facts pefile's row selection depends on beyond the reported rows. */ +export interface PefileImphashFacts { + readonly mappingCompatibility: boolean; + /** Issues recorded while reading the standard import table. */ + readonly standardIssues: number; + /** First descriptor pefile's import symbol cap truncates, if any. */ + readonly truncatedDescriptor: number | null; + readonly bits: 32 | 64; + readonly directoryRva: number; + readonly fileBytes: number; +} + +/** + * pefile `PE.get_imphash()` over a complete standard import table, or an + * explicit reason when the reported rows are not the sequence pefile hashes. + */ +export const pefileImphash = ( + imports: readonly PeImportDll[], + { + standardIssues, + mappingCompatibility, + truncatedDescriptor, + bits, + directoryRva, + fileBytes, + }: PefileImphashFacts, +): PeImports["imphash"] => { + if (standardIssues > 0) + return withheld( + `The standard import table is incomplete (${String(standardIssues)} issue(s)); a hash of the retained rows would not be this image's import hash.`, + ); + if (imports.length === 0) + return withheld("IMAGE_DIRECTORY_ENTRY_IMPORT is absent or empty."); + if (truncatedDescriptor !== null) + return withheld( + `Standard import descriptor ${String(truncatedDescriptor)} lies past pefile's ${String(PEFILE_MAX_IMPORT_SYMBOLS)}-thunk import symbol cap, so pefile hashes only a prefix of the table.`, + ); + if (!mappingCompatibility) + return withheld( + "Byte mapping compatibility with pefile is not established for the standard import table or its names.", + ); + const parts: string[] = []; + for (const dll of imports) { + const divergence = pefileTableDivergence( + dll, + bits, + directoryRva, + fileBytes, + ); + if (divergence !== null) return withheld(divergence); + const dllName = dll.name as string; + const library = stripImportExtension(dllName); + for (const imported of dll.functions) { + const functionName = + imported.name ?? pefileOrdinalName(dllName, imported.ordinal as number); + parts.push(`${library}.${functionName.toLowerCase()}`); + } + } + return { + algorithm: "pefile", + md5: createHash("md5").update(parts.join(","), "utf8").digest("hex"), + reason: null, + }; +}; diff --git a/src/native/pe/pefileOrdinalNames.ts b/src/native/pe/pefileOrdinalNames.ts new file mode 100644 index 0000000000..46c9307916 --- /dev/null +++ b/src/native/pe/pefileOrdinalNames.ts @@ -0,0 +1,713 @@ +/** + * Derived from pefile (c) 2004-2024 Ero Carrera, MIT. + * Retain licenses/pefile-2024.8.26-LICENSE.txt when redistributing these tables. + * Ordinal-to-name tables pefile 2024.8.26 consults in `ordlookup.ords` for + * its import hash. Ordinals missing here hash as `ord`, matching + * `ordlookup.formatOrdString`. Regenerate from pefile when it changes. + */ +export const PEFILE_ORDINAL_NAMES: Readonly< + Record> +> = { + "oleaut32.dll": new Map([ + [2, "SysAllocString"], + [3, "SysReAllocString"], + [4, "SysAllocStringLen"], + [5, "SysReAllocStringLen"], + [6, "SysFreeString"], + [7, "SysStringLen"], + [8, "VariantInit"], + [9, "VariantClear"], + [10, "VariantCopy"], + [11, "VariantCopyInd"], + [12, "VariantChangeType"], + [13, "VariantTimeToDosDateTime"], + [14, "DosDateTimeToVariantTime"], + [15, "SafeArrayCreate"], + [16, "SafeArrayDestroy"], + [17, "SafeArrayGetDim"], + [18, "SafeArrayGetElemsize"], + [19, "SafeArrayGetUBound"], + [20, "SafeArrayGetLBound"], + [21, "SafeArrayLock"], + [22, "SafeArrayUnlock"], + [23, "SafeArrayAccessData"], + [24, "SafeArrayUnaccessData"], + [25, "SafeArrayGetElement"], + [26, "SafeArrayPutElement"], + [27, "SafeArrayCopy"], + [28, "DispGetParam"], + [29, "DispGetIDsOfNames"], + [30, "DispInvoke"], + [31, "CreateDispTypeInfo"], + [32, "CreateStdDispatch"], + [33, "RegisterActiveObject"], + [34, "RevokeActiveObject"], + [35, "GetActiveObject"], + [36, "SafeArrayAllocDescriptor"], + [37, "SafeArrayAllocData"], + [38, "SafeArrayDestroyDescriptor"], + [39, "SafeArrayDestroyData"], + [40, "SafeArrayRedim"], + [41, "SafeArrayAllocDescriptorEx"], + [42, "SafeArrayCreateEx"], + [43, "SafeArrayCreateVectorEx"], + [44, "SafeArraySetRecordInfo"], + [45, "SafeArrayGetRecordInfo"], + [46, "VarParseNumFromStr"], + [47, "VarNumFromParseNum"], + [48, "VarI2FromUI1"], + [49, "VarI2FromI4"], + [50, "VarI2FromR4"], + [51, "VarI2FromR8"], + [52, "VarI2FromCy"], + [53, "VarI2FromDate"], + [54, "VarI2FromStr"], + [55, "VarI2FromDisp"], + [56, "VarI2FromBool"], + [57, "SafeArraySetIID"], + [58, "VarI4FromUI1"], + [59, "VarI4FromI2"], + [60, "VarI4FromR4"], + [61, "VarI4FromR8"], + [62, "VarI4FromCy"], + [63, "VarI4FromDate"], + [64, "VarI4FromStr"], + [65, "VarI4FromDisp"], + [66, "VarI4FromBool"], + [67, "SafeArrayGetIID"], + [68, "VarR4FromUI1"], + [69, "VarR4FromI2"], + [70, "VarR4FromI4"], + [71, "VarR4FromR8"], + [72, "VarR4FromCy"], + [73, "VarR4FromDate"], + [74, "VarR4FromStr"], + [75, "VarR4FromDisp"], + [76, "VarR4FromBool"], + [77, "SafeArrayGetVartype"], + [78, "VarR8FromUI1"], + [79, "VarR8FromI2"], + [80, "VarR8FromI4"], + [81, "VarR8FromR4"], + [82, "VarR8FromCy"], + [83, "VarR8FromDate"], + [84, "VarR8FromStr"], + [85, "VarR8FromDisp"], + [86, "VarR8FromBool"], + [87, "VarFormat"], + [88, "VarDateFromUI1"], + [89, "VarDateFromI2"], + [90, "VarDateFromI4"], + [91, "VarDateFromR4"], + [92, "VarDateFromR8"], + [93, "VarDateFromCy"], + [94, "VarDateFromStr"], + [95, "VarDateFromDisp"], + [96, "VarDateFromBool"], + [97, "VarFormatDateTime"], + [98, "VarCyFromUI1"], + [99, "VarCyFromI2"], + [100, "VarCyFromI4"], + [101, "VarCyFromR4"], + [102, "VarCyFromR8"], + [103, "VarCyFromDate"], + [104, "VarCyFromStr"], + [105, "VarCyFromDisp"], + [106, "VarCyFromBool"], + [107, "VarFormatNumber"], + [108, "VarBstrFromUI1"], + [109, "VarBstrFromI2"], + [110, "VarBstrFromI4"], + [111, "VarBstrFromR4"], + [112, "VarBstrFromR8"], + [113, "VarBstrFromCy"], + [114, "VarBstrFromDate"], + [115, "VarBstrFromDisp"], + [116, "VarBstrFromBool"], + [117, "VarFormatPercent"], + [118, "VarBoolFromUI1"], + [119, "VarBoolFromI2"], + [120, "VarBoolFromI4"], + [121, "VarBoolFromR4"], + [122, "VarBoolFromR8"], + [123, "VarBoolFromDate"], + [124, "VarBoolFromCy"], + [125, "VarBoolFromStr"], + [126, "VarBoolFromDisp"], + [127, "VarFormatCurrency"], + [128, "VarWeekdayName"], + [129, "VarMonthName"], + [130, "VarUI1FromI2"], + [131, "VarUI1FromI4"], + [132, "VarUI1FromR4"], + [133, "VarUI1FromR8"], + [134, "VarUI1FromCy"], + [135, "VarUI1FromDate"], + [136, "VarUI1FromStr"], + [137, "VarUI1FromDisp"], + [138, "VarUI1FromBool"], + [139, "VarFormatFromTokens"], + [140, "VarTokenizeFormatString"], + [141, "VarAdd"], + [142, "VarAnd"], + [143, "VarDiv"], + [144, "BSTR_UserFree64"], + [145, "BSTR_UserMarshal64"], + [146, "DispCallFunc"], + [147, "VariantChangeTypeEx"], + [148, "SafeArrayPtrOfIndex"], + [149, "SysStringByteLen"], + [150, "SysAllocStringByteLen"], + [151, "BSTR_UserSize64"], + [152, "VarEqv"], + [153, "VarIdiv"], + [154, "VarImp"], + [155, "VarMod"], + [156, "VarMul"], + [157, "VarOr"], + [158, "VarPow"], + [159, "VarSub"], + [160, "CreateTypeLib"], + [161, "LoadTypeLib"], + [162, "LoadRegTypeLib"], + [163, "RegisterTypeLib"], + [164, "QueryPathOfRegTypeLib"], + [165, "LHashValOfNameSys"], + [166, "LHashValOfNameSysA"], + [167, "VarXor"], + [168, "VarAbs"], + [169, "VarFix"], + [170, "OaBuildVersion"], + [171, "ClearCustData"], + [172, "VarInt"], + [173, "VarNeg"], + [174, "VarNot"], + [175, "VarRound"], + [176, "VarCmp"], + [177, "VarDecAdd"], + [178, "VarDecDiv"], + [179, "VarDecMul"], + [180, "CreateTypeLib2"], + [181, "VarDecSub"], + [182, "VarDecAbs"], + [183, "LoadTypeLibEx"], + [184, "SystemTimeToVariantTime"], + [185, "VariantTimeToSystemTime"], + [186, "UnRegisterTypeLib"], + [187, "VarDecFix"], + [188, "VarDecInt"], + [189, "VarDecNeg"], + [190, "VarDecFromUI1"], + [191, "VarDecFromI2"], + [192, "VarDecFromI4"], + [193, "VarDecFromR4"], + [194, "VarDecFromR8"], + [195, "VarDecFromDate"], + [196, "VarDecFromCy"], + [197, "VarDecFromStr"], + [198, "VarDecFromDisp"], + [199, "VarDecFromBool"], + [200, "GetErrorInfo"], + [201, "SetErrorInfo"], + [202, "CreateErrorInfo"], + [203, "VarDecRound"], + [204, "VarDecCmp"], + [205, "VarI2FromI1"], + [206, "VarI2FromUI2"], + [207, "VarI2FromUI4"], + [208, "VarI2FromDec"], + [209, "VarI4FromI1"], + [210, "VarI4FromUI2"], + [211, "VarI4FromUI4"], + [212, "VarI4FromDec"], + [213, "VarR4FromI1"], + [214, "VarR4FromUI2"], + [215, "VarR4FromUI4"], + [216, "VarR4FromDec"], + [217, "VarR8FromI1"], + [218, "VarR8FromUI2"], + [219, "VarR8FromUI4"], + [220, "VarR8FromDec"], + [221, "VarDateFromI1"], + [222, "VarDateFromUI2"], + [223, "VarDateFromUI4"], + [224, "VarDateFromDec"], + [225, "VarCyFromI1"], + [226, "VarCyFromUI2"], + [227, "VarCyFromUI4"], + [228, "VarCyFromDec"], + [229, "VarBstrFromI1"], + [230, "VarBstrFromUI2"], + [231, "VarBstrFromUI4"], + [232, "VarBstrFromDec"], + [233, "VarBoolFromI1"], + [234, "VarBoolFromUI2"], + [235, "VarBoolFromUI4"], + [236, "VarBoolFromDec"], + [237, "VarUI1FromI1"], + [238, "VarUI1FromUI2"], + [239, "VarUI1FromUI4"], + [240, "VarUI1FromDec"], + [241, "VarDecFromI1"], + [242, "VarDecFromUI2"], + [243, "VarDecFromUI4"], + [244, "VarI1FromUI1"], + [245, "VarI1FromI2"], + [246, "VarI1FromI4"], + [247, "VarI1FromR4"], + [248, "VarI1FromR8"], + [249, "VarI1FromDate"], + [250, "VarI1FromCy"], + [251, "VarI1FromStr"], + [252, "VarI1FromDisp"], + [253, "VarI1FromBool"], + [254, "VarI1FromUI2"], + [255, "VarI1FromUI4"], + [256, "VarI1FromDec"], + [257, "VarUI2FromUI1"], + [258, "VarUI2FromI2"], + [259, "VarUI2FromI4"], + [260, "VarUI2FromR4"], + [261, "VarUI2FromR8"], + [262, "VarUI2FromDate"], + [263, "VarUI2FromCy"], + [264, "VarUI2FromStr"], + [265, "VarUI2FromDisp"], + [266, "VarUI2FromBool"], + [267, "VarUI2FromI1"], + [268, "VarUI2FromUI4"], + [269, "VarUI2FromDec"], + [270, "VarUI4FromUI1"], + [271, "VarUI4FromI2"], + [272, "VarUI4FromI4"], + [273, "VarUI4FromR4"], + [274, "VarUI4FromR8"], + [275, "VarUI4FromDate"], + [276, "VarUI4FromCy"], + [277, "VarUI4FromStr"], + [278, "VarUI4FromDisp"], + [279, "VarUI4FromBool"], + [280, "VarUI4FromI1"], + [281, "VarUI4FromUI2"], + [282, "VarUI4FromDec"], + [283, "BSTR_UserSize"], + [284, "BSTR_UserMarshal"], + [285, "BSTR_UserUnmarshal"], + [286, "BSTR_UserFree"], + [287, "VARIANT_UserSize"], + [288, "VARIANT_UserMarshal"], + [289, "VARIANT_UserUnmarshal"], + [290, "VARIANT_UserFree"], + [291, "LPSAFEARRAY_UserSize"], + [292, "LPSAFEARRAY_UserMarshal"], + [293, "LPSAFEARRAY_UserUnmarshal"], + [294, "LPSAFEARRAY_UserFree"], + [295, "LPSAFEARRAY_Size"], + [296, "LPSAFEARRAY_Marshal"], + [297, "LPSAFEARRAY_Unmarshal"], + [298, "VarDecCmpR8"], + [299, "VarCyAdd"], + [300, "BSTR_UserUnmarshal64"], + [301, "DllCanUnloadNow"], + [302, "DllGetClassObject"], + [303, "VarCyMul"], + [304, "VarCyMulI4"], + [305, "VarCySub"], + [306, "VarCyAbs"], + [307, "VarCyFix"], + [308, "VarCyInt"], + [309, "VarCyNeg"], + [310, "VarCyRound"], + [311, "VarCyCmp"], + [312, "VarCyCmpR8"], + [313, "VarBstrCat"], + [314, "VarBstrCmp"], + [315, "VarR8Pow"], + [316, "VarR4CmpR8"], + [317, "VarR8Round"], + [318, "VarCat"], + [319, "VarDateFromUdateEx"], + [320, "DllRegisterServer"], + [321, "DllUnregisterServer"], + [322, "GetRecordInfoFromGuids"], + [323, "GetRecordInfoFromTypeInfo"], + [324, "LPSAFEARRAY_UserFree64"], + [325, "SetVarConversionLocaleSetting"], + [326, "GetVarConversionLocaleSetting"], + [327, "SetOaNoCache"], + [328, "LPSAFEARRAY_UserMarshal64"], + [329, "VarCyMulI8"], + [330, "VarDateFromUdate"], + [331, "VarUdateFromDate"], + [332, "GetAltMonthNames"], + [333, "VarI8FromUI1"], + [334, "VarI8FromI2"], + [335, "VarI8FromR4"], + [336, "VarI8FromR8"], + [337, "VarI8FromCy"], + [338, "VarI8FromDate"], + [339, "VarI8FromStr"], + [340, "VarI8FromDisp"], + [341, "VarI8FromBool"], + [342, "VarI8FromI1"], + [343, "VarI8FromUI2"], + [344, "VarI8FromUI4"], + [345, "VarI8FromDec"], + [346, "VarI2FromI8"], + [347, "VarI2FromUI8"], + [348, "VarI4FromI8"], + [349, "VarI4FromUI8"], + [350, "LPSAFEARRAY_UserSize64"], + [351, "LPSAFEARRAY_UserUnmarshal64"], + [352, "OACreateTypeLib2"], + [353, "SafeArrayAddRef"], + [354, "SafeArrayReleaseData"], + [355, "SafeArrayReleaseDescriptor"], + [356, "SysAddRefString"], + [357, "SysReleaseString"], + [358, "VARIANT_UserFree64"], + [359, "VARIANT_UserMarshal64"], + [360, "VarR4FromI8"], + [361, "VarR4FromUI8"], + [362, "VarR8FromI8"], + [363, "VarR8FromUI8"], + [364, "VarDateFromI8"], + [365, "VarDateFromUI8"], + [366, "VarCyFromI8"], + [367, "VarCyFromUI8"], + [368, "VarBstrFromI8"], + [369, "VarBstrFromUI8"], + [370, "VarBoolFromI8"], + [371, "VarBoolFromUI8"], + [372, "VarUI1FromI8"], + [373, "VarUI1FromUI8"], + [374, "VarDecFromI8"], + [375, "VarDecFromUI8"], + [376, "VarI1FromI8"], + [377, "VarI1FromUI8"], + [378, "VarUI2FromI8"], + [379, "VarUI2FromUI8"], + [380, "VARIANT_UserSize64"], + [381, "VARIANT_UserUnmarshal64"], + [401, "OleLoadPictureEx"], + [402, "OleLoadPictureFileEx"], + [411, "SafeArrayCreateVector"], + [412, "SafeArrayCopyData"], + [413, "VectorFromBstr"], + [414, "BstrFromVector"], + [415, "OleIconToCursor"], + [416, "OleCreatePropertyFrameIndirect"], + [417, "OleCreatePropertyFrame"], + [418, "OleLoadPicture"], + [419, "OleCreatePictureIndirect"], + [420, "OleCreateFontIndirect"], + [421, "OleTranslateColor"], + [422, "OleLoadPictureFile"], + [423, "OleSavePictureFile"], + [424, "OleLoadPicturePath"], + [425, "VarUI4FromI8"], + [426, "VarUI4FromUI8"], + [427, "VarI8FromUI8"], + [428, "VarUI8FromI8"], + [429, "VarUI8FromUI1"], + [430, "VarUI8FromI2"], + [431, "VarUI8FromR4"], + [432, "VarUI8FromR8"], + [433, "VarUI8FromCy"], + [434, "VarUI8FromDate"], + [435, "VarUI8FromStr"], + [436, "VarUI8FromDisp"], + [437, "VarUI8FromBool"], + [438, "VarUI8FromI1"], + [439, "VarUI8FromUI2"], + [440, "VarUI8FromUI4"], + [441, "VarUI8FromDec"], + [442, "RegisterTypeLibForUser"], + [443, "UnRegisterTypeLibForUser"], + [444, "OaEnablePerUserTLibRegistration"], + [445, "HWND_UserFree"], + [446, "HWND_UserMarshal"], + [447, "HWND_UserSize"], + [448, "HWND_UserUnmarshal"], + [449, "HWND_UserFree64"], + [450, "HWND_UserMarshal64"], + [451, "HWND_UserSize64"], + [452, "HWND_UserUnmarshal64"], + [500, "OACleanup"], + ]), + "ws2_32.dll": new Map([ + [1, "accept"], + [2, "bind"], + [3, "closesocket"], + [4, "connect"], + [5, "getpeername"], + [6, "getsockname"], + [7, "getsockopt"], + [8, "htonl"], + [9, "htons"], + [10, "ioctlsocket"], + [11, "inet_addr"], + [12, "inet_ntoa"], + [13, "listen"], + [14, "ntohl"], + [15, "ntohs"], + [16, "recv"], + [17, "recvfrom"], + [18, "select"], + [19, "send"], + [20, "sendto"], + [21, "setsockopt"], + [22, "shutdown"], + [23, "socket"], + [24, "WSApSetPostRoutine"], + [25, "FreeAddrInfoEx"], + [26, "FreeAddrInfoExW"], + [27, "FreeAddrInfoW"], + [28, "GetAddrInfoExA"], + [29, "GetAddrInfoExCancel"], + [30, "GetAddrInfoExOverlappedResult"], + [31, "GetAddrInfoExW"], + [32, "GetAddrInfoW"], + [33, "GetHostNameW"], + [34, "GetNameInfoW"], + [35, "InetNtopW"], + [36, "InetPtonW"], + [37, "ProcessSocketNotifications"], + [38, "SetAddrInfoExA"], + [39, "SetAddrInfoExW"], + [40, "WPUCompleteOverlappedRequest"], + [41, "WPUGetProviderPathEx"], + [42, "WSAAccept"], + [43, "WSAAddressToStringA"], + [44, "WSAAddressToStringW"], + [45, "WSAAdvertiseProvider"], + [46, "WSACloseEvent"], + [47, "WSAConnect"], + [48, "WSAConnectByList"], + [49, "WSAConnectByNameA"], + [50, "WSAConnectByNameW"], + [51, "gethostbyaddr"], + [52, "gethostbyname"], + [53, "getprotobyname"], + [54, "getprotobynumber"], + [55, "getservbyname"], + [56, "getservbyport"], + [57, "gethostname"], + [58, "WSACreateEvent"], + [59, "WSADuplicateSocketA"], + [60, "WSADuplicateSocketW"], + [61, "WSAEnumNameSpaceProvidersA"], + [62, "WSAEnumNameSpaceProvidersExA"], + [63, "WSAEnumNameSpaceProvidersExW"], + [64, "WSAEnumNameSpaceProvidersW"], + [65, "WSAEnumNetworkEvents"], + [66, "WSAEnumProtocolsA"], + [67, "WSAEnumProtocolsW"], + [68, "WSAEventSelect"], + [69, "WSAGetOverlappedResult"], + [70, "WSAGetQOSByName"], + [71, "WSAGetServiceClassInfoA"], + [72, "WSAGetServiceClassInfoW"], + [73, "WSAGetServiceClassNameByClassIdA"], + [74, "WSAGetServiceClassNameByClassIdW"], + [75, "WSAHtonl"], + [76, "WSAHtons"], + [77, "WSAInstallServiceClassA"], + [78, "WSAInstallServiceClassW"], + [79, "WSAIoctl"], + [80, "WSAJoinLeaf"], + [81, "WSALookupServiceBeginA"], + [82, "WSALookupServiceBeginW"], + [83, "WSALookupServiceEnd"], + [84, "WSALookupServiceNextA"], + [85, "WSALookupServiceNextW"], + [86, "WSANSPIoctl"], + [87, "WSANtohl"], + [88, "WSANtohs"], + [89, "WSAPoll"], + [90, "WSAProviderCompleteAsyncCall"], + [91, "WSAProviderConfigChange"], + [92, "WSARecv"], + [93, "WSARecvDisconnect"], + [94, "WSARecvFrom"], + [95, "WSARemoveServiceClass"], + [96, "WSAResetEvent"], + [97, "WSASend"], + [98, "WSASendDisconnect"], + [99, "WSASendMsg"], + [100, "WSASendTo"], + [101, "WSAAsyncSelect"], + [102, "WSAAsyncGetHostByAddr"], + [103, "WSAAsyncGetHostByName"], + [104, "WSAAsyncGetProtoByNumber"], + [105, "WSAAsyncGetProtoByName"], + [106, "WSAAsyncGetServByPort"], + [107, "WSAAsyncGetServByName"], + [108, "WSACancelAsyncRequest"], + [109, "WSASetBlockingHook"], + [110, "WSAUnhookBlockingHook"], + [111, "WSAGetLastError"], + [112, "WSASetLastError"], + [113, "WSACancelBlockingCall"], + [114, "WSAIsBlocking"], + [115, "WSAStartup"], + [116, "WSACleanup"], + [117, "WSASetEvent"], + [118, "WSASetServiceA"], + [119, "WSASetServiceW"], + [120, "WSASocketA"], + [121, "WSASocketW"], + [122, "WSAStringToAddressA"], + [123, "WSAStringToAddressW"], + [124, "WSAUnadvertiseProvider"], + [125, "WSAWaitForMultipleEvents"], + [126, "WSCDeinstallProvider"], + [127, "WSCDeinstallProvider32"], + [128, "WSCDeinstallProviderEx"], + [129, "WSCEnableNSProvider"], + [130, "WSCEnableNSProvider32"], + [131, "WSCEnumNameSpaceProviders32"], + [132, "WSCEnumNameSpaceProvidersEx32"], + [133, "WSCEnumProtocols"], + [134, "WSCEnumProtocols32"], + [135, "WSCEnumProtocolsEx"], + [136, "WSCGetApplicationCategory"], + [137, "WSCGetApplicationCategoryEx"], + [138, "WSCGetProviderInfo"], + [139, "WSCGetProviderInfo32"], + [140, "WSCGetProviderPath"], + [141, "WSCGetProviderPath32"], + [142, "WSCInstallNameSpace"], + [143, "WSCInstallNameSpace32"], + [144, "WSCInstallNameSpaceEx"], + [145, "WSCInstallNameSpaceEx2"], + [146, "WSCInstallNameSpaceEx32"], + [147, "WSCInstallProvider"], + [148, "WSCInstallProvider64_32"], + [149, "WSCInstallProviderAndChains64_32"], + [150, "WSCInstallProviderEx"], + [151, "__WSAFDIsSet"], + [152, "WSCSetApplicationCategory"], + [153, "WSCSetApplicationCategoryEx"], + [154, "WSCSetProviderInfo"], + [155, "WSCSetProviderInfo32"], + [156, "WSCUnInstallNameSpace"], + [157, "WSCUnInstallNameSpace32"], + [158, "WSCUnInstallNameSpaceEx2"], + [159, "WSCUpdateProvider"], + [160, "WSCUpdateProvider32"], + [161, "WSCUpdateProviderEx"], + [162, "WSCWriteNameSpaceOrder"], + [163, "WSCWriteNameSpaceOrder32"], + [164, "WSCWriteProviderOrder"], + [165, "WSCWriteProviderOrder32"], + [166, "WSCWriteProviderOrderEx"], + [167, "WahCloseApcHelper"], + [168, "WahCloseHandleHelper"], + [169, "WahCloseNotificationHandleHelper"], + [170, "WahCloseSocketHandle"], + [171, "WahCloseThread"], + [172, "WahCompleteRequest"], + [173, "WahCreateHandleContextTable"], + [174, "WahCreateNotificationHandle"], + [175, "WahCreateSocketHandle"], + [176, "WahDestroyHandleContextTable"], + [177, "WahDisableNonIFSHandleSupport"], + [178, "WahEnableNonIFSHandleSupport"], + [179, "WahEnumerateHandleContexts"], + [180, "WahInsertHandleContext"], + [181, "WahNotifyAllProcesses"], + [182, "WahOpenApcHelper"], + [183, "WahOpenCurrentThread"], + [184, "WahOpenHandleHelper"], + [185, "WahOpenNotificationHandleHelper"], + [186, "WahQueueUserApc"], + [187, "WahReferenceContextByHandle"], + [188, "WahRemoveHandleContext"], + [189, "WahWaitForNotification"], + [190, "WahWriteLSPEvent"], + [191, "freeaddrinfo"], + [192, "getaddrinfo"], + [193, "getnameinfo"], + [194, "inet_ntop"], + [195, "inet_pton"], + [500, "WEP"], + ]), + "wsock32.dll": new Map([ + [1, "accept"], + [2, "bind"], + [3, "closesocket"], + [4, "connect"], + [5, "getpeername"], + [6, "getsockname"], + [7, "getsockopt"], + [8, "htonl"], + [9, "htons"], + [10, "inet_addr"], + [11, "inet_ntoa"], + [12, "ioctlsocket"], + [13, "listen"], + [14, "ntohl"], + [15, "ntohs"], + [16, "recv"], + [17, "recvfrom"], + [18, "select"], + [19, "send"], + [20, "sendto"], + [21, "setsockopt"], + [22, "shutdown"], + [23, "socket"], + [24, "MigrateWinsockConfiguration"], + [51, "gethostbyaddr"], + [52, "gethostbyname"], + [53, "getprotobyname"], + [54, "getprotobynumber"], + [55, "getservbyname"], + [56, "getservbyport"], + [57, "gethostname"], + [101, "WSAAsyncSelect"], + [102, "WSAAsyncGetHostByAddr"], + [103, "WSAAsyncGetHostByName"], + [104, "WSAAsyncGetProtoByNumber"], + [105, "WSAAsyncGetProtoByName"], + [106, "WSAAsyncGetServByPort"], + [107, "WSAAsyncGetServByName"], + [108, "WSACancelAsyncRequest"], + [109, "WSASetBlockingHook"], + [110, "WSAUnhookBlockingHook"], + [111, "WSAGetLastError"], + [112, "WSASetLastError"], + [113, "WSACancelBlockingCall"], + [114, "WSAIsBlocking"], + [115, "WSAStartup"], + [116, "WSACleanup"], + [151, "__WSAFDIsSet"], + [500, "WEP"], + [1000, "WSApSetPostRoutine"], + [1100, "inet_network"], + [1101, "getnetbyname"], + [1102, "rcmd"], + [1103, "rexec"], + [1104, "rresvport"], + [1105, "sethostname"], + [1106, "dn_expand"], + [1107, "WSARecvEx"], + [1108, "s_perror"], + [1109, "GetAddressByNameA"], + [1110, "GetAddressByNameW"], + [1111, "EnumProtocolsA"], + [1112, "EnumProtocolsW"], + [1113, "GetTypeByNameA"], + [1114, "GetTypeByNameW"], + [1115, "GetNameByTypeA"], + [1116, "GetNameByTypeW"], + [1117, "SetServiceA"], + [1118, "SetServiceW"], + [1119, "GetServiceA"], + [1120, "GetServiceW"], + [1130, "NPLoadNameSpaces"], + [1140, "TransmitFile"], + [1141, "AcceptEx"], + [1142, "GetAcceptExSockaddrs"], + ]), +}; diff --git a/src/process/WindowsAuthority.ts b/src/process/WindowsAuthority.ts index 3c83fe6885..bef4f0c544 100644 --- a/src/process/WindowsAuthority.ts +++ b/src/process/WindowsAuthority.ts @@ -1,5 +1,6 @@ import { systemWindowsNativeAuthority, + WINDOWS_NATIVE_LOAD_REFUSED_REMEDIATION, type WindowsNativeLoadResult, } from "../windows/WindowsNativeLoader.js"; @@ -82,17 +83,19 @@ export const inspectWindowsNativeReadiness = ( code: failureStage === "host" ? "unsupported_host" - : failureStage === "artifact" + : failureStage === "artifact" || failureStage === "load" ? "runtime_missing" : "runtime_unavailable", remediation: failureStage === "host" ? "Use a Windows x64 host and a supported Node.js runtime with Node-API 8 or newer." - : failureStage === "artifact" - ? "Use the matching Windows x64 REA package, or restore native/windows/build/manifest.json and native/windows/build/rea-windows-x64.node from the same-version rea-agents tarball or build them with native/windows/README.md. The loader checks version, ABI, architecture, and digest before granting authority; no download or install is performed automatically." - : failureStage === "inspection" - ? "Install the matching Windows x64 REA package; its native inspection must prove every required security control." - : "Inspect the preserved native self-test error. Check that the process's temporary directory is writable local NTFS and that its filesystem, DACL, Job Object, and cleanup operations are permitted. Resolve the reported constraint, then rerun rea doctor; reinstalling Ghidra or Java cannot fix this self-test.", + : failureStage === "load" + ? WINDOWS_NATIVE_LOAD_REFUSED_REMEDIATION + : failureStage === "artifact" + ? "Use the matching Windows x64 REA package, or restore native/windows/build/manifest.json and native/windows/build/rea-windows-x64.node from the same-version rea-agents tarball or build them with native/windows/README.md. The loader checks version, ABI, architecture, and digest before granting authority; no download or install is performed automatically." + : failureStage === "inspection" + ? "Install the matching Windows x64 REA package; its native inspection must prove every required security control." + : "Inspect the preserved native self-test error. Check that the process's temporary directory is writable local NTFS and that its filesystem, DACL, Job Object, and cleanup operations are permitted. Resolve the reported constraint, then rerun rea doctor; reinstalling Ghidra or Java cannot fix this self-test.", }; }; diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 1bf0b8938b..3e474a11b0 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -25,15 +25,18 @@ import { captureProviderEnvironment } from "./providerEnvironment.js"; import { createElfNotesService, createPeCertificatesService, + createPeImportsService, createNativeExportsService, createPeResourcesService, } from "../composition/binaryDiagnostics.js"; import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js"; import type { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js"; +import type { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.js"; import type { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js"; import type { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js"; import { registerElfNotesTool } from "./registerElfNotesTool.js"; import { registerPeCertificatesTool } from "./registerPeCertificatesTool.js"; +import { registerPeImportsTool } from "./registerPeImportsTool.js"; import { registerPeResourcesTool } from "./registerPeResourcesTool.js"; import { registerNativeExportsTool } from "./registerNativeExportsTool.js"; import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js"; @@ -468,6 +471,7 @@ export const createServer = ( const nativeExports = createNativeExportsService(); const elfNotes = createElfNotesService(); const peCertificates = createPeCertificatesService(); + const peImports = createPeImportsService(); const electronFuses = options.electronFuses ?? createElectronFuseService(); const pythonPyc = options.pythonPyc ?? createPythonPycService(); const webNetworkCapture = @@ -579,6 +583,7 @@ export const createServer = ( nativeExports.close(), elfNotes.close(), peCertificates.close(), + peImports.close(), electronFuses.close(), pythonPyc.close(), webNetworkCapture.close(), @@ -647,6 +652,7 @@ export const createServer = ( nativeExports, elfNotes, peCertificates, + peImports, electronFuses, pythonPyc, webNetworkCapture, @@ -884,6 +890,7 @@ const registerConfiguredAnalysisTools = ( nativeExports, elfNotes, peCertificates, + peImports, pythonPyc, webNetworkCapture, artifactResources, @@ -957,6 +964,13 @@ const registerConfiguredAnalysisTools = ( recordEvidence, withAdmittedAnalysis, ); + registerPeImportsTool( + server, + peImports, + toolLogger, + recordEvidence, + withAdmittedAnalysis, + ); registerGoTools( server, goBinary, @@ -995,6 +1009,7 @@ const registerConfiguredAnalysisTools = ( toolLogger, recordEvidence, withAdmittedAnalysis, + evidenceById, ); registerWasmTools( server, @@ -1113,6 +1128,7 @@ interface ServerToolContext extends ReturnType { readonly nativeExports: NativeExportsService; readonly elfNotes: ElfNotesService; readonly peCertificates: PeCertificatesService; + readonly peImports: PeImportsService; readonly electronFuses: ElectronFuseService; readonly pythonPyc: PythonPycService; readonly webNetworkCapture: WebNetworkCaptureService; diff --git a/src/server/registerErlangTools.ts b/src/server/registerErlangTools.ts index 53c20c9f95..af7260c3a6 100644 --- a/src/server/registerErlangTools.ts +++ b/src/server/registerErlangTools.ts @@ -3,29 +3,62 @@ import type { BeamService } from "../application/erlang/BeamService.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; -import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; import { createEvidenceToolHandler } from "./contractToolHandler.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; +import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; +import { + requireAnalysisViewRetention, + projectRetainedAnalysis, +} from "../application/analysisView/AnalysisViewService.js"; +import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; +import { err } from "../domain/result.js"; -/** Bind the exact BEAM contract to its shared workflow and owning session Evidence ledger. */ +/** Register the explicit-file BEAM workflow and retain its complete Evidence. */ export const registerErlangTools = ( server: EvidenceMcpServer, service: BeamService, logger: Logger, recordEvidence?: EvidenceWriter["recordEvidence"], withAdmittedAnalysis?: WithAdmittedAnalysis, + evidenceById?: EvidenceLookup, ): void => { const contract = toolContract("inspect_erlang_beam"); const handler = createEvidenceToolHandler( server, - (input, options) => service.inspect(input, options), + async (rawInput, options) => { + const parsed = contract.inputSchema.safeParse(rawInput); + if (!parsed.success) + return err( + analysisInputErrorFromIssues( + contract.name, + parsed.error.issues, + rawInput, + { cause: parsed.error }, + ), + ); + const { detail, view, ...input } = parsed.data; + const retention = { recordEvidence, evidenceById }; + if (detail === "summary") { + const ready = requireAnalysisViewRetention(contract.name, retention); + if (!ready.ok) return ready; + } + const inspected = await service.inspect(input, options); + return detail === "summary" && inspected.ok + ? projectRetainedAnalysis( + inspected.value, + retention, + view ?? { kind: "summary" }, + ) + : inspected; + }, logger, recordEvidence, withAdmittedAnalysis, ); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), handler(contract), ); }; diff --git a/src/server/registerPeImportsTool.ts b/src/server/registerPeImportsTool.ts new file mode 100644 index 0000000000..1274957943 --- /dev/null +++ b/src/server/registerPeImportsTool.ts @@ -0,0 +1,31 @@ +import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; +import type { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.js"; +import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import { toolContract } from "../contracts/toolContracts.js"; +import type { Logger } from "pino"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; + +/** Register the explicit-file PE import-table workflow and retain its complete Evidence. */ +export const registerPeImportsTool = ( + server: EvidenceMcpServer, + service: PeImportsService, + logger: Logger, + recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, +): void => { + const contract = toolContract("inspect_pe_imports"); + const handler = createEvidenceToolHandler( + server, + (input, options) => service.inspect(input, options), + logger, + recordEvidence, + withAdmittedAnalysis, + ); + server.registerTool( + contract.name, + toolRegistrationOptions(contract), + handler(contract), + ); +}; diff --git a/src/windows/WindowsNativeLoader.test.ts b/src/windows/WindowsNativeLoader.test.ts index eada66fa8b..74db2eac37 100644 --- a/src/windows/WindowsNativeLoader.test.ts +++ b/src/windows/WindowsNativeLoader.test.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; +import { inspectWindowsNativeReadiness } from "../process/WindowsAuthority.js"; import { loadWindowsNativeAuthority, type WindowsNativeLoadHost, @@ -224,6 +225,34 @@ describe("packaged Windows native artifact validation", () => { }); }); + it("explains an OS policy refusal of the verified artifact without file-restore advice", () => { + const value = fixture(); + const loaded = loadWindowsNativeAuthority({ + ...value.host, + loadArtifact: () => { + throw new Error( + "An Application Control policy has blocked this file.\r\n\\\\?\\C:\\rea\\rea-windows-x64.node", + ); + }, + }); + expect(loaded).toMatchObject({ + available: false, + failureStage: "load", + reason: expect.stringContaining( + "An Application Control policy has blocked this file.", + ), + }); + expect(loaded).not.toHaveProperty("retryable"); + const readiness = inspectWindowsNativeReadiness("win32", loaded); + expect(readiness).toMatchObject({ + code: "runtime_missing", + remediation: expect.stringContaining("Smart App Control"), + }); + expect(readiness).not.toMatchObject({ + remediation: expect.stringContaining("restore native/windows/build"), + }); + }); + it("requires every inspected native fact", () => { const value = fixture(); const loaded = loadWindowsNativeAuthority({ diff --git a/src/windows/WindowsNativeLoader.ts b/src/windows/WindowsNativeLoader.ts index adf59fbd27..f213f85039 100644 --- a/src/windows/WindowsNativeLoader.ts +++ b/src/windows/WindowsNativeLoader.ts @@ -71,7 +71,12 @@ export type WindowsNativeLoadResult = | { readonly available: false; readonly reason: string; - readonly failureStage: "host" | "artifact" | "self-test" | "inspection"; + readonly failureStage: + | "host" + | "artifact" + | "load" + | "self-test" + | "inspection"; /** Only a failed native invocation can recover on a later query. */ readonly retryable?: true; }; @@ -90,6 +95,10 @@ export interface WindowsNativeLoadHost { const missingWindowsNativeFileReason = (file: string): string => `Missing packaged Windows native file native/windows/build/${file} (ENOENT). Restore both native/windows/build/manifest.json and native/windows/build/rea-windows-x64.node from the rea-agents@${PACKAGE_METADATA.version} tarball or build them with native/windows/README.md; no native artifact was loaded.`; +/** Recovery when Windows refuses to load the verified artifact, for example by code-integrity policy. */ +export const WINDOWS_NATIVE_LOAD_REFUSED_REMEDIATION = + "The file matched the package manifest, so restoring or reinstalling it will not help. If Windows reports that an Application Control policy blocked the file, Smart App Control or an App Control for Business policy refused it. Windows native controls, which Ghidra and JADX need on Windows, stay unavailable; rea setup can still register clients. Run REA on a host whose policy allows this file, or ask the REA maintainers about a signed build."; + const readPackagedWindowsNativeFile = (file: string, read: () => T): T => { try { return read(); @@ -139,7 +148,17 @@ export const loadWindowsNativeAuthority = ( "Windows native artifact SHA-256 does not match package metadata", ); assertWindowsX64Artifact(artifact); - const binding = bindingSchema.parse(host.loadArtifact()); + let loadedModule: unknown; + try { + loadedModule = host.loadArtifact(); + } catch (cause: unknown) { + return { + available: false, + failureStage: "load", + reason: `REA Windows native controls are unavailable: Windows refused to load the verified native/windows/build/rea-windows-x64.node: ${cause instanceof Error ? cause.message : String(cause)} ${WINDOWS_NATIVE_LOAD_REFUSED_REMEDIATION}`, + }; + } + const binding = bindingSchema.parse(loadedModule); let observed: unknown; try { observed = binding.call("inspect", []); diff --git a/tests/acceptance/applications/applicationWorkflowCli.test.ts b/tests/acceptance/applications/applicationWorkflowCli.test.ts index b710e76a81..2a25b876c2 100644 --- a/tests/acceptance/applications/applicationWorkflowCli.test.ts +++ b/tests/acceptance/applications/applicationWorkflowCli.test.ts @@ -5,6 +5,7 @@ import { createRequire } from "node:module"; import { join, relative, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { promisify } from "node:util"; +import { runInNewContext } from "node:vm"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; @@ -74,15 +75,6 @@ afterAll(async () => { }); const WEBPACK_FACTORY_BODY = ` const key = "dynamic"; - const __webpack_require__ = { - d(target, definitions) { - for (const name of Object.keys(definitions)) - Object.defineProperty(target, name, { - enumerable: true, - get: definitions[name], - }); - }, - }; exports[key] = 1; exports["dot.key"] = 2; exports[""] = 3; @@ -1106,14 +1098,28 @@ describe("Webpack factory CommonJS export keys", () => { }]);`; await writeFile(join(root, "bundle.js"), bundle); - // Execute the same factory body as CommonJS, outside the analyzer, to - // verify Node's alias behavior and the exact runtime object keys. + // Execute the exact bundle and invoke its registered factory with a loader. + // This also rejects invalid factory syntax before trusting the export oracle. + const actual = runInNewContext(` + globalThis.webpackChunkStaticExports = []; + ${bundle} + const module = { exports: {} }; + const loader = { + d(target, definitions) { + for (const name of Object.keys(definitions)) + Object.defineProperty(target, name, { + enumerable: true, + get: definitions[name], + }); + }, + }; + const [, factories] = globalThis.webpackChunkStaticExports[0]; + factories[1](module, module.exports, loader); + module.exports; + `) as Record; const oracleRoot = await createTestTempDirectory( "rea-webpack-export-oracle-", ); - const oraclePath = join(oracleRoot, "oracle.cjs"); - await writeFile(oraclePath, WEBPACK_FACTORY_BODY); - const actual = requireFixture(oraclePath) as Record; expect(Reflect.ownKeys(actual).sort()).toEqual([ "", "dot.key", @@ -1226,16 +1232,20 @@ describe("Webpack factory CommonJS export keys", () => { describe("application workflow CLI input", () => { it("rejects Evidence ID-only workflow inputs", async () => { - const result = await runCli([ - "compare-application-versions", - JSON.stringify({ - left: JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.left - .evidence_id, - right: - JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.right.evidence_id, - }), - "--json", - ]); + const result = await runCli( + [ + "compare-application-versions", + JSON.stringify({ + left: JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.left + .evidence_id, + right: + JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.right.evidence_id, + }), + "--json", + ], + {}, + 1, + ); expect(result).toMatchObject({ code: "evidence_integrity_mismatch" }); }); }); @@ -1490,6 +1500,51 @@ describe("application workflow CLI and MCP capture lifetimes", () => { }); describe("application workflow CLI export Evidence", () => { + it("compares primitive changes through local spread copies with CLI and stdio MCP", async () => { + const root = await createTestTempDirectory("rea-spread-copy-comparison-"); + const [left, right] = await analyzeComparisonSources(root, [ + `export default () => { + const o = { a: 1, b: 2 }; const box = { ...o }; + return { type: "x", a: box.a, b: box.b }; + };`, + `export default () => { + const o = { a: 9, b: 2 }; const box = { ...o }; + return { type: "x", a: box.a, b: box.b }; + };`, + ]); + const input = { + left, + right, + left_module_path: "parser.mjs", + left_export_name: "default", + right_module_path: "parser.mjs", + right_export_name: "default", + }; + const expected = { + operation: "compare_javascript_export_shapes", + normalized_result: { + summary: { added: 0, removed: 0, changed: 1, unknown: 0 }, + coverage: { status: "complete-within-inputs" }, + changes: [ + expect.objectContaining({ + path: "/a", + status: "changed", + left: { availability: "literal", value: 1 }, + right: { availability: "literal", value: 9 }, + }), + ], + }, + }; + expect( + await runCliWithRequestFile( + root, + "compare-javascript-export-shapes", + input, + ), + ).toMatchObject(expected); + expect(await compareThroughStdioMcp(input)).toMatchObject(expected); + }, 20_000); + it("preserves uncertainty after a helper can mutate an awaited return object", async () => { const root = await createTestTempDirectory("rea-awaited-export-shape-cli-"); const sources = [ @@ -1684,11 +1739,11 @@ describe("application workflow CLI export property presence", () => { describe("application workflow CLI validation", () => { it("returns safe actionable JSON validation details", async () => { - const malformed = await runCli([ - "trace-application-feature", - "{not-json", - "--json", - ]); + const malformed = await runCli( + ["trace-application-feature", "{not-json", "--json"], + {}, + 1, + ); expect(malformed).toMatchObject({ code: "invalid_request", retryable: false, @@ -1698,7 +1753,11 @@ describe("application workflow CLI validation", () => { }); expect(JSON.stringify(malformed)).not.toContain("not-json"); - const missing = await runCli(["trace-application-feature", "{}", "--json"]); + const missing = await runCli( + ["trace-application-feature", "{}", "--json"], + {}, + 1, + ); expect(missing).toMatchObject({ code: "invalid_request", details: { @@ -1819,6 +1878,7 @@ it("retains bundled isolated-world contextBridge APIs through dedicated and gene const runCli = async ( arguments_: readonly string[], environment: Readonly> = {}, + expectedExitCode: 0 | 1 = 0, ): Promise => { cliSession ??= createCliCommandSession(); const result = await cliSession.run({ @@ -1826,13 +1886,12 @@ const runCli = async ( cwd: process.cwd(), environment: definedCliEnvironment({ ...process.env, ...environment }), }); - const stdout = result.stdout.trim(); - if (stdout.length > 0) return JSON.parse(stdout) as unknown; - throw new Error( - result.stderr.trim().length > 0 - ? result.stderr - : `Compiled CLI command failed with status ${String(result.exitCode)}`, + expect(result.signal, result.stderr || result.stdout).toBeNull(); + expect(result.exitCode, result.stderr || result.stdout).toBe( + expectedExitCode, ); + expect(result.json, result.stderr || result.stdout).toBeDefined(); + return result.json; }; /** @@ -1852,14 +1911,18 @@ const runCliWithRequestFile = async ( }; const assertSourceFileView = async (sourceCompared: unknown) => { - const wrongApplication = await runCli([ - "compare-source-to-bundle", - JSON.stringify({ - reference: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference, - application: sourceCompared, - }), - "--json", - ]); + const wrongApplication = await runCli( + [ + "compare-source-to-bundle", + JSON.stringify({ + reference: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference, + application: sourceCompared, + }), + "--json", + ], + {}, + 1, + ); expect(wrongApplication).toMatchObject({ error: "Application workflow failed", }); diff --git a/tests/acceptance/applications/catchInitializerOwnership.test.ts b/tests/acceptance/applications/catchInitializerOwnership.test.ts new file mode 100644 index 0000000000..24cc0e3d79 --- /dev/null +++ b/tests/acceptance/applications/catchInitializerOwnership.test.ts @@ -0,0 +1,68 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; +import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js"; + +cliTest.for(["CLI", "stdio MCP"] as const)( + "preserves catch-shadowed initializer mutations in exported Evidence through %s", + async (surface, { cli }) => { + const root = await createTestTempDirectory( + "rea-catch-initializer-ownership-", + ); + await writeFile( + join(root, "app.cjs"), + `throw new Error("This analysis fixture must not execute"); +exports.shadowed = function shadowed() { + const state = { value: 1 }; + try { throw 0; } catch (handler) { + var { handler } = { handler: state }; + handler.value = 2; + } + return state.value; +}; +exports.unshadowed = function unshadowed() { + const state = { value: 1 }; + try { throw 0; } catch (error) { + var { handler } = { handler: state }; + handler.value = 2; + } + return state.value; +}; +exports.readOnly = function readOnly() { + const state = { value: 1 }; + try { throw 0; } catch (handler) { + var { handler } = { handler: state }; + const saved = handler.value; + } + return state.value; +}; +`, + ); + const { graph } = await analyzeJavaScriptDirectory(surface, cli, root); + for (const [name, status] of [ + ["shadowed", "unknown"], + ["unshadowed", "unknown"], + ["readOnly", "literal"], + ] as const) { + const observation = graph.nodes + .flatMap(({ observations }) => observations) + .find( + ({ properties }) => + properties.semantic_role === "export-return-shapes" && + properties.exported_name === name, + ); + expect(observation, name).toBeDefined(); + expect(observation?.properties.static_return_shapes).toMatchObject([ + { + value_status: status, + fields: [ + { path: "", state: status, value: status === "unknown" ? null : 1 }, + ], + }, + ]); + } + }, +); diff --git a/tests/acceptance/applications/erlangBeamViews.test.ts b/tests/acceptance/applications/erlangBeamViews.test.ts new file mode 100644 index 0000000000..8eca0f211f --- /dev/null +++ b/tests/acceptance/applications/erlangBeamViews.test.ts @@ -0,0 +1,334 @@ +import { rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { z } from "zod"; +import { readEvidenceBundle } from "../../../src/application/EvidenceBundleFiles.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { erlangBeamSchema } from "../../../src/domain/erlang/beam.js"; +import { evidenceSchema } from "../../../src/domain/evidence.js"; +import { digestCanonicalValue } from "../../../src/domain/canonicalDigest.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { + erlangBeamFixture, + erlangBeamChunksFixture, +} from "../../../src/erlang/Beam.fixture.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest, type SharedMcpCall } from "../../support/cli/cliFixture.js"; + +const contract = toolContract("inspect_erlang_beam"); +const completeSchema = contract.outputSchema.safeExtend({ + normalized_result: erlangBeamSchema, +}); +const viewContract = toolContract("inspect_analysis_view"); +const deliveredSchema = z.object({ summary: evidenceSchema }); + +const checkSelectors = async ( + call: SharedMcpCall, + evidence_id: string, +): Promise => { + for (const [view, items] of [ + [ + { collection: "exports", indices: [0], fields: [] }, + [{ source_index: 0 }], + ], + [{ collection: "exports", indices: [], fields: [] }, []], + [{ collection: "exports", names: [], fields: [] }, []], + [{ collection: "exports", names: ["missing"], fields: [] }, []], + [ + { + collection: "exports", + names: ["module_info"], + indices: [0], + fields: [], + }, + [], + ], + [{ collection: "exports", offset: 999, fields: [] }, []], + [ + { + collection: "atoms", + names: ["module_info"], + fields: ["index", "name"], + }, + [{ source_index: 1, index: 2, name: "module_info" }], + ], + [ + { + collection: "imports", + names: ["get_module_info"], + modules: ["erlang"], + }, + [ + { + source_index: 0, + module_index: 4, + module: "erlang", + function_index: 5, + function: "get_module_info", + arity: 1, + }, + ], + ], + [{ collection: "imports", modules: [], fields: [] }, []], + [ + { collection: "lambdas", fields: ["index", "old_unique"] }, + [{ source_index: 0, index: 0, old_unique: 42 }], + ], + [ + { collection: "chunks", names: ["Code"], fields: ["index", "name"] }, + [{ source_index: 1, index: 1, name: "Code" }], + ], + ] as const) { + const result = await call("inspect_analysis_view", { + source: { kind: "retained-evidence", evidence_id }, + view: { kind: "beam", ...view }, + }); + expect(result.isError, JSON.stringify(result)).not.toBe(true); + expect( + viewContract.outputSchema.parse(result.structuredContent) + .normalized_result, + ).toHaveProperty("page.items", items); + } +}; + +cliTest( + "queries same-name BEAM export arities through CLI/MCP after deleting the source", + { timeout: 60_000 }, + async ({ cli }) => { + const root = await createTestTempDirectory("rea-beam-views-"); + const path = join(root, "app.beam"); + await writeFile(path, erlangBeamFixture("views")); + const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli); + const complete = await run({ + arguments: ["inspect-erlang-beam", path, "--json"], + }); + expect(complete.exitCode, complete.stderr).toBe(0); + const parent = completeSchema.parse(complete.json); + const bundle = join(root, "source.bundle.json"); + const view = { + kind: "beam", + collection: "exports", + names: ["module_info"], + fields: ["function_index", "function", "arity", "label"], + offset: 0, + limit: 1, + }; + const directCli = await run({ + arguments: [ + "inspect-erlang-beam", + path, + "--detail", + "summary", + "--view", + JSON.stringify(view), + "--evidence_output", + bundle, + "--json", + ], + }); + expect(directCli.exitCode, directCli.stderr).toBe(0); + const delivered = deliveredSchema.parse(directCli.json).summary; + const saved = await readEvidenceBundle(bundle); + if (!saved.ok) throw saved.error; + expect(saved.value.records).toEqual([parent]); + const call = await cli.sharedMcpCall?.(); + if (call === undefined) + throw new Error("Expected production stdio MCP session"); + const direct = await call("inspect_erlang_beam", { + path, + detail: "summary", + view, + }); + expect(direct.isError).not.toBe(true); + const selected = contract.outputSchema.parse(direct.structuredContent); + expect(selected.normalized_result).toEqual(delivered.normalized_result); + expect(selected.evidence_links).toContain(parent.evidence_id); + expect(selected.normalized_result).toMatchObject({ + page: { + collection: "exports", + items: [ + { + source_index: 1, + function_index: 2, + function: "module_info", + arity: 0, + label: 1, + }, + ], + }, + beam: { + counts: { atoms: 5, exports: 3, imports: 1, lambdas: 1, chunks: 5 }, + coverage: { status: "complete", examined_chunks: 5 }, + }, + coverage: { examined: 1, total: 2, next_offset: 1, exhausted: false }, + }); + await rm(path); + const source = { + kind: "retained-evidence", + evidence_id: parent.evidence_id, + }; + const request = { source, view: { ...view, offset: 1 } }; + const second = await call("inspect_analysis_view", request); + expect(second.isError).not.toBe(true); + const secondEvidence = viewContract.outputSchema.parse( + second.structuredContent, + ); + const page = analysisViewResultSchema.parse( + secondEvidence.normalized_result, + ); + expect(page).toHaveProperty("page.items", [ + { + source_index: 2, + function_index: 2, + function: "module_info", + arity: 1, + label: 3, + }, + ]); + const { view_digest, ...content } = page; + expect(view_digest).toBe(digestCanonicalValue(content, "Analysis view")); + const offline = await run({ + arguments: [ + "inspect-analysis-view", + JSON.stringify(request), + "--evidence-bundle", + bundle, + "--json", + ], + }); + expect(offline.exitCode, offline.stderr).toBe(0); + expect(viewContract.outputSchema.parse(offline.json)).toEqual( + secondEvidence, + ); + const fromFile = await call("inspect_analysis_view", { + ...request, + source: { + kind: "evidence-file", + path: bundle, + evidence_id: parent.evidence_id, + }, + }); + expect(fromFile.isError).not.toBe(true); + expect( + viewContract.outputSchema.parse(fromFile.structuredContent) + .normalized_result, + ).toEqual(page); + await checkSelectors(call, parent.evidence_id); + }, +); + +cliTest( + "preserves missing tables, empty exports and unresolved atom references in BEAM views", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-beam-missing-tables-"); + const path = join(root, "app.beam"); + const imports = Buffer.alloc(16); + [1, 999, 998, 2].forEach((value, i) => imports.writeUInt32BE(value, i * 4)); + await writeFile( + path, + erlangBeamChunksFixture([ + ["ExpT", Buffer.alloc(4)], + ["ImpT", imports], + ["FunT", Buffer.alloc(0)], + ]), + ); + const call = await cli.sharedMcpCall?.(); + if (call === undefined) + throw new Error("Expected production stdio MCP session"); + const response = await call("inspect_erlang_beam", { + path, + detail: "summary", + }); + expect(response.isError).not.toBe(true); + const summary = contract.outputSchema.parse( + response.structuredContent, + ).normalized_result; + expect(summary).toMatchObject({ + summary: { + counts: { + atoms: null, + exports: 0, + imports: 1, + lambdas: null, + chunks: 3, + }, + module: null, + atom_encoding: null, + code: null, + }, + }); + if (!("parent_evidence_id" in summary)) + throw new Error("Expected summary view"); + await rm(path); + for (const [collection, items, count] of [ + [ + "imports", + [ + { + source_index: 0, + module_index: 999, + module: null, + function_index: 998, + function: null, + arity: 2, + }, + ], + 1, + ], + ["exports", [], 0], + ["lambdas", [], null], + ] as const) { + const result = await call("inspect_analysis_view", { + source: { + kind: "retained-evidence", + evidence_id: summary.parent_evidence_id, + }, + view: { kind: "beam", collection }, + }); + expect(result.isError).not.toBe(true); + const projected = viewContract.outputSchema.parse( + result.structuredContent, + ).normalized_result; + expect(projected).toHaveProperty("page.items", items); + expect(projected).toHaveProperty(`beam.counts.${collection}`, count); + expect(projected.unknowns).toContain( + "No atoms table was decoded; runtime absence is not established.", + ); + } + }, +); + +cliTest( + "rejects invalid BEAM delivery before reading the target", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-beam-invalid-view-"); + const output = join(root, "unused.json"); + const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli); + const view = JSON.stringify({ kind: "beam", collection: "exports" }); + for (const [flags, key] of [ + [["--detail", "summary"], "evidence_output"], + [ + ["--detail", "complete", "--view", view, "--evidence_output", output], + "view", + ], + [ + ["--detail", "summary", "--view", "{", "--evidence_output", output], + "view", + ], + ] as const) { + const result = await run({ + arguments: [ + "inspect-erlang-beam", + "/rea-missing/app.beam", + ...flags, + "--json", + ], + }); + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { issues: [{ path: [key] }] }, + }); + } + }, +); diff --git a/tests/acceptance/applications/exportedFunctionBindingsCli.test.ts b/tests/acceptance/applications/exportedFunctionBindingsCli.test.ts index 9a6d0cd66c..bc9a6b3ca5 100644 --- a/tests/acceptance/applications/exportedFunctionBindingsCli.test.ts +++ b/tests/acceptance/applications/exportedFunctionBindingsCli.test.ts @@ -15,9 +15,9 @@ import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/j import { javaScriptExportShapeComparisonResultSchema } from "../../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { - startCompiledCliSession, - type CompiledCliSession, -} from "../../support/cli/compiledCliSession.js"; + createCliCommandSession, + type CliCommandSession, +} from "../../support/cli/cliCommandSession.js"; const execute = promisify(execFile); const requireFixture = createRequire(import.meta.url); @@ -27,11 +27,10 @@ const comparisonEnvelope = z.object({ const applicationEnvelope = z.object({ normalized_result: javascriptApplicationAnalysisResultSchema, }); -let cliSession: CompiledCliSession | undefined; +let cliSession: CliCommandSession | undefined; -// Source semantics use one real stdio MCP session. Cases that also check the -// compiled CLI adapter share one warm process: it loads the production command -// runtime once and serves the same JSON commands as scripts/rea.mjs. +// Source semantics use one real stdio MCP session. CLI parity uses the shared +// file-scoped session, including its worker rotation and cleanup policy. let mcpConnection: | { readonly client: Client; @@ -1207,8 +1206,22 @@ const expectCountChange = ( }; const runCli = async (args: readonly string[]): Promise => { - cliSession ??= startCompiledCliSession(); - return cliSession.run(args); + cliSession ??= createCliCommandSession({ + startTimeoutMs: 20_000, + requestTimeoutMs: 20_000, + }); + const environment = Object.fromEntries( + Object.entries({ ...process.env, REA_LOG_LEVEL: "silent" }).filter( + (entry): entry is [string, string] => entry[1] !== undefined, + ), + ); + const result = await cliSession.run({ + arguments: args, + cwd: process.cwd(), + environment, + }); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + return result.json; }; const analyzeVersions = async ( diff --git a/tests/acceptance/applications/javascriptSourceSelectionCli.test.ts b/tests/acceptance/applications/javascriptSourceSelectionCli.test.ts index 1e26196c37..9fc6f49f6f 100644 --- a/tests/acceptance/applications/javascriptSourceSelectionCli.test.ts +++ b/tests/acceptance/applications/javascriptSourceSelectionCli.test.ts @@ -182,6 +182,135 @@ cliTest( }, ); +cliTest.for([ + { command: "analyze", source: "file", format: "asar" }, + { + command: "analyze-javascript-application", + source: "inline", + format: "directory", + }, +])( + "appends $source --source-paths-json selections for $format through $command", + { timeout: 60_000 }, + async ({ command, source, format }, { cli }) => { + const directory = await createTestTempDirectory( + "rea-js-source-paths-json-", + ); + await writeJavaScriptSourceSelectionFixture(directory); + let target = directory; + if (format === "asar") { + target = join( + await createTestTempDirectory("rea-js-json-selected-asar-"), + "app.asar", + ); + await createPackageWithOptions(directory, target, {}); + } + const listed = ["main.mjs", "package.json"]; + let sourcePathsJson = JSON.stringify(listed); + if (source === "file") { + sourcePathsJson = join( + await createTestTempDirectory("rea-js-source-paths-list-"), + "paths.json", + ); + await writeFile(sourcePathsJson, JSON.stringify(listed)); + } + const response = await cli.run({ + arguments: [ + command, + target, + "--source-path", + "package.json", + "--source-paths-json", + sourcePathsJson, + "--format", + "json", + ], + }); + expect(response.exitCode).toBe(0); + const evidence = parseEvidence(response.json); + const result = javascriptApplicationAnalysisResultSchema.parse( + evidence.normalized_result, + ); + const requested = ["package.json", ...listed]; + expect(result.source_selection).toEqual({ + requested_paths: requested, + unselected_files: 4, + }); + expect(evidence.parameters.source_paths).toEqual(requested); + expect(result.statistics.parsed_javascript_files).toBe(1); + if (format === "asar") { + expect(evidence.subject).toMatchObject({ + local_path: target, + digest: { + sha256: createHash("sha256") + .update(await readFile(target)) + .digest("hex"), + }, + }); + } + }, +); + +cliTest.for([ + { label: "an object", value: '{"paths":["main.mjs"]}', path: [] }, + { label: "an empty array", value: "[]", path: [] }, + { label: "a blank entry", value: '["main.mjs",""]', path: [1] }, +])( + "rejects --source-paths-json holding $label", + async ({ value, path }, { cli }) => { + const directory = await createTestTempDirectory( + "rea-js-source-paths-json-invalid-", + ); + await writeJavaScriptSourceSelectionFixture(directory); + const response = await cli.run({ + arguments: [ + "analyze-javascript-application", + directory, + "--source-paths-json", + value, + "--format", + "json", + ], + }); + expect(response.exitCode).toBe(1); + expect(response.json).toMatchObject({ + code: "invalid_request", + details: { + issues: [{ path: ["source_paths_json", ...path] }], + }, + }); + }, +); + +cliTest( + "rejects --source-paths-json when an explicit provider chooses the native lane", + async ({ cli }) => { + const directory = await createTestTempDirectory( + "rea-js-source-paths-json-native-refusal-", + ); + await writeJavaScriptSourceSelectionFixture(directory); + const response = await cli.run({ + arguments: [ + "analyze", + directory, + "--provider", + "ghidra", + "--source-paths-json", + '["main.mjs"]', + "--format", + "json", + ], + }); + expect(response.exitCode).toBe(1); + expect(response.json).toMatchObject({ + code: "invalid_request", + details: { + issues: [{ path: ["source_paths"], reason: "invalid_value" }], + }, + }); + }, +); + cliTest( "accepts legal ASAR JSON whitespace while preserving selected-source identity", { timeout: 60_000 }, diff --git a/tests/acceptance/applications/jvmAdmissionIdentity.test.ts b/tests/acceptance/applications/jvmAdmissionIdentity.test.ts new file mode 100644 index 0000000000..e13ec98933 --- /dev/null +++ b/tests/acceptance/applications/jvmAdmissionIdentity.test.ts @@ -0,0 +1,39 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { javaClassFile } from "../../fixtures/javaClassFile.js"; +import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest( + "refuses JVM CPU lookalikes consistently across the admission prefix through CLI and stdio MCP", + async ({ cli }) => { + const directory = await createTestTempDirectory("rea-jvm-admission-"); + const { call } = await connectLocalToolsMcp(); + for (const constantBytes of [4000, 9000]) { + const path = join(directory, `class-${constantBytes}.class`); + const bytes = javaClassFile(constantBytes, true); + await writeFile(path, bytes); + const refusal = { + code: "target_unavailable", + category: "unsupported_target", + details: { reason: "unsupported binary format" }, + }; + + const analyzed = await cli.run({ + arguments: ["analyze", path, "--format", "json"], + }); + expect(analyzed.exitCode, analyzed.stderr).toBe(1); + expect(analyzed.json).toMatchObject(refusal); + expect(await readFile(path)).toEqual(bytes); + + const opened = await call("open_binary", { path }); + expect(opened.isError, JSON.stringify(opened)).toBe(true); + expect(parseMcpToolError(opened).error).toMatchObject(refusal); + expect(await readFile(path)).toEqual(bytes); + } + }, +); diff --git a/tests/acceptance/applications/mobileDirectoryComponents.test.ts b/tests/acceptance/applications/mobileDirectoryComponents.test.ts index b1d20ec2a5..c01e330d74 100644 --- a/tests/acceptance/applications/mobileDirectoryComponents.test.ts +++ b/tests/acceptance/applications/mobileDirectoryComponents.test.ts @@ -211,3 +211,145 @@ cliTest.for(fixtures)( } }, ); + +cliTest.for([ + { + family: "Apple IPA", + extension: "ipa", + paths: ["Payload/Fixture.app/Info.plist"], + command: "project-apple-application-graph", + operation: "project_apple_application_graph", + }, + { + family: "Apple macOS bundle", + extension: "zip", + paths: [ + "Fixture.app/Contents/Info.plist", + "Fixture.app/Contents/_CodeSignature/CodeResources", + "Fixture.app/Contents/Frameworks/Core.framework/Versions/A/Resources/Info.plist", + ], + command: "project-apple-application-graph", + operation: "project_apple_application_graph", + }, + { + family: "Harmony Stage", + extension: "hap", + paths: ["module.json"], + command: "project-harmony-application-graph", + operation: "project_harmony_application_graph", + }, + { + family: "Harmony FA", + extension: "hsp", + paths: ["config.json"], + command: "project-harmony-application-graph", + operation: "project_harmony_application_graph", + }, + { + family: "Harmony App Pack", + extension: "app", + paths: ["entry.hap", "feature.hsp"], + command: "project-harmony-application-graph", + operation: "project_harmony_application_graph", + }, +])( + "preserves encrypted $family path facts through CLI and stdio MCP", + { timeout: 60_000 }, + async (fixture, { cli }) => { + const root = await createTestTempDirectory("rea-mobile-encrypted-paths-"); + const environment = { HOME: root, USERPROFILE: root }; + const { call } = await connectLocalToolsMcp(environment); + for (const encrypted of [false, true]) { + const writer = new ZipWriter(new Uint8ArrayWriter()); + for (const member of fixture.paths) + await writer.add(member, new TextReader("opaque fixture content"), { + ...(encrypted ? { password: "fixture-only" } : {}), + }); + const bytes = await writer.close(); + const path = join(root, `${encrypted}.${fixture.extension}`); + await writeFile(path, bytes); + const inspected = await cli.run({ + arguments: ["inspect-artifact", path, "--json"], + environment, + }); + expect(inspected.exitCode, inspected.stderr).toBe(0); + const inventory = parseEvidence(inspected.json); + const input = { inventory_evidence: [inventory] }; + const projected = await cli.run({ + arguments: [fixture.command, JSON.stringify(input), "--json"], + environment, + }); + expect(projected.exitCode, projected.stderr).toBe(0); + const evidence = parseEvidence(projected.json); + const mcp = await call(fixture.operation, input); + expect(mcp.isError, JSON.stringify(mcp)).not.toBe(true); + expect(parseEvidence(mcp.structuredContent)).toEqual(evidence); + const graph = + fixture.operation === "project_apple_application_graph" + ? appleApplicationProjectionResultSchema.parse( + evidence.normalized_result, + ) + : harmonyApplicationProjectionResultSchema.parse( + evidence.normalized_result, + ); + expect(graph.source_evidence_ids).toEqual([inventory.evidence_id]); + expect(graph.coverage).toEqual({ + status: encrypted ? "partial" : "complete-within-inventory", + inventory_complete: true, + }); + expect( + graph.unavailable_components.map(({ path }) => path).sort(), + ).toEqual(encrypted ? [...fixture.paths].sort() : []); + for (const component of graph.unavailable_components) + expect(component).toMatchObject({ + artifact_id: null, + sha256: null, + encrypted: true, + }); + expect(graph.runtime_families).toEqual([]); + expect(graph.bridge_candidates).toEqual([]); + if ("bundles" in graph) { + expect(graph.platforms).toEqual([ + fixture.extension === "ipa" ? "ios" : "macos", + ]); + expect( + graph.bundles.map(({ info_plist_path }) => info_plist_path).sort(), + ).toEqual( + fixture.paths + .filter((member) => member.endsWith("Info.plist")) + .sort(), + ); + expect( + graph.bundles.flatMap(({ signing_paths }) => signing_paths), + ).toEqual( + fixture.paths.filter((member) => member.endsWith("CodeResources")), + ); + expect( + graph.bundles.flatMap( + ({ executable_candidates }) => executable_candidates, + ), + ).toEqual([]); + } else { + expect(graph.packaging_model).toBe( + fixture.extension === "hap" + ? "stage" + : fixture.extension === "hsp" + ? "fa" + : "unknown", + ); + expect(graph.app_pack_children.map(({ path }) => path)).toEqual( + fixture.extension === "app" ? fixture.paths : [], + ); + for (const child of graph.app_pack_children) + if (encrypted) + expect(child).toMatchObject({ artifact_id: null, sha256: null }); + else { + expect(child.artifact_id).toMatch(/^art_/u); + expect(child.sha256).toMatch(/^[a-f0-9]{64}$/u); + } + } + if (encrypted) expect(Object.values(graph.components).flat()).toEqual([]); + expect(await readFile(path)).toEqual(Buffer.from(bytes)); + } + }, +); diff --git a/tests/acceptance/applications/nativePlistNesting.test.ts b/tests/acceptance/applications/nativePlistNesting.test.ts index 6515819ff6..a7f4488fcc 100644 --- a/tests/acceptance/applications/nativePlistNesting.test.ts +++ b/tests/acceptance/applications/nativePlistNesting.test.ts @@ -1,3 +1,4 @@ +import { execFile } from "node:child_process"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { buildBinary, type PlistValue } from "plist"; @@ -11,6 +12,25 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; +const lintXmlPlist = (path: string) => + new Promise<{ exitCode: number; stdout: string; stderr: string }>( + (resolve, reject) => { + execFile( + "/usr/bin/plutil", + ["-lint", "--", path], + { timeout: 10_000 }, + (error, stdout, stderr) => { + const exitCode = error === null ? 0 : error.code; + if (typeof exitCode !== "number") { + reject(error); + return; + } + resolve({ exitCode, stdout, stderr }); + }, + ); + }, + ); + describe.skipIf(process.platform !== "darwin")( "ordinary plist source nesting", () => { @@ -34,6 +54,7 @@ describe.skipIf(process.platform !== "darwin")( { format: "binary", depth: 129, data: true }, { format: "xml", depth: 129, data: true }, { format: "binary", depth: 200, data: false }, + { format: "xml", depth: 200, data: false }, { format: "json", depth: 200, data: false }, { format: "binary", depth: 998, data: true }, { format: "binary", depth: 999, data: true }, @@ -53,9 +74,16 @@ describe.skipIf(process.platform !== "darwin")( : Buffer.from( format === "json" ? JSON.stringify(value) - : `${"level".repeat(depth)}AAE=${"".repeat(depth)}`, + : `${"level".repeat(depth)}${data ? "AAE=" : "1"}${"".repeat(depth)}`, ); await writeFile(path, bytes); + // The decoder ceiling does not override native XML admission. + // Compare its upper-edge admission with the actual utility, while + // requiring the historical depth-129/200 controls to succeed below. + const native = + format === "xml" && depth === 998 + ? await lintXmlPlist(path) + : undefined; const output = await cli.run({ arguments: ["inspect-plist", path, "--json"], environment, @@ -83,6 +111,26 @@ describe.skipIf(process.platform !== "darwin")( ); continue; } + if (native !== undefined && native.exitCode !== 0) { + expect(output.exitCode, output.stderr).toBe(1); + const expectedFailure = { + code: "invalid_request", + retryable: false, + details: { + operation: "inspect_plist", + captured_output: { + exit_code: native.exitCode, + stdout: native.stdout, + stderr: native.stderr, + }, + }, + }; + expect(output.json).toMatchObject(expectedFailure); + expect(parseMcpToolError(response).error).toMatchObject( + expectedFailure, + ); + continue; + } expect( output.exitCode, JSON.stringify({ @@ -105,6 +153,7 @@ describe.skipIf(process.platform !== "darwin")( expect(cliResult.format).toBe(format === "json" ? "unknown" : format); expect(cliResult.provenance.map((item) => item.tool)).toEqual([ "file", + ...(format === "xml" ? ["plutil"] : []), ]); let leaf: unknown = cliResult.value; for (let level = 0; level < depth; level += 1) { diff --git a/tests/acceptance/applications/webpackRenamedExportsCli.test.ts b/tests/acceptance/applications/webpackRenamedExportsCli.test.ts index 33a1bd56c4..6b5693ea99 100644 --- a/tests/acceptance/applications/webpackRenamedExportsCli.test.ts +++ b/tests/acceptance/applications/webpackRenamedExportsCli.test.ts @@ -3,14 +3,13 @@ import { join } from "node:path"; import { expect } from "vitest"; -import { parseEvidence } from "../../../src/domain/evidence.js"; -import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; +import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js"; -cliTest( - "reports Webpack exports attached to renamed factory parameters", - async ({ cli }) => { +cliTest.for(["CLI", "stdio MCP"] as const)( + "reports Webpack exports attached to original renamed factory parameter values through %s", + async (surface, { cli }) => { const root = await createTestTempDirectory("rea-webpack-renamed-exports-"); await mkdir(join(root, "src")); await writeFile( @@ -21,20 +20,24 @@ cliTest( Object.defineProperty(__webpack_exports__, "delta", { enumerable: true, get() { return 3; } }); __webpack_require__.d(__webpack_exports__.nested, { nested_target: () => 0 }); __webpack_exports__ = { rebound: () => 0 }; + __webpack_exports__.ghost = 4; + Object.defineProperty(__webpack_exports__, "definedGhost", {value: 5}); + __webpack_require__.d(__webpack_exports__, { declaredGhost: () => 6 }); + module.exports.moduleBefore = 7; + module = {exports: {}}; + module.exports.moduleGhost = 8; __webpack_require__.d({}, { unrelated: () => 0 }); { const exports = {}; __webpack_require__.d(exports, { shadowed_exports: () => 5 }); } }, - 23(m, e, r) { r.d(e, { alpha: () => 1 }); } + 23(m, e, r) { + r.d(e, { alpha: () => 1 }); + r = {d() {}}; + r.d(e, { loaderGhost: () => 2 }); + e.afterLoaderReplacement = 3; + } }]);`, ); - const response = await cli.run({ - arguments: ["analyze-javascript-application", root, "--format", "json"], - environment: { REA_LOG_LEVEL: "silent" }, - }); - expect(response.exitCode).toBe(0); - const result = javascriptApplicationAnalysisResultSchema.parse( - parseEvidence(response.json).normalized_result, - ); + const result = await analyzeJavaScriptDirectory(surface, cli, root); const modules = result.graph.nodes .filter(({ kind }) => kind === "javascript-module") .map(({ observations }) => observations[0]?.properties); @@ -42,9 +45,12 @@ cliTest( expect.arrayContaining([ expect.objectContaining({ module_key: "17", - exports: ["alpha", "beta.gamma", "delta"], + exports: ["alpha", "beta.gamma", "delta", "moduleBefore"], + }), + expect.objectContaining({ + module_key: "23", + exports: ["afterLoaderReplacement", "alpha"], }), - expect.objectContaining({ module_key: "23", exports: ["alpha"] }), ]), ); expect(result.graph.coverage.status).toBe("complete"); diff --git a/tests/acceptance/erlangBeamCli.test.ts b/tests/acceptance/erlangBeamCli.test.ts index d7cc3c040c..e0fbf2ba16 100644 --- a/tests/acceptance/erlangBeamCli.test.ts +++ b/tests/acceptance/erlangBeamCli.test.ts @@ -1,3 +1,4 @@ +import { erlangBeamSchema } from "../../src/domain/erlang/beam.js"; import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -13,6 +14,10 @@ import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { cliTest } from "../support/cli/cliFixture.js"; +const completeBeamEvidenceSchema = toolContract( + "inspect_erlang_beam", +).outputSchema.safeExtend({ normalized_result: erlangBeamSchema }); + const oneAtomBeam = (paddingByte: number): Buffer => { const bytes = Buffer.alloc(28); bytes.write("FOR1", 0, "latin1"); @@ -65,9 +70,7 @@ cliTest("reports BEAM exports through the public CLI", async ({ cli }) => { arguments: ["inspect-erlang-beam", path, "--json"], }); expect(result.exitCode, result.stderr).toBe(0); - const evidence = toolContract("inspect_erlang_beam").outputSchema.parse( - result.json, - ); + const evidence = completeBeamEvidenceSchema.parse(result.json); expect(evidence.normalized_result.module).toBe("cli_mod"); expect( evidence.normalized_result.exports?.some( @@ -118,15 +121,14 @@ cliTest( arguments: ["inspect-erlang-beam", path, "--json"], }); expect(output.exitCode, output.stderr).toBe(0); - const report = toolContract("inspect_erlang_beam").outputSchema.parse( + const report = completeBeamEvidenceSchema.parse( output.json, ).normalized_result; const reply = await call("inspect_erlang_beam", { path }); expect(reply.isError, JSON.stringify(reply)).not.toBe(true); expect( - toolContract("inspect_erlang_beam").outputSchema.parse( - reply.structuredContent, - ).normalized_result, + completeBeamEvidenceSchema.parse(reply.structuredContent) + .normalized_result, ).toEqual(report); expect(report).toMatchObject({ module, @@ -174,7 +176,7 @@ cliTest( arguments: ["inspect-erlang-beam", path, "--json"], }); expect(result.exitCode, result.stderr).toBe(0); - const report = toolContract("inspect_erlang_beam").outputSchema.parse( + const report = completeBeamEvidenceSchema.parse( result.json, ).normalized_result; expect(report.module).toBe("rea_beam_fixture"); @@ -211,9 +213,7 @@ cliTest( arguments: ["inspect-erlang-beam", path, "--json"], }); expect(result.exitCode, result.stderr).toBe(0); - return toolContract("inspect_erlang_beam").outputSchema.parse( - result.json, - ); + return completeBeamEvidenceSchema.parse(result.json); }; const control = await inspect(controlPath); const extended = await inspect(extendedPath); @@ -253,9 +253,7 @@ cliTest( arguments: ["inspect-erlang-beam", path, "--json"], }); expect(result.exitCode, result.stderr).toBe(0); - return toolContract("inspect_erlang_beam").outputSchema.parse( - result.json, - ); + return completeBeamEvidenceSchema.parse(result.json); }; const clean = await inspect(cleanPath); const padded = await inspect(paddedPath); @@ -288,9 +286,7 @@ cliTest( arguments: ["inspect-erlang-beam", path, "--json"], }); expect(result.exitCode, result.stderr).toBe(0); - return toolContract("inspect_erlang_beam").outputSchema.parse( - result.json, - ); + return completeBeamEvidenceSchema.parse(result.json); }; const control = await inspect(controlPath); const trailing = await inspect(trailingPath); @@ -344,9 +340,7 @@ cliTest( cwd: root, }); expect(result.exitCode, result.stderr).toBe(0); - const evidence = toolContract("inspect_erlang_beam").outputSchema.parse( - result.json, - ); + const evidence = completeBeamEvidenceSchema.parse(result.json); expect(evidence.subject?.local_path).toBe(join(root, "app.beam")); }, ); diff --git a/tests/acceptance/peImportsCli.test.ts b/tests/acceptance/peImportsCli.test.ts new file mode 100644 index 0000000000..84c3fcfe2f --- /dev/null +++ b/tests/acceptance/peImportsCli.test.ts @@ -0,0 +1,191 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { toolContract } from "../../src/contracts/toolContracts.js"; +import { + peImportImage, + peImportFixtureOffset, + peImportLeadingSectionImage, + peImportManySectionsImage, + peImportDirectoryAtRva, + peImportDirectoryMappingStops, + peWithoutImportTableFixture, +} from "../fixtures/binaryDiagnostics/peImports.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { cliTest } from "../support/cli/cliFixture.js"; + +cliTest("reports PE import names through the public CLI", async ({ cli }) => { + const root = await createTestTempDirectory("rea-pe-imports-cli-"); + const path = join(root, "app.exe"); + await writeFile(path, peImportImage()); + const result = await cli.run({ + arguments: ["inspect-pe-imports", path, "--json"], + }); + expect(result.exitCode, result.stderr).toBe(0); + const evidence = toolContract("inspect_pe_imports").outputSchema.parse( + result.json, + ); + expect(evidence.normalized_result.imports[0]?.name).toBe("KERNEL32.dll"); + expect(evidence.normalized_result.imphash.md5).toBe( + "92cef6cc91a85825f3c8a0516517c66f", + ); +}); + +cliTest( + "resolves a relative PE path against the working directory", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-pe-imports-cli-rel-"); + await writeFile(join(root, "unsigned.exe"), peWithoutImportTableFixture()); + const result = await cli.run({ + arguments: ["inspect-pe-imports", "unsigned.exe", "--json"], + cwd: root, + }); + expect(result.exitCode, result.stderr).toBe(0); + const evidence = toolContract("inspect_pe_imports").outputSchema.parse( + result.json, + ); + expect(evidence.normalized_result.import_directory).toBeNull(); + expect(evidence.subject?.local_path).toBe(join(root, "unsigned.exe")); + }, +); + +cliTest("reports a truncated PE as invalid input", async ({ cli }) => { + const root = await createTestTempDirectory("rea-pe-imports-cli-bad-"); + const path = join(root, "broken.exe"); + await writeFile(path, Buffer.from("MZ")); + const result = await cli.run({ + arguments: ["inspect-pe-imports", path, "--json"], + }); + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { operation: "inspect_pe_imports" }, + }); +}); + +cliTest( + "preserves directory declarations and partial rows at RVA and mapping boundaries", + async ({ cli }) => { + const root = await createTestTempDirectory( + "rea-pe-imports-cli-directory-rva-", + ); + for (const [kind, rva, retained, mappingGap] of [ + ["standard", 0xffffffec, 1, false], + ["standard", 0xfffffff0, 0, false], + ["delay", 0xffffffe0, 1, false], + ["delay", 0xfffffff0, 0, false], + ["standard", 0x40000000, 0, true], + ["standard", 0x40000000, 1, true], + ["delay", 0x40000000, 0, true], + ["delay", 0x40000000, 1, true], + ] as const) { + const path = join(root, `${kind}-${String(rva)}-${String(retained)}.exe`); + await writeFile( + path, + mappingGap + ? peImportDirectoryMappingStops(kind, retained) + : peImportDirectoryAtRva(kind, rva), + ); + const result = await cli.run({ + arguments: ["inspect-pe-imports", path, "--json"], + }); + expect(result.exitCode, result.stderr).toBe(0); + const report = toolContract("inspect_pe_imports").outputSchema.parse( + result.json, + ).normalized_result; + const directory = + kind === "standard" + ? report.import_directory + : report.delay_import_directory; + const rows = kind === "standard" ? report.imports : report.delay_imports; + expect(directory).toMatchObject({ rva }); + expect(directory?.locations).toHaveLength(retained); + expect(rows).toHaveLength(retained); + expect(report.coverage.status).toBe("partial"); + expect(report.imphash.md5).toBe( + kind === "delay" ? "92cef6cc91a85825f3c8a0516517c66f" : null, + ); + } + }, +); + +cliTest( + "retains a partial inventory when an IAT RVA overflows", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-pe-imports-cli-overflow-"); + for (const bits of [32, 64] as const) { + const bytes = peImportImage({ bits }); + bytes.writeUInt32LE( + 0x100000000 - bits / 8, + peImportFixtureOffset(0x1000) + 16, + ); + const path = join(root, `app-${String(bits)}.exe`); + await writeFile(path, bytes); + const result = await cli.run({ + arguments: ["inspect-pe-imports", path, "--json"], + }); + expect(result.exitCode, result.stderr).toBe(0); + const report = toolContract("inspect_pe_imports").outputSchema.parse( + result.json, + ).normalized_result; + expect(report.imports[0]?.functions.map((row) => row.name)).toEqual([ + "GetProcAddress", + ]); + expect(report.coverage.status).toBe("partial"); + expect(report.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + code: "malformed_entry", + location: "0x100000000", + }), + ]), + ); + expect(report.imphash.md5).toBeNull(); + } + }, +); + +cliTest( + "keeps complete facts when pefile cannot see the same import table", + async ({ cli }) => { + const root = await createTestTempDirectory( + "rea-pe-imports-cli-compatibility-", + ); + const functions = Array.from({ length: 140 }, (_, i) => ({ + name: `Function${String(i).padStart(3, "0")}`, + })); + const cases = [ + [ + "cross-section", + peImportImage({ + sectionBytes: 0x4000, + splitAt: 0x1000, + dlls: [{ name: "KERNEL32.dll", functions }], + }), + 140, + ], + ["section-cap", peImportManySectionsImage(2049, 2), 2], + ["null-section", peImportLeadingSectionImage(false), 2], + ] as const; + for (const [name, bytes, count] of cases) { + const path = join(root, `${name}.exe`); + await writeFile(path, bytes); + const result = await cli.run({ + arguments: ["inspect-pe-imports", path, "--json"], + }); + expect(result.exitCode, result.stderr).toBe(0); + const report = toolContract("inspect_pe_imports").outputSchema.parse( + result.json, + ).normalized_result; + expect(report.imports[0]?.name).toBe("KERNEL32.dll"); + expect(report.imports[0]?.functions).toHaveLength(count); + expect(report.coverage.status).toBe("complete"); + expect(report.imphash).toMatchObject({ + md5: null, + reason: expect.stringMatching(/mapping|compatibility/u), + }); + if (name === "cross-section") + expect(report.imports[0]?.functions[128]?.name).toBe("Function128"); + } + }, +); diff --git a/tests/acceptance/process/processFinalization.test.ts b/tests/acceptance/process/processFinalization.test.ts index 7dcb4e969e..dc35f2d829 100644 --- a/tests/acceptance/process/processFinalization.test.ts +++ b/tests/acceptance/process/processFinalization.test.ts @@ -14,15 +14,19 @@ import { type ProcessCapture, } from "../../../src/domain/process/processCaptureParsing.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest, type TestCli } from "../../support/cli/cliFixture.js"; import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { itWithCaptureCapability as captureTest } from "../../boundary/process/processCaptureCapability.js"; +import { + CAPTURE_SKIP_REASON, + itWithCaptureCapability as captureTest, +} from "../../boundary/process/processCaptureCapability.js"; import { expectSerializedUnverifiedHostCleanup } from "../../support/hostCleanup.js"; const finalizationFixture = fileURLToPath( new URL("../../fixtures/processFinalization.mjs", import.meta.url), ); +const cliCaptureTest = cliTest.skipIf(CAPTURE_SKIP_REASON); type CaptureScenario = Record; type CaptureOutcome = @@ -49,31 +53,35 @@ const captureScenario = ( }); const captureViaCli = async ( + cli: TestCli, root: string, scenario: CaptureScenario, ): Promise => { const scenarioPath = join(root, "scenario.json"); await writeFile(scenarioPath, JSON.stringify(scenario)); - const document = await runCliJson([ - "capture-process", - scenarioPath, - "--format", - "json", - ]); + const result = await cli.run({ + arguments: ["capture-process", scenarioPath, "--format", "json"], + environment: process.env, + }); + expect(result.signal, result.stderr).toBeNull(); + const document = result.json; if ( typeof document === "object" && document !== null && "code" in document && document.code === "cleanup_incomplete" - ) + ) { + expect(result.exitCode, result.stderr).toBe(1); return { kind: "partial", partial: expectSerializedUnverifiedHostCleanup({ error: document }), }; + } if (typeof document === "object" && document !== null && "code" in document) throw new Error( `CLI capture returned an unexpected error: ${JSON.stringify(document).slice(0, 1_000)}`, ); + expect(result.exitCode, result.stderr).toBe(0); return { kind: "capture", capture: parseProcessCapture(parseEvidence(document).normalized_result), @@ -261,9 +269,10 @@ const expectIgnoringFinalization = (outcome: CaptureOutcome): void => { ).toBeUndefined(); }; -captureTest.each(["CLI", "MCP"] as const)( +cliCaptureTest.for(["CLI", "MCP"] as const)( "captures a cooperative finalizer through the compiled %s workflow", - async (adapter) => { + { timeout: 20_000 }, + async (adapter, { cli }) => { const root = await createTestTempDirectory("rea-finalization-accept-"); const scenario = captureScenario("cooperative", root, { timeout_ms: 1_500, @@ -271,16 +280,16 @@ captureTest.each(["CLI", "MCP"] as const)( }); const outcome = adapter === "CLI" - ? await captureViaCli(root, scenario) + ? await captureViaCli(cli, root, scenario) : await captureViaMcp(scenario); expectCooperativeFinalization(outcome); }, - 20_000, ); -captureTest.each(["CLI", "MCP"] as const)( +cliCaptureTest.for(["CLI", "MCP"] as const)( "records the non-zero exit code of a finalizer through the compiled %s workflow", - async (adapter) => { + { timeout: 20_000 }, + async (adapter, { cli }) => { const root = await createTestTempDirectory("rea-finalization-accept-"); const scenario = captureScenario( "cooperative", @@ -290,16 +299,16 @@ captureTest.each(["CLI", "MCP"] as const)( ); const outcome = adapter === "CLI" - ? await captureViaCli(root, scenario) + ? await captureViaCli(cli, root, scenario) : await captureViaMcp(scenario); expectCooperativeFinalization(outcome, 3); }, - 20_000, ); -captureTest.each(["CLI", "MCP"] as const)( +cliCaptureTest.for(["CLI", "MCP"] as const)( "forces SIGKILL after the finalization interval through the compiled %s workflow", - async (adapter) => { + { timeout: 20_000 }, + async (adapter, { cli }) => { const root = await createTestTempDirectory("rea-finalization-accept-"); const scenario = captureScenario("ignoring", root, { timeout_ms: 1_500, @@ -307,11 +316,10 @@ captureTest.each(["CLI", "MCP"] as const)( }); const outcome = adapter === "CLI" - ? await captureViaCli(root, scenario) + ? await captureViaCli(cli, root, scenario) : await captureViaMcp(scenario); expectIgnoringFinalization(outcome); }, - 20_000, ); captureTest( @@ -376,14 +384,15 @@ captureTest( 20_000, ); -captureTest.each(["CLI", "MCP"] as const)( +cliCaptureTest.for(["CLI", "MCP"] as const)( "keeps the default identity through the compiled %s workflow without finalization_ms", - async (adapter) => { + { timeout: 20_000 }, + async (adapter, { cli }) => { const root = await createTestTempDirectory("rea-finalization-default-"); const scenario = captureScenario("ignoring", root, { timeout_ms: 500 }); const outcome = adapter === "CLI" - ? await captureViaCli(root, scenario) + ? await captureViaCli(cli, root, scenario) : await captureViaMcp(scenario); if (outcome.kind === "partial") { expect( @@ -405,12 +414,11 @@ captureTest.each(["CLI", "MCP"] as const)( "the comparison contract keeps its legacy identity", ).not.toHaveProperty("finalization_ms"); }, - 20_000, ); -captureTest.each(["CLI", "MCP"] as const)( +cliCaptureTest.for(["CLI", "MCP"] as const)( "rejects an unrepresentable combined capture budget through the compiled %s workflow", - async (adapter) => { + async (adapter, { cli }) => { const root = await createTestTempDirectory("rea-finalization-budget-"); const scenario = captureScenario("cooperative", root, { timeout_ms: Number.MAX_SAFE_INTEGER, @@ -420,14 +428,14 @@ captureTest.each(["CLI", "MCP"] as const)( if (adapter === "CLI") { const scenarioPath = join(root, "scenario.json"); await writeFile(scenarioPath, JSON.stringify(scenario)); - const response = await runCliJson([ - "capture-process", - scenarioPath, - "--format", - "json", - ]); + const response = await cli.run({ + arguments: ["capture-process", scenarioPath, "--format", "json"], + environment: process.env, + }); + expect(response.exitCode, response.stderr).toBe(1); + expect(response.signal, response.stderr).toBeNull(); expect( - JSON.stringify(response), + JSON.stringify(response.json), "CLI rejects the combined budget", ).toContain("timeout_ms + finalization_ms + settle_ms"); } else { diff --git a/tests/acceptance/process/processSignalExit.test.ts b/tests/acceptance/process/processSignalExit.test.ts index 8a4b24c2c8..6ea4499395 100644 --- a/tests/acceptance/process/processSignalExit.test.ts +++ b/tests/acceptance/process/processSignalExit.test.ts @@ -5,16 +5,20 @@ import { expect } from "vitest"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { parseProcessCapture } from "../../../src/domain/process/processCaptureParsing.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { itWithCaptureCapability as captureTest } from "../../boundary/process/processCaptureCapability.js"; +import { + CAPTURE_SKIP_REASON, + itWithCaptureCapability as captureTest, +} from "../../boundary/process/processCaptureCapability.js"; const hang = ["-e", "setTimeout(() => {}, 5000)"]; +const cliCaptureTest = cliTest.skipIf(CAPTURE_SKIP_REASON); -captureTest( +cliCaptureTest( "records no normal exit code for a target a scenario signal terminated", - async () => { + async ({ cli }) => { const root = await createTestTempDirectory("rea-process-signal-exit-"); for (const [name, additions, expected] of [ [ @@ -50,14 +54,14 @@ captureTest( ...additions, }), ); - const document = await runCliJson([ - "capture-process", - scenarioPath, - "--format", - "json", - ]); + const result = await cli.run({ + arguments: ["capture-process", scenarioPath, "--format", "json"], + environment: process.env, + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.signal, result.stderr).toBeNull(); const capture = parseProcessCapture( - parseEvidence(document).normalized_result, + parseEvidence(result.json).normalized_result, ); expect(capture.exit, name).toMatchObject(expected); } diff --git a/tests/acceptance/tools/localToolContracts.test.ts b/tests/acceptance/tools/localToolContracts.test.ts index 6cbb5d9c93..acf88f37e0 100644 --- a/tests/acceptance/tools/localToolContracts.test.ts +++ b/tests/acceptance/tools/localToolContracts.test.ts @@ -219,6 +219,7 @@ const pathRequests: readonly [string, Record][] = [ ], ["inspect_pe_resources", { path: nul }], ["inspect_pe_certificates", { path: nul }], + ["inspect_pe_imports", { path: nul }], ["inspect_native_exports", { path: nul }], ["inspect_elf_notes", { path: nul }], ["inspect_python_pyc", { path: nul }], diff --git a/tests/boundary/browser/cdpBrowserProvider.targetInventory.test.ts b/tests/boundary/browser/cdpBrowserProvider.targetInventory.test.ts index 9f3c3153ac..77da640c8f 100644 --- a/tests/boundary/browser/cdpBrowserProvider.targetInventory.test.ts +++ b/tests/boundary/browser/cdpBrowserProvider.targetInventory.test.ts @@ -1,93 +1,102 @@ import { createServer } from "node:http"; -import { describe, expect, it } from "vitest"; +import { describe, expect } from "vitest"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { browserTargetListSchema } from "../../../src/domain/browserObservationSchemas.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; describe("CDP target inventory", () => { - it("returns complete target inventory and original URL paths through the CLI", async () => { - let port = 0; - const targets = Array.from({ length: 1_101 }, (_, index) => { - const id = `target-${String(index).padStart(4, "0")}`; - return { - id, - type: "page", - title: `Page ${String(index)}`, - url: - index === 0 - ? String.raw`https://user:pass@allowed.example\path@tail/../keep?x=1#frag` - : `https://allowed.example/page/${String(index)}`, - ...(index === 0 ? {} : { attached: index === 1 ? false : true }), - webSocketDebuggerUrl: `ws://127.0.0.1:${String(port)}/devtools/page/${id}`, - }; - }); - const server = createServer((request, response) => { - response.setHeader("content-type", "application/json"); - if (request.url === "/json/version") { - response.end( - JSON.stringify({ - Browser: "Chrome/1", - "Protocol-Version": "1.3", - "User-Agent": "Chrome", - "V8-Version": "1", - "WebKit-Version": "1", - webSocketDebuggerUrl: `ws://127.0.0.1:${String(port)}/devtools/browser/browser`, - }), - ); - return; - } - if (request.url === "/json/list") { - response.end(JSON.stringify(targets)); - return; - } - response.writeHead(404).end(); - }); - - try { - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", resolve); + cliTest( + "returns complete target inventory and original URL paths through the CLI", + async ({ cli }) => { + let port = 0; + const targets = Array.from({ length: 1_101 }, (_, index) => { + const id = `target-${String(index).padStart(4, "0")}`; + return { + id, + type: "page", + title: `Page ${String(index)}`, + url: + index === 0 + ? String.raw`https://user:pass@allowed.example\path@tail/../keep?x=1#frag` + : `https://allowed.example/page/${String(index)}`, + ...(index === 0 ? {} : { attached: index === 1 ? false : true }), + webSocketDebuggerUrl: `ws://127.0.0.1:${String(port)}/devtools/page/${id}`, + }; }); - const address = server.address(); - if (address === null || typeof address === "string") - throw new TypeError("Expected a TCP listener address"); - port = address.port; - const endpoint = `http://127.0.0.1:${String(port)}`; - - const evidence = parseEvidence( - await runCliJson([ - "list-browser-targets", - endpoint, - "--allowed-origins", - "https://allowed.example", - "--json", - ]), - ); - const result = browserTargetListSchema.parse(evidence.normalized_result); - - expect(result.targets).toHaveLength(targets.length); - expect(result.targets[0]?.target_id).toBe("target-0000"); - expect(result.targets[0]?.url).toBe( - String.raw`https://allowed.example\path@tail/../keep?x=1#frag`, - ); - expect(result.targets[0]?.origin).toBe("https://allowed.example"); - expect(JSON.stringify(evidence)).not.toContain("user:pass"); - expect(result.targets.at(-1)?.target_id).toBe("target-1100"); - expect( - result.targets.slice(0, 3).map(({ attached }) => attached), - ).toEqual([null, false, true]); - expect( - result.limitations.some((item) => item.includes("attachment state")), - ).toBe(true); - } finally { - if (server.listening) - await new Promise((resolve, reject) => { - server.close((error) => - error === undefined ? resolve() : reject(error), + const server = createServer((request, response) => { + response.setHeader("content-type", "application/json"); + if (request.url === "/json/version") { + response.end( + JSON.stringify({ + Browser: "Chrome/1", + "Protocol-Version": "1.3", + "User-Agent": "Chrome", + "V8-Version": "1", + "WebKit-Version": "1", + webSocketDebuggerUrl: `ws://127.0.0.1:${String(port)}/devtools/browser/browser`, + }), ); + return; + } + if (request.url === "/json/list") { + response.end(JSON.stringify(targets)); + return; + } + response.writeHead(404).end(); + }); + + try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); }); - } - }); + const address = server.address(); + if (address === null || typeof address === "string") + throw new TypeError("Expected a TCP listener address"); + port = address.port; + const endpoint = `http://127.0.0.1:${String(port)}`; + + const cliResult = await cli.run({ + arguments: [ + "list-browser-targets", + endpoint, + "--allowed-origins", + "https://allowed.example", + "--json", + ], + environment: process.env, + }); + expect(cliResult.exitCode).toBe(0); + expect(cliResult.signal).toBeNull(); + const evidence = parseEvidence(cliResult.json); + const result = browserTargetListSchema.parse( + evidence.normalized_result, + ); + + expect(result.targets).toHaveLength(targets.length); + expect(result.targets[0]?.target_id).toBe("target-0000"); + expect(result.targets[0]?.url).toBe( + String.raw`https://allowed.example\path@tail/../keep?x=1#frag`, + ); + expect(result.targets[0]?.origin).toBe("https://allowed.example"); + expect(JSON.stringify(evidence)).not.toContain("user:pass"); + expect(result.targets.at(-1)?.target_id).toBe("target-1100"); + expect( + result.targets.slice(0, 3).map(({ attached }) => attached), + ).toEqual([null, false, true]); + expect( + result.limitations.some((item) => item.includes("attachment state")), + ).toBe(true); + } finally { + if (server.listening) + await new Promise((resolve, reject) => { + server.close((error) => + error === undefined ? resolve() : reject(error), + ); + }); + } + }, + ); }); diff --git a/tests/boundary/browser/cdpDomComparisonCompleteness.test.ts b/tests/boundary/browser/cdpDomComparisonCompleteness.test.ts index d8c5386c32..72329b2852 100644 --- a/tests/boundary/browser/cdpDomComparisonCompleteness.test.ts +++ b/tests/boundary/browser/cdpDomComparisonCompleteness.test.ts @@ -9,7 +9,7 @@ import { compareWebCaptures } from "../../../src/domain/webCaptureDiff.js"; import { compareWebCapturesInputSchema } from "../../../src/domain/webCaptureDiffSchemas.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; import { trackBrowser } from "./cdpBrowserProvider.support.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; type SnapshotMode = @@ -228,39 +228,50 @@ describe("complete frame and DOM comparison", () => { }); describe("DOM evidence through the public CLI", () => { - it("preserves omissions and independently proven frame changes", async () => { - const { state, inspect } = await startBrowser(); - const root = await createTestTempDirectory("rea-dom-comparison-"); - const before = join(root, "before.json"); - const after = join(root, "after.json"); - await writeFile(before, JSON.stringify({ inspection: await inspect() })); - state.mode = "omitted-node"; - for (const nested of [false, true]) { - state.nested = nested; + cliTest( + "preserves omissions and independently proven frame changes", + async ({ cli }) => { + const { state, inspect } = await startBrowser(); + const root = await createTestTempDirectory("rea-dom-comparison-"); + const before = join(root, "before.json"); + const after = join(root, "after.json"); + await writeFile(before, JSON.stringify({ inspection: await inspect() })); + state.mode = "omitted-node"; + for (const nested of [false, true]) { + state.nested = nested; + await writeFile(after, JSON.stringify({ inspection: await inspect() })); + const cliResult = await cli.run({ + arguments: ["compare-web-captures", before, after, "--json"], + environment: process.env, + }); + expect(cliResult.exitCode).toBe(0); + expect(cliResult.signal).toBeNull(); + expect(cliResult.json).toMatchObject({ + operation: "compare_web_captures", + normalized_result: { + overall_status: nested ? "changed" : "unknown", + dimensions: { + dom_structure: { status: nested ? "changed" : "unknown" }, + metadata: { status: "unknown" }, + }, + }, + }); + } + state.mode = "missing-nodes"; await writeFile(after, JSON.stringify({ inspection: await inspect() })); - expect( - await runCliJson(["compare-web-captures", before, after, "--json"]), - ).toMatchObject({ + const cliResult = await cli.run({ + arguments: ["compare-web-captures", after, after, "--json"], + environment: process.env, + }); + expect(cliResult.exitCode).toBe(0); + expect(cliResult.signal).toBeNull(); + expect(cliResult.json).toMatchObject({ operation: "compare_web_captures", normalized_result: { - overall_status: nested ? "changed" : "unknown", - dimensions: { - dom_structure: { status: nested ? "changed" : "unknown" }, - metadata: { status: "unknown" }, - }, + overall_status: "unknown", + dimensions: { dom_structure: { status: "unknown" } }, }, }); - } - state.mode = "missing-nodes"; - await writeFile(after, JSON.stringify({ inspection: await inspect() })); - expect( - await runCliJson(["compare-web-captures", after, after, "--json"]), - ).toMatchObject({ - operation: "compare_web_captures", - normalized_result: { - overall_status: "unknown", - dimensions: { dom_structure: { status: "unknown" } }, - }, - }); - }); + }, + ); }); diff --git a/tests/boundary/browser/cdpResourceCompleteness.test.ts b/tests/boundary/browser/cdpResourceCompleteness.test.ts index 048d49e98b..a0329707b9 100644 --- a/tests/boundary/browser/cdpResourceCompleteness.test.ts +++ b/tests/boundary/browser/cdpResourceCompleteness.test.ts @@ -12,7 +12,7 @@ import { parseEvidence } from "../../../src/domain/evidence.js"; import { compareWebCaptures } from "../../../src/domain/webCaptureDiff.js"; import { compareWebCapturesInputSchema } from "../../../src/domain/webCaptureDiffSchemas.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { trackBrowser } from "./cdpBrowserProvider.support.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; @@ -441,34 +441,41 @@ describe("resource uncertainty in production MCP registration", () => { }); describe("resource uncertainty in public CLI Evidence", () => { - it("preserves partial membership while complete inventories still prove removal", async () => { - const { state, inspect } = await startBrowser(); - const root = await createTestTempDirectory("rea-resource-comparison-"); - const before = join(root, "before.json"); - const after = join(root, "after.json"); - await writeFile(before, JSON.stringify({ inspection: await inspect() })); - for (const { mode, fields } of [ - { mode: "invalid-entry", fields: undefined }, - { mode: "valid", fields: { type: undefined } }, - { mode: "valid", fields: { contentSize: -1 } }, - { mode: "empty", fields: undefined }, - ] as const) { - state.mode = mode; - state.peerFields = fields; - await writeFile(after, JSON.stringify({ inspection: await inspect() })); - expect( - await runCliJson(["compare-web-captures", before, after, "--json"]), - ).toMatchObject({ - operation: "compare_web_captures", - normalized_result: { - dimensions: { - resources: { - status: mode === "empty" ? "changed" : "unknown", - total_changes: mode === "empty" ? 2 : 0, + cliTest( + "preserves partial membership while complete inventories still prove removal", + async ({ cli }) => { + const { state, inspect } = await startBrowser(); + const root = await createTestTempDirectory("rea-resource-comparison-"); + const before = join(root, "before.json"); + const after = join(root, "after.json"); + await writeFile(before, JSON.stringify({ inspection: await inspect() })); + for (const { mode, fields } of [ + { mode: "invalid-entry", fields: undefined }, + { mode: "valid", fields: { type: undefined } }, + { mode: "valid", fields: { contentSize: -1 } }, + { mode: "empty", fields: undefined }, + ] as const) { + state.mode = mode; + state.peerFields = fields; + await writeFile(after, JSON.stringify({ inspection: await inspect() })); + const cliResult = await cli.run({ + arguments: ["compare-web-captures", before, after, "--json"], + environment: process.env, + }); + expect(cliResult.exitCode).toBe(0); + expect(cliResult.signal).toBeNull(); + expect(cliResult.json).toMatchObject({ + operation: "compare_web_captures", + normalized_result: { + dimensions: { + resources: { + status: mode === "empty" ? "changed" : "unknown", + total_changes: mode === "empty" ? 2 : 0, + }, }, }, - }, - }); - } - }); + }); + } + }, + ); }); diff --git a/tests/boundary/cli/accessibilityCaptureComparison.test.ts b/tests/boundary/cli/accessibilityCaptureComparison.test.ts index d9d4e531a7..5d136db29c 100644 --- a/tests/boundary/cli/accessibilityCaptureComparison.test.ts +++ b/tests/boundary/cli/accessibilityCaptureComparison.test.ts @@ -1,8 +1,8 @@ -import { expect, it } from "vitest"; +import { expect } from "vitest"; import { classifyBrowserCompleteness } from "../../../src/domain/browserCompleteness.js"; import type { WebPageInspection } from "../../../src/domain/browserObservationSchemas.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest, type TestCli } from "../../support/cli/cliFixture.js"; import { passiveNetworkCapture } from "../../fixtures/passiveNetworkCapture.js"; type Capture = WebPageInspection; @@ -83,25 +83,26 @@ const setCoverage = (capture: Capture, coverage: Coverage): void => { }); }; -const compare = async (before: Capture, after: Capture) => - await runCliJson( - [ +const compare = async (cli: TestCli, before: Capture, after: Capture) => + await cli.run({ + arguments: [ "compare-web-captures", JSON.stringify({ inspection: before }), JSON.stringify({ inspection: after }), "--json", ], - process.env, - ); + environment: process.env, + }); -it.each([ +cliTest.for([ "truncated", "unavailable", "attach_limited", "policy_filtered", ] as const)( "does not infer an accessibility change from a %s tree", - async (coverage) => { + { timeout: 60_000 }, + async (coverage, { cli }) => { const before = accessibleCapture(); const after = structuredClone(before); after.accessibility.nodes.pop(); @@ -109,9 +110,11 @@ it.each([ after.accessibility.text_capture.retained_bytes = 4; setCoverage(after, coverage); - const result = await compare(before, after); + const result = await compare(cli, before, after); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.signal).toBeNull(); - expect(result).toMatchObject({ + expect(result.json).toMatchObject({ operation: "compare_web_captures", normalized_result: { dimensions: { @@ -125,41 +128,48 @@ it.each([ }, }); }, - 60_000, ); -it("keeps a count-mismatched tree unknown without a completeness marker", async () => { - const before = accessibleCapture(); - const after = structuredClone(before); - after.accessibility.total_nodes += 1; +cliTest( + "keeps a count-mismatched tree unknown without a completeness marker", + async ({ cli }) => { + const before = accessibleCapture(); + const after = structuredClone(before); + after.accessibility.total_nodes += 1; - const result = await compare(before, after); + const result = await compare(cli, before, after); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.signal).toBeNull(); - expect(result).toMatchObject({ - normalized_result: { - dimensions: { - accessibility: { status: "unknown", total_changes: 0, changes: [] }, + expect(result.json).toMatchObject({ + normalized_result: { + dimensions: { + accessibility: { status: "unknown", total_changes: 0, changes: [] }, + }, }, - }, - }); -}); + }); + }, +); -it.each([ +cliTest.for([ ["known to missing role", "button", null, "unknown"], ["missing to known role", null, "button", "unknown"], ["both roles missing", null, null, "unknown"], ["known roles differ", "button", "link", "changed"], ] as const)( "does not infer role equality or change when %s", - async (_label, beforeRole, afterRole, status) => { + { timeout: 60_000 }, + async ([_label, beforeRole, afterRole, status], { cli }) => { const before = accessibleCapture(); const after = structuredClone(before); before.accessibility.nodes[1]!.role = beforeRole; after.accessibility.nodes[1]!.role = afterRole; - const result = await compare(before, after); + const result = await compare(cli, before, after); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.signal).toBeNull(); - expect(result).toMatchObject({ + expect(result.json).toMatchObject({ normalized_result: { dimensions: { accessibility: { @@ -181,100 +191,117 @@ it.each([ }, }); }, +); + +cliTest( + "retains an independent accessibility difference when another role is missing", + async ({ cli }) => { + const before = accessibleCapture(); + const after = structuredClone(before); + before.accessibility.nodes[1]!.role = null; + after.accessibility.nodes[1]!.role = "button"; + after.accessibility.nodes[1]!.name = "Send"; + + const result = await compare(cli, before, after); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.signal).toBeNull(); + + expect(result.json).toMatchObject({ + normalized_result: { + dimensions: { + accessibility: { + status: "changed", + total_changes: 1, + changes: [{ identity: "accessibility_tree", change: "modified" }], + }, + }, + }, + }); + }, 60_000, ); -it("retains an independent accessibility difference when another role is missing", async () => { - const before = accessibleCapture(); - const after = structuredClone(before); - before.accessibility.nodes[1]!.role = null; - after.accessibility.nodes[1]!.role = "button"; - after.accessibility.nodes[1]!.name = "Send"; +cliTest( + "compares known structure when only text was not approved", + async ({ cli }) => { + const before = accessibleCapture(); + const after = structuredClone(before); + after.accessibility.text_capture = { + status: "not_approved", + retained_bytes: 0, + excluded_fields: 2, + }; + for (const node of after.accessibility.nodes) { + node.name = null; + node.description = null; + } + after.completeness = classifyBrowserCompleteness({ + policyFilteredSections: new Set(["accessibility"]), + attachLimitedSections: new Set(), + truncatedSections: new Set(), + unavailableSections: new Set(), + excluded: [ + { section: "accessibility", reason: "not_approved", count: 2 }, + ], + droppedEvents: { + scripts: 0, + network_requests: 0, + console_events: 0, + websocket_connections: 0, + websocket_frames: 0, + webmcp_tools: 0, + timeline_events: 0, + }, + }); + after.accessibility.nodes[1]!.role = "menuitem"; - const result = await compare(before, after); + const result = await compare(cli, before, after); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.signal).toBeNull(); - expect(result).toMatchObject({ - normalized_result: { - dimensions: { - accessibility: { - status: "changed", - total_changes: 1, - changes: [{ identity: "accessibility_tree", change: "modified" }], + expect(result.json).toMatchObject({ + normalized_result: { + dimensions: { + accessibility: { + status: "changed", + total_changes: 1, + changes: [{ identity: "accessibility_tree", change: "modified" }], + }, }, }, - }, - }); -}, 60_000); + }); + }, +); -it("compares known structure when only text was not approved", async () => { - const before = accessibleCapture(); - const after = structuredClone(before); - after.accessibility.text_capture = { - status: "not_approved", - retained_bytes: 0, - excluded_fields: 2, - }; - for (const node of after.accessibility.nodes) { - node.name = null; - node.description = null; - } - after.completeness = classifyBrowserCompleteness({ - policyFilteredSections: new Set(["accessibility"]), - attachLimitedSections: new Set(), - truncatedSections: new Set(), - unavailableSections: new Set(), - excluded: [{ section: "accessibility", reason: "not_approved", count: 2 }], - droppedEvents: { - scripts: 0, - network_requests: 0, - console_events: 0, - websocket_connections: 0, - websocket_frames: 0, - webmcp_tools: 0, - timeline_events: 0, - }, - }); - after.accessibility.nodes[1]!.role = "menuitem"; +cliTest( + "keeps independent resource changes visible beside an unknown tree", + async ({ cli }) => { + const before = accessibleCapture(); + const after = structuredClone(before); + after.accessibility.nodes.pop(); + after.accessibility.total_nodes = after.accessibility.nodes.length; + setCoverage(after, "truncated"); + after.resources.push({ + resource_key: `res_${"a".repeat(64)}`, + url: "https://app.example.test/new.css", + origin: "https://app.example.test", + type: "Stylesheet", + mime_type: "text/css", + content_size: 10, + }); - const result = await compare(before, after); + const result = await compare(cli, before, after); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.signal).toBeNull(); - expect(result).toMatchObject({ - normalized_result: { - dimensions: { - accessibility: { - status: "changed", - total_changes: 1, - changes: [{ identity: "accessibility_tree", change: "modified" }], + expect(result.json).toMatchObject({ + normalized_result: { + overall_status: "changed", + dimensions: { + accessibility: { status: "unknown", total_changes: 0 }, + resources: { status: "changed", total_changes: 1 }, }, }, - }, - }); -}); - -it("keeps independent resource changes visible beside an unknown tree", async () => { - const before = accessibleCapture(); - const after = structuredClone(before); - after.accessibility.nodes.pop(); - after.accessibility.total_nodes = after.accessibility.nodes.length; - setCoverage(after, "truncated"); - after.resources.push({ - resource_key: `res_${"a".repeat(64)}`, - url: "https://app.example.test/new.css", - origin: "https://app.example.test", - type: "Stylesheet", - mime_type: "text/css", - content_size: 10, - }); - - const result = await compare(before, after); - - expect(result).toMatchObject({ - normalized_result: { - overall_status: "changed", - dimensions: { - accessibility: { status: "unknown", total_changes: 0 }, - resources: { status: "changed", total_changes: 1 }, - }, - }, - }); -}); + }); + }, +); diff --git a/tests/boundary/cli/analysisViewCli.test.ts b/tests/boundary/cli/analysisViewCli.test.ts index 6603b9b22c..74d9cd2780 100644 --- a/tests/boundary/cli/analysisViewCli.test.ts +++ b/tests/boundary/cli/analysisViewCli.test.ts @@ -1100,6 +1100,80 @@ cliTest( }, ); +cliTest.for([ + { + spelling: "minute-only offset timestamps", + starts: ["2026-10-20T09:00Z", "2026-10-20T09:02Z", "2026-10-20T09:01Z"], + }, + { + spelling: "ten-digit fractional timestamps", + starts: [ + "2026-10-20T09:00:00.0000000001Z", + "2026-10-20T09:00:00.0000000003Z", + "2026-10-20T09:00:00.0000000002Z", + ], + }, +])( + "keeps the pre-write environment state through the compiled CLI for $spelling", + async ({ starts }, { cli }) => { + const root = await createTestTempDirectory("rea-state-chronology-cli-"); + const path = join(root, "view.json"); + const entries = starts.map((startedDateTime, ordinal) => ({ + startedDateTime, + time: 0, + request: { + method: ordinal === 2 ? "PATCH" : "GET", + url: "http://app.example/api/settings", + ...(ordinal === 2 + ? { + postData: { + mimeType: "application/json", + text: '{"theme":"dark"}', + }, + } + : {}), + }, + response: { + status: 200, + content: { + mimeType: "application/json", + text: ordinal === 0 ? '{"theme":"light"}' : '{"theme":"dark"}', + }, + }, + })); + const parent = analysisViewCaptureEvidence( + environmentManifestCaptureFixture(entries), + ); + expect(parent.normalized_result).toMatchObject({ + records: entries.map((reported, ordinal) => ({ ordinal, reported })), + }); + const request = JSON.stringify({ + source: { kind: "inline", evidence: parent }, + view: { kind: "environment-state" }, + }); + await writeFile(path, request); + const result = await cli.run({ + arguments: ["inspect-analysis-view", path, "--json"], + }); + expect(result.exitCode, result.stdout).toBe(0); + const evidence = parseEvidence(result.json); + expect(evidence.evidence_links).toEqual([parent.evidence_id]); + expect(evidence.normalized_result).toMatchObject({ + kind: "environment-state", + timeline: "recorded-start-time", + documents: [ + { + tool: "get_settings", + path: "/api/settings", + value: { theme: "light" }, + record_ordinal: 0, + }, + ], + }); + expect(await readFile(path, "utf8")).toBe(request); + }, +); + cliTest( "decodes a Grafana-shaped HAR and projects its environment manifest through the compiled workflows", async ({ cli }) => { diff --git a/tests/boundary/cli/browserCli.test.ts b/tests/boundary/cli/browserCli.test.ts index 6f0023f52b..c4ba15ab48 100644 --- a/tests/boundary/cli/browserCli.test.ts +++ b/tests/boundary/cli/browserCli.test.ts @@ -1,6 +1,6 @@ -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect } from "vitest"; -import { runCliJson as runCli } from "../../fixtures/cliJsonProcess.js"; +import { cliTest, type TestCli } from "../../support/cli/cliFixture.js"; import { DEFAULT_BROWSER_SCENARIO_CAPTURE_LIMITS } from "../../../src/domain/browserScenarioCaptureLimits.js"; import { BROWSER_STORAGE_FINGERPRINT_VERSION } from "../../../src/domain/browserScenarioCaptureValues.js"; import { @@ -34,9 +34,9 @@ afterEach(async () => { await Promise.all(browsers.splice(0).map(async (browser) => browser.close())); }); -it( +cliTest( "browser CLI parity returns the same Evidence discovery and inspection contracts", - async () => { + async ({ cli }) => { const browser = await startFakeCdpBrowser({ sessionTimeline: "same_origin", webMcpTools: true, @@ -45,10 +45,13 @@ it( eventsBeforeReply: ["Network.enable"], }); browsers.push(browser); - const listed = await runCli( - ["list-browser-targets", browser.endpoint, "--json"], - process.env, - ); + const listedInvocation = await cli.run({ + arguments: ["list-browser-targets", browser.endpoint, "--json"], + environment: process.env, + }); + expect(listedInvocation.exitCode, listedInvocation.stderr).toBe(0); + expect(listedInvocation.signal).toBeNull(); + const listed = listedInvocation.json; expect(listed).toMatchObject({ operation: "list_browser_targets", provider: { id: "rea-cdp-browser" }, @@ -59,8 +62,8 @@ it( ]), }, }); - const inspected = await runCli( - [ + const inspectedInvocation = await cli.run({ + arguments: [ "inspect-web-page", browser.endpoint, "allowed-page", @@ -71,8 +74,11 @@ it( "--include-websocket-shapes", "--json", ], - process.env, - ); + environment: process.env, + }); + expect(inspectedInvocation.exitCode, inspectedInvocation.stderr).toBe(0); + expect(inspectedInvocation.signal).toBeNull(); + const inspected = inspectedInvocation.json; expect(inspected).toMatchObject({ operation: "inspect_web_page", provider: { id: "rea-cdp-browser" }, @@ -92,15 +98,18 @@ it( }, }); const inspection = normalizedResult(inspected); - const compared = await runCli( - [ + const comparedInvocation = await cli.run({ + arguments: [ "compare-web-captures", JSON.stringify({ inspection, scenario: { caller_metadata: true } }), JSON.stringify({ inspection }), "--json", ], - process.env, - ); + environment: process.env, + }); + expect(comparedInvocation.exitCode, comparedInvocation.stderr).toBe(0); + expect(comparedInvocation.signal).toBeNull(); + const compared = comparedInvocation.json; expect(compared).toMatchObject({ operation: "compare_web_captures", normalized_result: { @@ -110,88 +119,18 @@ it( }, }, }); - const scenarioCapture = completeScenarioCapture(); - const scenarioCompared = await runCli( - [ - "compare-web-captures", - JSON.stringify(scenarioCapture), - JSON.stringify(scenarioCapture), - "--normalization-json", - '{"rules":[]}', - "--json", - ], - process.env, - ); - expect(scenarioCompared).toMatchObject({ - operation: "compare_web_captures", - normalized_result: { - comparison_kind: "browser_scenario", - overall_status: "unchanged", - alignment: { status: "aligned", aligned_steps: 2 }, - }, - }); - const intentionalPolicy = await runCli( - [ - "compare-web-captures", - JSON.stringify(scenarioCapture), - JSON.stringify(scenarioCapture), - "--normalization-json", - JSON.stringify({ - rules: [ - { - rule_id: "domain", - artifacts: ["url"], - match: "app.example.test", - replacement: "normalized.test", - }, - ], - }), - "--json", - ], - process.env, - ); - expect(intentionalPolicy).toMatchObject({ - normalized_result: { - normalization: { rules: [{ rule_id: "domain" }] }, - }, - }); - - const malformedPolicy = await runCli( - [ - "compare-web-captures", - JSON.stringify(scenarioCapture), - JSON.stringify(scenarioCapture), - "--normalization-json", - "{invalid-policy", - "--json", - ], - process.env, - ); - expect(malformedPolicy).toMatchObject({ - error: "Browser observation failed", - code: "invalid_request", - details: { - operation: "compare_web_captures", - issues: [ - { - path: ["normalization"], - reason: "invalid_format", - expected: "JSON", - }, - ], - }, - }); + await assertScenarioComparisonPolicy(cli); }, INTEGRATION_TEST_TIMEOUT_MS, ); -it( +cliTest( "compares redirect metadata uncertainty through the public CLI", - async () => { + async ({ cli }) => { const browser = await startFakeCdpBrowser(); browsers.push(browser); - const inspected = await runCli( - [ + const inspectedInvocation = await cli.run({ + arguments: [ "inspect-web-page", browser.endpoint, "allowed-page", @@ -199,13 +138,23 @@ it( "0", "--json", ], - process.env, - ); + environment: process.env, + }); + expect(inspectedInvocation.exitCode, inspectedInvocation.stderr).toBe(0); + expect(inspectedInvocation.signal).toBeNull(); + const inspected = inspectedInvocation.json; const inspection = normalizedResult(inspected); - const missingVsKnown = await compareCapturesCli( + const missingVsKnownInvocation = await compareCapturesCli( + cli, redirectCapture(inspection, browser.allowedOrigin, null), redirectCapture(inspection, browser.allowedOrigin, 302), ); + expect( + missingVsKnownInvocation.exitCode, + missingVsKnownInvocation.stderr, + ).toBe(0); + expect(missingVsKnownInvocation.signal).toBeNull(); + const missingVsKnown = missingVsKnownInvocation.json; expect(missingVsKnown).toMatchObject({ normalized_result: { dimensions: { @@ -217,10 +166,17 @@ it( }, }, }); - const knownDifference = await compareCapturesCli( + const knownDifferenceInvocation = await compareCapturesCli( + cli, redirectCapture(inspection, browser.allowedOrigin, 302), redirectCapture(inspection, browser.allowedOrigin, 307), ); + expect( + knownDifferenceInvocation.exitCode, + knownDifferenceInvocation.stderr, + ).toBe(0); + expect(knownDifferenceInvocation.signal).toBeNull(); + const knownDifference = knownDifferenceInvocation.json; expect(knownDifference).toMatchObject({ normalized_result: { dimensions: { network: { status: "unknown", total_changes: 0 } }, @@ -236,12 +192,17 @@ it( closed.inspection.completeness.attach_limited_sections = limited.filter( (section) => section !== "network_requests", ); + const closedDifferenceInvocation = await compareCapturesCli( + cli, + closed, + redirectCapture(inspection, browser.allowedOrigin, 307), + ); expect( - await compareCapturesCli( - closed, - redirectCapture(inspection, browser.allowedOrigin, 307), - ), - ).toMatchObject({ + closedDifferenceInvocation.exitCode, + closedDifferenceInvocation.stderr, + ).toBe(0); + expect(closedDifferenceInvocation.signal).toBeNull(); + expect(closedDifferenceInvocation.json).toMatchObject({ normalized_result: { dimensions: { network: { status: "changed", total_changes: 1 } }, }, @@ -250,13 +211,13 @@ it( INTEGRATION_TEST_TIMEOUT_MS, ); -it( +cliTest( "browser CLI attaches through a browser socket and records it redacted", - async () => { + async ({ cli }) => { const browser = await startFakeCdpBrowser(); browsers.push(browser); - const inspected = await runCli( - [ + const inspectedInvocation = await cli.run({ + arguments: [ "inspect-web-page", browser.browserWebSocketUrl, "allowed-page", @@ -264,8 +225,11 @@ it( "0", "--json", ], - process.env, - ); + environment: process.env, + }); + expect(inspectedInvocation.exitCode, inspectedInvocation.stderr).toBe(0); + expect(inspectedInvocation.signal).toBeNull(); + const inspected = inspectedInvocation.json; expect(inspected).toMatchObject({ operation: "inspect_web_page", parameters: { @@ -278,14 +242,17 @@ it( }); expect(JSON.stringify(inspected)).not.toContain("/devtools/browser/fake"); - const refused = await runCli( - [ + const refusedInvocation = await cli.run({ + arguments: [ "list-browser-targets", browser.browserWebSocketUrl.replace(/fake$/u, "not-this-browser"), "--json", ], - process.env, - ); + environment: process.env, + }); + expect(refusedInvocation.exitCode, refusedInvocation.stderr).toBe(1); + expect(refusedInvocation.signal).toBeNull(); + const refused = refusedInvocation.json; expect(refused).toMatchObject({ code: "provider_unavailable", details: { @@ -305,7 +272,7 @@ describe("scenario comparison CLI diagnostics", () => { match: "app.example.test", replacement: "normalized.test", }; - it.each([ + cliTest.for([ { name: "duplicate rule IDs", normalization: { rules: [rule, rule] }, @@ -335,16 +302,22 @@ describe("scenario comparison CLI diagnostics", () => { }, ])( "retains scenario normalization diagnostics for $name", - async ({ normalization, issue }) => { + async ({ normalization, issue }, { cli }) => { const capture = JSON.stringify(completeScenarioCapture()); - const result = await runCli([ - "compare-web-captures", - capture, - capture, - "--normalization-json", - JSON.stringify(normalization), - "--json", - ]); + const resultInvocation = await cli.run({ + arguments: [ + "compare-web-captures", + capture, + capture, + "--normalization-json", + JSON.stringify(normalization), + "--json", + ], + environment: process.env, + }); + expect(resultInvocation.exitCode, resultInvocation.stderr).toBe(1); + expect(resultInvocation.signal).toBeNull(); + const result = resultInvocation.json; expect(result).toMatchObject({ code: "invalid_request", details: { operation: "compare_web_captures", issues: [issue] }, @@ -352,42 +325,52 @@ describe("scenario comparison CLI diagnostics", () => { }, ); - it("keeps malformed scenario diagnostics in the selected capture group", async () => { - const capture = completeScenarioCapture(); - const result = await runCli([ - "compare-web-captures", - JSON.stringify({ ...capture, duration_ms: "invalid" }), - JSON.stringify(capture), - "--json", - ]); - expect(result).toMatchObject({ - code: "invalid_request", - details: { - operation: "compare_web_captures", - issues: [ - { - path: ["before_scenario", "duration_ms"], - reason: "invalid_type", - expected: "number", - }, + cliTest( + "keeps malformed scenario diagnostics in the selected capture group", + async ({ cli }) => { + const capture = completeScenarioCapture(); + const resultInvocation = await cli.run({ + arguments: [ + "compare-web-captures", + JSON.stringify({ ...capture, duration_ms: "invalid" }), + JSON.stringify(capture), + "--json", ], - }, - }); - }); + environment: process.env, + }); + expect(resultInvocation.exitCode, resultInvocation.stderr).toBe(1); + expect(resultInvocation.signal).toBeNull(); + const result = resultInvocation.json; + expect(result).toMatchObject({ + code: "invalid_request", + details: { + operation: "compare_web_captures", + issues: [ + { + path: ["before_scenario", "duration_ms"], + reason: "invalid_type", + expected: "number", + }, + ], + }, + }); + }, + ); - it.each(["omitted", "misspelled"])( + cliTest.for(["omitted", "misspelled"])( "reports the missing scenario manifest when it is %s on both captures", - async (kind) => { + async (kind, { cli }) => { const { scenario, ...capture } = completeScenarioCapture(); const input = JSON.stringify( kind === "omitted" ? capture : { ...capture, scneario: scenario }, ); - const result = await runCli([ - "compare-web-captures", - input, - input, - "--json", - ]); + const resultInvocation = await cli.run({ + arguments: ["compare-web-captures", input, input, "--json"], + environment: process.env, + }); + expect(resultInvocation.exitCode, resultInvocation.stderr).toBe(1); + expect(resultInvocation.signal).toBeNull(); + const result = resultInvocation.json; expect(result).toMatchObject({ code: "invalid_request", details: { @@ -412,71 +395,85 @@ describe("scenario comparison CLI diagnostics", () => { }); describe("scenario comparison recovery guidance", () => { - it("reports independent network recovery alongside console differences", async () => { - const base = completeScenarioCapture(); - const matchingSelection = scenarioCaptureWithConsole( - "before", - base.scenario.network_content, - ); - const differentSelection = scenarioCaptureWithConsole("after", { - ...base.scenario.network_content, - response_body: true, - }); - const changed = await compareCapturesCli( - matchingSelection, - differentSelection, - ); - expect(changed).toMatchObject({ - normalized_result: { - overall_status: "changed", - steps: expect.arrayContaining([ - expect.objectContaining({ - step_id: "open-settings", - artifact_diffs: expect.arrayContaining([ - expect.objectContaining({ - artifact: "events", - status: "changed", - reason: expect.stringContaining( - "Network content selections differ", - ), - }), - ]), - }), - ]), - }, - }); - const quietMismatch = await compareCapturesCli( - matchingSelection, - scenarioCaptureWithConsole( + cliTest( + "reports independent network recovery alongside console differences", + async ({ cli }) => { + const base = completeScenarioCapture(); + const matchingSelection = scenarioCaptureWithConsole( "before", - differentSelection.scenario.network_content, - ), - ); - expect(quietMismatch).toMatchObject({ - normalized_result: { - steps: expect.arrayContaining([ - expect.objectContaining({ - step_id: "open-settings", - artifact_diffs: expect.arrayContaining([ - expect.objectContaining({ - artifact: "events", - status: "unknown", - reason: expect.stringContaining( - "Network content selections differ", - ), - }), - ]), - }), - ]), - }, - }); - }); + base.scenario.network_content, + ); + const differentSelection = scenarioCaptureWithConsole("after", { + ...base.scenario.network_content, + response_body: true, + }); + const changedInvocation = await compareCapturesCli( + cli, + matchingSelection, + differentSelection, + ); + expect(changedInvocation.exitCode, changedInvocation.stderr).toBe(0); + expect(changedInvocation.signal).toBeNull(); + const changed = changedInvocation.json; + expect(changed).toMatchObject({ + normalized_result: { + overall_status: "changed", + steps: expect.arrayContaining([ + expect.objectContaining({ + step_id: "open-settings", + artifact_diffs: expect.arrayContaining([ + expect.objectContaining({ + artifact: "events", + status: "changed", + reason: expect.stringContaining( + "Network content selections differ", + ), + }), + ]), + }), + ]), + }, + }); + const quietMismatchInvocation = await compareCapturesCli( + cli, + matchingSelection, + scenarioCaptureWithConsole( + "before", + differentSelection.scenario.network_content, + ), + ); + expect( + quietMismatchInvocation.exitCode, + quietMismatchInvocation.stderr, + ).toBe(0); + expect(quietMismatchInvocation.signal).toBeNull(); + const quietMismatch = quietMismatchInvocation.json; + expect(quietMismatch).toMatchObject({ + normalized_result: { + steps: expect.arrayContaining([ + expect.objectContaining({ + step_id: "open-settings", + artifact_diffs: expect.arrayContaining([ + expect.objectContaining({ + artifact: "events", + status: "unknown", + reason: expect.stringContaining( + "Network content selections differ", + ), + }), + ]), + }), + ]), + }, + }); + }, + ); }); describe("browser CLI capture parity", () => { - it( + cliTest( "runs analysis, session, WebMCP, screenshot, and comparison from explicit request scope", - async () => { + async ({ cli }) => { const browser = await startFakeCdpBrowser({ sessionTimeline: "same_origin", webMcpTools: true, @@ -484,8 +481,8 @@ describe("browser CLI capture parity", () => { }); browsers.push(browser); const scopeArgs = ["--allowed-origins", browser.allowedOrigin]; - const analyzed = await runCli( - [ + const analyzedInvocation = await cli.run({ + arguments: [ "analyze-web-bundle", browser.endpoint, "allowed-page", @@ -494,8 +491,11 @@ describe("browser CLI capture parity", () => { "0", "--json", ], - process.env, - ); + environment: process.env, + }); + expect(analyzedInvocation.exitCode, analyzedInvocation.stderr).toBe(0); + expect(analyzedInvocation.signal).toBeNull(); + const analyzed = analyzedInvocation.json; expect(analyzed).toMatchObject({ operation: "analyze_web_bundle", normalized_result: { @@ -503,8 +503,8 @@ describe("browser CLI capture parity", () => { observations: { source_maps: { status: "not_requested" } }, }, }); - const observedSession = await runCli( - [ + const observedSessionInvocation = await cli.run({ + arguments: [ "observe-web-session", browser.endpoint, "allowed-page", @@ -513,8 +513,14 @@ describe("browser CLI capture parity", () => { "5", "--json", ], - process.env, - ); + environment: process.env, + }); + expect( + observedSessionInvocation.exitCode, + observedSessionInvocation.stderr, + ).toBe(0); + expect(observedSessionInvocation.signal).toBeNull(); + const observedSession = observedSessionInvocation.json; expect(observedSession).toMatchObject({ operation: "observe_web_session", normalized_result: { @@ -524,8 +530,8 @@ describe("browser CLI capture parity", () => { ]), }, }); - const webMcp = await runCli( - [ + const webMcpInvocation = await cli.run({ + arguments: [ "discover-webmcp-tools", browser.endpoint, "allowed-page", @@ -534,8 +540,11 @@ describe("browser CLI capture parity", () => { "0", "--json", ], - process.env, - ); + environment: process.env, + }); + expect(webMcpInvocation.exitCode, webMcpInvocation.stderr).toBe(0); + expect(webMcpInvocation.signal).toBeNull(); + const webMcp = webMcpInvocation.json; expect(webMcp).toMatchObject({ operation: "discover_webmcp_tools", normalized_result: { @@ -551,27 +560,35 @@ describe("browser CLI capture parity", () => { }, }, }); - const screenshot = await runCli( - [ + const screenshotInvocation = await cli.run({ + arguments: [ "capture-web-screenshot", browser.endpoint, "allowed-page", ...scopeArgs, "--json", ], - process.env, + environment: process.env, + }); + expect(screenshotInvocation.exitCode, screenshotInvocation.stderr).toBe( + 0, ); + expect(screenshotInvocation.signal).toBeNull(); + const screenshot = screenshotInvocation.json; const artifact = screenshotArtifact(screenshot); expect(artifact).toMatchObject({ media_type: "image/png", bytes: 70 }); - const visual = await runCli( - [ + const visualInvocation = await cli.run({ + arguments: [ "compare-web-screenshots", JSON.stringify(artifact), JSON.stringify(artifact), "--json", ], - process.env, - ); + environment: process.env, + }); + expect(visualInvocation.exitCode, visualInvocation.stderr).toBe(0); + expect(visualInvocation.signal).toBeNull(); + const visual = visualInvocation.json; expect(visual).toMatchObject({ operation: "compare_web_screenshots", normalized_result: { status: "identical", changed_pixels: 0 }, @@ -623,16 +640,108 @@ const redirectCapture = ( return { inspection: copy }; }; -const compareCapturesCli = (before: unknown, after: unknown) => - runCli( - [ +const assertScenarioComparisonPolicy = async (cli: TestCli): Promise => { + const scenarioCapture = completeScenarioCapture(); + const scenarioComparedInvocation = await cli.run({ + arguments: [ + "compare-web-captures", + JSON.stringify(scenarioCapture), + JSON.stringify(scenarioCapture), + "--normalization-json", + '{"rules":[]}', + "--json", + ], + environment: process.env, + }); + expect( + scenarioComparedInvocation.exitCode, + scenarioComparedInvocation.stderr, + ).toBe(0); + expect(scenarioComparedInvocation.signal).toBeNull(); + const scenarioCompared = scenarioComparedInvocation.json; + expect(scenarioCompared).toMatchObject({ + operation: "compare_web_captures", + normalized_result: { + comparison_kind: "browser_scenario", + overall_status: "unchanged", + alignment: { status: "aligned", aligned_steps: 2 }, + }, + }); + const intentionalPolicyInvocation = await cli.run({ + arguments: [ + "compare-web-captures", + JSON.stringify(scenarioCapture), + JSON.stringify(scenarioCapture), + "--normalization-json", + JSON.stringify({ + rules: [ + { + rule_id: "domain", + artifacts: ["url"], + match: "app.example.test", + replacement: "normalized.test", + }, + ], + }), + "--json", + ], + environment: process.env, + }); + expect( + intentionalPolicyInvocation.exitCode, + intentionalPolicyInvocation.stderr, + ).toBe(0); + expect(intentionalPolicyInvocation.signal).toBeNull(); + const intentionalPolicy = intentionalPolicyInvocation.json; + expect(intentionalPolicy).toMatchObject({ + normalized_result: { + normalization: { rules: [{ rule_id: "domain" }] }, + }, + }); + + const malformedPolicyInvocation = await cli.run({ + arguments: [ + "compare-web-captures", + JSON.stringify(scenarioCapture), + JSON.stringify(scenarioCapture), + "--normalization-json", + "{invalid-policy", + "--json", + ], + environment: process.env, + }); + expect( + malformedPolicyInvocation.exitCode, + malformedPolicyInvocation.stderr, + ).toBe(1); + expect(malformedPolicyInvocation.signal).toBeNull(); + const malformedPolicy = malformedPolicyInvocation.json; + expect(malformedPolicy).toMatchObject({ + error: "Browser observation failed", + code: "invalid_request", + details: { + operation: "compare_web_captures", + issues: [ + { + path: ["normalization"], + reason: "invalid_format", + expected: "JSON", + }, + ], + }, + }); +}; + +const compareCapturesCli = (cli: TestCli, before: unknown, after: unknown) => + cli.run({ + arguments: [ "compare-web-captures", JSON.stringify(before), JSON.stringify(after), "--json", ], - process.env, - ); + environment: process.env, + }); const completeScenarioCapture = () => { const url = { @@ -709,9 +818,9 @@ const completeScenarioCapture = () => { }; }; -it( +cliTest( "compares reordered scenario storage through the public CLI and commits the rule", - async () => { + async ({ cli }) => { const capture = completeScenarioCapture(); const entries = [ { name: "theme", value_state: "hashed", value_sha256: "a".repeat(64) }, @@ -735,10 +844,14 @@ it( }, })), }); - const compared = await compareCapturesCli( + const comparedInvocation = await compareCapturesCli( + cli, withStorage(entries), withStorage(entries.toReversed()), ); + expect(comparedInvocation.exitCode, comparedInvocation.stderr).toBe(0); + expect(comparedInvocation.signal).toBeNull(); + const compared = comparedInvocation.json; expect(compared).toMatchObject({ operation: "compare_web_captures", normalized_result: { @@ -751,10 +864,14 @@ it( }, }, }); - const changed = await compareCapturesCli( + const changedInvocation = await compareCapturesCli( + cli, withStorage(entries), withStorage(entries.slice(1)), ); + expect(changedInvocation.exitCode, changedInvocation.stderr).toBe(0); + expect(changedInvocation.signal).toBeNull(); + const changed = changedInvocation.json; expect(changed).toMatchObject({ normalized_result: { overall_status: "changed" }, }); diff --git a/tests/boundary/cli/browserCompareJsonFiles.test.ts b/tests/boundary/cli/browserCompareJsonFiles.test.ts index 497101febe..96ee1f5cde 100644 --- a/tests/boundary/cli/browserCompareJsonFiles.test.ts +++ b/tests/boundary/cli/browserCompareJsonFiles.test.ts @@ -3,10 +3,10 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { crc32, deflateSync } from "node:zlib"; -import { describe, expect, it } from "vitest"; +import { describe, expect } from "vitest"; import { createWebScreenshotArtifact } from "../../../src/domain/webScreenshot.js"; -import { runCliJson as runCli } from "../../fixtures/cliJsonProcess.js"; +import { cliTest, type TestCli } from "../../support/cli/cliFixture.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const INTEGRATION_TEST_TIMEOUT_MS = 60_000; @@ -40,9 +40,9 @@ const noisePng = (width: number, height: number): Buffer => { }; describe("browser comparison JSON file inputs", () => { - it( + cliTest( "compares screenshot artifacts too large for a command-line argument", - async () => { + async ({ cli }) => { const root = await createTestTempDirectory("rea-screenshot-files-"); const artifact = JSON.stringify( createWebScreenshotArtifact(noisePng(640, 640)), @@ -55,9 +55,13 @@ describe("browser comparison JSON file inputs", () => { await writeFile(before, artifact); await writeFile(after, artifact); - expect( - await runCli(["compare-web-screenshots", before, after, "--json"]), - ).toMatchObject({ + const comparison = await cli.run({ + arguments: ["compare-web-screenshots", before, after, "--json"], + environment: process.env, + }); + expect(comparison.exitCode, comparison.stderr).toBe(0); + expect(comparison.signal).toBeNull(); + expect(comparison.json).toMatchObject({ operation: "compare_web_screenshots", normalized_result: { status: "identical", changed_pixels: 0 }, }); @@ -65,22 +69,26 @@ describe("browser comparison JSON file inputs", () => { INTEGRATION_TEST_TIMEOUT_MS, ); - it( + cliTest( "reports which screenshot artifact failed validation", - async () => { + async ({ cli }) => { const root = await createTestTempDirectory("rea-screenshot-invalid-"); const valid = createWebScreenshotArtifact(noisePng(2, 2)); const before = join(root, "before.json"); await writeFile(before, JSON.stringify({ ...valid, bytes: 1 })); - expect( - await runCli([ + const invalidArtifact = await cli.run({ + arguments: [ "compare-web-screenshots", before, JSON.stringify(valid), "--json", - ]), - ).toMatchObject({ + ], + environment: process.env, + }); + expect(invalidArtifact.exitCode, invalidArtifact.stderr).toBe(1); + expect(invalidArtifact.signal).toBeNull(); + expect(invalidArtifact.json).toMatchObject({ error: "Browser observation failed", code: "invalid_request", details: { @@ -97,23 +105,27 @@ describe("browser comparison JSON file inputs", () => { INTEGRATION_TEST_TIMEOUT_MS, ); - it( + cliTest( "reports malformed PNG chunks as invalid input and valid unsupported PNGs as unsupported targets", - async () => { - await assertPngInputErrorProjections(); + async ({ cli }) => { + await assertPngInputErrorProjections(cli); }, INTEGRATION_TEST_TIMEOUT_MS, ); - it( + cliTest( "identifies a missing capture file instead of reporting malformed JSON", - async () => { + async ({ cli }) => { const root = await createTestTempDirectory("rea-capture-missing-"); const missing = join(root, "before.json"); - expect( - await runCli(["compare-web-captures", missing, "{}", "--json"]), - ).toMatchObject({ + const missingCapture = await cli.run({ + arguments: ["compare-web-captures", missing, "{}", "--json"], + environment: process.env, + }); + expect(missingCapture.exitCode, missingCapture.stderr).toBe(1); + expect(missingCapture.signal).toBeNull(); + expect(missingCapture.json).toMatchObject({ code: "invalid_request", details: { operation: "compare_web_captures", @@ -132,13 +144,17 @@ describe("browser comparison JSON file inputs", () => { ); }); -const assertPngInputErrorProjections = async (): Promise => { +const assertPngInputErrorProjections = async (cli: TestCli): Promise => { const damagedBytes = noisePng(1, 1); damagedBytes[29] = (damagedBytes[29] ?? 0) ^ 1; const damaged = JSON.stringify(createWebScreenshotArtifact(damagedBytes)); - expect( - await runCli(["compare-web-screenshots", damaged, damaged, "--json"]), - ).toMatchObject({ + const damagedPng = await cli.run({ + arguments: ["compare-web-screenshots", damaged, damaged, "--json"], + environment: process.env, + }); + expect(damagedPng.exitCode, damagedPng.stderr).toBe(1); + expect(damagedPng.signal).toBeNull(); + expect(damagedPng.json).toMatchObject({ code: "invalid_request", details: { operation: "compare_web_screenshots", @@ -162,14 +178,18 @@ const assertPngInputErrorProjections = async (): Promise => { ]), ), ); - expect( - await runCli([ + const malformedDeflatePng = await cli.run({ + arguments: [ "compare-web-screenshots", malformedDeflate, malformedDeflate, "--json", - ]), - ).toMatchObject({ + ], + environment: process.env, + }); + expect(malformedDeflatePng.exitCode, malformedDeflatePng.stderr).toBe(1); + expect(malformedDeflatePng.signal).toBeNull(); + expect(malformedDeflatePng.json).toMatchObject({ code: "invalid_request", details: { operation: "compare_web_screenshots", @@ -191,9 +211,13 @@ const assertPngInputErrorProjections = async (): Promise => { ]), ), ); - expect( - await runCli(["compare-web-screenshots", grayscale, grayscale, "--json"]), - ).toMatchObject({ + const unsupportedPng = await cli.run({ + arguments: ["compare-web-screenshots", grayscale, grayscale, "--json"], + environment: process.env, + }); + expect(unsupportedPng.exitCode, unsupportedPng.stderr).toBe(1); + expect(unsupportedPng.signal).toBeNull(); + expect(unsupportedPng.json).toMatchObject({ code: "unsupported_target", details: { operation: "compare_web_screenshots" }, }); @@ -212,14 +236,18 @@ const assertPngInputErrorProjections = async (): Promise => { ]), ), ); - expect( - await runCli([ + const oversizedPng = await cli.run({ + arguments: [ "compare-web-screenshots", hugeDimensions, hugeDimensions, "--json", - ]), - ).toMatchObject({ + ], + environment: process.env, + }); + expect(oversizedPng.exitCode, oversizedPng.stderr).toBe(1); + expect(oversizedPng.signal).toBeNull(); + expect(oversizedPng.json).toMatchObject({ code: "resource_constraint", details: { operation: "compare_web_screenshots", diff --git a/tests/boundary/cli/browserRedirectShapeCompleteness.test.ts b/tests/boundary/cli/browserRedirectShapeCompleteness.test.ts index 6340c5c34a..b8d067a014 100644 --- a/tests/boundary/cli/browserRedirectShapeCompleteness.test.ts +++ b/tests/boundary/cli/browserRedirectShapeCompleteness.test.ts @@ -1,6 +1,6 @@ -import { afterEach, expect, it } from "vitest"; +import { afterEach, expect } from "vitest"; -import { runCliJson as runCli } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { startFakeCdpBrowser, type FakeCdpBrowser, @@ -12,69 +12,82 @@ afterEach(async () => { await Promise.all(browsers.splice(0).map(async (browser) => browser.close())); }); -it("reports missing JSON shapes only when body shapes are selected", async () => { - const browser = await startFakeCdpBrowser({ - malformedRedirectResponse: true, - redirectResponseEnvelope: null, - }); - browsers.push(browser); - const commonArgs = [ - "inspect-web-page", - browser.endpoint, - "allowed-page", - "--allowed-origins", - browser.allowedOrigin, - "--observation-ms", - "0", - ]; - const selected = normalizedResult( - await runCli([...commonArgs, "--include-json-body-shapes", "--json"]), - ); - expect(selected).toMatchObject({ - network: { - requests: [ - { - body_shapes: { - status: "partial", - request: expect.any(Object), - response: null, +cliTest( + "reports missing JSON shapes only when body shapes are selected", + async ({ cli }) => { + const browser = await startFakeCdpBrowser({ + malformedRedirectResponse: true, + redirectResponseEnvelope: null, + }); + browsers.push(browser); + const commonArgs = [ + "inspect-web-page", + browser.endpoint, + "allowed-page", + "--allowed-origins", + browser.allowedOrigin, + "--observation-ms", + "0", + ]; + const selectedInvocation = await cli.run({ + arguments: [...commonArgs, "--include-json-body-shapes", "--json"], + environment: process.env, + }); + expect(selectedInvocation.exitCode, selectedInvocation.stderr).toBe(0); + expect(selectedInvocation.signal).toBeNull(); + const selected = normalizedResult(selectedInvocation.json); + expect(selected).toMatchObject({ + network: { + requests: [ + { + body_shapes: { + status: "partial", + request: expect.any(Object), + response: null, + }, }, - }, - ], - }, - completeness: { - unavailable_sections: expect.arrayContaining([ - "network_requests", - "json_body_shapes", - ]), - excluded: expect.arrayContaining([ - { - section: "json_body_shapes", - reason: "invalid_protocol_value", - count: expect.any(Number), - }, - ]), - }, - }); + ], + }, + completeness: { + unavailable_sections: expect.arrayContaining([ + "network_requests", + "json_body_shapes", + ]), + excluded: expect.arrayContaining([ + { + section: "json_body_shapes", + reason: "invalid_protocol_value", + count: expect.any(Number), + }, + ]), + }, + }); - const defaulted = normalizedResult(await runCli([...commonArgs, "--json"])); - expect(defaulted).toMatchObject({ - network: { - requests: [ - { - body_shapes: { - status: "not_approved", - request: null, - response: null, + const defaultedInvocation = await cli.run({ + arguments: [...commonArgs, "--json"], + environment: process.env, + }); + expect(defaultedInvocation.exitCode, defaultedInvocation.stderr).toBe(0); + expect(defaultedInvocation.signal).toBeNull(); + const defaulted = normalizedResult(defaultedInvocation.json); + expect(defaulted).toMatchObject({ + network: { + requests: [ + { + body_shapes: { + status: "not_approved", + request: null, + response: null, + }, }, - }, - ], - }, - }); - expect(defaulted.completeness).toMatchObject({ - unavailable_sections: expect.not.arrayContaining(["json_body_shapes"]), - }); -}); + ], + }, + }); + expect(defaulted.completeness).toMatchObject({ + unavailable_sections: expect.not.arrayContaining(["json_body_shapes"]), + }); + }, +); const normalizedResult = (value: unknown): Record => { if (!isRecord(value) || !isRecord(value.normalized_result)) diff --git a/tests/boundary/cli/browserWorkerOwnershipCli.test.ts b/tests/boundary/cli/browserWorkerOwnershipCli.test.ts index fae680d3ca..c5e338a093 100644 --- a/tests/boundary/cli/browserWorkerOwnershipCli.test.ts +++ b/tests/boundary/cli/browserWorkerOwnershipCli.test.ts @@ -2,11 +2,11 @@ import { rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; -import { afterEach, expect, it } from "vitest"; +import { afterEach, expect } from "vitest"; import { webPageInspectionSchema } from "../../../src/domain/browserObservationSchemas.js"; import { electronPageInspectionSchema } from "../../../src/domain/javascript/electronObservation.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { startFakeCdpBrowser, type FakeCdpBrowser, @@ -35,149 +35,175 @@ const normalizedResult = (value: unknown): unknown => { return value.normalized_result; }; -it("keeps missing worker ownership unknown through inspect-web-page", async () => { - const browser = await startFakeCdpBrowser({ - commandResult: ({ method }, origin) => { - if (method !== "Target.getTargets") return undefined; - const worker = { - type: "worker", - title: "Fixture", - attached: false, - canAccessOpener: false, - url: `${origin}/worker.js`, - }; - return { - targetInfos: [ - { ...worker, targetId: "related-opener", openerId: "allowed-page" }, - { - ...worker, - targetId: "related-parent", - parentFrameId: "frame-main", - }, - { ...worker, targetId: "withheld-unknown" }, - { ...worker, targetId: "withheld-empty-url", url: "" }, - { ...worker, targetId: "withheld-foreign", openerId: "other-page" }, - { - ...worker, - targetId: "withheld-foreign-parent", - parentFrameId: "other-frame", - }, - { ...worker, targetId: "withheld-invalid", openerId: null }, - { - ...worker, - targetId: "withheld-disallowed", - openerId: "allowed-page", - url: "https://foreign.test/worker.js", - }, - ], - }; - }, - }); - browsers.push(browser); - const evidence = await runCliJson([ - "inspect-web-page", - browser.endpoint, - "allowed-page", - "--observation-ms", - "0", - "--json", - ]); - expect(evidence).toMatchObject({ - operation: "inspect_web_page", - provider: { id: "rea-cdp-browser" }, - }); - const inspection = webPageInspectionSchema.parse(normalizedResult(evidence)); - expect(inspection.workers).toEqual([ - expect.objectContaining({ - target_id: "related-opener", - opener_target_id: "allowed-page", - parent_frame_id: null, - }), - expect.objectContaining({ - target_id: "related-parent", - opener_target_id: null, - parent_frame_id: "frame-main", - }), - ]); - expect(inspection.completeness.unavailable_sections).toContain("workers"); - expect( - inspection.completeness.excluded.filter( - ({ section }) => section === "workers", - ), - ).toEqual([ - { section: "workers", reason: "disallowed_origin", count: 1 }, - { section: "workers", reason: "invalid_protocol_value", count: 1 }, - { section: "workers", reason: "out_of_target_scope", count: 2 }, - { section: "workers", reason: "unattributed_target", count: 2 }, - ]); - expect(JSON.stringify(evidence)).not.toContain("withheld-"); - expect( - browser.commands.some(({ method }) => method === "Target.detachFromTarget"), - ).toBe(true); -}, 20_000); +cliTest( + "keeps missing worker ownership unknown through inspect-web-page", + async ({ cli }) => { + const browser = await startFakeCdpBrowser({ + commandResult: ({ method }, origin) => { + if (method !== "Target.getTargets") return undefined; + const worker = { + type: "worker", + title: "Fixture", + attached: false, + canAccessOpener: false, + url: `${origin}/worker.js`, + }; + return { + targetInfos: [ + { ...worker, targetId: "related-opener", openerId: "allowed-page" }, + { + ...worker, + targetId: "related-parent", + parentFrameId: "frame-main", + }, + { ...worker, targetId: "withheld-unknown" }, + { ...worker, targetId: "withheld-empty-url", url: "" }, + { ...worker, targetId: "withheld-foreign", openerId: "other-page" }, + { + ...worker, + targetId: "withheld-foreign-parent", + parentFrameId: "other-frame", + }, + { ...worker, targetId: "withheld-invalid", openerId: null }, + { + ...worker, + targetId: "withheld-disallowed", + openerId: "allowed-page", + url: "https://foreign.test/worker.js", + }, + ], + }; + }, + }); + browsers.push(browser); + const evidenceInvocation = await cli.run({ + arguments: [ + "inspect-web-page", + browser.endpoint, + "allowed-page", + "--observation-ms", + "0", + "--json", + ], + environment: process.env, + }); + expect(evidenceInvocation.exitCode, evidenceInvocation.stderr).toBe(0); + expect(evidenceInvocation.signal).toBeNull(); + const evidence = evidenceInvocation.json; + expect(evidence).toMatchObject({ + operation: "inspect_web_page", + provider: { id: "rea-cdp-browser" }, + }); + const inspection = webPageInspectionSchema.parse( + normalizedResult(evidence), + ); + expect(inspection.workers).toEqual([ + expect.objectContaining({ + target_id: "related-opener", + opener_target_id: "allowed-page", + parent_frame_id: null, + }), + expect.objectContaining({ + target_id: "related-parent", + opener_target_id: null, + parent_frame_id: "frame-main", + }), + ]); + expect(inspection.completeness.unavailable_sections).toContain("workers"); + expect( + inspection.completeness.excluded.filter( + ({ section }) => section === "workers", + ), + ).toEqual([ + { section: "workers", reason: "disallowed_origin", count: 1 }, + { section: "workers", reason: "invalid_protocol_value", count: 1 }, + { section: "workers", reason: "out_of_target_scope", count: 2 }, + { section: "workers", reason: "unattributed_target", count: 2 }, + ]); + expect(JSON.stringify(evidence)).not.toContain("withheld-"); + expect( + browser.commands.some( + ({ method }) => method === "Target.detachFromTarget", + ), + ).toBe(true); + }, + 20_000, +); -it("uses the same ownership facts for passive Electron worker inspection", async () => { - const root = await createTestTempDirectory("rea-worker-ownership-"); - temporary.push(root); - await writeFile(join(root, "index.html"), ""); - await writeFile(join(root, "worker.js"), "void 0;"); - const browser = await startFakeCdpBrowser({ - electronFileUrl: pathToFileURL(join(root, "index.html")).href, - commandResult: ({ method }) => { - if (method !== "Target.getTargets") return undefined; - const worker = { - type: "worker", - title: "Fixture", - attached: false, - canAccessOpener: false, - url: pathToFileURL(join(root, "worker.js")).href, - }; - return { - targetInfos: [ - { ...worker, targetId: "related", openerId: "electron-page" }, - { ...worker, targetId: "withheld-unknown" }, - { ...worker, targetId: "withheld-empty-url", url: "" }, - { ...worker, targetId: "withheld-foreign", openerId: "other-page" }, - { ...worker, targetId: "withheld-invalid", parentFrameId: 42 }, - ], - }; - }, - }); - browsers.push(browser); - const evidence = await runCliJson([ - "inspect-electron-page", - browser.endpoint, - "electron-page", - "--observation-ms", - "0", - "--json", - ]); - expect(evidence).toMatchObject({ - operation: "inspect_electron_page", - provider: { id: "rea-cdp-electron" }, - }); - const inspection = electronPageInspectionSchema.parse( - normalizedResult(evidence), - ); - expect(inspection.workers).toEqual([ - expect.objectContaining({ - target_id: "related", - opener_target_id: "electron-page", - parent_frame_id: null, - }), - ]); - expect(inspection.completeness.unavailable_sections).toContain("workers"); - expect( - inspection.completeness.excluded.filter( - ({ section }) => section === "workers", - ), - ).toEqual([ - { section: "workers", reason: "invalid_protocol_value", count: 1 }, - { section: "workers", reason: "out_of_target_scope", count: 1 }, - { section: "workers", reason: "unattributed_target", count: 2 }, - ]); - expect(JSON.stringify(evidence)).not.toContain("withheld-"); - expect( - browser.commands.some(({ method }) => method === "Target.detachFromTarget"), - ).toBe(true); -}, 20_000); +cliTest( + "uses the same ownership facts for passive Electron worker inspection", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-worker-ownership-"); + temporary.push(root); + await writeFile(join(root, "index.html"), ""); + await writeFile(join(root, "worker.js"), "void 0;"); + const browser = await startFakeCdpBrowser({ + electronFileUrl: pathToFileURL(join(root, "index.html")).href, + commandResult: ({ method }) => { + if (method !== "Target.getTargets") return undefined; + const worker = { + type: "worker", + title: "Fixture", + attached: false, + canAccessOpener: false, + url: pathToFileURL(join(root, "worker.js")).href, + }; + return { + targetInfos: [ + { ...worker, targetId: "related", openerId: "electron-page" }, + { ...worker, targetId: "withheld-unknown" }, + { ...worker, targetId: "withheld-empty-url", url: "" }, + { ...worker, targetId: "withheld-foreign", openerId: "other-page" }, + { ...worker, targetId: "withheld-invalid", parentFrameId: 42 }, + ], + }; + }, + }); + browsers.push(browser); + const evidenceInvocation = await cli.run({ + arguments: [ + "inspect-electron-page", + browser.endpoint, + "electron-page", + "--observation-ms", + "0", + "--json", + ], + environment: process.env, + }); + expect(evidenceInvocation.exitCode, evidenceInvocation.stderr).toBe(0); + expect(evidenceInvocation.signal).toBeNull(); + const evidence = evidenceInvocation.json; + expect(evidence).toMatchObject({ + operation: "inspect_electron_page", + provider: { id: "rea-cdp-electron" }, + }); + const inspection = electronPageInspectionSchema.parse( + normalizedResult(evidence), + ); + expect(inspection.workers).toEqual([ + expect.objectContaining({ + target_id: "related", + opener_target_id: "electron-page", + parent_frame_id: null, + }), + ]); + expect(inspection.completeness.unavailable_sections).toContain("workers"); + expect( + inspection.completeness.excluded.filter( + ({ section }) => section === "workers", + ), + ).toEqual([ + { section: "workers", reason: "invalid_protocol_value", count: 1 }, + { section: "workers", reason: "out_of_target_scope", count: 1 }, + { section: "workers", reason: "unattributed_target", count: 2 }, + ]); + expect(JSON.stringify(evidence)).not.toContain("withheld-"); + expect( + browser.commands.some( + ({ method }) => method === "Target.detachFromTarget", + ), + ).toBe(true); + }, + 20_000, +); diff --git a/tests/boundary/cli/cliSetup.test.ts b/tests/boundary/cli/cliSetup.test.ts index 15cea67f50..00e45c5b17 100644 --- a/tests/boundary/cli/cliSetup.test.ts +++ b/tests/boundary/cli/cliSetup.test.ts @@ -9,14 +9,16 @@ import { symlink, writeFile, } from "node:fs/promises"; -import { dirname, join } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { promisify } from "node:util"; import { spawn } from "@lydell/node-pty"; import { describe, expect, it } from "vitest"; import { z } from "zod"; +import { parse as parseToml } from "smol-toml"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; const execute = promisify(execFile); const decisionMarker = "REA_SETUP_DECISION:"; @@ -176,6 +178,107 @@ describe("TTY setup dry run", () => { }); }); +cliTest.skipIf(process.platform === "win32")( + "repairs a Codex timeout without replacing an equivalent caller-selected launcher", + async ({ cli }) => { + const home = await createTestTempDirectory("rea-codex-alias-repair-"); + const codexHome = join(home, ".codex"); + const config = join(codexHome, "config.toml"); + const backup = `${config}.rea.backup`; + const alias = join(home, "rea-launcher-alias.mjs"); + await mkdir(codexHome); + await symlink(resolve("scripts/rea.mjs"), alias, "file"); + const original = [ + "# Caller-owned Codex approval policy", + 'approval_policy = "on-request"', + 'sandbox_mode = "read-only"', + "", + "[mcp_servers.rea]", + `command = ${JSON.stringify(process.execPath)}`, + `args = ${JSON.stringify([alias, "mcp"])}`, + "startup_timeout_sec = 0", + "tool_timeout_sec = 900", + 'default_tools_approval_mode = "prompt"', + "", + "[mcp_servers.rea.tools.open_binary]", + 'approval_mode = "approve"', + "output_token_limit = 2048", + "", + ].join("\n"); + await writeFile(config, original); + const setup = (approval: "--dry-run" | "--yes") => + cli.run({ + arguments: [ + "setup", + "--client", + "codex", + "--no-skill", + approval, + "--json", + ], + cwd: home, + environment: { + HOME: home, + USERPROFILE: home, + CODEX_HOME: codexHome, + XDG_CONFIG_HOME: join(home, ".config"), + XDG_DATA_HOME: join(home, ".local", "share"), + XDG_CACHE_HOME: join(home, ".cache"), + }, + timeoutMs: 20_000, + }); + + const planned = await setup("--dry-run"); + expect(planned.exitCode, planned.stderr).toBe(0); + expect(planned.json).toMatchObject({ + status: "planned", + plannedActions: [ + expect.objectContaining({ + id: "configure_client:codex", + operation: "update", + target: config, + backupPath: backup, + }), + ], + }); + expect(await readFile(config, "utf8")).toBe(original); + await expect(access(backup)).rejects.toMatchObject({ code: "ENOENT" }); + + const applied = await setup("--yes"); + expect(applied.exitCode, applied.stderr).toBe(0); + expect(applied.json).toMatchObject({ status: "ready" }); + const repaired = await readFile(config, "utf8"); + expect(parseToml(repaired)).toMatchObject({ + approval_policy: "on-request", + sandbox_mode: "read-only", + mcp_servers: { + rea: { + command: process.execPath, + args: [alias, "mcp"], + startup_timeout_sec: 30, + tool_timeout_sec: 900, + default_tools_approval_mode: "prompt", + tools: { + open_binary: { approval_mode: "approve", output_token_limit: 2048 }, + }, + }, + }, + }); + expect(await readFile(backup, "utf8")).toBe(original); + expect((await lstat(alias)).isSymbolicLink()).toBe(true); + + const repeated = await setup("--yes"); + expect(repeated.exitCode, repeated.stderr).toBe(0); + expect(repeated.json).toMatchObject({ + status: "ready", + plannedActions: [], + appliedActions: [], + }); + expect(await readFile(config, "utf8")).toBe(repaired); + expect(await readFile(backup, "utf8")).toBe(original); + }, +); + describe("interactive setup journey", () => { it("shows detected and absent clients without preselecting detected clients", async () => { const result = await runJourney( diff --git a/tests/boundary/cli/compiledCliSession.test.ts b/tests/boundary/cli/compiledCliSession.test.ts deleted file mode 100644 index 0499a5c44c..0000000000 --- a/tests/boundary/cli/compiledCliSession.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { EventEmitter, PassThrough } from "node:stream"; -import type { ChildProcessWithoutNullStreams } from "node:child_process"; -import { writeFile } from "node:fs/promises"; -import { join } from "node:path"; - -import { expect, onTestFinished, test } from "vitest"; - -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { startCompiledCliSession } from "../../support/cli/compiledCliSession.js"; - -interface FakeCliProcess extends EventEmitter { - stdin: PassThrough; - stdout: PassThrough; - stderr: PassThrough; - pid: number; - exitCode: number | null; - signalCode: NodeJS.Signals | null; - kill(signal?: NodeJS.Signals): boolean; -} - -const fakeProcess = (pid: number): FakeCliProcess => { - const child = new EventEmitter() as FakeCliProcess; - child.stdin = new PassThrough(); - child.stdout = new PassThrough(); - child.stderr = new PassThrough(); - child.pid = pid; - child.exitCode = null; - child.signalCode = null; - child.kill = (signal: NodeJS.Signals = "SIGTERM") => { - if (child.exitCode !== null || child.signalCode !== null) return true; - child.signalCode = signal; - child.emit("exit", null, signal); - child.emit("close", null, signal); - return true; - }; - return child; -}; - -const asCliProcess = (child: FakeCliProcess): ChildProcessWithoutNullStreams => - child as unknown as ChildProcessWithoutNullStreams; - -const ready = (child: FakeCliProcess): void => { - child.stdout.write(`${JSON.stringify({ ready: true })}\n`); -}; - -const exitWhenStdinCloses = (child: FakeCliProcess): void => { - child.stdin.on("finish", () => { - if (child.exitCode !== null || child.signalCode !== null) return; - child.exitCode = 0; - child.emit("exit", 0, null); - child.emit("close", 0, null); - }); -}; - -const replyWhenAsked = ( - child: FakeCliProcess, - stdout: string, - exitCode = 0, -): void => { - let buffer = ""; - child.stdin.setEncoding("utf8"); - child.stdin.on("data", (chunk: string) => { - buffer += chunk; - let newline = buffer.indexOf("\n"); - while (newline !== -1) { - const line = buffer.slice(0, newline); - buffer = buffer.slice(newline + 1); - if (line.length > 0) { - const request = JSON.parse(line) as { id: number }; - child.stdout.write( - `${JSON.stringify({ id: request.id, exitCode, stdout })}\n`, - ); - } - newline = buffer.indexOf("\n"); - } - }); -}; - -test("rejects a compiled CLI command when close wins the ready handshake", async () => { - let child: FakeCliProcess | undefined; - let releaseReady: (() => void) | undefined; - const session = startCompiledCliSession({ - stopTimeoutMs: 50, - spawnWorker: () => { - const started = fakeProcess(60); - child = started; - exitWhenStdinCloses(started); - releaseReady = () => { - ready(started); - }; - return asCliProcess(started); - }, - }); - - const pending = session.run(["early"]); - for (let attempt = 0; child === undefined && attempt < 30; attempt += 1) - await new Promise((resolve) => { - setImmediate(resolve); - }); - if (child === undefined) throw new Error("compiled CLI worker did not start"); - const closing = session.close(); - releaseReady?.(); - await expect(pending).rejects.toThrow("Compiled CLI session is closed"); - await closing; -}); - -test("rejects a compiled CLI command that never replies and serves the next one", async () => { - const children: FakeCliProcess[] = []; - const session = startCompiledCliSession({ - requestTimeoutMs: 30, - spawnWorker: () => { - const child = fakeProcess(70 + children.length); - children.push(child); - exitWhenStdinCloses(child); - ready(child); - if (children.length > 1) - replyWhenAsked(child, JSON.stringify({ ok: true })); - return asCliProcess(child); - }, - }); - - await expect(session.run(["stuck"])).rejects.toThrow(/did not respond/); - expect(children[0]?.signalCode).toBe("SIGKILL"); - await expect(session.run(["next"])).resolves.toEqual({ ok: true }); - expect(children).toHaveLength(2); - await session.close(); -}); - -test("keeps a timed-out compiled CLI worker until it exits", async () => { - const children: FakeCliProcess[] = []; - let releaseExit: (() => void) | undefined; - const session = startCompiledCliSession({ - requestTimeoutMs: 20, - stopTimeoutMs: 40, - spawnWorker: () => { - const child = fakeProcess(90 + children.length); - children.push(child); - if (children.length === 1) { - child.kill = () => { - releaseExit = () => { - if (child.exitCode !== null || child.signalCode !== null) return; - child.signalCode = "SIGKILL"; - child.emit("exit", null, "SIGKILL"); - child.emit("close", null, "SIGKILL"); - }; - return true; - }; - } else { - exitWhenStdinCloses(child); - replyWhenAsked(child, JSON.stringify({ ok: true })); - } - ready(child); - return asCliProcess(child); - }, - }); - - const stalled = session.run(["stuck"]); - const next = session.run(["next"]); - await expect(stalled).rejects.toThrow(/did not respond/); - expect(children).toHaveLength(1); - expect(children[0]?.signalCode).toBeNull(); - releaseExit?.(); - await expect(next).resolves.toEqual({ ok: true }); - expect(children).toHaveLength(2); - await session.close(); -}); - -test("retries a timed-out compiled CLI worker that survives SIGKILL", async () => { - let child: FakeCliProcess | undefined; - const signals: NodeJS.Signals[] = []; - const session = startCompiledCliSession({ - requestTimeoutMs: 20, - stopTimeoutMs: 20, - spawnWorker: () => { - child = fakeProcess(91); - child.kill = (signal: NodeJS.Signals = "SIGTERM") => { - signals.push(signal); - return true; - }; - ready(child); - return asCliProcess(child); - }, - }); - - await expect(session.run(["stuck"])).rejects.toThrow(/did not respond/); - const replacement = session.run(["next"]); - await expect(replacement).rejects.toThrow(/stayed running after SIGKILL/); - await expect(session.close()).rejects.toThrow(/stayed running after SIGKILL/); - expect(signals).toEqual(["SIGKILL", "SIGKILL"]); - const stalledChild = child; - if (stalledChild === undefined) throw new Error("missing stalled worker"); - stalledChild.kill = (signal: NodeJS.Signals = "SIGKILL") => { - signals.push(signal); - stalledChild.signalCode = signal; - stalledChild.emit("exit", null, signal); - stalledChild.emit("close", null, signal); - return true; - }; - await session.close(); - expect(stalledChild.signalCode).toBe("SIGKILL"); -}); - -test("reports a compiled CLI session that needed a signal after stdin closed", async () => { - let child: FakeCliProcess | undefined; - const signals: NodeJS.Signals[] = []; - const session = startCompiledCliSession({ - stopTimeoutMs: 20, - spawnWorker: () => { - child = fakeProcess(81); - child.kill = (signal: NodeJS.Signals = "SIGTERM") => { - signals.push(signal); - if (signal !== "SIGKILL") return true; - if (child?.exitCode !== null || child?.signalCode !== null) return true; - child.signalCode = signal; - child.emit("exit", null, signal); - child.emit("close", null, signal); - return true; - }; - ready(child); - replyWhenAsked(child, JSON.stringify({ ok: true })); - return asCliProcess(child); - }, - }); - - await expect(session.run(["once"])).resolves.toEqual({ ok: true }); - await expect(session.close()).rejects.toThrow( - "Compiled CLI session did not exit after stdin closed", - ); - expect(signals).toEqual(["SIGTERM", "SIGKILL"]); - expect(child?.signalCode).toBe("SIGKILL"); -}); - -test("closes a compiled CLI session that exits after stdin closes", async () => { - const session = startCompiledCliSession({ - spawnWorker: () => { - const child = fakeProcess(82); - exitWhenStdinCloses(child); - ready(child); - replyWhenAsked(child, JSON.stringify({ ok: true })); - return asCliProcess(child); - }, - }); - - await expect(session.run(["once"])).resolves.toEqual({ ok: true }); - await session.close(); -}); - -test("keeps a compiled CLI failure status and serves the next command", async () => { - const session = startCompiledCliSession(); - onTestFinished(() => session.close()); - const root = await createTestTempDirectory("rea-warm-cli-status-"); - await writeFile( - join(root, "app.mjs"), - "export function current() { return 1; }\n", - ); - - await expect( - session.run(["compare-javascript-export-shapes", "{", "--json"]), - ).rejects.toThrow(/status 1:.*invalid_request/); - await expect( - session.run(["analyze-javascript-application", root, "--json"]), - ).resolves.toMatchObject({ - normalized_result: expect.any(Object), - }); - await session.close(); -}); diff --git a/tests/boundary/cli/dispatcher.test.ts b/tests/boundary/cli/dispatcher.test.ts index 16b7205361..e617ee6ab2 100644 --- a/tests/boundary/cli/dispatcher.test.ts +++ b/tests/boundary/cli/dispatcher.test.ts @@ -15,7 +15,7 @@ const installCliRuntime = async ( cliSource: string, resultOutput = "export const createCliResultOutput = () => undefined;\n", ): Promise => { - await workspace.mkdir("scripts"); + await workspace.mkdir("scripts/lib"); await workspace.mkdir("dist"); // The bundle's entrypoints and shared chunks form one emitted runtime. await cp("dist/cli-runtime", workspace.path("dist/cli-runtime"), { @@ -23,6 +23,10 @@ const installCliRuntime = async ( }); await Promise.all([ copyFile("scripts/rea.mjs", workspace.path("scripts/rea.mjs")), + copyFile( + "scripts/lib/cli-process-environment.mjs", + workspace.path("scripts/lib/cli-process-environment.mjs"), + ), workspace.write( "package.json", JSON.stringify({ type: "module", version: "1.0.0" }), @@ -289,8 +293,13 @@ describe("executable dispatcher", () => { "explains how to restore a missing compiled runtime", async ({ processes, workspace }) => { const scripts = await workspace.mkdir("scripts"); + await workspace.mkdir("scripts/lib"); await Promise.all([ copyFile("scripts/rea.mjs", join(scripts, "rea.mjs")), + copyFile( + "scripts/lib/cli-process-environment.mjs", + workspace.path("scripts/lib/cli-process-environment.mjs"), + ), workspace.write( "package.json", JSON.stringify({ type: "module", version: "1.0.0" }), diff --git a/tests/boundary/cli/electronCli.test.ts b/tests/boundary/cli/electronCli.test.ts index e9bdd34f07..4dcd71a1fa 100644 --- a/tests/boundary/cli/electronCli.test.ts +++ b/tests/boundary/cli/electronCli.test.ts @@ -1,13 +1,15 @@ import { parseEvidence } from "../../../src/domain/evidence.js"; -import { execFile, spawn } from "node:child_process"; +import { spawn } from "node:child_process"; import { rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; -import { promisify } from "node:util"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { closeVerifierResources } from "../../../scripts/lib/verifier-cleanup.mjs"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { createTestCli, type CliResult } from "../../support/cli/cliFixture.js"; +import { createTestProcessRegistry } from "../../support/process/processFixture.js"; import { startFakeCdpBrowser, @@ -22,18 +24,24 @@ import { } from "../../fixtures/javascriptRuntimeReconciliation.js"; import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; -const execute = promisify(execFile); +const cliProcesses = createTestProcessRegistry(); +const cli = createTestCli(cliProcesses); const INTEGRATION_TEST_TIMEOUT_MS = 20_000; const browsers: FakeCdpBrowser[] = []; const temporary: string[] = []; afterEach(async () => { - await Promise.all(browsers.splice(0).map(async (browser) => browser.close())); - await Promise.all( - temporary - .splice(0) - .map(async (path) => rm(path, { recursive: true, force: true })), - ); + await closeVerifierResources([ + () => cliProcesses.shutdown(), + ...browsers.map((browser) => async () => { + await browser.close(); + browsers.splice(browsers.indexOf(browser), 1); + }), + ...temporary.map((path) => async () => { + await rm(path, { recursive: true, force: true }); + temporary.splice(temporary.indexOf(path), 1); + }), + ]); }); it( @@ -116,8 +124,9 @@ it( ["list-electron-targets", "not a url", "--json"], process.env, ); + expect(invalidTargetList.signal).toBeNull(); expect(invalidTargetList.exitCode).toBe(1); - expect(invalidTargetList.result).toMatchObject({ + expect(invalidTargetList.json).toMatchObject({ error: "Electron analysis failed", code: "invalid_request", category: "invalid_input", @@ -136,8 +145,9 @@ it( ["inspect-electron-page", "http://127.0.0.1:9222", "", "--json"], process.env, ); + expect(invalidPageInspect.signal).toBeNull(); expect(invalidPageInspect.exitCode).toBe(1); - expect(invalidPageInspect.result).toMatchObject({ + expect(invalidPageInspect.json).toMatchObject({ error: "Electron analysis failed", code: "invalid_request", category: "invalid_input", @@ -331,6 +341,7 @@ describe("Electron CLI runtime reconciliation input", () => { const rejected = await runCli( ["reconcile-javascript-runtime", "{}", "--json"], process.env, + 1, ); expect(rejected).toMatchObject({ error: "Application workflow failed", @@ -371,6 +382,7 @@ describe("Electron CLI runtime reconciliation", () => { const missing = await runCli( ["reconcile-javascript-runtime", missingPath, "--json"], process.env, + 1, ); expect(missing).toMatchObject({ input_path: missingPath, @@ -499,29 +511,19 @@ const isRecord = (value: unknown): value is Record => const runCliResult = async ( arguments_: readonly string[], environment: NodeJS.ProcessEnv, -): Promise<{ readonly exitCode: number; readonly result: unknown }> => { - try { - const { stdout } = await execute( - process.execPath, - ["scripts/rea.mjs", ...arguments_], - { cwd: process.cwd(), env: environment, maxBuffer: 16 * 1_024 * 1_024 }, - ); - return { exitCode: 0, result: JSON.parse(stdout) }; - } catch (cause: unknown) { - if ( - typeof cause === "object" && - cause !== null && - "stdout" in cause && - typeof cause.stdout === "string" && - "code" in cause && - typeof cause.code === "number" - ) - return { exitCode: cause.code, result: JSON.parse(cause.stdout) }; - throw cause; - } -}; +): Promise => + cli.run({ arguments: arguments_, cwd: process.cwd(), environment }); const runCli = async ( arguments_: readonly string[], environment: NodeJS.ProcessEnv, -): Promise => (await runCliResult(arguments_, environment)).result; + expectedExitCode: 0 | 1 = 0, +): Promise => { + const result = await runCliResult(arguments_, environment); + expect(result.signal, result.stderr || result.stdout).toBeNull(); + expect(result.exitCode, result.stderr || result.stdout).toBe( + expectedExitCode, + ); + expect(result.json, result.stderr || result.stdout).toBeDefined(); + return result.json; +}; diff --git a/tests/boundary/cli/environmentCompareCli.test.ts b/tests/boundary/cli/environmentCompareCli.test.ts index e8d2308939..a965ca12d3 100644 --- a/tests/boundary/cli/environmentCompareCli.test.ts +++ b/tests/boundary/cli/environmentCompareCli.test.ts @@ -55,31 +55,40 @@ const writeExactClockFiles = async ( ["GET", "/api/books/1", BOOK_NOT_FOUND, 404], ]); const fixture = environmentStateServeFixture(capture); - const ordered = analysisViewCaptureEvidence( - environmentManifestCaptureFixture([ - { - startedDateTime: "0001-01-01T00:00:00.0000000002Z", - request: { method: "GET", url: "http://app.example/api/books/1" }, - response: { - status: 404, - content: { - mimeType: "application/json", - text: JSON.stringify(BOOK_NOT_FOUND), + const chronologyProbe = (later: string, earlier: string) => + analysisViewCaptureEvidence( + environmentManifestCaptureFixture([ + { + startedDateTime: later, + request: { method: "GET", url: "http://app.example/api/books/1" }, + response: { + status: 404, + content: { + mimeType: "application/json", + text: JSON.stringify(BOOK_NOT_FOUND), + }, }, }, - }, - { - startedDateTime: "0001-01-01T01:00:00.0000000001+01:00", - request: { - method: "DELETE", - url: "http://app.example/api/books/1", + { + startedDateTime: earlier, + request: { + method: "DELETE", + url: "http://app.example/api/books/1", + }, + response: { + status: 204, + content: { mimeType: "application/json", text: "" }, + }, }, - response: { - status: 204, - content: { mimeType: "application/json", text: "" }, - }, - }, - ]), + ]), + ); + const ordered = chronologyProbe( + "0001-01-01T00:00:00.0000000002Z", + "0001-01-01T01:00:00.0000000001+01:00", + ); + const legacyOrdered = chronologyProbe( + "2026-10-20 10:00:00.0002+0100", + "2026-10-20 09:00:00.0001+0000", ); const clocks = timedCaptureEvidence([ [ @@ -93,6 +102,7 @@ const writeExactClockFiles = async ( ["manifest.json", fixture.manifest], ["capture.json", capture], ["order.json", ordered], + ["legacy-order.json", legacyOrdered], ["clocks.json", clocks], ] as const) { const path = join(root, name); @@ -338,6 +348,58 @@ describe("rea environment-compare value context", () => { ); }); +describe("rea environment-compare recorded clock spelling", () => { + cliTest( + "replays compact-offset submillisecond probe timestamps in exact order", + async ({ cli }) => { + const root = await createTestTempDirectory( + "rea-environment-legacy-chronology-", + ); + const originals = await writeExactClockFiles(root); + const result = await cli.run({ + arguments: [ + "environment-compare", + join(root, "manifest.json"), + "--evidence", + join(root, "capture.json"), + "--probe", + join(root, "legacy-order.json"), + "--json", + ], + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.json).toMatchObject({ + status: "complete", + environment: { state: "served" }, + summary: { probes: 1, steps: 2, equal: 2, agreement: 1 }, + probes: [ + { + label: "legacy-order.json", + results: [ + { + ordinal: 1, + method: "DELETE", + live_status: 204, + env_status: 204, + equal: true, + }, + { + ordinal: 0, + method: "GET", + live_status: 404, + env_status: 404, + equal: true, + }, + ], + }, + ], + }); + for (const [path, text] of originals) + expect(await readFile(path, "utf8")).toBe(text); + }, + ); +}); + describe("rea environment-compare replay and chronology", () => { cliTest( "preserves exact probe chronology and first-century clock-format differences", diff --git a/tests/boundary/cli/httpCaptureComparison.test.ts b/tests/boundary/cli/httpCaptureComparison.test.ts index e49d5758d3..86be2d58bf 100644 --- a/tests/boundary/cli/httpCaptureComparison.test.ts +++ b/tests/boundary/cli/httpCaptureComparison.test.ts @@ -1,86 +1,120 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; -import { expect, it } from "vitest"; +import { expect } from "vitest"; import { webPageInspectionSchema } from "../../../src/domain/browserObservationSchemas.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { startHttpComparisonBrowser } from "../../fixtures/httpComparisonBrowser.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -it("CLI compares incomplete HTTP responses without inventing changes", async () => { - const root = await createTestTempDirectory("rea-http-comparison-"); - const response = { - requestId: "request-1", - status: 200, - mimeType: "application/json", - }; - const fixture = await startHttpComparisonBrowser([response]); - try { - const inspect = async () => - webPageInspectionSchema.parse( - parseEvidence( - await runCliJson([ +cliTest( + "CLI compares incomplete HTTP responses without inventing changes", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-http-comparison-"); + const response = { + requestId: "request-1", + status: 200, + mimeType: "application/json", + }; + const fixture = await startHttpComparisonBrowser([response]); + try { + const inspect = () => + cli.run({ + arguments: [ "inspect-web-page", fixture.browser.endpoint, "allowed-page", "--observation-ms", "0", "--json", - ]), - ).normalized_result, + ], + environment: process.env, + }); + const pendingInvocation = await inspect(); + expect(pendingInvocation.exitCode, pendingInvocation.stderr).toBe(0); + expect(pendingInvocation.signal).toBeNull(); + const pending = webPageInspectionSchema.parse( + parseEvidence(pendingInvocation.json).normalized_result, ); - const pending = await inspect(); - fixture.setRequests([{ ...response, encodedDataLength: 100 }]); - const finished = await inspect(); - fixture.setRequests([{ ...response, status: 500, encodedDataLength: 100 }]); - const changed = await inspect(); - expect(pending.network.requests).toMatchObject([ - { status: 200, mime_type: "application/json", encoded_data_length: null }, - ]); - expect(finished.network.requests).toMatchObject([ - { status: 200, mime_type: "application/json", encoded_data_length: 100 }, - ]); - // Passive attachment cannot establish the complete same-URL request - // inventory. Closed imported captures can, and retain the positive oracle. - const closed = structuredClone(pending); - closed.completeness.attach_limited_sections = - closed.completeness.attach_limited_sections.filter( - (section) => section !== "network_requests", + fixture.setRequests([{ ...response, encodedDataLength: 100 }]); + const finishedInvocation = await inspect(); + expect(finishedInvocation.exitCode, finishedInvocation.stderr).toBe(0); + expect(finishedInvocation.signal).toBeNull(); + const finished = webPageInspectionSchema.parse( + parseEvidence(finishedInvocation.json).normalized_result, ); - const captures = [pending, finished, changed, closed]; - const paths = captures.map((_, index) => - join(root, `capture-${String(index)}.json`), - ); - await Promise.all( - captures.map((inspection, index) => - writeFile(paths[index]!, JSON.stringify({ inspection })), - ), - ); - for (const [left, right, status, count] of [ - [0, 1, "unknown", 0], - [1, 0, "unknown", 0], - [0, 2, "unknown", 0], - [3, 2, "changed", 1], - ] as const) { - const compared = await runCliJson([ - "compare-web-captures", - paths[left]!, - paths[right]!, - "--json", + fixture.setRequests([ + { ...response, status: 500, encodedDataLength: 100 }, ]); - expect(compared).toMatchObject({ - operation: "compare_web_captures", - normalized_result: { - overall_status: status, - dimensions: { - network: { status, total_changes: count }, - }, + const changedInvocation = await inspect(); + expect(changedInvocation.exitCode, changedInvocation.stderr).toBe(0); + expect(changedInvocation.signal).toBeNull(); + const changed = webPageInspectionSchema.parse( + parseEvidence(changedInvocation.json).normalized_result, + ); + expect(pending.network.requests).toMatchObject([ + { + status: 200, + mime_type: "application/json", + encoded_data_length: null, }, - }); + ]); + expect(finished.network.requests).toMatchObject([ + { + status: 200, + mime_type: "application/json", + encoded_data_length: 100, + }, + ]); + // Passive attachment cannot establish the complete same-URL request + // inventory. Closed imported captures can, and retain the positive oracle. + const closed = structuredClone(pending); + closed.completeness.attach_limited_sections = + closed.completeness.attach_limited_sections.filter( + (section) => section !== "network_requests", + ); + const captures = [pending, finished, changed, closed]; + const paths = captures.map((_, index) => + join(root, `capture-${String(index)}.json`), + ); + await Promise.all( + captures.map((inspection, index) => + writeFile(paths[index]!, JSON.stringify({ inspection })), + ), + ); + for (const [left, right, status, count] of [ + [0, 1, "unknown", 0], + [1, 0, "unknown", 0], + [0, 2, "unknown", 0], + [3, 2, "changed", 1], + ] as const) { + const comparedInvocation = await cli.run({ + arguments: [ + "compare-web-captures", + paths[left]!, + paths[right]!, + "--json", + ], + environment: process.env, + }); + expect(comparedInvocation.exitCode, comparedInvocation.stderr).toBe(0); + expect(comparedInvocation.signal).toBeNull(); + const compared = comparedInvocation.json; + expect(compared).toMatchObject({ + operation: "compare_web_captures", + normalized_result: { + overall_status: status, + dimensions: { + network: { status, total_changes: count }, + }, + }, + }); + } + } finally { + await fixture.browser.close(); } - } finally { - await fixture.browser.close(); - } -}, 60_000); + }, + 60_000, +); diff --git a/tests/boundary/cli/kunaDoctorHeldOutput.test.ts b/tests/boundary/cli/kunaDoctorHeldOutput.test.ts index a73dcbda9a..893fe70b02 100644 --- a/tests/boundary/cli/kunaDoctorHeldOutput.test.ts +++ b/tests/boundary/cli/kunaDoctorHeldOutput.test.ts @@ -33,6 +33,7 @@ fs.writeFileSync(${JSON.stringify(marker)}, JSON.stringify({ leaderPid: process.pid, descendantPid: child.pid, runId: process.env.REA_PROCESS_RUN_ID, + probeReadyAt: Date.now(), })); process.stdout.write("kuna 1.833\\n"); process.stderr.write("probe stderr\\n"); @@ -42,6 +43,7 @@ process.exit(0); ); await chmod(executable, 0o700); + const commandStartedAt = Date.now(); const pending = cli.run({ arguments: ["doctor", "--provider", "kuna", "--json"], environment: { @@ -67,7 +69,6 @@ process.exit(0); }, ); let ownership: OwnedProcessGroup | undefined; - const startedAt = Date.now(); try { let markerContents: string | undefined; for (;;) { @@ -90,13 +91,13 @@ process.exit(0); await delay(50); } if (markerContents === undefined) { - const markerWaitMs = Date.now() - startedAt; + const markerWaitMs = Date.now() - commandStartedAt; const result = await pending; expect( markerContents, `Kuna probe marker was absent when the CLI settled: ${JSON.stringify({ markerWaitMs, - cliElapsedMs: Date.now() - startedAt, + cliElapsedMs: Date.now() - commandStartedAt, ...result, })}`, ).toBeDefined(); @@ -106,6 +107,7 @@ process.exit(0); leaderPid: z.number().int().positive(), descendantPid: z.number().int().positive(), runId: z.string().min(1), + probeReadyAt: z.number().int().positive(), }) .parse(JSON.parse(markerContents!)); ownership = { @@ -114,8 +116,10 @@ process.exit(0); runId: observed.runId, }; + // Time the producer's ready phase, including any marker-observation + // delay. The existing command deadline separately bounds CLI startup. const result = await pending; - expect(Date.now() - startedAt).toBeLessThan(16_000); + expect(Date.now() - observed.probeReadyAt).toBeLessThan(16_000); expect(result.exitCode, result.stderr).toBe(1); expect(result.json).toMatchObject({ providerInspections: expect.arrayContaining([ diff --git a/tests/boundary/cli/nativePlistCli.test.ts b/tests/boundary/cli/nativePlistCli.test.ts index 9f0648c802..192f2337ab 100644 --- a/tests/boundary/cli/nativePlistCli.test.ts +++ b/tests/boundary/cli/nativePlistCli.test.ts @@ -281,7 +281,17 @@ describe.skipIf(process.platform !== "darwin")( ], }, bundle: { identifier: "com.example.types", executable: "App" }, - provenance: [{ tool: "file" }], + // An XML source also records its successful native format validation. + provenance: + format === "xml" + ? [ + { tool: "file" }, + { + tool: "plutil", + command: ["/usr/bin/plutil", "-lint", "--", "$PLIST"], + }, + ] + : [{ tool: "file" }], limitations: [expect.stringContaining("2 non-finite real")], }, }); diff --git a/tests/boundary/cli/nativePlistSelectionFailureCli.test.ts b/tests/boundary/cli/nativePlistSelectionFailureCli.test.ts index 2b9e9c088a..5caa814b8e 100644 --- a/tests/boundary/cli/nativePlistSelectionFailureCli.test.ts +++ b/tests/boundary/cli/nativePlistSelectionFailureCli.test.ts @@ -148,12 +148,18 @@ describe.skipIf(process.platform !== "darwin")( "native plist CLI decode diagnostics", () => { cliTest.for([ - "not a property list", - "value 42 ", - "valuex", - ])( + ["not a property list", "Property List error:"], + [ + "value 42 ", + "Unknown character", + ], + [ + "valuex", + "Encountered non-empty ", + ], + ] as const)( "preserves native decode diagnostics for %s", - async (source, { cli }) => { + async ([source, diagnostic], { cli }) => { const app = await fixtureApp(); const malformed = join(app, "Contents/Broken.plist"); await writeFile(malformed, source); @@ -171,15 +177,11 @@ describe.skipIf(process.platform !== "darwin")( expect(selected.json).toMatchObject({ code: "invalid_request", details: { - issues: [ - { message: expect.stringContaining("Property List error:") }, - ], + issues: [{ message: expect.stringContaining(diagnostic) }], captured_output: { exit_code: 1 }, }, }); - expect(capturedDiagnostic(selected.json)).toContain( - "Property List error:", - ); + expect(capturedDiagnostic(selected.json)).toContain(diagnostic); const directory = await createTestTempDirectory( "rea-plist-default-diagnostic-", ); @@ -196,13 +198,11 @@ describe.skipIf(process.platform !== "darwin")( expect(fallback.json).toMatchObject({ code: "capability_unavailable", details: { - reason: expect.stringContaining("Property List error:"), + reason: expect.stringContaining(diagnostic), captured_output: { exit_code: 1 }, }, }); - expect(capturedDiagnostic(fallback.json)).toContain( - "Property List error:", - ); + expect(capturedDiagnostic(fallback.json)).toContain(diagnostic); expect(await readFile(malformed, "utf8")).toBe(source); expect(await readFile(defaultPlist, "utf8")).toBe(source); }, @@ -214,11 +214,27 @@ describe.skipIf(process.platform !== "darwin")( "native plist CLI direct target selection", () => { cliTest.for([ - "value 42 ", - "valuex", - ])( + [ + "value 42 ", + "Unknown character", + ], + [ + "valuex", + "Encountered non-empty ", + ], + [ + "__proto__x", + "Encountered non-empty ", + ], + ["", "Encountered unknown tag null"], + ["", "Encountered empty plist tag"], + [ + "12", + "plist can only include one object", + ], + ] as const)( "classifies the selected malformed plist as input: %s", - async (source, { cli }) => { + async ([source, diagnostic], { cli }) => { const root = await createTestTempDirectory("rea-plist-target-input-"); const path = join(root, "chosen.plist"); await writeFile(path, source); @@ -234,15 +250,13 @@ describe.skipIf(process.platform !== "darwin")( issues: [ { path: ["path"], - message: expect.stringContaining("Property List error:"), + message: expect.stringContaining(diagnostic), }, ], captured_output: { exit_code: 1 }, }, }); - expect(capturedDiagnostic(result.json)).toContain( - "Property List error:", - ); + expect(capturedDiagnostic(result.json)).toContain(diagnostic); } expect(await readFile(path, "utf8")).toBe(source); }, @@ -250,6 +264,111 @@ describe.skipIf(process.platform !== "darwin")( }, ); +describe.skipIf(process.platform !== "darwin")( + "native plist CLI source admission", + () => { + cliTest( + "accepts native nested plist syntax without imposing an XML grammar allowlist", + async ({ cli }) => { + const root = await createTestTempDirectory( + "rea-plist-nested-admission-", + ); + const path = join(root, "chosen.plist"); + const source = "1"; + await writeFile(path, source); + const result = await cli.run({ + arguments: ["inspect-plist", path, "--json"], + environment, + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.json).toMatchObject({ + normalized_result: { + source_path: path, + provenance: expect.arrayContaining([ + expect.objectContaining({ + tool: "plutil", + command: ["/usr/bin/plutil", "-lint", "--", "$PLIST"], + }), + ]), + }, + }); + expect(await readFile(path, "utf8")).toBe(source); + }, + ); + cliTest( + "omits a valid prototype key after native validation", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-plist-valid-omission-"); + const path = join(root, "chosen.plist"); + const source = + "__proto__42keptvalue"; + await writeFile(path, source); + const result = await cli.run({ + arguments: ["inspect-plist", path, "--json"], + environment, + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.json).toMatchObject({ + normalized_result: { + value: { kept: "value" }, + provenance: [ + expect.objectContaining({ tool: "file" }), + expect.objectContaining({ + tool: "plutil", + command: ["/usr/bin/plutil", "-lint", "--", "$PLIST"], + }), + ], + limitations: expect.arrayContaining([ + expect.stringContaining( + "1 dictionary entry keyed __proto__ was omitted", + ), + ]), + }, + }); + expect(await readFile(path, "utf8")).toBe(source); + }, + ); + cliTest( + "retains original unknown scalar facts after successful native validation", + async ({ cli }) => { + const root = await createTestTempDirectory( + "rea-plist-source-admission-", + ); + const path = join(root, "chosen.plist"); + const source = + "value1e999"; + await writeFile(path, source); + const result = await cli.run({ + arguments: ["inspect-plist", path, "--json"], + environment, + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.json).toMatchObject({ + normalized_result: { + format: "xml", + source_path: path, + value: { + value: { + $plist_type: "unknown", + element: "real", + literal: "1e999", + reason: "unrepresentable", + }, + }, + provenance: expect.arrayContaining([ + expect.objectContaining({ + tool: "plutil", + command: ["/usr/bin/plutil", "-lint", "--", "$PLIST"], + }), + ]), + }, + }); + expect(await readFile(path, "utf8")).toBe(source); + }, + ); + }, +); + describe.skipIf(process.platform !== "darwin")( "native plist CLI fixed allowances", () => { diff --git a/tests/boundary/cli/sharedJavaScriptCliSession.test.ts b/tests/boundary/cli/sharedJavaScriptCliSession.test.ts index 3794f70486..6ef66cc142 100644 --- a/tests/boundary/cli/sharedJavaScriptCliSession.test.ts +++ b/tests/boundary/cli/sharedJavaScriptCliSession.test.ts @@ -1,7 +1,8 @@ import { EventEmitter, PassThrough } from "node:stream"; -import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { fileURLToPath } from "node:url"; -import { expect, test } from "vitest"; +import { expect, onTestFinished, test } from "vitest"; import { z } from "zod"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; @@ -126,8 +127,13 @@ test("stops an unready shared CLI process when the session closes", async () => command(["--version"], { PATH: "/usr/bin", REA_LOG_LEVEL: "silent" }), ); await waitFor(() => spawned !== undefined); - await session.close(); - await expect(pending).rejects.toThrow("Shared CLI session is closed"); + const rejected = expect(pending).rejects.toThrow( + "Shared CLI session is closed", + ); + const closing = session.close(); + spawned?.stdout.write(encodeCliSessionFrame({ ready: true })); + await closing; + await rejected; expect(spawned?.signalCode).toBe("SIGTERM"); await expect( session.run( @@ -155,8 +161,11 @@ test("stops an in-flight shared CLI command when the session closes", async () = command(["--version"], { PATH: "/usr/bin", REA_LOG_LEVEL: "silent" }), ); await waitFor(() => received); + const rejected = expect(pending).rejects.toThrow( + "Shared CLI session is closed", + ); await session.close(); - await expect(pending).rejects.toThrow("Shared CLI session is closed"); + await rejected; expect(spawned?.signalCode).toBe("SIGTERM"); }); @@ -249,6 +258,369 @@ const respondingProcess = ( return child; }; +test("recovers after a shared CLI command exceeds its selected deadline", async () => { + const children: FakeCliProcess[] = []; + const session = createCliCommandSession({ + requestTimeoutMs: 20, + shutdownGraceMs: 20, + spawnWorker: () => { + const child = respondingProcess( + 130 + children.length, + (request, current) => { + if (children.length > 1) + reply(current, request.id, { stdout: "recovered" }); + }, + ); + children.push(child); + return asCliProcess(child); + }, + }); + await expect( + session.run(command(["stuck"], { PATH: "/usr/bin" })), + ).rejects.toThrow(/did not respond/); + expect(children[0]?.signalCode).not.toBeNull(); + await expect( + session.run(command(["next"], { PATH: "/usr/bin" })), + ).resolves.toMatchObject({ stdout: "recovered" }); + expect(children).toHaveLength(2); + await session.close(); +}); + +test("times out a shared CLI command whose stdin write callback stays pending", async () => { + const children: FakeCliProcess[] = []; + let releaseWrite: (() => void) | undefined; + let observeWrite: (() => void) | undefined; + const writing = new Promise((resolve) => { + observeWrite = resolve; + }); + const session = createCliCommandSession({ + requestTimeoutMs: 20, + shutdownGraceMs: 20, + spawnWorker: () => { + const child = respondingProcess( + 135 + children.length, + (request, current) => + reply(current, request.id, { stdout: "recovered" }), + ); + if (children.length === 0) { + child.stdin._write = (_chunk, _encoding, callback) => { + releaseWrite = () => callback(); + observeWrite?.(); + }; + } + children.push(child); + return asCliProcess(child); + }, + }); + const failed = expect( + session.run(command(["stuck"], { PATH: "/usr/bin" })), + ).rejects.toThrow(/did not respond/); + await writing; + expect(releaseWrite).toEqual(expect.any(Function)); + await failed; + expect(children[0]?.signalCode).not.toBeNull(); + releaseWrite?.(); + await expect( + session.run(command(["next"], { PATH: "/usr/bin" })), + ).resolves.toMatchObject({ stdout: "recovered" }); + expect(children).toHaveLength(2); + await session.close(); +}); + +test("recovers after a shared CLI worker misses its selected readiness deadline", async () => { + const children: FakeCliProcess[] = []; + const session = createCliCommandSession({ + startTimeoutMs: 20, + shutdownGraceMs: 20, + spawnWorker: () => { + const child = + children.length === 0 + ? fakeProcess(140) + : respondingProcess(141, (request, current) => + reply(current, request.id, { stdout: "ready" }), + ); + children.push(child); + return asCliProcess(child); + }, + }); + await expect( + session.run(command(["stuck"], { PATH: "/usr/bin" })), + ).rejects.toThrow(/did not become ready/); + expect(children[0]?.signalCode).not.toBeNull(); + await expect( + session.run(command(["next"], { PATH: "/usr/bin" })), + ).resolves.toMatchObject({ stdout: "ready" }); + expect(children).toHaveLength(2); + await session.close(); +}); + +test("waits for the timed-out shared CLI worker to exit before starting its replacement", async () => { + const children: FakeCliProcess[] = []; + let releaseExit: (() => void) | undefined; + let observeStop: (() => void) | undefined; + const stopping = new Promise((resolve) => { + observeStop = resolve; + }); + const session = createCliCommandSession({ + requestTimeoutMs: 20, + shutdownGraceMs: 100, + spawnWorker: () => { + const child = respondingProcess( + 150 + children.length, + (request, current) => { + if (children.length > 1) + reply(current, request.id, { stdout: "next" }); + }, + ); + if (children.length === 0) { + const stop = child.kill.bind(child); + child.kill = (signal: NodeJS.Signals = "SIGTERM") => { + releaseExit = () => stop(signal); + observeStop?.(); + return true; + }; + } + children.push(child); + return asCliProcess(child); + }, + }); + const stalled = expect( + session.run(command(["stuck"], { PATH: "/usr/bin" })), + ).rejects.toThrow(/did not respond/); + const next = session.run(command(["next"], { PATH: "/usr/bin" })); + await stopping; + expect(children).toHaveLength(1); + releaseExit?.(); + await stalled; + await expect(next).resolves.toMatchObject({ stdout: "next" }); + expect(children).toHaveLength(2); + await session.close(); +}); + +test("retains a shared CLI worker that survives SIGKILL and retries its exact owner on close", async () => { + const children: FakeCliProcess[] = []; + const signals: NodeJS.Signals[] = []; + const session = createCliCommandSession({ + shutdownGraceMs: 20, + spawnWorker: () => { + const child = respondingProcess( + 160 + children.length, + (request, current) => { + reply(current, request.id, { + exitCode: 1, + stderr: "fatal", + fatal: true, + }); + }, + ); + child.kill = (signal: NodeJS.Signals = "SIGTERM") => { + signals.push(signal); + return true; + }; + children.push(child); + return asCliProcess(child); + }, + }); + await expect( + session.run(command(["fatal"], { PATH: "/usr/bin" })), + ).rejects.toThrow(/did not exit after SIGKILL/); + await expect( + session.run(command(["blocked"], { PATH: "/usr/bin" })), + ).rejects.toThrow(/did not exit after SIGKILL/); + expect(children).toHaveLength(1); + await expect(session.close()).rejects.toThrow(/did not exit after SIGKILL/); + const retained = children[0]; + if (retained === undefined) throw new Error("missing retained worker"); + const stopsBeforeRecovery = signals.length; + retained.kill = (signal: NodeJS.Signals = "SIGTERM") => { + signals.push(signal); + retained.signalCode = signal; + retained.emit("exit", null, signal); + retained.emit("close", null, signal); + return true; + }; + await session.close(); + expect(signals.length).toBeGreaterThan(stopsBeforeRecovery); + expect(retained.signalCode).not.toBeNull(); + expect(children).toHaveLength(1); + await session.close(); +}); + +test.each([ + { + phase: "command", + reason: /did not respond/, + }, + { + phase: "startup", + reason: /did not become ready/, + }, +])( + "preserves the $phase timeout when shared CLI cleanup also fails", + async ({ phase, reason }) => { + const children: FakeCliProcess[] = []; + const signals: NodeJS.Signals[] = []; + const session = createCliCommandSession({ + ...(phase === "command" + ? { requestTimeoutMs: 20 } + : { startTimeoutMs: 20 }), + shutdownGraceMs: 20, + spawnWorker: () => { + const child = + phase === "command" + ? respondingProcess(190, () => undefined) + : fakeProcess(190); + child.kill = (signal: NodeJS.Signals = "SIGTERM") => { + signals.push(signal); + return true; + }; + children.push(child); + return asCliProcess(child); + }, + }); + const failure: unknown = await session + .run(command(["stuck"], { PATH: "/usr/bin" })) + .catch((error: unknown) => error); + expect(failure).toBeInstanceOf(AggregateError); + if (!(failure instanceof AggregateError)) + throw new Error("missing combined command and cleanup failure", { + cause: failure, + }); + expect(failure.message).toMatch(reason); + expect(failure.message).toMatch(/did not exit after SIGKILL/); + expect(failure.errors).toHaveLength(2); + expect(failure.errors[0]).toBeInstanceOf(Error); + expect(failure.errors[0]).toHaveProperty( + "message", + expect.stringMatching(reason), + ); + expect(failure.errors[1]).toHaveProperty( + "message", + expect.stringMatching(/did not exit after SIGKILL/), + ); + expect(failure.cause).toBe(failure.errors[0]); + expect(signals).toEqual(["SIGTERM", "SIGKILL"]); + await expect( + session.run(command(["blocked"], { PATH: "/usr/bin" })), + ).rejects.toThrow(/did not exit after SIGKILL/); + expect(children).toHaveLength(1); + await expect(session.close()).rejects.toThrow(/did not exit after SIGKILL/); + const retained = children[0]; + if (retained === undefined) throw new Error("missing retained worker"); + retained.kill = (signal: NodeJS.Signals = "SIGTERM") => { + signals.push(signal); + retained.signalCode = signal; + retained.emit("exit", null, signal); + retained.emit("close", null, signal); + return true; + }; + await session.close(); + expect(retained.signalCode).not.toBeNull(); + expect(children).toHaveLength(1); + }, +); + +test("confirms shared CLI shutdown after escalating an ignored SIGTERM", async () => { + let child: FakeCliProcess | undefined; + const signals: NodeJS.Signals[] = []; + const session = createCliCommandSession({ + shutdownGraceMs: 20, + spawnWorker: () => { + const started = respondingProcess(170, (request, current) => + reply(current, request.id, { stdout: "done" }), + ); + const stop = started.kill.bind(started); + started.kill = (signal: NodeJS.Signals = "SIGTERM") => { + signals.push(signal); + return signal === "SIGKILL" ? stop(signal) : true; + }; + child = started; + return asCliProcess(started); + }, + }); + await expect( + session.run(command(["once"], { PATH: "/usr/bin" })), + ).resolves.toMatchObject({ stdout: "done" }); + await session.close(); + expect(signals).toEqual(["SIGTERM", "SIGKILL"]); + expect(child?.signalCode).toBe("SIGKILL"); + await session.close(); +}); + +test("lets the shared CLI worker finish its resource cleanup before signaling", async () => { + const events: string[] = []; + const session = createCliCommandSession({ + spawnWorker: () => { + const child = respondingProcess(180, (request, current) => + reply(current, request.id, { stdout: "done" }), + ); + child.stdin.once("finish", () => { + events.push("stdin closed", "resources released"); + child.exitCode = 0; + child.emit("exit", 0, null); + child.emit("close", 0, null); + }); + const stop = child.kill.bind(child); + child.kill = (signal: NodeJS.Signals = "SIGTERM") => { + events.push(signal); + return stop(signal); + }; + return asCliProcess(child); + }, + }); + await expect( + session.run(command(["once"], { PATH: "/usr/bin" })), + ).resolves.toMatchObject({ stdout: "done" }); + await session.close(); + expect(events).toEqual(["stdin closed", "resources released"]); + await session.close(); + expect(events).toHaveLength(2); +}); + +test("shares one shared CLI cleanup attempt across concurrent close calls", async () => { + let cleanupStarts = 0; + let releaseExit: (() => void) | undefined; + let observeCleanup: (() => void) | undefined; + const cleaning = new Promise((resolve) => { + observeCleanup = resolve; + }); + const signals: NodeJS.Signals[] = []; + const session = createCliCommandSession({ + spawnWorker: () => { + const child = respondingProcess(185, (request, current) => + reply(current, request.id, { stdout: "done" }), + ); + child.stdin.once("finish", () => { + cleanupStarts += 1; + releaseExit = () => { + child.exitCode = 0; + child.emit("exit", 0, null); + child.emit("close", 0, null); + }; + observeCleanup?.(); + }); + const stop = child.kill.bind(child); + child.kill = (signal: NodeJS.Signals = "SIGTERM") => { + signals.push(signal); + return stop(signal); + }; + return asCliProcess(child); + }, + }); + await session.run(command(["once"], { PATH: "/usr/bin" })); + const first = session.close(); + const second = session.close(); + expect(second).toBe(first); + await cleaning; + expect(cleanupStarts).toBe(1); + expect(signals).toEqual([]); + releaseExit?.(); + await Promise.all([first, second]); + await session.close(); + expect(cleanupStarts).toBe(1); + expect(signals).toEqual([]); +}); + test("starts another process after the shared CLI process exits", async () => { const children: FakeCliProcess[] = []; const session = createCliCommandSession({ @@ -457,6 +829,81 @@ test("starts a new process after the real worker reports a fatal failure", async await session.close(); }); +test("keeps a malformed JSON command's failure status and serves the next command", async () => { + const session = createCliCommandSession(); + onTestFinished(() => session.close()); + const environment = { + PATH: process.env.PATH ?? "", + REA_LOG_LEVEL: "silent", + }; + const failed = await session.run( + command(["compare-javascript-export-shapes", "{", "--json"], environment), + ); + expect(failed.exitCode).toBe(1); + expect(failed.stdout).toContain("invalid_request"); + const worker = session.processId(); + expect(worker).toEqual(expect.any(Number)); + const root = await createTestTempDirectory("rea-warm-cli-status-"); + await writeFixtureFiles(root, { + "app.mjs": "export function current() { return 1; }\n", + }); + const recovered = await session.run( + command(["analyze-javascript-application", root, "--json"], environment), + ); + expect(recovered.exitCode, recovered.stderr).toBe(0); + expect(recovered.json).toMatchObject({ + normalized_result: expect.any(Object), + }); + expect(session.processId()).toBe(worker); + await session.close(); +}); + +test("closes the real JavaScript CLI worker through its resource cleanup", async () => { + let worker: ChildProcessWithoutNullStreams | undefined; + const session = createCliCommandSession({ + spawnWorker: (environment) => { + worker = spawn( + process.execPath, + [ + fileURLToPath( + new URL( + "../../support/cli/cliCommandSessionWorker.mjs", + import.meta.url, + ), + ), + ], + { + cwd: process.cwd(), + env: environment, + stdio: ["pipe", "pipe", "pipe"], + }, + ); + return worker; + }, + }); + onTestFinished(() => session.close()); + const root = await createTestTempDirectory("rea-warm-cli-graceful-"); + await writeFixtureFiles(root, { + "app.mjs": "export function current() { return 1; }\n", + }); + const analyzed = await session.run( + command(["analyze-javascript-application", root, "--json"], { + PATH: process.env.PATH ?? "", + REA_LOG_LEVEL: "silent", + }), + ); + expect(analyzed.exitCode, analyzed.stderr).toBe(0); + expect(analyzed.json).toMatchObject({ + normalized_result: expect.any(Object), + }); + expect(worker?.exitCode).toBeNull(); + expect(worker?.signalCode).toBeNull(); + await session.close(); + expect(worker?.exitCode).toBe(0); + expect(worker?.signalCode).toBeNull(); + expect(session.processId()).toBeUndefined(); +}); + const requireSharedCli = (cli: { sharedCliRun?: (invocation: CliInvocation) => Promise; }): ((invocation: CliInvocation) => Promise) => { diff --git a/tests/boundary/cli/wasmCleanupCli.test.ts b/tests/boundary/cli/wasmCleanupCli.test.ts index 4486059b36..405b511b42 100644 --- a/tests/boundary/cli/wasmCleanupCli.test.ts +++ b/tests/boundary/cli/wasmCleanupCli.test.ts @@ -3,18 +3,18 @@ import { execFileSync } from "node:child_process"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; -import { expect, it } from "vitest"; +import { expect } from "vitest"; import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const canDenyCleanup = process.platform !== "win32" && process.getuid?.() !== 0; -it.runIf(canDenyCleanup)( +cliTest.runIf(canDenyCleanup)( "retains wasm evidence and primary diagnostics when real scratch cleanup is denied", - async () => { + async ({ cli }) => { const directory = await createTestTempDirectory("rea-wasm-cleanup-cli-"); const tools = join(directory, "tools"); const artifact = join(directory, "module.wasm"); @@ -63,9 +63,12 @@ it.runIf(canDenyCleanup)( TEMP: directory, }; const invoke = (extra: NodeJS.ProcessEnv = {}) => - runCliJson(["inspect-wasm-artifact", artifact, "--format", "json"], { - ...environment, - ...extra, + cli.run({ + arguments: ["inspect-wasm-artifact", artifact, "--format", "json"], + environment: { + ...environment, + ...extra, + }, }); const restoreDeniedDirectories = async () => { let paths: string[] = []; @@ -84,11 +87,22 @@ it.runIf(canDenyCleanup)( }; try { - const control = parseEvidence(await invoke()); + const controlInvocation = await invoke(); + expect(controlInvocation.exitCode, controlInvocation.stderr).toBe(0); + expect(controlInvocation.signal).toBeNull(); + const control = parseEvidence(controlInvocation.json); expect(control.normalized_result).toMatchObject({ validation: "valid" }); + const completedThenCleanupFailsInvocation = await invoke({ + REA_DENY_CLEANUP: "1", + }); + expect( + completedThenCleanupFailsInvocation.exitCode, + completedThenCleanupFailsInvocation.stderr, + ).toBe(1); + expect(completedThenCleanupFailsInvocation.signal).toBeNull(); const completedThenCleanupFails = analysisErrorProjectionSchema.parse( - await invoke({ REA_DENY_CLEANUP: "1" }), + completedThenCleanupFailsInvocation.json, ); expect(completedThenCleanupFails).toMatchObject({ code: "cleanup_incomplete", @@ -102,8 +116,16 @@ it.runIf(canDenyCleanup)( await restoreDeniedDirectories(); await writeFile(locked, ""); + const primaryAndCleanupFailInvocation = await invoke({ + REA_PRIMARY_FAILURE: "1", + }); + expect( + primaryAndCleanupFailInvocation.exitCode, + primaryAndCleanupFailInvocation.stderr, + ).toBe(1); + expect(primaryAndCleanupFailInvocation.signal).toBeNull(); const primaryAndCleanupFail = analysisErrorProjectionSchema.parse( - await invoke({ REA_PRIMARY_FAILURE: "1" }), + primaryAndCleanupFailInvocation.json, ); expect(primaryAndCleanupFail).toMatchObject({ code: "cleanup_incomplete", diff --git a/tests/boundary/filesystem/binaryTarget.test.ts b/tests/boundary/filesystem/binaryTarget.test.ts index 22b946e333..0a417aea13 100644 --- a/tests/boundary/filesystem/binaryTarget.test.ts +++ b/tests/boundary/filesystem/binaryTarget.test.ts @@ -32,6 +32,10 @@ import { parseExecutableHeader } from "../../../src/domain/binaryTarget.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { javaClassFile } from "../../fixtures/javaClassFile.js"; +import { + MAX_JVM_CLASS_IDENTITY_BYTES, + JVM_CLASS_IDENTITY_LIMIT_REASON, +} from "../../../src/domain/apple/jvmClassFile.js"; import { arArchive, CPU, @@ -41,12 +45,22 @@ import { codeSignatureCommand, } from "../../../src/artifacts/apple/MachoImage.fixture.js"; -it.each([4000, 9000])( - "refuses a JVM class with %i unused constant bytes without claiming FAT identity", - async (constantBytes) => { +it.each([ + [4000, false], + [9000, false], + [4000, true], + [9000, true], +] as const)( + "refuses a JVM class with %i unused constant bytes and CPU lookalike %s without claiming FAT identity", + async (constantBytes, fatCpuLookalike) => { const directory = await createTestTempDirectory("rea-class-target-"); const path = join(directory, "input.class"); - await writeFile(path, javaClassFile(constantBytes)); + const bytes = javaClassFile(constantBytes, fatCpuLookalike); + expect(parseExecutableHeader(bytes, "arm64")).toEqual({ + ok: false, + error: "unsupported binary format", + }); + await writeFile(path, bytes); const resolved = await parseBinaryTarget(path); expect(resolved).toMatchObject({ ok: false, @@ -61,6 +75,35 @@ it.each([4000, 9000])( }, ); +it.each([MAX_JVM_CLASS_IDENTITY_BYTES, MAX_JVM_CLASS_IDENTITY_BYTES + 1])( + "preserves JVM identity uncertainty at the class scan boundary for %i bytes", + async (size) => { + const directory = await createTestTempDirectory("rea-class-boundary-"); + const path = join(directory, "input.class"); + const classPrefix = javaClassFile(4000, true); + classPrefix.writeUInt16BE(1, classPrefix.length - 2); + const attribute = Buffer.alloc(6); + attribute.writeUInt16BE(5, 0); + attribute.writeUInt32BE(size - classPrefix.length - attribute.length, 2); + const handle = await open(path, "w"); + try { + await handle.write(Buffer.concat([classPrefix, attribute])); + await handle.truncate(size); + } finally { + await handle.close(); + } + expect(await parseBinaryTarget(path)).toMatchObject({ + ok: false, + error: { + reason: + size === MAX_JVM_CLASS_IDENTITY_BYTES + ? "unsupported binary format" + : JVM_CLASS_IDENTITY_LIMIT_REASON, + }, + }); + }, +); + it.each([0x01c0, 0x01c2, 0x01c4, 0xaa64])( "retains original PE COFF machine %i through file-backed target resolution", async (peMachine) => { diff --git a/tests/boundary/filesystem/ciChangeScope.test.ts b/tests/boundary/filesystem/ciChangeScope.test.ts index 6939bbf0a6..ce3217f79b 100644 --- a/tests/boundary/filesystem/ciChangeScope.test.ts +++ b/tests/boundary/filesystem/ciChangeScope.test.ts @@ -1246,6 +1246,50 @@ it.each([ "tests/conformance/readiness/javascript-cli/package.json", { code: true, fixtures: false, readiness: true, package: false }, ], + [ + "scripts/verify-reconstruction-readiness.mjs", + { + code: true, + readiness: true, + fixtures: false, + runtime: false, + package: false, + }, + ], + [ + "scripts/lib/cli-process-environment.mjs", + { + code: true, + package: true, + runtime: true, + javascript_recovery: true, + readiness: false, + }, + ], + [ + "tests/conformance/readiness/report/reconstructionReadiness.ts", + { + code: true, + readiness: true, + fixtures: false, + runtime: false, + package: false, + }, + ], + [ + "tests/conformance/readiness/report/reconstructionReadinessExample.ts", + { + code: true, + readiness: true, + fixtures: false, + runtime: false, + package: false, + }, + ], + [ + "tests/conformance/readiness/report/reconstructionReadiness.test.ts", + { code: true, readiness: false, runtime: false, package: false }, + ], [ "tests/conformance/ghidra/no-return.c", { code: true, fixtures: false, readiness: false, package: false }, diff --git a/tests/boundary/filesystem/ciTestProjects.test.ts b/tests/boundary/filesystem/ciTestProjects.test.ts index 5641f700de..ac5c481dd1 100644 --- a/tests/boundary/filesystem/ciTestProjects.test.ts +++ b/tests/boundary/filesystem/ciTestProjects.test.ts @@ -78,37 +78,33 @@ describe("canonical test-project ownership", () => { }); it.each([ - { flags: [], heap: 384, timeout: 30_000, lines: 68 }, - { flags: ["--coverage"], heap: 512, timeout: 60_000, lines: 68 }, - { flags: ["--coverage.enabled"], heap: 512, timeout: 60_000, lines: 68 }, - { flags: ["--coverage=false"], heap: 384, timeout: 30_000, lines: 68 }, + { flags: [], timeout: 30_000, lines: 68 }, + { flags: ["--coverage"], timeout: 60_000, lines: 68 }, + { flags: ["--coverage.enabled"], timeout: 60_000, lines: 68 }, + { flags: ["--coverage=false"], timeout: 30_000, lines: 68 }, { flags: ["--coverage.enabled=false"], - heap: 384, timeout: 30_000, lines: 68, }, { flags: ["--coverage.provider=v8"], - heap: 384, timeout: 30_000, lines: 68, }, { flags: ["--coverage.enabled", "--shard", "1/6"], - heap: 512, timeout: 60_000, lines: 0, }, { flags: ["--coverage.enabled=false", "--shard=1/6"], - heap: 384, timeout: 30_000, lines: 0, }, ])( "resolves worker resources from actual CLI options: $flags", - async ({ flags, heap, timeout, lines }) => { + async ({ flags, timeout, lines }) => { const root = await createTestTempDirectory("rea-vitest-worker-policy-"); const script = join(root, "worker-policy.mjs"); await writeFile( @@ -134,7 +130,7 @@ describe("canonical test-project ownership", () => { }, ); expect(JSON.parse(result.stdout)).toEqual({ - execArgv: [`--max-old-space-size=${String(heap)}`], + execArgv: ["--max-old-space-size=512"], timeout, lines, }); diff --git a/tests/boundary/filesystem/cliJsonInput.test.ts b/tests/boundary/filesystem/cliJsonInput.test.ts index d2bdcd5403..097811fcd5 100644 --- a/tests/boundary/filesystem/cliJsonInput.test.ts +++ b/tests/boundary/filesystem/cliJsonInput.test.ts @@ -7,6 +7,7 @@ import { describe, expect, it, onTestFinished } from "vitest"; import { createCli } from "../../../src/cli.js"; import { parseCliJsonInput } from "../../../src/cliJsonInput.js"; +import { cliJavaScriptSourcePaths } from "../../../src/cli/javascriptApplicationAnalysis.js"; import { readJsonInputFile } from "../../../src/application/JsonInputFile.js"; import { analysisCliErrorEnvelopeSchema } from "../../../src/contracts/errorSchemas.js"; import { JSON_BYTE_ORDER_MARK_MESSAGE } from "../../../src/application/Utf8JsonInput.js"; @@ -307,3 +308,41 @@ describe("CLI JSON input", () => { }, ); }); + +describe("CLI JSON file cancellation", () => { + const writeSelection = async (): Promise => { + const root = await createTestTempDirectory("rea-json-input-cancel-"); + const path = join(root, "paths.json"); + await writeFile(path, JSON.stringify(["main.js", "package.json"])); + return path; + }; + + it("reports a cancelled file read as cancellation, not a file failure", async () => { + const path = await writeSelection(); + const controller = new AbortController(); + controller.abort(); + expect( + await parseCliJsonInput(path, "test-input", controller.signal), + ).toMatchObject({ ok: false, error: { code: "cancelled" } }); + }); + + it("reports cancellation through --source-paths-json selection", async () => { + const path = await writeSelection(); + const controller = new AbortController(); + controller.abort(); + expect( + await cliJavaScriptSourcePaths(["index.js"], path, controller.signal), + ).toMatchObject({ ok: false, error: { code: "cancelled" } }); + }); + + it("still reads the selection when the signal is not aborted", async () => { + const path = await writeSelection(); + expect( + await cliJavaScriptSourcePaths( + ["index.js"], + path, + new AbortController().signal, + ), + ).toEqual({ ok: true, value: ["index.js", "main.js", "package.json"] }); + }); +}); diff --git a/tests/boundary/filesystem/clientRegistrationOwnership.test.ts b/tests/boundary/filesystem/clientRegistrationOwnership.test.ts new file mode 100644 index 0000000000..efc85ad9aa --- /dev/null +++ b/tests/boundary/filesystem/clientRegistrationOwnership.test.ts @@ -0,0 +1,155 @@ +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; + +import { parse as parseToml } from "smol-toml"; +import { expect, it } from "vitest"; + +import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js"; +import { systemUninstallHost } from "../../../src/application/Uninstall.js"; +import { PRODUCT_IDENTITY } from "../../../src/identity.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const codexSource = (invalid: string): string => + [ + 'caller_root = "keep"', + "[mcp_servers.rea]", + `command = "${PRODUCT_IDENTITY.cliBinary}"`, + 'args = ["mcp"]', + invalid, + "[mcp_servers.peer]", + 'command = "peer"', + "args = []", + "", + ].join("\n"); + +it.each([ + { + client: "codex", + path: ".codex/config.toml", + invalid: "invalid tool policy", + source: codexSource('enabled_tools = "not-an-array"'), + parse: (text: string): unknown => parseToml(text), + remaining: { + caller_root: "keep", + mcp_servers: { peer: { command: "peer", args: [] } }, + }, + }, + { + client: "codex", + path: ".codex/config.toml", + invalid: "a nonpositive startup timeout", + source: codexSource("startup_timeout_sec = 0"), + parse: (text: string): unknown => parseToml(text), + remaining: { + caller_root: "keep", + mcp_servers: { peer: { command: "peer", args: [] } }, + }, + }, + { + client: "opencode", + path: ".config/opencode/opencode.json", + invalid: "a non-string environment value", + source: JSON.stringify({ + caller_root: "keep", + mcp: { + servers: { + rea: { + type: "local", + command: [PRODUCT_IDENTITY.cliBinary, "mcp"], + environment: { CALLER_SETTING: 42 }, + }, + peer: { type: "local", command: ["peer"] }, + }, + }, + }), + parse: (text: string): unknown => JSON.parse(text), + remaining: { + caller_root: "keep", + mcp: { servers: { peer: { type: "local", command: ["peer"] } } }, + }, + }, +])( + "removes the owned $client registration despite $invalid", + async (sample) => { + const home = await createTestTempDirectory("rea-registration-ownership-"); + const configPath = join(home, sample.path); + const environment = { + CODEX_HOME: join(home, ".codex"), + XDG_CONFIG_HOME: join(home, ".config"), + }; + await mkdir(dirname(configPath), { recursive: true }); + await writeFile(configPath, sample.source); + const statuses = await readClientRegistrationStatuses(home, undefined, { + environment, + }); + expect( + statuses.find(({ client }) => client === sample.client), + ).toMatchObject({ + state: "invalid", + }); + const host = systemUninstallHost(home, undefined, environment); + try { + const client = (await host.clients()).find( + (client) => + client.name === sample.client && client.configPath === configPath, + ); + if (client === undefined) + throw new Error("Expected the configured client"); + expect(await host.removeClient(client)).toMatchObject({ + name: sample.client, + status: "removed", + }); + expect(sample.parse(await readFile(configPath, "utf8"))).toEqual( + sample.remaining, + ); + expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe( + sample.source, + ); + } finally { + expect(await host.close?.()).toBeUndefined(); + } + }, +); + +it("retains an OpenCode remote registration with a REA-like command", async () => { + const home = await createTestTempDirectory("rea-registration-remote-owner-"); + const configPath = join(home, ".config/opencode/opencode.json"); + const environment = { XDG_CONFIG_HOME: join(home, ".config") }; + const source = JSON.stringify({ + caller_root: "keep", + mcp: { + servers: { + rea: { + type: "remote", + url: "https://caller.example/mcp", + command: [PRODUCT_IDENTITY.cliBinary, "mcp"], + }, + peer: { type: "local", command: ["peer"] }, + }, + }, + }); + await mkdir(dirname(configPath), { recursive: true }); + await writeFile(configPath, source); + const statuses = await readClientRegistrationStatuses(home, undefined, { + environment, + }); + expect(statuses.find(({ client }) => client === "opencode")).toMatchObject({ + state: "invalid", + }); + const host = systemUninstallHost(home, undefined, environment); + try { + const client = (await host.clients()).find( + (client) => + client.name === "opencode" && client.configPath === configPath, + ); + if (client === undefined) throw new Error("Expected the configured client"); + expect(await host.removeClient(client)).toMatchObject({ + name: "opencode", + status: "retained", + }); + expect(await readFile(configPath, "utf8")).toBe(source); + expect(await readdir(dirname(configPath))).toEqual(["opencode.json"]); + } finally { + expect(await host.close?.()).toBeUndefined(); + } +}); diff --git a/tests/boundary/filesystem/harmony/application.test.ts b/tests/boundary/filesystem/harmony/application.test.ts index 6b4625608f..ddfa250039 100644 --- a/tests/boundary/filesystem/harmony/application.test.ts +++ b/tests/boundary/filesystem/harmony/application.test.ts @@ -36,44 +36,6 @@ const stageHapEntries = [ { path: "META-INF/FIXTURE.p7b", content: "opaque signing" }, ] as const; -it.each(["hap", "app"] as const)( - "retains encrypted path-only packaging facts for a %s inventory", - async (suffix) => { - const root = await createTestTempDirectory("rea-harmony-encrypted-paths-"); - const path = join(root, `Fixture.${suffix}`); - const writer = new ZipWriter(new Uint8ArrayWriter()); - const member = suffix === "app" ? "entry.hap" : "module.json"; - await writer.add(member, new TextReader("opaque inaccessible content"), { - password: "fixture-only", - }); - await writeFile(path, await writer.close()); - const inventory = await inventoryApplicationPackage(path); - const graph = requireDeterministicProjection( - projectHarmonyApplicationEvidence, - inventory, - harmonyApplicationProjectionResultSchema, - ); - expect(graph.unavailable_components).toContainEqual( - expect.objectContaining({ - path: member, - artifact_id: null, - sha256: null, - encrypted: true, - }), - ); - expect(graph.coverage.status).toBe("partial"); - if (suffix === "app") - expect(graph.app_pack_children).toContainEqual( - expect.objectContaining({ - path: member, - artifact_id: null, - sha256: null, - }), - ); - else expect(graph.packaging_model).toBe("stage"); - }, -); - it("retains inventoried certificate-block suffixes without claiming verified signatures", async () => { const names = ["CERT.RSA", "CERT.DSA", "CERT.EC", "FIXTURE.p7b"]; const path = await writeApplicationZip( diff --git a/tests/boundary/filesystem/hopperFatCoordinates.test.ts b/tests/boundary/filesystem/hopperFatCoordinates.test.ts new file mode 100644 index 0000000000..87ea3e1ff7 --- /dev/null +++ b/tests/boundary/filesystem/hopperFatCoordinates.test.ts @@ -0,0 +1,229 @@ +import { createHash } from "node:crypto"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect, it, onTestFinished } from "vitest"; + +import { + CPU, + fatImage, + machoImage, +} from "../../../src/artifacts/apple/MachoImage.fixture.js"; +import type { BinaryTarget } from "../../../src/domain/binaryTargetTypes.js"; +import { mapHopperFileOffset } from "../../../src/hopper/HopperFileOffset.js"; +import { resolveHopperMachOImage } from "../../../src/hopper/HopperMachOImage.js"; +import { createInspectionResources } from "../../fixtures/inspectionResources.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const sourceTarget = async (bytes: Uint8Array, sliceSize: number) => { + const directory = await createTestTempDirectory( + "rea-hopper-fat-coordinates-", + ); + const path = join(directory, "source.fat"); + await writeFile(path, bytes); + return { + path, + sha256: createHash("sha256").update(bytes).digest("hex"), + kind: "executable", + format: "mach-o-universal", + architecture: "arm64", + availableArchitectures: ["arm64"], + universalSlice: { offset: 4096, size: sliceSize }, + } satisfies BinaryTarget; +}; + +const resolveImage = ( + target: BinaryTarget & { readonly kind: "executable" }, +) => { + const resources = createInspectionResources(); + onTestFinished(() => resources.close()); + return resolveHopperMachOImage(target, { resourceScope: resources.owner }); +}; + +const loadedObservation = (header: Uint8Array) => ({ + address: "0x1000", + provider_file_offset: 8, + provider_source_path: null, + provider_image_header_hex: Buffer.from(header) + .subarray(0, 12) + .toString("hex"), +}); + +it.each([ + { wide: false, littleEndian: false }, + { wide: false, littleEndian: true }, + { wide: true, littleEndian: false }, + { wide: true, littleEndian: true }, +])( + "keeps launch selection separate from loaded-image coordinates (wide=$wide littleEndian=$littleEndian)", + async ({ wide, littleEndian }) => { + const plain = machoImage({ cpu: CPU.arm64 }); + const arm64e = machoImage({ cpu: CPU.arm64e }); + const bytes = fatImage( + [ + { cpu: CPU.arm64, bytes: plain }, + { cpu: CPU.arm64e, bytes: arm64e }, + ], + wide, + littleEndian, + ); + const target = await sourceTarget(bytes, plain.byteLength); + const selected = await resolveImage(target); + expect(selected).toEqual({ + ok: true, + value: { + method: wide ? "fat64_thin_slice" : "fat32_thin_slice", + architecture_count: 2, + container_table_sha256: createHash("sha256") + .update(bytes.subarray(0, 8 + 2 * (wide ? 32 : 20))) + .digest("hex"), + source_offset: 4096, + byte_length: plain.byteLength, + cpu_type: CPU.arm64.type, + cpu_subtype: CPU.arm64.subtype, + }, + }); + const mapped = await mapHopperFileOffset(target, loadedObservation(arm64e)); + expect(mapped).toEqual({ + ok: true, + value: { + address: "0x1000", + file_offset: 8192 + 8, + provider_file_offset: 8, + image_base_file_offset: 8192, + source_path: target.path, + }, + }); + }, +); + +it.each([false, true])( + "refuses a nonzero FAT64 reserved field in both consumers (littleEndian=%s)", + async (littleEndian) => { + const thin = machoImage({ cpu: CPU.arm64 }); + const bytes = fatImage( + [{ cpu: CPU.arm64, bytes: thin }], + true, + littleEndian, + ); + new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).setUint32( + 8 + 28, + 0x89abcdef, + littleEndian, + ); + const target = await sourceTarget(bytes, thin.byteLength); + const reason = "FAT64 architecture 0 has a nonzero reserved field"; + expect(await resolveImage(target)).toMatchObject({ + ok: false, + error: { _tag: "BinaryTargetError", reason }, + }); + expect( + await mapHopperFileOffset(target, loadedObservation(thin)), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCapabilityUnavailableError", + reason, + partialObservation: { + ...loadedObservation(thin), + original_file_offset: { available: false, reason }, + }, + }, + }); + }, +); + +it.each([ + { wide: false, littleEndian: false }, + { wide: false, littleEndian: true }, + { wide: true, littleEndian: false }, + { wide: true, littleEndian: true }, +])( + "refuses overlapping valid slice headers in both consumers (wide=$wide littleEndian=$littleEndian)", + async ({ wide, littleEndian }) => { + const plain = machoImage({ cpu: CPU.arm64 }); + const arm64e = machoImage({ cpu: CPU.arm64e }); + const bytes = fatImage( + [ + { cpu: CPU.arm64, bytes: plain }, + { cpu: CPU.arm64e, bytes: arm64e }, + ], + wide, + littleEndian, + ); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + // Both headers and ranges remain valid; the first range contains the second. + if (wide) + view.setBigUint64( + 8 + 16, + 4096n + BigInt(arm64e.byteLength), + littleEndian, + ); + else view.setUint32(8 + 12, 4096 + arm64e.byteLength, littleEndian); + const target = await sourceTarget(bytes, 4096 + arm64e.byteLength); + expect(await resolveImage(target)).toMatchObject({ + ok: false, + error: { _tag: "BinaryTargetError", reason: "FAT slices overlap" }, + }); + expect( + await mapHopperFileOffset(target, loadedObservation(arm64e)), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCapabilityUnavailableError", + reason: "Original FAT slices overlap", + }, + }); + }, +); + +it("requires a unique exact raw CPU identity for loaded-image attribution", async () => { + const arm64e = machoImage({ cpu: CPU.arm64e }); + const bytes = fatImage([ + { cpu: CPU.arm64e, bytes: arm64e }, + { cpu: CPU.arm64e, bytes: arm64e }, + ]); + const target = await sourceTarget(bytes, arm64e.byteLength); + expect( + await mapHopperFileOffset(target, loadedObservation(arm64e)), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCapabilityUnavailableError", + reason: "Original FAT slice matching Hopper's loaded header is ambiguous", + }, + }); + const withoutCapabilities = Buffer.from(arm64e); + withoutCapabilities.writeUInt32LE(2, 8); + expect( + await mapHopperFileOffset(target, loadedObservation(withoutCapabilities)), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCapabilityUnavailableError", + reason: "Original FAT slice matching Hopper's loaded header is missing", + }, + }); +}); + +it("checks all 12 loaded-header bytes after matching its CPU identity", async () => { + const thin = machoImage({ cpu: CPU.arm64 }); + const target = await sourceTarget( + fatImage([{ cpu: CPU.arm64, bytes: thin }]), + thin.byteLength, + ); + const loadedHeader = Buffer.alloc(12); + // A big-endian header claims the same CPU identity but is not the source header. + loadedHeader.writeUInt32BE(0xfeedfacf, 0); + loadedHeader.writeUInt32BE(CPU.arm64.type, 4); + loadedHeader.writeUInt32BE(CPU.arm64.subtype, 8); + expect( + await mapHopperFileOffset(target, loadedObservation(loadedHeader)), + ).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCapabilityUnavailableError", + reason: "Original FAT slice header disagrees with Hopper's loaded header", + }, + }); +}); diff --git a/tests/boundary/filesystem/peImports.test.ts b/tests/boundary/filesystem/peImports.test.ts new file mode 100644 index 0000000000..9c79798e35 --- /dev/null +++ b/tests/boundary/filesystem/peImports.test.ts @@ -0,0 +1,108 @@ +import { symlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { createPeImportsService } from "../../../src/composition/binaryDiagnostics.js"; +import { peImportImage } from "../../fixtures/binaryDiagnostics/peImports.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +describe("PE import stable filesystem boundary", () => { + it("reports non-file and malformed PE selections as caller input errors", async () => { + const root = await createTestTempDirectory("rea-pe-imports-files-"); + const path = join(root, "app.exe"); + const alias = join(root, "alias.exe"); + const malformed = join(root, "malformed.exe"); + await writeFile(path, peImportImage()); + await writeFile(malformed, Buffer.from("not a pe")); + await symlink(path, alias); + const service = createPeImportsService(); + for (const [candidate, reason] of [ + [root, "invalid_format"], + [alias, "invalid_format"], + [malformed, "invalid_format"], + [join(root, "missing.exe"), "invalid_value"], + [join(path, "child.exe"), "invalid_value"], + ] as const) { + const result = await service.inspect({ path: candidate }); + if (result.ok) throw new Error("Expected file acquisition failure"); + expect(result.error).toMatchObject({ + _tag: "AnalysisInputError", + operation: "inspect_pe_imports", + issues: [ + { + path: ["path"], + reason, + message: expect.any(String), + }, + ], + }); + } + }); + + it("returns typed resource and cancellation failures", async () => { + const root = await createTestTempDirectory("rea-pe-imports-budget-"); + const path = join(root, "app.exe"); + await writeFile(path, peImportImage()); + const service = createPeImportsService(); + const tooSmall = await service.inspect({ path, max_file_bytes: 64 }); + if (tooSmall.ok) throw new Error("Expected resource budget failure"); + expect(tooSmall.error._tag).toBe("AnalysisResourceConstraintError"); + const cancelled = await service.inspect( + { path }, + { signal: AbortSignal.abort() }, + ); + if (cancelled.ok) throw new Error("Expected cancellation"); + expect(cancelled.error._tag).toBe("AnalysisCancelledError"); + }); + + it("derives limit recovery from the parser's structured limit tag", async () => { + const root = await createTestTempDirectory("rea-pe-imports-limits-"); + const twoDlls = join(root, "two.exe"); + const longNames = join(root, "names.exe"); + await writeFile( + twoDlls, + peImportImage({ + dlls: [ + { name: "A.dll", functions: [{ name: "a" }] }, + { name: "B.dll", functions: [{ name: "b" }] }, + ], + }), + ); + const name = "A".repeat(4096); + await writeFile( + longNames, + peImportImage({ + sectionBytes: 0x1200000, + dlls: [ + { + name: "KERNEL32.dll", + functions: Array.from({ length: 4097 }, () => ({ name })), + }, + ], + }), + ); + const service = createPeImportsService(); + const dlls = await service.inspect({ path: twoDlls, max_dlls: 1 }); + if (dlls.ok) throw new Error("Expected DLL budget failure"); + expect(dlls.error).toMatchObject({ + _tag: "AnalysisResourceConstraintError", + reportedLimits: { limit: "dlls", maximum: 1 }, + remediationAction: expect.stringContaining("Raise max_dlls above 1"), + retryable: false, + }); + const text = await service.inspect({ + path: longNames, + max_file_bytes: 536870912, + }); + if (text.ok) throw new Error("Expected retained-text failure"); + expect(text.error).toMatchObject({ + _tag: "AnalysisResourceConstraintError", + reportedLimits: { limit: "retained_text", maximum: 16777216 }, + remediationAction: undefined, + fixedAllowance: { + name: expect.stringContaining("16777216-byte PE import retained-text"), + kind: "bytes", + }, + retryable: false, + }); + }); +}); diff --git a/tests/boundary/filesystem/releaseProposal.test.ts b/tests/boundary/filesystem/releaseProposal.test.ts new file mode 100644 index 0000000000..293d255d2a --- /dev/null +++ b/tests/boundary/filesystem/releaseProposal.test.ts @@ -0,0 +1,439 @@ +import { execFile } from "node:child_process"; +import { readFile, writeFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { dirname, join } from "node:path"; +import { promisify } from "node:util"; +import { beforeAll, expect, it } from "vitest"; +import { z } from "zod"; + +import { + applyReleasePleasePatch, + releaseProposalSearchDepth, +} from "../../../scripts/lib/release-proposal.mjs"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const execute = promisify(execFile); +const require = createRequire(import.meta.url); +let upstream: typeof import("release-please"); +const baseline = "a".repeat(40); +const latest = "b".repeat(40); +const silent = { + info() {}, + warn() {}, + error() {}, + debug() {}, + trace() {}, +}; + +beforeAll(async () => { + await applyReleasePleasePatch(); + upstream = await import("release-please"); +}); + +function respond(value: unknown) { + return new Response(JSON.stringify(value), { + status: 200, + headers: { "content-type": "application/json" }, + }); +} + +type CommitRecord = { sha: string; message: string; files: string[] }; + +function historyPage( + commits: CommitRecord[], + page: { num: number; cursor?: string | undefined }, + pullRequest: boolean, +) { + const offset = Number(page.cursor ?? "0"); + const selected = commits.slice(offset, offset + page.num); + return { + data: { + repository: { + ref: { + target: { + history: { + nodes: selected.map((commit) => ({ + sha: commit.sha, + message: commit.message, + associatedPullRequests: { + nodes: + pullRequest && commit.sha === latest + ? [ + { + number: 42, + title: "Original title", + baseRefName: "main", + headRefName: "feature", + body: "BEGIN_COMMIT_OVERRIDE\nfix: attributed override (#42)\nEND_COMMIT_OVERRIDE", + mergeCommit: { oid: latest }, + labels: { nodes: [{ name: "reviewed" }] }, + files: { + nodes: [{ path: "src/feature.ts" }], + pageInfo: { hasNextPage: false }, + }, + }, + ] + : [], + }, + })), + pageInfo: { + hasNextPage: offset + selected.length < commits.length, + endCursor: String(offset + selected.length), + }, + }, + }, + }, + }, + }, + }; +} + +async function releaseFixture({ + config = {}, + commits = [ + { sha: latest, message: "fix: direct change", files: ["docs/changed.md"] }, + { sha: baseline, message: "chore: release 6.7.0", files: ["package.json"] }, + ], + depth = 500, + pullRequest = false, +}: { + config?: Record; + commits?: CommitRecord[]; + depth?: number; + pullRequest?: boolean; +} = {}) { + const requests: { method: string; pathname: string }[] = []; + const pages: { num: number; cursor?: string | undefined }[] = []; + const configuration = { + "release-type": "node", + versioning: "always-bump-minor", + "include-v-in-tag": false, + packages: { + ".": { + "package-name": "rea-agents", + "extra-files": [{ type: "generic", path: "version.txt" }], + }, + }, + ...config, + }; + const github = await upstream.GitHub.create({ + owner: "fixture", + repo: "rea", + defaultBranch: "main", + logger: silent, + fetch: async ( + input: string, + options: { method?: string; body?: string }, + ) => { + const url = new URL(input); + requests.push({ + method: options.method ?? "GET", + pathname: url.pathname, + }); + if (url.pathname === "/repos/fixture/rea/tags") return respond([]); + if (url.pathname === "/repos/fixture/rea/releases/generate-notes") { + return respond({ body: "Generated release notes" }); + } + if (url.pathname === "/graphql") { + const query = z + .object({ + query: z.string(), + variables: z.object({ + num: z.number(), + cursor: z.string().optional(), + }), + }) + .parse(JSON.parse(options.body ?? "")); + if (query.query.includes("query releases(")) { + return respond({ + data: { + repository: { + releases: { + nodes: [ + { + name: "6.7.0", + tag: { name: "rea-agents-6.7.0" }, + tagCommit: { oid: baseline }, + description: "", + isDraft: false, + url: "https://github.com/fixture/rea/releases/tag/rea-agents-6.7.0", + }, + ], + pageInfo: { hasNextPage: false }, + }, + }, + }, + }); + } + if (!query.query.includes("query pullRequestsSince(")) { + throw new Error(`Unexpected GraphQL operation: ${query.query}`); + } + pages.push(query.variables); + return respond(historyPage(commits, query.variables, pullRequest)); + } + const files: Record = { + "release-please-config.json": configuration, + ".release-please-manifest.json": { ".": "6.7.0", pkg: "6.7.0" }, + "package.json": { name: "rea-agents", version: "6.7.0" }, + "pkg/package.json": { name: "rea-agents", version: "6.7.0" }, + }; + const entries = Object.entries(files).map(([path, value], index) => ({ + path, + sha: (index + 1).toString(16).padStart(40, "0"), + text: JSON.stringify(value), + })); + if (url.pathname === "/repos/fixture/rea/git/trees/main") { + return respond({ + truncated: false, + sha: baseline, + tree: entries.map(({ path, sha, text }) => ({ + path, + sha, + type: "blob", + mode: "100644", + size: text.length, + })), + }); + } + const blobPrefix = "/repos/fixture/rea/git/blobs/"; + if (url.pathname.startsWith(blobPrefix)) { + const entry = entries.find( + (item) => item.sha === url.pathname.slice(blobPrefix.length), + ); + if (!entry) throw new Error("Unexpected blob read"); + return respond({ + content: Buffer.from(entry.text).toString("base64"), + encoding: "base64", + sha: entry.sha, + size: entry.text.length, + }); + } + const commitPath = "/repos/fixture/rea/commits/"; + if (url.pathname.startsWith(commitPath)) { + const commit = commits.find( + (item) => item.sha === url.pathname.slice(commitPath.length), + ); + if (!commit) throw new Error("Unexpected commit lookup"); + return respond({ + files: commit.files.map((filename) => ({ filename })), + }); + } + throw new Error( + `Unexpected HTTP request: ${options.method} ${url.pathname}`, + ); + }, + }); + const manifest = await upstream.Manifest.fromManifest( + github, + "main", + undefined, + undefined, + { + commitSearchDepth: depth, + logger: silent, + }, + ); + return { manifest, github, requests, pages }; +} + +it("builds the root Node proposal without commit-file lookups and retains extra-file updates", async () => { + const fixture = await releaseFixture(); + const candidates = await fixture.manifest.buildPullRequests(); + expect(candidates).toHaveLength(1); + expect(candidates[0]?.body.toString()).toContain("direct change"); + const version = candidates[0]?.updates.find( + (update) => update.path === "version.txt", + ); + expect( + version?.updater.updateContent("6.7.0 // x-release-please-version"), + ).toContain("6.8.0"); + expect( + fixture.requests.filter((request) => + request.pathname.includes("/commits/"), + ), + ).toEqual([]); + expect(fixture.pages.map((page) => page.num)).toEqual([10]); + const commits = []; + for await (const commit of fixture.github.mergeCommitIterator("main", { + backfillFiles: false, + })) + commits.push(commit); + expect(commits[0]?.files).toBeUndefined(); +}); + +it("keeps real associated PR metadata, body overrides and attribution", async () => { + const fixture = await releaseFixture({ pullRequest: true }); + const candidates = await fixture.manifest.buildPullRequests(); + const body = candidates[0]?.body.toString(); + expect(body).toContain("attributed override"); + expect(body).toContain("https://github.com/fixture/rea/issues/42"); + expect(body).not.toContain("direct change"); + const commits = []; + for await (const commit of fixture.github.mergeCommitIterator("main", { + backfillFiles: false, + })) + commits.push(commit); + expect(commits[0]?.pullRequest).toMatchObject({ + number: 42, + labels: ["reviewed"], + mergeCommitOid: latest, + body: expect.stringContaining("BEGIN_COMMIT_OVERRIDE"), + }); +}); + +it.each([ + { + name: "inherited exclusion", + config: { "exclude-paths": ["docs"] }, + excluded: true, + }, + { + name: "package exclusion", + config: { + packages: { + ".": { "package-name": "rea-agents", "exclude-paths": ["docs"] }, + }, + }, + excluded: true, + }, + { + name: "explicit plugin", + config: { plugins: ["sentence-case"] }, + excluded: false, + }, + { + name: "nondefault changelog", + config: { "changelog-type": "github" }, + excluded: false, + }, + { + name: "other versioning strategy", + config: { versioning: "always-bump-patch" }, + excluded: false, + }, + { + name: "subpackage", + config: { packages: { pkg: { "package-name": "rea-agents" } } }, + excluded: true, + }, +])("retains file-backed semantics for $name", async ({ config, excluded }) => { + const fixture = await releaseFixture({ config }); + const candidates = await fixture.manifest.buildPullRequests(); + expect( + fixture.requests.filter((request) => + request.pathname.includes("/commits/"), + ), + ).toHaveLength(2); + expect(candidates).toHaveLength(excluded ? 0 : 1); +}); + +it("collects beyond 500 commits and rejects an unobserved known release boundary", async () => { + const commits = Array.from({ length: 501 }, (_, index) => ({ + sha: index.toString(16).padStart(40, "0"), + message: `fix: unreleased change ${index}`, + files: ["src/change.ts"], + })); + commits.push({ + sha: baseline, + message: "chore: release 6.7.0", + files: ["package.json"], + }); + const complete = await releaseFixture({ commits, depth: commits.length }); + const candidates = await complete.manifest.buildPullRequests(); + expect(candidates[0]?.body.toString()).toContain("unreleased change 500"); + expect(complete.pages).toHaveLength(51); + expect( + complete.requests.filter((request) => + request.pathname.includes("/commits/"), + ), + ).toEqual([]); + const truncated = await releaseFixture({ commits }); + await expect(truncated.manifest.createPullRequests()).rejects.toThrow( + "Incomplete release history", + ); + expect( + truncated.requests.every( + (request) => request.method === "GET" || request.pathname === "/graphql", + ), + ).toBe(true); +}); + +it("preserves a reached explicit last-release override", async () => { + const fixture = await releaseFixture({ + config: { "last-release-sha": latest }, + }); + await expect(fixture.manifest.buildPullRequests()).resolves.toEqual([]); +}); + +it("preserves complete mixed bootstrap history with all known release boundaries", async () => { + const fixture = await releaseFixture({ + config: { + packages: { + ".": { "package-name": "rea-agents" }, + pkg: { "package-name": "unreleased" }, + }, + }, + }); + await expect(fixture.manifest.buildPullRequests()).resolves.toHaveLength(1); +}); + +it("requires complete local history when sizing the upstream search", async () => { + const directory = await createTestTempDirectory("rea-release-depth-"); + await execute("git", ["init", "--quiet", directory]); + await execute("git", [ + "-C", + directory, + "-c", + "user.name=Test", + "-c", + "user.email=test@example.com", + "commit", + "--quiet", + "--allow-empty", + "-m", + "fixture", + ]); + const run = async (command: string, args: string[]) => + (await execute(command, args, { cwd: directory })).stdout; + const head = (await run("git", ["rev-parse", "HEAD"])).trim(); + await expect(releaseProposalSearchDepth(run, head)).resolves.toBe(1); + await writeFile(join(directory, ".git/shallow"), `${head}\n`); + await expect(releaseProposalSearchDepth(run, head)).rejects.toThrow( + "complete Git history", + ); +}); + +it("applies the exact dependency patch idempotently and rejects another version", async () => { + await expect(applyReleasePleasePatch()).resolves.toBeUndefined(); + const directory = dirname(require.resolve("release-please/package.json")); + const metadata = await readFile(join(directory, "package.json"), "utf8"); + expect(JSON.parse(metadata).version).toBe("17.3.0"); + const fixture = await createTestTempDirectory("rea-release-patch-"); + await writeFile( + join(fixture, "package.json"), + JSON.stringify({ version: "17.4.0" }), + ); + await expect(applyReleasePleasePatch(fixture)).rejects.toThrow( + "requires release-please 17.3.0", + ); +}); + +it("rejects a partial or mismatched dependency patch", async () => { + const directory = dirname(require.resolve("release-please/package.json")); + const manifest = join(directory, "build/src/manifest.js"); + const original = await readFile(manifest, "utf8"); + try { + await writeFile( + manifest, + original.replace( + "const rootOnlyNode =", + "const mismatchedRootOnlyNode =", + ), + ); + await expect(applyReleasePleasePatch()).rejects.toThrow( + "does not match the installed source", + ); + } finally { + await writeFile(manifest, original); + } +}); diff --git a/tests/boundary/filesystem/releaseWorkflow.test.ts b/tests/boundary/filesystem/releaseWorkflow.test.ts index fca652daa8..473659dacc 100644 --- a/tests/boundary/filesystem/releaseWorkflow.test.ts +++ b/tests/boundary/filesystem/releaseWorkflow.test.ts @@ -1,7 +1,6 @@ import { execFile } from "node:child_process"; import { readFile, writeFile } from "node:fs/promises"; import { delimiter, join } from "node:path"; -import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { expect, it } from "vitest"; import { parse } from "yaml"; @@ -88,7 +87,7 @@ it("validates the source before release creation and the prepared candidate befo expect(candidate).toBeGreaterThan(action); expect(candidate).toBeLessThan(generation); expect(steps[candidate]?.if).toBe( - "inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'", + "inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'", ); expect(steps[candidate]?.run).toContain("--stage candidate"); for (const name of [ @@ -110,16 +109,28 @@ it("keeps main pushes proposal-only and gates merged-release automation", async }); const proposal = workflow.jobs["release-proposal"]; expect(proposal.if).toBe("github.event_name == 'push'"); - const action = proposal.steps.find((step) => - step.uses?.startsWith("googleapis/release-please-action@"), + const controller = proposal.steps.find((step) => + step.run?.includes("scripts/release-proposal.mjs"), ); - expect(action?.with).toMatchObject({ - token: "${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}", - "target-branch": "main", - "skip-github-release": true, + expect(controller?.env?.GH_TOKEN).toBe( + "${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}", + ); + expect(controller?.run).toContain("--target-branch main"); + expect(controller?.run).toContain('--source-sha "$SOURCE_SHA"'); + expect(controller?.env?.SOURCE_SHA).toBe("${{ github.sha }}"); + expect(controller?.if).toBe("steps.editorial.outputs.frozen != 'true'"); + expect( + proposal.steps.some((step) => + step.uses?.startsWith("googleapis/release-please-action@"), + ), + ).toBe(false); + expect( + proposal.steps.find((step) => step.uses?.startsWith("actions/checkout@")) + ?.with, + ).toMatchObject({ + "fetch-depth": 0, + "persist-credentials": false, }); - expect(action?.with?.["skip-github-pull-request"]).not.toBe(true); - expect(action?.if).toBe("steps.editorial.outputs.frozen != 'true'"); expect(proposal.steps[0]?.run).toContain("rea:release-notes-finalized"); expect(proposal.steps[0]?.run).toContain("--paginate"); const merged = workflow.jobs["merged-release"]; @@ -368,10 +379,17 @@ it("keeps frozen candidate preparation and publication explicit", async () => { expect(release?.with).toMatchObject({ token: "${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}", "target-branch": "${{ inputs.release_branch }}", - "skip-github-release": "${{ inputs.phase == 'prepare' }}", - "skip-github-pull-request": "${{ inputs.phase == 'publish' }}", + "skip-github-release": false, + "skip-github-pull-request": true, }); + expect(release?.if).toBe("inputs.phase == 'publish'"); const steps = workflow.jobs["release-please"].steps; + const proposal = steps.find((step) => step.id === "proposal"); + expect(proposal?.if).toBe("inputs.phase == 'prepare'"); + expect(proposal?.run).toContain("scripts/release-proposal.mjs"); + expect(proposal?.run).toContain('--target-branch "$RELEASE_BRANCH"'); + expect(proposal?.env?.SOURCE_SHA).toBe("${{ steps.branch.outputs.sha }}"); + expect(proposal?.run).toContain('--source-sha "$SOURCE_SHA"'); const notesIndex = steps.findIndex((step) => step.run?.includes("release-notes.mjs publication"), ); @@ -513,7 +531,7 @@ it("binds npm and MCP publication to the same immutable release SHA", async () = workflow.jobs["release-please"].steps.find((step) => step.name === name) ?.if, ).toBe( - "inputs.phase == 'prepare' && steps.release.outputs.prs_created == 'true'", + "inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'", ); } const publishCommand = workflow.jobs.publish.steps.find( @@ -523,20 +541,6 @@ it("binds npm and MCP publication to the same immutable release SHA", async () = expect(publishCommand).toContain( '--registry=https://registry.npmjs.org --access=public --tag="$NPM_RELEASE_TAG"', ); - for (const [version, tag] of [ - ["6.1.0", "latest"], - ["6.1.0-rc.1", "next"], - ] as const) { - const helper = new URL( - "../../../scripts/release-npm-tag.mjs", - import.meta.url, - ); - const result = await execFileAsync(process.execPath, [ - fileURLToPath(helper), - version, - ]); - expect(result.stdout).toBe(tag); - } }); it.skipIf(process.platform === "win32").each([ diff --git a/tests/boundary/filesystem/setupConfig.test.ts b/tests/boundary/filesystem/setupConfig.test.ts index 91bbff9301..ea21eec1b3 100644 --- a/tests/boundary/filesystem/setupConfig.test.ts +++ b/tests/boundary/filesystem/setupConfig.test.ts @@ -9,6 +9,7 @@ import { } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; +import { parse as parseToml } from "smol-toml"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -69,6 +70,57 @@ it.skipIf(process.platform === "win32")( }, ); +it.skipIf(process.platform === "win32")( + "repairs a Codex timeout without replacing an equivalent caller launcher alias", + async () => { + const home = await createTestTempDirectory( + "rea-config-timeout-launcher-alias-", + ); + const real = join(home, "rea-current"); + const alias = join(home, "rea-alias"); + await writeFile(real, "current launcher\n"); + await symlink(real, alias); + await mkdir(join(home, ".codex")); + const configPath = join(home, ".codex", "config.toml"); + const original = [ + "[mcp_servers.rea]", + `command = ${JSON.stringify(alias)}`, + 'args = ["mcp"]', + "startup_timeout_sec = 0", + "tool_timeout_sec = 450", + "future_policy = { retain = true }", + "", + "[mcp_servers.rea.env]", + 'CALLER_SETTING = "keep exactly"', + "", + ].join("\n"); + await writeFile(configPath, original); + const client = { name: "codex", format: "toml", configPath } as const; + expect( + await configureClientConfiguration(client, {}, [real, "mcp"]), + ).toMatchObject({ status: "configured" }); + const configured = await readFile(configPath, "utf8"); + expect(parseToml(configured)).toMatchObject({ + mcp_servers: { + rea: { + command: alias, + args: ["mcp"], + startup_timeout_sec: 30, + tool_timeout_sec: 450, + future_policy: { retain: true }, + env: { CALLER_SETTING: "keep exactly" }, + }, + }, + }); + expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); + expect( + await configureClientConfiguration(client, {}, [real, "mcp"]), + ).toEqual({ status: "unchanged" }); + expect(await readFile(configPath, "utf8")).toBe(configured); + expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); + }, +); + describe("JSON client configuration transaction: writing configuration through safe paths", () => { it.skipIf(typeof process.getuid !== "function")( "rejects a symlink target not owned by the current user", diff --git a/tests/boundary/mcp/agentKnownAnswerMcp.test.ts b/tests/boundary/mcp/agentKnownAnswerMcp.test.ts index 11e8d365a3..a2f5f67971 100644 --- a/tests/boundary/mcp/agentKnownAnswerMcp.test.ts +++ b/tests/boundary/mcp/agentKnownAnswerMcp.test.ts @@ -5,12 +5,12 @@ import { describe, expect, it } from "vitest"; import { createPackage } from "@electron/asar"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; -import { evaluateCodexEvents } from "../../../src/evaluation/CodexAgentEval.js"; +import { evaluateCodexEvents } from "../../evaluation/model-evals/CodexAgentEval.js"; import { createAgentEvaluationFixtures, agentFixtureClaims, -} from "../../../src/evaluation/AgentEvaluationFixtures.js"; -import type { FixtureClaimExpectation } from "../../../src/evaluation/KnownAnswerEvaluation.js"; +} from "../../evaluation/model-evals/AgentEvaluationFixtures.js"; +import type { FixtureClaimExpectation } from "../../evaluation/model-evals/KnownAnswerEvaluation.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts index e888740e94..bb52c778ff 100644 --- a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts +++ b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts @@ -1,4 +1,4 @@ -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { byteEntropySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -175,86 +175,95 @@ it("returns the summary of retained binary layout Evidence", async () => { }); }); -it("retains complete entropy Evidence, summarizes runs and matches CLI JSON", async () => { - const root = await createTestTempDirectory("rea-entropy-summary-"); - const path = join(root, "bytes"); - await writeFile(path, Buffer.alloc(65536)); - const client = await connect(true); - const input = { path, block_size: 256, offset: 7, length: 32769 }; - const complete = await client.callTool({ - name: "inspect_byte_entropy", - arguments: input, - }); - expect(complete.isError).not.toBe(true); - const evidence = parseEvidence(complete.structuredContent); - const cli = parseEvidence( - await runCliJson([ - "inspect-byte-entropy", - path, - "--block-size", - "256", - "--offset", - "7", - "--length", - "32769", - "--json", - ]), - ); - expect(cli.normalized_result).toEqual(evidence.normalized_result); - expect(cli.evidence_id).toBe(evidence.evidence_id); - const summary = await client.callTool({ - name: "inspect_byte_entropy", - arguments: { ...input, detail: "summary" }, - }); - expect(summary.isError).not.toBe(true); - const view = byteEntropySummaryViewResultSchema.parse( - parseEvidence(summary.structuredContent).normalized_result, - ); - expect(view).toMatchObject({ - parent_evidence_id: evidence.evidence_id, - summary: { - file_entropy: 0, - block_count: 129, - run_count: 1, - top_runs: [{ offset: 7, length: 32769, classification: "low" }], - }, - }); - const retained = await client.callTool({ - name: "inspect_analysis_view", - arguments: { - source: { - kind: "retained-evidence", - evidence_id: view.parent_evidence_id, +cliTest( + "retains complete entropy Evidence, summarizes runs and matches CLI JSON", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-entropy-summary-"); + const path = join(root, "bytes"); + await writeFile(path, Buffer.alloc(65536)); + const client = await connect(true); + const input = { path, block_size: 256, offset: 7, length: 32769 }; + const complete = await client.callTool({ + name: "inspect_byte_entropy", + arguments: input, + }); + expect(complete.isError).not.toBe(true); + const evidence = parseEvidence(complete.structuredContent); + const cliResult = await cli.run({ + arguments: [ + "inspect-byte-entropy", + path, + "--block-size", + "256", + "--offset", + "7", + "--length", + "32769", + "--json", + ], + environment: process.env, + }); + expect(cliResult.exitCode).toBe(0); + expect(cliResult.signal).toBeNull(); + const cliEvidence = parseEvidence(cliResult.json); + expect(cliEvidence.normalized_result).toEqual(evidence.normalized_result); + expect(cliEvidence.evidence_id).toBe(evidence.evidence_id); + const summary = await client.callTool({ + name: "inspect_byte_entropy", + arguments: { ...input, detail: "summary" }, + }); + expect(summary.isError).not.toBe(true); + const view = byteEntropySummaryViewResultSchema.parse( + parseEvidence(summary.structuredContent).normalized_result, + ); + expect(view).toMatchObject({ + parent_evidence_id: evidence.evidence_id, + summary: { + file_entropy: 0, + block_count: 129, + run_count: 1, + top_runs: [{ offset: 7, length: 32769, classification: "low" }], }, - view: { kind: "summary" }, - }, - }); - expect(retained.isError).not.toBe(true); - expect(parseEvidence(retained.structuredContent).normalized_result).toEqual( - view, - ); - expect(textBytes(summary.content)).toBeLessThan(textBytes(complete.content)); - expect( - ( - await client.callTool({ - name: "inspect_analysis_view", - arguments: { - source: { - kind: "retained-evidence", - evidence_id: view.parent_evidence_id, - }, - view: { kind: "page", collection: "sections", offset: 0, limit: 1 }, + }); + const retained = await client.callTool({ + name: "inspect_analysis_view", + arguments: { + source: { + kind: "retained-evidence", + evidence_id: view.parent_evidence_id, }, - }) - ).isError, - ).toBe(true); - const fixed = await connect(false); - expect( - ( - await fixed.callTool({ - name: "inspect_byte_entropy", - arguments: { path, detail: "summary" }, - }) - ).isError, - ).toBe(true); -}); + view: { kind: "summary" }, + }, + }); + expect(retained.isError).not.toBe(true); + expect(parseEvidence(retained.structuredContent).normalized_result).toEqual( + view, + ); + expect(textBytes(summary.content)).toBeLessThan( + textBytes(complete.content), + ); + expect( + ( + await client.callTool({ + name: "inspect_analysis_view", + arguments: { + source: { + kind: "retained-evidence", + evidence_id: view.parent_evidence_id, + }, + view: { kind: "page", collection: "sections", offset: 0, limit: 1 }, + }, + }) + ).isError, + ).toBe(true); + const fixed = await connect(false); + expect( + ( + await fixed.callTool({ + name: "inspect_byte_entropy", + arguments: { path, detail: "summary" }, + }) + ).isError, + ).toBe(true); + }, +); diff --git a/tests/boundary/mcp/browserMcp.test.ts b/tests/boundary/mcp/browserMcp.test.ts index f10446165d..f1c0247f09 100644 --- a/tests/boundary/mcp/browserMcp.test.ts +++ b/tests/boundary/mcp/browserMcp.test.ts @@ -18,7 +18,7 @@ import { import { expandToolAvailability } from "../../../src/contracts/toolOutputSchemaPrimitives.js"; import { createServer } from "../../../src/server/createServer.js"; import { observed } from "../../fixtures/analysisExecution.js"; -import { runCliJson } from "../../fixtures/cliJsonProcess.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { startFakeCdpBrowser, type FakeCdpBrowser, @@ -175,9 +175,9 @@ it("preserves Link parameter order and context through the advertised MCP contra ).toBe(true); }); -it( +cliTest( "preserves full document URL identity through CLI and MCP", - async () => { + async ({ cli }) => { const source = 'document.modelContext.registerTool({name:"catalog",[``]:0,inputSchema:{properties:{"":{},ordinary:{}}}});'; const browser = await startFakeCdpBrowser({ @@ -219,21 +219,26 @@ it( allowed_origins: [browser.allowedOrigin], target_id: "allowed-page", }; - const cli = await runCliJson([ - "analyze-web-bundle", - browser.endpoint, - "allowed-page", - "--allowed-origins", - browser.allowedOrigin, - "--observation-ms", - "0", - "--json", - ]); + const cliResult = await cli.run({ + arguments: [ + "analyze-web-bundle", + browser.endpoint, + "allowed-page", + "--allowed-origins", + browser.allowedOrigin, + "--observation-ms", + "0", + "--json", + ], + environment: process.env, + }); + expect(cliResult.exitCode).toBe(0); + expect(cliResult.signal).toBeNull(); const mcp = await client.callTool({ name: "analyze_web_bundle", arguments: { ...args, observation_ms: 0 }, }); - for (const result of [cli, mcp.structuredContent]) + for (const result of [cliResult.json, mcp.structuredContent]) expect(result).toMatchObject({ normalized_result: { capture: { @@ -589,100 +594,109 @@ const assertScreenshotPngFailures = async (client: Client): Promise => { }); }; -it("delivers the source-map response URL through authenticated MCP Evidence", async () => { - const terminalPath = "/maps/v1/app.js.map?revision=2&token=source-value"; - const sources = [ - ["../src/original.js", "text/javascript"], - ["../src/plain.ts", "text/typescript"], - ["../src/main.ts?revision=2", "text/typescript"], - ["../src/main.tsx?revision=2", "text/typescript"], - ["../src/main.mts?revision=2", "text/typescript"], - ["../src/main.cts?revision=2", "text/typescript"], - ["../src/fragment.ts#original", "text/typescript"], - ["../src/control.js?filename=main.ts", "text/javascript"], - ["../src/extensionless?revision=2", "text/javascript"], - ] as const; - const sourceText = "export const value = 1;"; - const browser = await startFakeCdpBrowser({ - sourceMapRedirect: terminalPath, - sourceMapBody: JSON.stringify({ - version: 3, - names: [], - sources: sources.map(([source]) => source), - sourcesContent: sources.map(() => sourceText), - mappings: "AAAA", - }), - }); - browsers.push(browser); - const connected = await connectBrowser(); - const result = await connected.client.callTool({ - name: "analyze_web_bundle", - arguments: { - cdp_endpoint: browser.endpoint, - target_id: "allowed-page", - allowed_origins: [browser.allowedOrigin], - observation_ms: 0, - fetch_source_maps: true, - }, - }); - expect(result.isError).not.toBe(true); - const evidence = parseEvidence(result.structuredContent); - const expectedResult = { - observations: { - source_maps: { - status: "included", - items: [ - { - declared_url: `${browser.allowedOrigin}/app.js.map?token=map-secret`, - effective_url: `${browser.allowedOrigin}${terminalPath}`, - original_sources: sources.map(([source, mediaType]) => ({ - source: new URL(source, `${browser.allowedOrigin}${terminalPath}`) - .href, - artifact: { media_type: mediaType, text: sourceText }, - })), - }, - ], +cliTest( + "delivers the source-map response URL through authenticated MCP Evidence", + async ({ cli }) => { + const terminalPath = "/maps/v1/app.js.map?revision=2&token=source-value"; + const sources = [ + ["../src/original.js", "text/javascript"], + ["../src/plain.ts", "text/typescript"], + ["../src/main.ts?revision=2", "text/typescript"], + ["../src/main.tsx?revision=2", "text/typescript"], + ["../src/main.mts?revision=2", "text/typescript"], + ["../src/main.cts?revision=2", "text/typescript"], + ["../src/fragment.ts#original", "text/typescript"], + ["../src/control.js?filename=main.ts", "text/javascript"], + ["../src/extensionless?revision=2", "text/javascript"], + ] as const; + const sourceText = "export const value = 1;"; + const browser = await startFakeCdpBrowser({ + sourceMapRedirect: terminalPath, + sourceMapBody: JSON.stringify({ + version: 3, + names: [], + sources: sources.map(([source]) => source), + sourcesContent: sources.map(() => sourceText), + mappings: "AAAA", + }), + }); + browsers.push(browser); + const connected = await connectBrowser(); + const result = await connected.client.callTool({ + name: "analyze_web_bundle", + arguments: { + cdp_endpoint: browser.endpoint, + target_id: "allowed-page", + allowed_origins: [browser.allowedOrigin], + observation_ms: 0, + fetch_source_maps: true, }, - }, - }; - expect(evidence.normalized_result).toMatchObject(expectedResult); - expect(result.content).toHaveLength(1); - expect(result.content[0]).toMatchObject({ - type: "text", - text: JSON.stringify(evidence), - }); - const cliEvidence = parseEvidence( - await runCliJson([ - "analyze-web-bundle", - browser.endpoint, - "allowed-page", - "--allowed-origins", - browser.allowedOrigin, - "--observation-ms", - "0", - "--fetch-source-maps", - "--json", - ]), - ); - expect(cliEvidence.normalized_result).toMatchObject(expectedResult); - const mapRequests = browser.httpRequests.filter(({ url }) => - url.includes(".map"), - ); - expect(mapRequests.map(({ url }) => url)).toEqual([ - "/app.js.map?token=map-secret", - terminalPath, - "/app.js.map?token=map-secret", - terminalPath, - ]); - expect( - mapRequests.every( - ({ authorization, cookie, referer }) => - authorization === undefined && - cookie === undefined && - referer === undefined, - ), - ).toBe(true); -}); + }); + expect(result.isError).not.toBe(true); + const evidence = parseEvidence(result.structuredContent); + const expectedResult = { + observations: { + source_maps: { + status: "included", + items: [ + { + declared_url: `${browser.allowedOrigin}/app.js.map?token=map-secret`, + effective_url: `${browser.allowedOrigin}${terminalPath}`, + original_sources: sources.map(([source, mediaType]) => ({ + source: new URL( + source, + `${browser.allowedOrigin}${terminalPath}`, + ).href, + artifact: { media_type: mediaType, text: sourceText }, + })), + }, + ], + }, + }, + }; + expect(evidence.normalized_result).toMatchObject(expectedResult); + expect(result.content).toHaveLength(1); + expect(result.content[0]).toMatchObject({ + type: "text", + text: JSON.stringify(evidence), + }); + const cliResult = await cli.run({ + arguments: [ + "analyze-web-bundle", + browser.endpoint, + "allowed-page", + "--allowed-origins", + browser.allowedOrigin, + "--observation-ms", + "0", + "--fetch-source-maps", + "--json", + ], + environment: process.env, + }); + expect(cliResult.exitCode).toBe(0); + expect(cliResult.signal).toBeNull(); + const cliEvidence = parseEvidence(cliResult.json); + expect(cliEvidence.normalized_result).toMatchObject(expectedResult); + const mapRequests = browser.httpRequests.filter(({ url }) => + url.includes(".map"), + ); + expect(mapRequests.map(({ url }) => url)).toEqual([ + "/app.js.map?token=map-secret", + terminalPath, + "/app.js.map?token=map-secret", + terminalPath, + ]); + expect( + mapRequests.every( + ({ authorization, cookie, referer }) => + authorization === undefined && + cookie === undefined && + referer === undefined, + ), + ).toBe(true); + }, +); it.each(["full", "compact"] as const)( "records a browser-socket endpoint redacted and reconciles its Evidence (%s)", diff --git a/tests/boundary/mcp/deferredRuntime.test.ts b/tests/boundary/mcp/deferredRuntime.test.ts index 3718f20650..14196fe55b 100644 --- a/tests/boundary/mcp/deferredRuntime.test.ts +++ b/tests/boundary/mcp/deferredRuntime.test.ts @@ -619,8 +619,13 @@ it("runs the real static artifact workflow after deferred initialization", async const copyCompiledPackage = async (prefix: string): Promise => { const directory = await createTestTempDirectory(prefix); await cp(resolve("dist"), join(directory, "dist"), { recursive: true }); - await mkdir(join(directory, "scripts")); + await mkdir(join(directory, "scripts/lib"), { recursive: true }); await cp(resolve("scripts/rea.mjs"), join(directory, "scripts/rea.mjs")); + // The launcher imports its process-environment owner, as the package ships it. + await cp( + resolve("scripts/lib/cli-process-environment.mjs"), + join(directory, "scripts/lib/cli-process-environment.mjs"), + ); await cp(resolve("package.json"), join(directory, "package.json")); await symlink( resolve("node_modules"), diff --git a/tests/boundary/mcp/erlangBeamMcp.test.ts b/tests/boundary/mcp/erlangBeamMcp.test.ts index 0b80c589ec..df0702aa27 100644 --- a/tests/boundary/mcp/erlangBeamMcp.test.ts +++ b/tests/boundary/mcp/erlangBeamMcp.test.ts @@ -1,3 +1,10 @@ +import pino from "pino"; +import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js"; +import { registerErlangTools } from "../../../src/server/registerErlangTools.js"; +import { createToolResultDelivery } from "../../../src/server/toolResult.js"; +import { BeamService } from "../../../src/application/erlang/BeamService.js"; +import { BeamProvider } from "../../../src/erlang/BeamProvider.js"; +import { erlangBeamSchema } from "../../../src/domain/erlang/beam.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; @@ -14,6 +21,10 @@ import { mcpTest, type TestMcp } from "../../support/mcp/mcpFixture.js"; import { MAX_INSPECT_ERLANG_BEAM_TABLE_ROWS } from "../../../src/domain/erlang/beam.js"; +const completeBeamEvidenceSchema = toolContract( + "inspect_erlang_beam", +).outputSchema.safeExtend({ normalized_result: erlangBeamSchema }); + const connect = async (mcp: TestMcp) => { const session = createTestBinarySession(() => { throw new Error("BEAM inspection must not start a deep provider"); @@ -42,7 +53,7 @@ mcpTest( arguments: { path }, }); expect(response.isError, JSON.stringify(response.content)).not.toBe(true); - const evidence = toolContract("inspect_erlang_beam").outputSchema.parse( + const evidence = completeBeamEvidenceSchema.parse( response.structuredContent, ); expect(evidence.normalized_result.module).toBe("mcp_mod"); @@ -120,3 +131,48 @@ mcpTest( } }, ); + +mcpTest( + "refuses BEAM summary before file access without retention and still delivers complete output", + async ({ mcp }) => { + const root = await createTestTempDirectory("rea-beam-no-retention-"); + const service = new BeamService(new BeamProvider()); + const server = new EvidenceMcpServer( + { name: "beam-no-retention", version: "1" }, + {}, + undefined, + createToolResultDelivery(2 * 1024 * 1024), + ); + registerErlangTools(server, service, pino({ enabled: false })); + const client = await mcp.connect(server); + try { + for (const delivery of [ + { detail: "summary" }, + { detail: "summary", view: { kind: "beam", collection: "exports" } }, + ]) { + const reply = await client.callTool({ + name: "inspect_erlang_beam", + arguments: { path: join(root, "does-not-exist.beam"), ...delivery }, + }); + expect(reply.isError).toBe(true); + expect(parseMcpToolError(reply).error).toMatchObject({ + code: "capability_unavailable", + details: { operation: "inspect_erlang_beam" }, + }); + } + const path = join(root, "complete.beam"); + await writeFile(path, erlangBeamFixture("without_retention")); + const complete = await client.callTool({ + name: "inspect_erlang_beam", + arguments: { path }, + }); + expect(complete.isError).not.toBe(true); + expect( + completeBeamEvidenceSchema.parse(complete.structuredContent) + .normalized_result.module, + ).toBe("without_retention"); + } finally { + await service.close(); + } + }, +); diff --git a/tests/boundary/mcp/nativeDispatchBoundTarget.test.ts b/tests/boundary/mcp/nativeDispatchBoundTarget.test.ts new file mode 100644 index 0000000000..7f9dc78b73 --- /dev/null +++ b/tests/boundary/mcp/nativeDispatchBoundTarget.test.ts @@ -0,0 +1,135 @@ +import { Client } from "@modelcontextprotocol/client"; +import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; +import { chmod, mkdir, rm, symlink, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { nativeDispatchMetadataResultSchema } from "../../../src/domain/native/objcSwiftMetadata.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { objcRecordBudgetMacho } from "../../fixtures/objcRecordBudgetMacho.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +type Replacement = "missing" | "directory" | "loop" | "unreadable"; + +// Real native-macos dispatch acquisition, through the compiled stdio server. +// The self-authored Mach-O fixture is only read, never executed. +const inspectReplacements = async (replacements: readonly Replacement[]) => { + const root = await createTestTempDirectory("rea-dispatch-bound-target-"); + const path = join(root, "fixture.macho"); + const bytes = objcRecordBudgetMacho("class"); + await writeFile(path, bytes); + const env: Record = { + PATH: process.env.PATH ?? "", + REA_LOG_LEVEL: "silent", + }; + for (const key of [ + "HOME", + "XDG_CONFIG_HOME", + "XDG_DATA_HOME", + "XDG_CACHE_HOME", + "XDG_STATE_HOME", + "TMPDIR", + ]) { + const directory = join(root, key.toLowerCase()); + env[key] = directory; + await mkdir(directory); + } + const client = new Client({ name: "dispatch-bound-target", version: "1" }); + const transport = new StdioClientTransport({ + command: process.execPath, + args: [resolve("scripts/rea.mjs"), "mcp"], + cwd: process.cwd(), + env, + stderr: "pipe", + }); + let stderr = ""; + transport.stderr?.on("data", (value) => { + stderr += value; + }); + const inspect = () => + client.callTool({ + name: "inspect_native_dispatch_metadata", + arguments: { max_records: 100 }, + }); + const successfulRead = async () => { + const reply = await inspect(); + expect(reply.isError).not.toBe(true); + return nativeDispatchMetadataResultSchema.parse( + parseEvidence(reply.structuredContent).normalized_result, + ); + }; + try { + await client.connect(transport); + const opened = await client.callTool({ + name: "open_binary", + arguments: { path }, + }); + expect(opened.isError).not.toBe(true); + const control = await successfulRead(); + for (const replacement of replacements) { + await rm(path, { recursive: true, force: true }); + if (replacement === "directory") await mkdir(path); + if (replacement === "loop") await symlink("fixture.macho", path); + if (replacement === "unreadable") { + await writeFile(path, bytes); + await chmod(path, 0); + } + try { + const { error } = parseMcpToolError(await inspect()); + const denied = replacement === "unreadable"; + expect(error).toMatchObject({ + code: denied ? "access_denied" : "artifact_changed", + category: denied ? "unavailable" : "integrity_mismatch", + details: { + operation: "inspect_native_dispatch_metadata", + path, + boundary: denied ? "filesystem-read" : "stable-artifact-read", + }, + }); + if (replacement === "unreadable") + expect(error.details?.system_code).toBe("EACCES"); + else { + const code = { + missing: "ENOENT", + directory: "EISDIR", + loop: "ELOOP", + }[replacement]; + expect(error.details?.reason).toContain(code); + } + expect(error.remediation.action).not.toContain("rea doctor"); + } finally { + if (replacement === "unreadable") await chmod(path, 0o600); + } + await rm(path, { recursive: true, force: true }); + await writeFile(path, bytes); + expect(await successfulRead()).toEqual(control); + } + const closed = await client.callTool({ + name: "close_binary", + arguments: {}, + }); + expect(closed.isError).not.toBe(true); + expect(stderr).toBe(""); + } finally { + await client.close(); + await transport.close(); + } +}; + +describe.skipIf(process.platform !== "darwin")( + "bound dispatch target read refusals", + () => { + it("reports missing, directory and symlink-loop replacements as artifact changes and recovers in the same session", async () => { + await inspectReplacements(["missing", "directory", "loop"]); + }, 30_000); + + it.skipIf(process.getuid?.() === 0)( + "keeps permission denial distinct and recovers after restoring read access", + async () => { + await inspectReplacements(["unreadable"]); + }, + 30_000, + ); + }, +); diff --git a/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts b/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts index b8e9bc0016..040441a9e7 100644 --- a/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts +++ b/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts @@ -20,7 +20,8 @@ import type { JsonValue } from "../../../src/domain/jsonValue.js"; import { mcpTest } from "../../support/mcp/mcpFixture.js"; import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js"; -import { verifyManagedNativeBoundaries } from "../../../src/domain/managed/managedNativeVerification.js"; +import { verifyManagedNativeVerificationSource } from "../../../src/domain/managed/managedNativeVerification.js"; +import { readManagedNativeVerificationSource } from "../../../src/domain/managed/managedNativeVerificationSource.js"; import { processCaptureSchema } from "../../../src/domain/process/processCapture.js"; const record = (value: unknown): Record => { if (typeof value !== "object" || value === null || Array.isArray(value)) @@ -173,11 +174,15 @@ it("advertises semantic output invariants and keeps producers aligned with Zod", expect(managed.result.description).toContain( "accepted plus unsupported equals total", ); - const validVerification = verifyManagedNativeBoundaries( + const verificationSource = readManagedNativeVerificationSource( managedNativeVerificationInputSchema.parse( MANAGED_NATIVE_VERIFICATION_EXAMPLE, ), ); + if (!verificationSource.ok) throw verificationSource.error; + const validVerification = verifyManagedNativeVerificationSource( + verificationSource.value, + ); expect(managed.validate(validVerification)).toBe(true); const invalidVerification = structuredClone(validVerification); invalidVerification.summary.verified += 1; diff --git a/tests/boundary/mcp/peImportsMcp.test.ts b/tests/boundary/mcp/peImportsMcp.test.ts new file mode 100644 index 0000000000..c4efbc4f8d --- /dev/null +++ b/tests/boundary/mcp/peImportsMcp.test.ts @@ -0,0 +1,105 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { + peImportImage, + peImportDirectoryAtRva, + peImportDirectoryMappingStops, +} from "../../fixtures/binaryDiagnostics/peImports.js"; +import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { mcpTest, type TestMcp } from "../../support/mcp/mcpFixture.js"; + +const connect = async (mcp: TestMcp) => { + const session = createTestBinarySession(() => { + throw new Error("PE import inspection must not start a deep provider"); + }); + const client = await mcp.connect(createServer({ kind: "session", session })); + return { client, session }; +}; + +mcpTest( + "advertises inspect_pe_imports and reads named imports", + async ({ mcp }) => { + const { client, session } = await connect(mcp); + try { + const { + toolDefinition: advertised, + ajv, + outputSchema, + } = await advertisedMcpToolSchemas(client, "inspect_pe_imports"); + expect(advertised?.annotations).toMatchObject({ + readOnlyHint: false, + idempotentHint: true, + }); + const root = await createTestTempDirectory("rea-pe-imports-mcp-"); + const path = join(root, "app.exe"); + await writeFile(path, peImportImage({ bits: 64 })); + const response = await client.callTool({ + name: "inspect_pe_imports", + arguments: { path }, + }); + expect(response.isError, JSON.stringify(response.content)).not.toBe(true); + expect( + ajv.validate(outputSchema, response.structuredContent), + JSON.stringify(ajv.errors), + ).toBe(true); + const evidence = toolContract("inspect_pe_imports").outputSchema.parse( + response.structuredContent, + ); + expect(evidence.normalized_result.format).toBe("pe32-plus"); + expect(evidence.normalized_result.imports[0]?.name).toBe("KERNEL32.dll"); + expect( + evidence.normalized_result.imports[0]?.functions.map((row) => row.name), + ).toEqual(["GetProcAddress", "ExitProcess"]); + for (const [label, bytes, rva] of [ + ["rva", peImportDirectoryAtRva("standard", 0xfffffff0), 0xfffffff0], + ["mapping", peImportDirectoryMappingStops("standard", 0), 0x40000000], + ] as const) { + const partialPath = join(root, `invalid-first-descriptor-${label}.exe`); + await writeFile(partialPath, bytes); + const partial = await client.callTool({ + name: "inspect_pe_imports", + arguments: { path: partialPath }, + }); + expect(partial.isError, JSON.stringify(partial.content)).not.toBe(true); + expect( + ajv.validate(outputSchema, partial.structuredContent), + JSON.stringify(ajv.errors), + ).toBe(true); + const partialReport = toolContract( + "inspect_pe_imports", + ).outputSchema.parse(partial.structuredContent).normalized_result; + expect(partialReport.import_directory).toMatchObject({ + rva, + locations: [], + }); + expect(partialReport.coverage.status).toBe("partial"); + expect(partialReport.imphash.md5).toBeNull(); + } + } finally { + await session.close(); + } + }, +); + +mcpTest("requires an absolute path for inspect_pe_imports", async ({ mcp }) => { + const { client, session } = await connect(mcp); + try { + const response = await client.callTool({ + name: "inspect_pe_imports", + arguments: { path: "app.exe" }, + }); + expect(response.isError).toBe(true); + expect(parseMcpToolError(response).error).toMatchObject({ + code: "invalid_request", + details: { operation: "inspect_pe_imports" }, + }); + } finally { + await session.close(); + } +}); diff --git a/tests/boundary/mcp/webRuntimeMcp.test.ts b/tests/boundary/mcp/webRuntimeMcp.test.ts index f66c0abaec..ab1927c8e9 100644 --- a/tests/boundary/mcp/webRuntimeMcp.test.ts +++ b/tests/boundary/mcp/webRuntimeMcp.test.ts @@ -8,6 +8,7 @@ import { toolContract } from "../../../src/contracts/toolContracts.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createServer } from "../../../src/server/createServer.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { runtimeSourceA, @@ -86,86 +87,94 @@ it("advertises actual SDK schemas, instrumentation effects and session-owned inl expect(invalid.isError).toBe(true); }); -it("preserves the same reconstructed runtime identity through compiled CLI and MCP evidence", async () => { - const { runCliJson } = await import("../../fixtures/cliJsonProcess.js"); - const browser = await startRuntimeBrowser({ - commandResult: ({ method }, origin) => - method === "Page.getFrameTree" - ? { - frameTree: { - frame: { - id: "runtime-main", - loaderId: "document-1", - url: `${origin}/app?caller=retained#old`, - urlFragment: "#details", +cliTest( + "preserves the same reconstructed runtime identity through compiled CLI and MCP evidence", + async ({ cli }) => { + const browser = await startRuntimeBrowser({ + commandResult: ({ method }, origin) => + method === "Page.getFrameTree" + ? { + frameTree: { + frame: { + id: "runtime-main", + loaderId: "document-1", + url: `${origin}/app?caller=retained#old`, + urlFragment: "#details", + }, }, - }, - } - : undefined, - }); - const provider = new CdpWebRuntimeProvider(); - const session = createTestBinarySession(() => { - throw new Error("Binary provider must not start"); - }); - const server = createServer( - { kind: "session", session }, - { webRuntime: new WebRuntimeService(provider) }, - ); - const client = new Client({ name: "runtime-frame-identity", version: "1" }); - onTestFinished(async () => { - await client.close(); - await server.close(); - await provider.close(); - await session.close(); - await browser.close(); - }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - await server.connect(serverTransport); - await client.connect(clientTransport); - for (const name of [ - "observe_web_execution", - "inspect_web_event_listeners", - ] as const) { - const response = await client.callTool({ - name, - arguments: { - cdp_endpoint: browser.endpoint, - target_id: "allowed-page", - allowed_origins: [browser.allowedOrigin], - ...(name === "observe_web_execution" - ? { observation_ms: 1 } - : { selector: "#selected" }), - }, + } + : undefined, }); - expect(response.isError).not.toBe(true); - const evidence = parseEvidence( - toolContract(name).outputSchema.parse(response.structuredContent), + const provider = new CdpWebRuntimeProvider(); + const session = createTestBinarySession(() => { + throw new Error("Binary provider must not start"); + }); + const server = createServer( + { kind: "session", session }, + { webRuntime: new WebRuntimeService(provider) }, ); - expect(evidence.normalized_result).toMatchObject({ - target: { - initial_url: `${browser.allowedOrigin}/app?caller=retained#details`, - }, + const client = new Client({ name: "runtime-frame-identity", version: "1" }); + onTestFinished(async () => { + await client.close(); + await server.close(); + await provider.close(); + await session.close(); + await browser.close(); }); - expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence); - const cli = await runCliJson([ - name.replaceAll("_", "-"), - browser.endpoint, - "allowed-page", - ...(name === "observe_web_execution" - ? ["--observation-ms", "1"] - : ["#selected"]), - "--allowed-origins", - browser.allowedOrigin, - "--json", - ]); - expect(parseEvidence(cli).normalized_result).toMatchObject({ - target: { - initial_url: `${browser.allowedOrigin}/app?caller=retained#details`, - }, - }); - } -}, 20_000); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + for (const name of [ + "observe_web_execution", + "inspect_web_event_listeners", + ] as const) { + const response = await client.callTool({ + name, + arguments: { + cdp_endpoint: browser.endpoint, + target_id: "allowed-page", + allowed_origins: [browser.allowedOrigin], + ...(name === "observe_web_execution" + ? { observation_ms: 1 } + : { selector: "#selected" }), + }, + }); + expect(response.isError).not.toBe(true); + const evidence = parseEvidence( + toolContract(name).outputSchema.parse(response.structuredContent), + ); + expect(evidence.normalized_result).toMatchObject({ + target: { + initial_url: `${browser.allowedOrigin}/app?caller=retained#details`, + }, + }); + expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence); + const cliResult = await cli.run({ + arguments: [ + name.replaceAll("_", "-"), + browser.endpoint, + "allowed-page", + ...(name === "observe_web_execution" + ? ["--observation-ms", "1"] + : ["#selected"]), + "--allowed-origins", + browser.allowedOrigin, + "--json", + ], + environment: process.env, + }); + expect(cliResult.exitCode).toBe(0); + expect(cliResult.signal).toBeNull(); + expect(parseEvidence(cliResult.json).normalized_result).toMatchObject({ + target: { + initial_url: `${browser.allowedOrigin}/app?caller=retained#details`, + }, + }); + } + }, + 20_000, +); it.each([ { diff --git a/tests/boundary/process/packageTarListing.test.ts b/tests/boundary/process/packageTarListing.test.ts index e876811bac..02a84327c7 100644 --- a/tests/boundary/process/packageTarListing.test.ts +++ b/tests/boundary/process/packageTarListing.test.ts @@ -25,6 +25,7 @@ const members = [ "package/bridge/ghidra/ReaGhidraAnalysisOptions.java", "package/bridge/ghidra/ReaGhidraNoReturnFix.java", "package/bridge/ghidra/ReaGhidraMipsSoftFloat.java", + "package/scripts/lib/cli-process-environment.mjs", ]; async function verifyListing(listing: string) { diff --git a/tests/boundary/providers/managed/managedStaticProvider.test.ts b/tests/boundary/providers/managed/managedStaticProvider.test.ts index d5fbc44bf7..84794d0959 100644 --- a/tests/boundary/providers/managed/managedStaticProvider.test.ts +++ b/tests/boundary/providers/managed/managedStaticProvider.test.ts @@ -105,6 +105,75 @@ describe("managed static snapshot capacity failure", () => { }); }); +describe("managed static completed snapshot cancellation", () => { + for (const warm of [false, true]) { + for (const retryClose of [false, true]) { + it.each([null, "stop after read", { caller: "stop after read" }])( + `retains completed facts after confirmed close (cached=${String(warm)}, retry=${String(retryClose)}): %s`, + async (reason) => { + const directory = await createTestTempDirectory( + "rea-managed-completed-close-", + ); + const path = join(directory, "fixture.dll"); + const bytes = buildManagedPeFixture(); + await writeFile(path, bytes); + const parsed = await parseBinaryTarget(path); + if (!parsed.ok) throw parsed.error; + const client = new ManagedStaticProvider().createClient(parsed.value); + if (warm) { + const initial = await client.execute( + "inspect_managed_artifact", + {}, + ); + if (!initial.ok) throw initial.error; + } + const controller = new AbortController(); + const nativeClose = OwnedFileHandle.prototype.close; + const owners: OwnedFileHandle[] = []; + const close = vi + .spyOn(OwnedFileHandle.prototype, "close") + .mockImplementation(async function (this: OwnedFileHandle) { + owners.push(this); + controller.abort(reason); + if (retryClose && owners.length === 1) + throw new Error("controlled transient close denial"); + await nativeClose.call(this); + }); + try { + const result = await client.execute( + "inspect_managed_artifact", + {}, + { signal: controller.signal }, + ); + if (result.ok) throw new Error("Expected cancellation after read"); + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(reason); + expect(result.error.cleanupIncomplete).toBe(false); + const observation = { + kind: "file-snapshot-hash", + path, + bytes: bytes.length, + sha256: parsed.value.sha256, + }; + expect(result.error.partialObservation).toEqual(observation); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "cancelled", + details: { partial_observation: observation }, + }); + expect(owners).toHaveLength(retryClose ? 2 : 1); + expect(owners.every((owner) => owner.handle.fd === -1)).toBe(true); + if (retryClose) expect(owners[0]).toBe(owners[1]); + expect(await client.close()).toEqual({ ok: true, value: null }); + } finally { + close.mockRestore(); + await client.close(); + } + }, + ); + } + } +}); + describe("managed static snapshot cancellation", () => { it.each([null, "stop after read", { caller: "stop after read" }])( "retains completed snapshot facts and exact cancellation during failed close: %s", diff --git a/tests/boundary/providers/native/artifactProvider.zipCancellation.test.ts b/tests/boundary/providers/native/artifactProvider.zipCancellation.test.ts new file mode 100644 index 0000000000..8e8a4d781a --- /dev/null +++ b/tests/boundary/providers/native/artifactProvider.zipCancellation.test.ts @@ -0,0 +1,113 @@ +import { Uint8ArrayReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; +import { access, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect, it, vi } from "vitest"; + +import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; +import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js"; +import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js"; +import { SafeOutputTree } from "../../../../src/artifacts/SafeOutputTree.js"; +import { artifactExtractionResultSchema } from "../../../../src/domain/artifactGraph.js"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; + +it("preserves a real ZIP stream's caller cancellation through extraction rollback", async () => { + const root = await createTestTempDirectory("rea-extract-zip-cancellation-"); + const source = join(root, "source.zip"); + const bytes = new Uint8Array(512 * 1024).fill(0x71); + const writer = new ZipWriter(new Uint8ArrayWriter()); + await writer.add("member.bin", new Uint8ArrayReader(bytes), { level: 0 }); + await writeFile(source, await writer.close()); + const target = await parseBinaryTarget(source); + if (!target.ok) throw target.error; + const client = new ArtifactProvider(process.env).createClient(target.value); + const output = join(root, "cancelled-output"); + const controller = new AbortController(); + const reason = { requested: "stop member extraction" }; + const originalWrite = SafeOutputTree.prototype.write; + let zipFailure: unknown; + let chunks = 0; + const writeSpy = vi + .spyOn(SafeOutputTree.prototype, "write") + .mockImplementation(async function ( + this: SafeOutputTree, + ...args: Parameters + ) { + const [relativePath, stream, expected, signal] = args; + const iterator = stream[Symbol.asyncIterator](); + const next = iterator.next.bind(iterator); + iterator.next = async (...nextArgs: Parameters) => { + if (chunks === 1) { + // The first ZIP chunk reached the actual output file. Abort before + // requesting the next chunk, so the ZIP stream owns this failure. + expect( + (await readFile(join(output, relativePath))).length, + ).toBeGreaterThan(0); + controller.abort(reason); + } + try { + const result = await next(...nextArgs); + if (!result.done) chunks += 1; + return result; + } catch (failure) { + zipFailure = failure; + throw failure; + } + }; + const iteratorSpy = vi + .spyOn(stream, Symbol.asyncIterator) + .mockReturnValue(iterator); + try { + return await originalWrite.call( + this, + relativePath, + stream, + expected, + signal, + ); + } finally { + iteratorSpy.mockRestore(); + } + }); + try { + const cancelled = await client.execute( + "extract_artifact", + { output_root: output }, + { signal: controller.signal }, + ); + expect(chunks).toBe(1); + expect(zipFailure).toBeInstanceOf(ArtifactReaderFailure); + if (!(zipFailure instanceof ArtifactReaderFailure)) + throw new Error("Expected the ZIP iterator to report cancellation"); + expect(zipFailure.reason).toBe("cancelled"); + expect(zipFailure.cancellation?.reason).toBe(reason); + expect(cancelled).toMatchObject({ + ok: false, + error: { _tag: "ArtifactOperationError", reason: "cancelled" }, + }); + if (cancelled.ok) throw new Error("Expected cancelled extraction"); + expect(cancelled.error.cause).toBe(reason); + expect(cancelled.error.cleanupIncomplete).toBe(false); + await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" }); + + writeSpy.mockRestore(); + const controlOutput = join(root, "control-output"); + const control = await client.execute("extract_artifact", { + output_root: controlOutput, + }); + if (!control.ok) throw control.error; + expect( + artifactExtractionResultSchema.parse(control.value.result), + ).toMatchObject({ + containment_verified: true, + artifacts: [expect.objectContaining({ relative_path: "member.bin" })], + }); + expect(await readFile(join(controlOutput, "member.bin"))).toEqual( + Buffer.from(bytes), + ); + expect(await client.close()).toMatchObject({ ok: true }); + } finally { + writeSpy.mockRestore(); + await client.close(); + } +}); diff --git a/tests/boundary/providers/native/nativeDispatchMetadataFileCleanup.test.ts b/tests/boundary/providers/native/nativeDispatchMetadataFileCleanup.test.ts index 5d973d2181..2be5071fad 100644 --- a/tests/boundary/providers/native/nativeDispatchMetadataFileCleanup.test.ts +++ b/tests/boundary/providers/native/nativeDispatchMetadataFileCleanup.test.ts @@ -11,7 +11,7 @@ import { import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; /** Open a real target and make only its admitted descriptor's close reject until released. */ -const fixture = async (bindActualDigest: boolean) => { +const fixture = async (bindActualDigest: boolean, admissionFailure?: Error) => { const root = await createTestTempDirectory("rea-native-dispatch-close-"); const path = join(root, "fixture.macho"); await writeFile(path, "bound target bytes"); @@ -41,6 +41,7 @@ const fixture = async (bindActualDigest: boolean) => { if (state.failing) throw new Error("dispatch target close failed"); await release!(); }; + if (admissionFailure !== undefined) throw admissionFailure; return Reflect.apply(original, this, args); }); onTestFinished(async () => { @@ -90,3 +91,24 @@ it("reports a completed read whose close rejects as retained cleanup", async () expect(state.attempts).toBe(2); expect(result.error.cleanupIncomplete).toBe(false); }); + +it.each([ + { code: "ENOENT", tag: "AnalysisArtifactChangedError" }, + { code: "EACCES", tag: "AnalysisAccessDeniedError" }, +])( + "retains the physical $code admission failure through close denial and retry", + async ({ code, tag }) => { + const physical = Object.assign(new Error(`admission ${code}`), { code }); + const { client, path, state } = await fixture(true, physical); + const result = await client.execute("inspect_native_dispatch_metadata", {}); + if (result.ok) throw new Error("Expected admission and cleanup failure"); + expect(result.error.cleanupIncomplete).toBe(true); + expect(result.error.cleanupResources).toEqual([path]); + expect(result.error.cleanupPrimary?._tag).toBe(tag); + expect(result.error.cleanupPrimary?.cause).toBe(physical); + state.failing = false; + expect((await client.close()).ok).toBe(true); + expect(result.error.cleanupIncomplete).toBe(false); + expect(result.error.cleanupPrimary?._tag).toBe(tag); + }, +); diff --git a/tests/boundary/providers/native/nativePlistFileCleanup.test.ts b/tests/boundary/providers/native/nativePlistFileCleanup.test.ts index c3037b25cd..fa7aaccf63 100644 --- a/tests/boundary/providers/native/nativePlistFileCleanup.test.ts +++ b/tests/boundary/providers/native/nativePlistFileCleanup.test.ts @@ -15,6 +15,7 @@ import { AnalysisCleanupLease } from "../../../../src/artifacts/AnalysisCleanupL import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js"; import { err, ok } from "../../../../src/domain/result.js"; import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; +import { inspectPlistSchema } from "../../../../src/domain/native/nativeInspection.js"; const fixture = async () => { const root = await createTestTempDirectory("rea-native-plist-lifetime-"); @@ -332,41 +333,121 @@ it("preserves an opaque cancellation observed during successful prefix close", a expect((await client.close()).ok).toBe(true); }); -it("joins an admitted prefix read when client close starts concurrently", async () => { - const { client, path, prototype } = await fixture(); - let enter!: () => void; - let resume!: () => void; - const entered = new Promise((resolve) => { - enter = resolve; - }); - const gate = new Promise((resolve) => { - resume = resolve; - }); - const original = prototype.read; - const read = vi - .spyOn(prototype, "read") - .mockImplementationOnce(async function ( +it.for([ + ["prefix", "source"], + ["source", "source"], + ["prefix", "json"], + ["prefix", "xml"], +] as const)( + "joins an admitted %s read and final %s command when client close starts concurrently", + async ([phase, route]) => { + const { client, path, prototype, runner } = await fixture(); + if (route !== "source") await writeFile(path, "{ name = value; }"); + let enter!: () => void; + let resume!: () => void; + let enterValidation!: () => void; + let resumeValidation!: () => void; + const entered = new Promise((resolve) => { + enter = resolve; + }); + const gate = new Promise((resolve) => { + resume = resolve; + }); + const validationEntered = new Promise((resolve) => { + enterValidation = resolve; + }); + const validationGate = new Promise((resolve) => { + resumeValidation = resolve; + }); + const original = prototype.read; + let reads = 0; + const read = vi.spyOn(prototype, "read").mockImplementation(async function ( this: FileHandle, ...args: Parameters ) { - enter(); - await gate; + reads += 1; + if (reads === (phase === "prefix" ? 1 : 2)) { + enter(); + await gate; + } return Reflect.apply(original, this, args); }); - onTestFinished(() => { + let validationStarted = false; + const run = runner.run.bind(runner); + const commands = vi + .spyOn(runner, "run") + .mockImplementation(async (tool, args, options) => { + if (tool === "plutil" && (route !== "xml" || args[1] === "xml1")) { + validationStarted = true; + enterValidation(); + await validationGate; + } + const result = await run(tool, args, options); + if (route !== "xml" || tool !== "plutil" || !result.ok) return result; + const stdout = + args[1] === "xml1" ? "value" : ""; + return ok({ + ...result.value, + stdout, + stdoutBytes: Buffer.byteLength(stdout), + exitCode: args[1] === "json" ? 1 : 0, + }); + }); + onTestFinished(() => { + resume(); + resumeValidation(); + read.mockRestore(); + commands.mockRestore(); + }); + const inspecting = client.execute("inspect_plist", { + path, + native_command_timeout_ms: 1500, + }); + await entered; + let finished = false; + const closing = client.close().then((result) => { + finished = true; + return result; + }); + await new Promise((resolve) => setImmediate(resolve)); + expect(finished).toBe(false); + expect(await client.execute("inspect_plist", { path })).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCapabilityUnavailableError" }, + }); resume(); - read.mockRestore(); - }); - const inspecting = client.execute("inspect_plist", { path }); - await entered; - let finished = false; - const closing = client.close().then((result) => { - finished = true; - return result; - }); - await new Promise((resolve) => setImmediate(resolve)); - expect(finished).toBe(false); - resume(); - expect((await inspecting).ok).toBe(true); - expect((await closing).ok).toBe(true); -}); + await Promise.race([validationEntered, inspecting]); + expect(validationStarted).toBe(true); + await new Promise((resolve) => setImmediate(resolve)); + expect(finished).toBe(false); + resumeValidation(); + const inspected = await inspecting; + if (!inspected.ok) throw inspected.error; + expect((await closing).ok).toBe(true); + const result = inspectPlistSchema.parse(inspected.value.result); + expect(result.value).toBe("value"); + expect(result.source_path).toBe(path); + const expectedCommands = [ + ["file", ["-b", path]], + [ + "plutil", + route === "source" + ? ["-lint", "--", path] + : ["-convert", "json", "-o", "-", "--", path], + ], + ...(route === "xml" + ? [["plutil", ["-convert", "xml1", "-o", "-", "--", path]]] + : []), + ]; + expect(commands.mock.calls.map(([tool, args]) => [tool, args])).toEqual( + expectedCommands, + ); + expect(result.provenance.map(({ tool }) => tool)).toEqual( + expectedCommands.map(([tool]) => tool), + ); + expect(commands.mock.calls.at(-1)?.[2]).toEqual({ + timeoutMs: 1500, + acceptNonZero: true, + }); + }, +); diff --git a/tests/boundary/providers/native/nativePlistTextSource.test.ts b/tests/boundary/providers/native/nativePlistTextSource.test.ts index ae1845d627..b16898cd5c 100644 --- a/tests/boundary/providers/native/nativePlistTextSource.test.ts +++ b/tests/boundary/providers/native/nativePlistTextSource.test.ts @@ -66,14 +66,14 @@ it.for([ }, { name: "unknown XML scalar", - bytes: Buffer.from(" 42 "), + bytes: Buffer.from("1e999"), description: "XML 1.0 document text", format: "xml", value: { $plist_type: "unknown", - element: "integer", - literal: " 42 ", - reason: "invalid-literal", + element: "real", + literal: "1e999", + reason: "unrepresentable", }, limitations: [ expect.stringContaining("1 XML scalar value(s) could not be established"), @@ -148,6 +148,7 @@ it.for([ const replay = new NativeFixtureRunner({ file: fixture.description }); const tools: string[] = []; let conversionIndex = 0; + let validations = 0; const client = new NativeMacOSProvider( {}, { @@ -155,10 +156,22 @@ it.for([ tools.push(tool); const result = await replay.run(tool, args); if (tool !== "plutil" || !result.ok) return result; + if (args[0] === "-lint") { + expect(args).toEqual(["-lint", "--", path]); + validations += 1; + return result; + } const conversion = fixture.conversions[conversionIndex++]; if (conversion === undefined) throw new Error("Unexpected conversion of directly supported source"); - expect(args[1]).toBe(conversion.format); + expect(args).toEqual([ + "-convert", + conversion.format, + "-o", + "-", + "--", + path, + ]); return ok({ ...result.value, stdout: conversion.output, @@ -181,7 +194,14 @@ it.for([ source_path: path, }); expect(result.provenance.map((item) => item.tool)).toEqual(tools); - expect(tools).toEqual(["file", ...fixture.conversions.map(() => "plutil")]); + const directXml = + fixture.format === "xml" && fixture.conversions.length === 0; + expect(validations).toBe(directXml ? 1 : 0); + expect(tools).toEqual([ + "file", + ...(directXml ? ["plutil"] : []), + ...fixture.conversions.map(() => "plutil"), + ]); expect(conversionIndex).toBe(fixture.conversions.length); if (fixture.limitations !== undefined) expect(result.limitations).toEqual(fixture.limitations); diff --git a/tests/boundary/providers/native/nativeProvider.test.ts b/tests/boundary/providers/native/nativeProvider.test.ts index d875279b7f..77d9d81365 100644 --- a/tests/boundary/providers/native/nativeProvider.test.ts +++ b/tests/boundary/providers/native/nativeProvider.test.ts @@ -7,6 +7,7 @@ import { join } from "node:path"; import { PassThrough } from "node:stream"; import { afterEach, beforeAll, describe, expect, it } from "vitest"; +import { build } from "plist"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; @@ -21,6 +22,7 @@ import { } from "../../../../src/native/CommandRunner.js"; import { err, ok } from "../../../../src/domain/result.js"; import { parseLipoOutput } from "../../../../src/native/parsers/lipo.js"; +import { inspectPlistSchema } from "../../../../src/domain/native/nativeInspection.js"; import { parseCodeSignature } from "../../../../src/native/parsers/codesign.js"; import { prepareProcessOwnershipInspection } from "../../../../src/process/ProcessOwnershipObservation.js"; import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; @@ -40,6 +42,11 @@ import { nativeMachoTargetForFile, } from "../../../fixtures/nativeCommands.js"; +/** The file runner identifies selected .plist inputs as XML. */ +const writeFixturePlist = async (path: string): Promise => { + await writeFile(path, build(JSON.parse(await fixture("plist.json")))); +}; + const cleanupOwner = (runId: string): ProviderProcessSupervisor => { const child = Object.assign(new EventEmitter(), { stdout: new PassThrough(), @@ -1320,9 +1327,9 @@ describe("native macOS provider inspection", () => { const executable = join(app, "Contents/MacOS/Fixture"); await mkdir(join(app, "Contents/MacOS"), { recursive: true }); await writeFile(executable, "fixture"); - await writeFile(join(app, "Contents/Info.plist"), "fixture"); + await writeFixturePlist(join(app, "Contents/Info.plist")); const externalPlist = join(directory, "Outside.plist"); - await writeFile(externalPlist, "fixture"); + await writeFixturePlist(externalPlist); const client = new NativeMacOSProvider( {}, new FixtureRunner(), @@ -1370,7 +1377,7 @@ describe("native macOS provider raw results", () => { const executable = join(app, "Contents/MacOS/Fixture"); await mkdir(join(app, "Contents/MacOS"), { recursive: true }); await writeFile(executable, "fixture"); - await writeFile(join(app, "Contents/Info.plist"), "fixture"); + await writeFixturePlist(join(app, "Contents/Info.plist")); const client = new NativeMacOSProvider( {}, new FixtureRunner(), @@ -1396,6 +1403,13 @@ describe("native macOS provider raw results", () => { expect.objectContaining({ tool: expect.any(String) }), ]), }); + if (operation === "inspect_plist") { + const plist = inspectPlistSchema.parse(execution.value.result); + expect(plist.provenance.map(({ command }) => command)).toEqual([ + ["/usr/bin/file", "-b", "$PLIST"], + ["/usr/bin/plutil", "-lint", "--", "$PLIST"], + ]); + } } }); }); @@ -1407,7 +1421,7 @@ describe("native plist defaults for iOS-style bundles", () => { const executable = join(app, "Flat"); await mkdir(app, { recursive: true }); await writeFile(executable, "fixture"); - await writeFile(join(app, "Info.plist"), "fixture"); + await writeFixturePlist(join(app, "Info.plist")); const client = new NativeMacOSProvider( {}, new FixtureRunner(), @@ -1424,6 +1438,37 @@ describe("native plist defaults for iOS-style bundles", () => { }); }); +it("rejects malformed selected plist bytes before mocked conversion can invent facts", async () => { + directory = await createTestTempDirectory("rea-native-malformed-plist-"); + const app = join(directory, "Malformed.app"); + const path = join(app, "Contents/Info.plist"); + await mkdir(join(app, "Contents"), { recursive: true }); + await writeFile(path, "fixture"); + const tools: string[] = []; + const replay = new FixtureRunner(); + const runner: NativeCommandRunner = { + run(tool, arguments_, options) { + tools.push(tool); + return replay.run(tool, arguments_, options); + }, + }; + const client = new NativeMacOSProvider({}, runner, "darwin").createClient( + machoTarget(join(app, "Contents/MacOS/Fixture"), app), + ); + for (const [parameters, tag] of [ + [{}, "AnalysisCapabilityUnavailableError"], + [{ path }, "AnalysisInputError"], + ] as const) { + const result = await client.execute("inspect_plist", parameters); + expect(result).toMatchObject({ + ok: false, + error: { _tag: tag, operation: "inspect_plist" }, + }); + } + expect(tools).toEqual(["file", "file"]); + expect(await readFile(path, "utf8")).toBe("fixture"); +}); + /** Emit entitlements whose dictionary also holds a legal `__proto__` key. */ class PrototypeEntitlementsRunner extends FixtureRunner { override async run(tool: string, arguments_: readonly string[]) { diff --git a/tests/boundary/providers/native/plistSourceFormat.test.ts b/tests/boundary/providers/native/plistSourceFormat.test.ts index bdfd6d8519..54d74b570b 100644 --- a/tests/boundary/providers/native/plistSourceFormat.test.ts +++ b/tests/boundary/providers/native/plistSourceFormat.test.ts @@ -27,6 +27,7 @@ const DATA = { $plist_type: "data", base64: "3q2+7w==" }; class PlistSourceRunner extends NativeFixtureRunner { constructor( + private readonly path: string, description: string, private readonly xmlFallback: boolean, private readonly convertedXml: string = XML, @@ -36,8 +37,23 @@ class PlistSourceRunner extends NativeFixtureRunner { override async run(tool: string, arguments_: readonly string[]) { const result = await super.run(tool, arguments_); - if (!result.ok || tool !== "plutil" || !this.xmlFallback) return result; - const json = arguments_.includes("json"); + if (!result.ok || tool !== "plutil") return result; + if (arguments_[0] === "-lint") { + expect(arguments_).toEqual(["-lint", "--", this.path]); + return ok({ ...result.value, stdout: "", stdoutBytes: 0 }); + } + const format = arguments_[1]; + expect(["json", "xml1"]).toContain(format); + expect(arguments_).toEqual([ + "-convert", + format, + "-o", + "-", + "--", + this.path, + ]); + if (!this.xmlFallback) return result; + const json = format === "json"; const stdout = json ? "" : this.convertedXml; return ok({ ...result.value, @@ -87,7 +103,7 @@ it.for([ contents: ` \n${XML}`, expected: "xml", xmlFallback: true, - expectedTools: ["file"], + expectedTools: ["file", "plutil"], expectedLimitations: [], }, { @@ -95,7 +111,7 @@ it.for([ contents: `\n${XML}`, expected: "xml", xmlFallback: true, - expectedTools: ["file"], + expectedTools: ["file", "plutil"], expectedLimitations: [], }, ...[false, true].map((bigEndian) => ({ @@ -105,7 +121,7 @@ it.for([ : Buffer.from(`\uFEFF${XML}`, "utf16le"), expected: "xml", xmlFallback: true, - expectedTools: ["file"], + expectedTools: ["file", "plutil"], expectedLimitations: [], })), { description: "ASCII text\n", expected: "unknown", xmlFallback: true }, @@ -120,7 +136,7 @@ it.for([ expected: "xml", xmlFallback: false, expectedValue: { ...VALUE, Blob: DATA }, - expectedTools: ["file"], + expectedTools: ["file", "plutil"], }, { description: @@ -128,7 +144,7 @@ it.for([ contents: Buffer.from("\uFEFF" + XML, "utf16le").swap16(), expected: "xml", xmlFallback: true, - expectedTools: ["file"], + expectedTools: ["file", "plutil"], expectedLimitations: [], }, { @@ -162,7 +178,7 @@ it.for([ xmlFallback: true, convertedXml: "nela\u0085blsa\u2028bpsa\u2029blfa\nbpayloadAA==", - expectedTools: ["file"], + expectedTools: ["file", "plutil"], expectedLimitations: [], expectedValue: { nel: "a\u0085b", @@ -192,7 +208,7 @@ it.for([ await writeFile(path, contents); const client = new NativeMacOSProvider( {}, - new PlistSourceRunner(description, xmlFallback, convertedXml), + new PlistSourceRunner(path, description, xmlFallback, convertedXml), "darwin", ).createClient(nativeMachoTarget(join(root, "target"))); onTestFinished(async () => { diff --git a/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts b/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts index 04f574a8c4..b39a3d2462 100644 --- a/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts +++ b/tests/boundary/sqlite/sqliteDatabaseProvider.test.ts @@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url"; import { expect, it as nativeTest, onTestFinished } from "vitest"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; +import { SQLITE_DATABASE_LIMITS } from "../../../src/sqlite/SqliteDatabaseLimits.js"; import { SqliteDatabaseProvider } from "../../../src/sqlite/SqliteDatabaseProvider.js"; import { inspectSqliteDatabaseSnapshot } from "../../../src/sqlite/SqliteDatabaseInspection.js"; import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js"; @@ -1269,3 +1270,78 @@ nativeTest( await expectPrivateRootRemoved(ownedRoot); }, ); + +nativeTest.each([ + ["cancelled", "cancelled"], + ["output-limit", "unreadable_output"], + ["heap-diagnostic", "resource_constraint"], +] as const)( + "preserves the typed %s worker failure through retained cleanup retry", + async (scenario, code) => { + const { database } = await createFixture(); + const controller = new AbortController(); + const reason = { requested: "cancel retained SQLite worker" }; + let cleanupAllowed = false; + let ownedRoot = ""; + let launches = 0; + const provider = new SqliteDatabaseProvider({}, async (spawn) => { + launches += 1; + ownedRoot = spawn.cwd ?? ""; + const script = + scenario === "heap-diagnostic" + ? "process.stderr.write('JavaScript heap out of memory'); process.exitCode = 3" + : scenario === "output-limit" + ? `process.stdout.write('x'.repeat(${String(SQLITE_DATABASE_LIMITS.diagnosticBytes + 1)})); setInterval(() => {}, 1000)` + : "process.stdout.write('ready\\n'); setInterval(() => {}, 1000)"; + const launched = await spawnOwnedProviderProcess({ + ...spawn, + arguments: ["-e", script], + }); + const cleanupOwned = + launched.cleanup ?? + (() => cleanupOwnedProcessGroup(launched.ownership)); + if (scenario === "cancelled") { + await waitForProviderProcessReady(launched.process); + setImmediate(() => controller.abort(reason)); + } + return { + ...launched, + cleanup: async () => { + const cleaned = await cleanupOwned(); + if (!cleanupAllowed) + throw new Error("controlled SQLite worker cleanup refusal"); + return cleaned; + }, + }; + }); + onTestFinished(async () => { + cleanupAllowed = true; + await provider.close(); + }); + const result = await provider.inspect( + { path: database.path }, + { signal: controller.signal }, + ); + if (result.ok) throw new Error("Expected retained worker cleanup"); + expect(projectAnalysisError(result.error).code).toBe("cleanup_incomplete"); + const primary = result.error.cleanupPrimary; + if (primary === undefined) throw new Error("Missing typed worker failure"); + expect(projectAnalysisError(primary).code).toBe(code); + if (scenario === "cancelled") + expect(result.error.cleanupPrimary?.cause).toBe(reason); + expect(await provider.inspect({ path: database.path })).toMatchObject({ + ok: false, + error: { cleanupIncomplete: true }, + }); + expect(launches).toBe(1); + await access(ownedRoot); + cleanupAllowed = true; + await provider.close(); + expect(result.error.cleanupIncomplete).toBe(false); + expect(result.error.cleanupResources).toEqual([]); + expect(projectAnalysisError(result.error).code).toBe(code); + if (scenario === "cancelled") + expect(result.error.cleanupPrimary?.cause).toBe(reason); + await expectPrivateRootRemoved(ownedRoot); + }, +); diff --git a/tests/conformance/README.md b/tests/conformance/README.md index 08f6b0576e..f9300d7b4e 100644 --- a/tests/conformance/README.md +++ b/tests/conformance/README.md @@ -30,6 +30,9 @@ or network dependencies. `readiness/` adds source-only generated JavaScript CLI and Electron fixtures for the repository's reconstruction-readiness verifier. `npm run verify:readiness` -combines them with the compiled native fixtures, exercises CLI and MCP parity -for real analysis operations, and checks the internal machine-readable report -documented in `docs/reconstruction-readiness.md`. +inspects the compiled native fixture through CLI and checks JavaScript/Electron +static-analysis Evidence identity through CLI and MCP. It does not execute the +fixture applications. The development-owned evaluator under `readiness/report/` +separately checks synthetic report conformance; its nine stages do not describe +observed runtime journeys. The report keeps these scopes separate as documented +in `docs/reconstruction-readiness.md`. diff --git a/tests/conformance/native/dispatch.m b/tests/conformance/native/dispatch.m index 2b83c1a4d1..86e4596aeb 100644 --- a/tests/conformance/native/dispatch.m +++ b/tests/conformance/native/dispatch.m @@ -1,6 +1,7 @@ #import @protocol ReaDispatchProtocol +@property(nonatomic, copy) NSString *fixtureName; - (void)performAction:(id)sender; @optional - (int)optionalFixtureValue; diff --git a/tests/conformance/native/dispatch.swift b/tests/conformance/native/dispatch.swift index 5946b85be7..78a2a84458 100644 --- a/tests/conformance/native/dispatch.swift +++ b/tests/conformance/native/dispatch.swift @@ -22,3 +22,20 @@ public struct ReaDeclFixtureGeneric { public actor ReaDeclFixtureActor { public init() {} } + + +// Distinct same-leaf protocols and conforming types exercise both parent chains. +public enum ReaNestedA { + public protocol Requirement { func nestedValue() -> Int } + public struct Implementation: Requirement { + public init() {} + public func nestedValue() -> Int { 31 } + } +} +public enum ReaNestedB { + public protocol Requirement { func nestedValue() -> Int } + public struct Implementation: Requirement { + public init() {} + public func nestedValue() -> Int { 37 } + } +} diff --git a/tests/conformance/providers/hopper/realHopperAssertions.test.ts b/tests/conformance/providers/hopper/realHopperAssertions.test.ts index feff8fd503..ad0d474ff7 100644 --- a/tests/conformance/providers/hopper/realHopperAssertions.test.ts +++ b/tests/conformance/providers/hopper/realHopperAssertions.test.ts @@ -5,7 +5,7 @@ import { requireFunctionDossierOracle, requireFunctionDossier, requirePseudocode, -} from "../../../../src/application/RealHopperAssertions.js"; +} from "../../../../scripts/lib/real-hopper-assertions.mjs"; describe("Hopper verifier semantic rejection", () => { it("rejects empty and embedded per-item decompilation failures", () => { diff --git a/tests/conformance/providers/managed/managedNativeVerification.test.ts b/tests/conformance/providers/managed/managedNativeVerification.test.ts index 3171f118a5..2da462f14c 100644 --- a/tests/conformance/providers/managed/managedNativeVerification.test.ts +++ b/tests/conformance/providers/managed/managedNativeVerification.test.ts @@ -21,7 +21,18 @@ import { managedNativeBoundaryInspectionSchema } from "../../../../src/domain/ma import { inspectMachoSchema } from "../../../../src/domain/native/nativeInspection.js"; import { functionDossierSchema } from "../../../../src/domain/hopperValues.js"; -import { verifyManagedNativeBoundaries } from "../../../../src/domain/managed/managedNativeVerification.js"; +import { verifyManagedNativeVerificationSource } from "../../../../src/domain/managed/managedNativeVerification.js"; +import { readManagedNativeVerificationSource } from "../../../../src/domain/managed/managedNativeVerificationSource.js"; + +const readFixtureSource = (input: unknown) => { + const source = readManagedNativeVerificationSource(input); + if (!source.ok) + throw new Error("Expected valid managed/native fixture Evidence", { + cause: source.error, + }); + return source.value; +}; + const exampleInput = () => managedNativeVerificationInputSchema.parse( MANAGED_NATIVE_VERIFICATION_EXAMPLE, @@ -168,12 +179,14 @@ it("separates CLR runtime-provided methods from unresolved file-backed native bo parameters: {}, result: inspection, }); - const result = verifyManagedNativeBoundaries({ - managed_boundaries: managed, - native_observations: [ - nativeFunctionEvidence("/fixtures/nativehelper.dll", "pe"), - ], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + managed_boundaries: managed, + native_observations: [ + nativeFunctionEvidence("/fixtures/nativehelper.dll", "pe"), + ], + }), + ); expect(result.summary.native_body_unresolved).toBe(0); expect(result.native_implementations).toMatchObject({ unresolved: 0, @@ -232,12 +245,14 @@ it.each([ parameters: {}, result: inspection, }); - const result = verifyManagedNativeBoundaries({ - managed_boundaries: managed, - native_observations: [ - nativeFunctionEvidence("/fixtures/nativehelper.dll", format, symbol), - ], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + managed_boundaries: managed, + native_observations: [ + nativeFunctionEvidence("/fixtures/nativehelper.dll", format, symbol), + ], + }), + ); expect(result.pinvoke_imports[0]).toMatchObject({ status, managed: { @@ -282,10 +297,12 @@ it("keeps Windows name candidates tied to their native Evidence format", () => { "pe", "ExampleW", ); - const result = verifyManagedNativeBoundaries({ - managed_boundaries: managed, - native_observations: [elf, pe], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + managed_boundaries: managed, + native_observations: [elf, pe], + }), + ); expect(result.pinvoke_imports[0]).toMatchObject({ status: "inferred", basis: "decorated-name-candidate", @@ -339,20 +356,28 @@ describe("managed/native boundary Evidence identity", () => { }, ); - expect(() => - verifyManagedNativeBoundaries({ - ...input, - managed_boundaries: mismatched, - }), - ).toThrow( - `Managed Evidence inspect_managed_native_boundaries (${mismatched.evidence_id}) subject SHA-256 ${result.artifact.sha256} does not match normalized artifact SHA-256 ${otherDigest}`, - ); - expect(verifyManagedNativeBoundaries(input).summary.verified).toBe(1); + const rejected = readManagedNativeVerificationSource({ + ...input, + managed_boundaries: mismatched, + }); + expect(rejected).toMatchObject({ + ok: false, + error: { + _tag: "EvidenceIntegrityError", + message: `Managed Evidence inspect_managed_native_boundaries (${mismatched.evidence_id}) subject SHA-256 ${result.artifact.sha256} does not match normalized artifact SHA-256 ${otherDigest}`, + }, + }); + expect( + verifyManagedNativeVerificationSource(readFixtureSource(input)).summary + .verified, + ).toBe(1); }); }); it("verifies a P/Invoke declaration against native export Evidence", () => { - const result = verifyManagedNativeBoundaries(exampleInput()); + const result = verifyManagedNativeVerificationSource( + readFixtureSource(exampleInput()), + ); expect(result).toMatchObject({ managed_boundary: { @@ -377,10 +402,12 @@ it("verifies a P/Invoke declaration against native export Evidence", () => { }); it("treats a Mach-O ABI-prefixed export as an exact declared import", () => { - const result = verifyManagedNativeBoundaries({ - ...exampleInput(), - native_observations: [nativeEvidenceWithExports(["_open_native"])], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...exampleInput(), + native_observations: [nativeEvidenceWithExports(["_open_native"])], + }), + ); expect(result.summary).toMatchObject({ verified: 1, inferred: 0 }); expect(result.pinvoke_imports[0]).toMatchObject({ @@ -419,10 +446,12 @@ it("reports module mismatch as a contradiction within supplied Evidence", () => }, ); - const result = verifyManagedNativeBoundaries({ - ...input, - native_observations: [mismatched], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...input, + native_observations: [mismatched], + }), + ); expect(result.summary).toMatchObject({ verified: 0, @@ -436,24 +465,28 @@ it("reports module mismatch as a contradiction within supplied Evidence", () => it("matches ELF module names case-sensitively and leaves unknown platform equivalence unresolved", () => { const linuxInput = inputWithImportScope("caseprobe"); - const elfExact = verifyManagedNativeBoundaries({ - ...linuxInput, - native_observations: [ - nativeFunctionEvidence("/examples/libcaseprobe.so", "elf"), - ], - }); + const elfExact = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...linuxInput, + native_observations: [ + nativeFunctionEvidence("/examples/libcaseprobe.so", "elf"), + ], + }), + ); expect(elfExact.pinvoke_imports[0]).toMatchObject({ status: "verified", basis: "exact-function-name", matched_native: { module_path: "/examples/libcaseprobe.so" }, }); - const elfCaseMismatch = verifyManagedNativeBoundaries({ - ...linuxInput, - native_observations: [ - nativeFunctionEvidence("/examples/libCaseProbe.so", "elf"), - ], - }); + const elfCaseMismatch = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...linuxInput, + native_observations: [ + nativeFunctionEvidence("/examples/libCaseProbe.so", "elf"), + ], + }), + ); expect(elfCaseMismatch.pinvoke_imports[0]).toMatchObject({ status: "contradicted", basis: "module-mismatch", @@ -477,12 +510,14 @@ it("matches ELF module names case-sensitively and leaves unknown platform equiva ], }); - const uncertainPlatform = verifyManagedNativeBoundaries({ - ...inputWithImportScope("foo"), - native_observations: [ - nativeEvidenceWithExports(["open_native"], "/examples/libFoo.dylib"), - ], - }); + const uncertainPlatform = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("foo"), + native_observations: [ + nativeEvidenceWithExports(["open_native"], "/examples/libFoo.dylib"), + ], + }), + ); expect(uncertainPlatform.pinvoke_imports[0]).toMatchObject({ status: "unresolved", basis: "module-identity-unknown", @@ -491,24 +526,28 @@ it("matches ELF module names case-sensitively and leaves unknown platform equiva }); it("keeps literal module names distinct and accepts the shared-library lib alias", () => { - const literalName = verifyManagedNativeBoundaries({ - ...inputWithImportScope("library"), - native_observations: [ - nativeEvidenceWithExports(["open_native"], "/examples/rary.dylib"), - ], - }); + const literalName = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("library"), + native_observations: [ + nativeEvidenceWithExports(["open_native"], "/examples/rary.dylib"), + ], + }), + ); expect(literalName.pinvoke_imports[0]).toMatchObject({ status: "contradicted", basis: "module-mismatch", matched_native: { module_path: "/examples/rary.dylib" }, }); - const sharedLibraryAlias = verifyManagedNativeBoundaries({ - ...inputWithImportScope("foo"), - native_observations: [ - nativeEvidenceWithExports(["open_native"], "/examples/libfoo.dylib"), - ], - }); + const sharedLibraryAlias = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("foo"), + native_observations: [ + nativeEvidenceWithExports(["open_native"], "/examples/libfoo.dylib"), + ], + }), + ); expect(sharedLibraryAlias.pinvoke_imports[0]).toMatchObject({ status: "verified", basis: "exact-export-name", @@ -518,10 +557,12 @@ it("keeps literal module names distinct and accepts the shared-library lib alias it("keeps explicit PE module suffixes distinct and permits the Windows DLL variation", () => { const verify = (scope: string, modulePath: string) => - verifyManagedNativeBoundaries({ - ...inputWithImportScope(scope), - native_observations: [nativeFunctionEvidence(modulePath, "pe")], - }).pinvoke_imports[0]; + verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope(scope), + native_observations: [nativeFunctionEvidence(modulePath, "pe")], + }), + ).pinvoke_imports[0]; expect( verify("nativehelper.dll", "/examples/nativehelper.dll"), @@ -551,10 +592,12 @@ it("keeps explicit PE module suffixes distinct and permits the Windows DLL varia it("keeps empty identities unknown and compares suffix-only identities as data", () => { for (const scope of [null, "", " "]) { - const result = verifyManagedNativeBoundaries({ - ...inputWithImportScope(scope), - native_observations: [nativeEvidenceWithExports(["open_native"])], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope(scope), + native_observations: [nativeEvidenceWithExports(["open_native"])], + }), + ); expect(result.pinvoke_imports[0]).toMatchObject({ status: "unresolved", basis: "module-identity-unknown", @@ -571,10 +614,12 @@ it("keeps empty identities unknown and compares suffix-only identities as data", "/examples/lib.so", "/examples/foo.dll", ]) { - const extensionOnly = verifyManagedNativeBoundaries({ - ...inputWithImportScope(".dll"), - native_observations: [nativeEvidenceWithExports(["open_native"], path)], - }); + const extensionOnly = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope(".dll"), + native_observations: [nativeEvidenceWithExports(["open_native"], path)], + }), + ); expect(extensionOnly.pinvoke_imports[0]).toMatchObject({ status: "contradicted", basis: "module-mismatch", @@ -582,12 +627,14 @@ it("keeps empty identities unknown and compares suffix-only identities as data", }); } - const subjectless = verifyManagedNativeBoundaries({ - ...inputWithImportScope("nativehelper.dll"), - native_observations: [ - nativeEvidenceWithExports(["open_native"], undefined, true), - ], - }); + const subjectless = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("nativehelper.dll"), + native_observations: [ + nativeEvidenceWithExports(["open_native"], undefined, true), + ], + }), + ); expect(subjectless.pinvoke_imports[0]).toMatchObject({ status: "unresolved", basis: "module-identity-unknown", @@ -601,31 +648,37 @@ it("keeps empty identities unknown and compares suffix-only identities as data", }); it("matches versioned ELF SONAME variations without dropping their version", () => { - const result = verifyManagedNativeBoundaries({ - ...inputWithImportScope("nativedep.so.6"), - native_observations: [ - nativeFunctionEvidence("/examples/libnativedep.so.6", "elf"), - ], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("nativedep.so.6"), + native_observations: [ + nativeFunctionEvidence("/examples/libnativedep.so.6", "elf"), + ], + }), + ); expect(result.pinvoke_imports[0]).toMatchObject({ status: "verified", matched_native: { module_path: "/examples/libnativedep.so.6" }, }); - const relativePath = verifyManagedNativeBoundaries({ - ...inputWithImportScope("relative/nativedep.so.6"), - native_observations: [ - nativeFunctionEvidence("/examples/relative/nativedep.so.6", "elf"), - ], - }); + const relativePath = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("relative/nativedep.so.6"), + native_observations: [ + nativeFunctionEvidence("/examples/relative/nativedep.so.6", "elf"), + ], + }), + ); expect(relativePath.pinvoke_imports[0]).toMatchObject({ status: "verified" }); - const basenameOnly = verifyManagedNativeBoundaries({ - ...inputWithImportScope("relative/nativedep.so.6"), - native_observations: [ - nativeFunctionEvidence("/examples/other/nativedep.so.6", "elf"), - ], - }); + const basenameOnly = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("relative/nativedep.so.6"), + native_observations: [ + nativeFunctionEvidence("/examples/other/nativedep.so.6", "elf"), + ], + }), + ); expect(basenameOnly.pinvoke_imports[0]).toMatchObject({ status: "unresolved", basis: "module-identity-unknown", @@ -634,18 +687,22 @@ it("matches versioned ELF SONAME variations without dropping their version", () it("keeps POSIX backslashes literal in absolute module paths", () => { const path = "/examples/native\\helper.so"; - const exact = verifyManagedNativeBoundaries({ - ...inputWithImportScope(path), - native_observations: [nativeFunctionEvidence(path, "elf")], - }); + const exact = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope(path), + native_observations: [nativeFunctionEvidence(path, "elf")], + }), + ); expect(exact.pinvoke_imports[0]).toMatchObject({ status: "verified" }); - const slashEquivalent = verifyManagedNativeBoundaries({ - ...inputWithImportScope(path), - native_observations: [ - nativeFunctionEvidence("/examples/native/helper.so", "elf"), - ], - }); + const slashEquivalent = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope(path), + native_observations: [ + nativeFunctionEvidence("/examples/native/helper.so", "elf"), + ], + }), + ); expect(slashEquivalent.pinvoke_imports[0]).toMatchObject({ status: "contradicted", basis: "module-mismatch", @@ -653,30 +710,36 @@ it("keeps POSIX backslashes literal in absolute module paths", () => { }); it("applies Unix suffix variations even when the name already has the suffix", () => { - const result = verifyManagedNativeBoundaries({ - ...inputWithImportScope("nativehelper.dylib"), - native_observations: [ - nativeFunctionEvidence("/examples/nativehelper.dylib.dylib", "mach-o"), - ], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("nativehelper.dylib"), + native_observations: [ + nativeFunctionEvidence("/examples/nativehelper.dylib.dylib", "mach-o"), + ], + }), + ); expect(result.pinvoke_imports[0]).toMatchObject({ status: "verified" }); }); it("does not append the Windows DLL suffix after a trailing period", () => { - const exact = verifyManagedNativeBoundaries({ - ...inputWithImportScope("nativehelper."), - native_observations: [ - nativeFunctionEvidence("/examples/nativehelper.", "pe"), - ], - }); + const exact = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("nativehelper."), + native_observations: [ + nativeFunctionEvidence("/examples/nativehelper.", "pe"), + ], + }), + ); expect(exact.pinvoke_imports[0]).toMatchObject({ status: "verified" }); - const appended = verifyManagedNativeBoundaries({ - ...inputWithImportScope("nativehelper."), - native_observations: [ - nativeFunctionEvidence("/examples/nativehelper.dll", "pe"), - ], - }); + const appended = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...inputWithImportScope("nativehelper."), + native_observations: [ + nativeFunctionEvidence("/examples/nativehelper.dll", "pe"), + ], + }), + ); expect(appended.pinvoke_imports[0]).toMatchObject({ status: "contradicted", basis: "module-mismatch", @@ -684,10 +747,12 @@ it("does not append the Windows DLL suffix after a trailing period", () => { }); it("treats supported native Evidence with no symbols as unresolved", () => { - const result = verifyManagedNativeBoundaries({ - ...exampleInput(), - native_observations: [nativeEvidenceWithExports([])], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...exampleInput(), + native_observations: [nativeEvidenceWithExports([])], + }), + ); expect(result.native_observations).toMatchObject({ accepted: 1, @@ -705,10 +770,12 @@ it("treats supported native Evidence with no symbols as unresolved", () => { }); it("does not verify a symbol whose spelling differs only by case", () => { - const result = verifyManagedNativeBoundaries({ - ...exampleInput(), - native_observations: [nativeEvidenceWithExports(["OPEN_NATIVE"])], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...exampleInput(), + native_observations: [nativeEvidenceWithExports(["OPEN_NATIVE"])], + }), + ); expect(result.summary).toMatchObject({ verified: 0, unresolved: 1 }); expect(result.pinvoke_imports[0]).toMatchObject({ @@ -719,12 +786,14 @@ it("does not verify a symbol whose spelling differs only by case", () => { }); it("retains every observed native candidate", () => { - const result = verifyManagedNativeBoundaries({ - ...exampleInput(), - native_observations: [ - nativeEvidenceWithExports(["open_native", "_open_native"]), - ], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...exampleInput(), + native_observations: [ + nativeEvidenceWithExports(["open_native", "_open_native"]), + ], + }), + ); expect(result.coverage).toMatchObject({ status: "complete-within-inputs", @@ -750,7 +819,9 @@ it("wraps verification in derived workflow Evidence", () => { describe("managed/native verification result algebra", () => { it("rejects states without their required native match", () => { - const result = verifyManagedNativeBoundaries(exampleInput()); + const result = verifyManagedNativeVerificationSource( + readFixtureSource(exampleInput()), + ); const verification = result.pinvoke_imports[0]; expect(verification).toBeDefined(); if (verification === undefined) return; @@ -794,10 +865,12 @@ describe("managed/native verification result algebra", () => { }); it("does not erase a C identifier underscore beyond the Mach-O ABI prefix", () => { - const result = verifyManagedNativeBoundaries({ - ...exampleInput(), - native_observations: [nativeEvidenceWithExports(["__open_native"])], - }); + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...exampleInput(), + native_observations: [nativeEvidenceWithExports(["__open_native"])], + }), + ); expect(result.summary).toMatchObject({ inferred: 0, unresolved: 1 }); expect(result.pinvoke_imports[0]).toMatchObject({ status: "unresolved", @@ -813,23 +886,25 @@ it("does not treat a raw Mach-O ABI name as an exact underscored C identifier", managed.normalized_result, ); const imports = normalized.pinvoke_imports; - const result = verifyManagedNativeBoundaries({ - ...input, - managed_boundaries: createEvidence(undefined, managed.provider, { - operation: managed.operation, - parameters: managed.parameters, - result: { - ...normalized, - pinvoke_imports: imports.map((item) => ({ - ...item, - import_name: "_open_native", - no_mangle: true, - })), - }, - rawResult: null, + const result = verifyManagedNativeVerificationSource( + readFixtureSource({ + ...input, + managed_boundaries: createEvidence(undefined, managed.provider, { + operation: managed.operation, + parameters: managed.parameters, + result: { + ...normalized, + pinvoke_imports: imports.map((item) => ({ + ...item, + import_name: "_open_native", + no_mangle: true, + })), + }, + rawResult: null, + }), + native_observations: [nativeEvidenceWithExports(["_open_native"])], }), - native_observations: [nativeEvidenceWithExports(["_open_native"])], - }); + ); expect(result.summary).toMatchObject({ verified: 0, inferred: 0, diff --git a/src/domain/reconstructionReadiness.test.ts b/tests/conformance/readiness/report/reconstructionReadiness.test.ts similarity index 98% rename from src/domain/reconstructionReadiness.test.ts rename to tests/conformance/readiness/report/reconstructionReadiness.test.ts index 64b98968b7..d8746c1b6b 100644 --- a/src/domain/reconstructionReadiness.test.ts +++ b/tests/conformance/readiness/report/reconstructionReadiness.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "vitest"; -import { RECONSTRUCTION_READINESS_EXAMPLE } from "../contracts/reconstructionReadinessExample.js"; +import { RECONSTRUCTION_READINESS_EXAMPLE } from "./reconstructionReadinessExample.js"; import { createReconstructionReadinessReport } from "./reconstructionReadiness.js"; import { reconstructionObligationLedgerClosureDigest, reconstructionObligationLedgerStatus, reconstructionObligationOwnershipGraph, summarizeReconstructionObligations, -} from "./reconstructionObligationLedgerIntegrity.js"; -import { reconstructionObligationLedgerSchema } from "./reconstructionObligationLedgerSchemas.js"; +} from "../../../../src/domain/reconstructionObligationLedgerIntegrity.js"; +import { reconstructionObligationLedgerSchema } from "../../../../src/domain/reconstructionObligationLedgerSchemas.js"; import { reconstructionReadinessReportSchema, reconstructionReadinessInputSchema, diff --git a/src/domain/reconstructionReadiness.ts b/tests/conformance/readiness/report/reconstructionReadiness.ts similarity index 96% rename from src/domain/reconstructionReadiness.ts rename to tests/conformance/readiness/report/reconstructionReadiness.ts index 26228108ba..5bf31e5e71 100644 --- a/src/domain/reconstructionReadiness.ts +++ b/tests/conformance/readiness/report/reconstructionReadiness.ts @@ -1,5 +1,5 @@ -import { digestCanonicalValue } from "./canonicalDigest.js"; -import { authenticateParsedEvidenceBundle } from "./evidenceBundle.js"; +import { digestCanonicalValue } from "../../../../src/domain/canonicalDigest.js"; +import { authenticateParsedEvidenceBundle } from "../../../../src/domain/evidenceBundle.js"; import { readinessFindingOrder } from "./reconstructionReadinessFindingHelpers.js"; import { readinessJourneyFindings } from "./reconstructionReadinessJourneyFindings.js"; import { readinessIntegrityFindings } from "./reconstructionReadinessIntegrityFindings.js"; @@ -12,7 +12,7 @@ import { type ReconstructionReadinessReport, } from "./reconstructionReadinessSchemas.js"; -/** Evaluate one public-contract journey into a deterministic fail-closed report. */ +/** Evaluate synthetic conformance observations into a deterministic fail-closed report. */ export const createReconstructionReadinessReport = ( inputValue: unknown, ): ReconstructionReadinessReport => { diff --git a/src/contracts/reconstructionReadinessExample.ts b/tests/conformance/readiness/report/reconstructionReadinessExample.ts similarity index 95% rename from src/contracts/reconstructionReadinessExample.ts rename to tests/conformance/readiness/report/reconstructionReadinessExample.ts index b96f5dcbf8..4fa2063c6a 100644 --- a/src/contracts/reconstructionReadinessExample.ts +++ b/tests/conformance/readiness/report/reconstructionReadinessExample.ts @@ -1,15 +1,15 @@ -import { createEvidence } from "../domain/evidence.js"; -import { createEvidenceBundle } from "../domain/evidenceBundle.js"; -import { READINESS_REQUIRED_CHECKS } from "../domain/reconstructionReadinessJourneyFindings.js"; +import { createEvidence } from "../../../../src/domain/evidence.js"; +import { createEvidenceBundle } from "../../../../src/domain/evidenceBundle.js"; +import { READINESS_REQUIRED_CHECKS } from "./reconstructionReadinessJourneyFindings.js"; import { reconstructionObligationLedgerClosureDigest, summarizeReconstructionObligations, -} from "../domain/reconstructionObligationLedgerIntegrity.js"; -import type { ReconstructionObligationLedger } from "../domain/reconstructionObligationLedgerSchemas.js"; +} from "../../../../src/domain/reconstructionObligationLedgerIntegrity.js"; +import type { ReconstructionObligationLedger } from "../../../../src/domain/reconstructionObligationLedgerSchemas.js"; import { readinessStageIdSchema, type ReconstructionReadinessInput, -} from "../domain/reconstructionReadinessSchemas.js"; +} from "./reconstructionReadinessSchemas.js"; const HASH = "a".repeat(64); const sourceEvidence = (variant: string) => @@ -159,7 +159,7 @@ const READINESS_OBLIGATION_LEDGER = (() => { }; })(); -/** Complete synthetic input shared by docs, tests, CLI, MCP, and verifier. */ +/** Complete synthetic input for the repository's evaluator conformance checks. */ export const RECONSTRUCTION_READINESS_EXAMPLE: ReconstructionReadinessInput = { identity: { cli_version: "2.4.0", diff --git a/src/domain/reconstructionReadinessFindingHelpers.ts b/tests/conformance/readiness/report/reconstructionReadinessFindingHelpers.ts similarity index 100% rename from src/domain/reconstructionReadinessFindingHelpers.ts rename to tests/conformance/readiness/report/reconstructionReadinessFindingHelpers.ts diff --git a/src/domain/reconstructionReadinessIntegrityFindings.ts b/tests/conformance/readiness/report/reconstructionReadinessIntegrityFindings.ts similarity index 100% rename from src/domain/reconstructionReadinessIntegrityFindings.ts rename to tests/conformance/readiness/report/reconstructionReadinessIntegrityFindings.ts diff --git a/src/domain/reconstructionReadinessJourneyFindings.ts b/tests/conformance/readiness/report/reconstructionReadinessJourneyFindings.ts similarity index 99% rename from src/domain/reconstructionReadinessJourneyFindings.ts rename to tests/conformance/readiness/report/reconstructionReadinessJourneyFindings.ts index 4fb5a85715..6215427663 100644 --- a/src/domain/reconstructionReadinessJourneyFindings.ts +++ b/tests/conformance/readiness/report/reconstructionReadinessJourneyFindings.ts @@ -1,4 +1,4 @@ -import { compositeKey } from "./unicodeCodePointOrder.js"; +import { compositeKey } from "../../../../src/domain/unicodeCodePointOrder.js"; import { addMissingEvidenceFindings, readinessFinding as finding, diff --git a/src/domain/reconstructionReadinessSchemas.ts b/tests/conformance/readiness/report/reconstructionReadinessSchemas.ts similarity index 95% rename from src/domain/reconstructionReadinessSchemas.ts rename to tests/conformance/readiness/report/reconstructionReadinessSchemas.ts index 685cb3a4c1..190569600e 100644 --- a/src/domain/reconstructionReadinessSchemas.ts +++ b/tests/conformance/readiness/report/reconstructionReadinessSchemas.ts @@ -1,10 +1,13 @@ import { z } from "zod"; -import { STABLE_IDENTIFIER_PATTERN } from "./stringPatterns.js"; +import { STABLE_IDENTIFIER_PATTERN } from "../../../../src/domain/stringPatterns.js"; -import { evidenceBundleSchema } from "./evidenceBundle.js"; -import { reconstructionObligationLedgerSchema } from "./reconstructionObligationLedgerSchemas.js"; -import { digestSchema, prefixedDigestSchema } from "./digests.js"; +import { evidenceBundleSchema } from "../../../../src/domain/evidenceBundle.js"; +import { reconstructionObligationLedgerSchema } from "../../../../src/domain/reconstructionObligationLedgerSchemas.js"; +import { + digestSchema, + prefixedDigestSchema, +} from "../../../../src/domain/digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const stableIdSchema = z.string().regex(STABLE_IDENTIFIER_PATTERN); diff --git a/src/evaluation/AgentEvaluationFixtures.ts b/tests/evaluation/model-evals/AgentEvaluationFixtures.ts similarity index 96% rename from src/evaluation/AgentEvaluationFixtures.ts rename to tests/evaluation/model-evals/AgentEvaluationFixtures.ts index 2a1e65bfe2..3fb8d88c04 100644 --- a/src/evaluation/AgentEvaluationFixtures.ts +++ b/tests/evaluation/model-evals/AgentEvaluationFixtures.ts @@ -4,24 +4,24 @@ import { join } from "node:path"; import { createPackage } from "@electron/asar"; -import type { JsonValue } from "../domain/jsonValue.js"; -import { javascriptApplicationAnalysisResultSchema } from "../domain/javascript/javascriptApplicationAnalysis.js"; -import type { ApplicationNode } from "../domain/javascript/javascriptApplicationGraphSchemas.js"; -import { javaScriptExportShapeComparisonResultSchema } from "../domain/javascript/javascriptExportShapeComparisonSchemas.js"; +import type { JsonValue } from "../../../src/domain/jsonValue.js"; +import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; +import type { ApplicationNode } from "../../../src/domain/javascript/javascriptApplicationGraphSchemas.js"; +import { javaScriptExportShapeComparisonResultSchema } from "../../../src/domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { applicationFeatureTraceResultSchema, featureSeedMatchMode, -} from "../domain/javascript/javascriptFeatureTraceSchemas.js"; -import { javaScriptSemanticTraceResultSchema } from "../domain/javascript/javascriptSemanticTraceSchemas.js"; +} from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js"; +import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js"; import { managedArtifactInspectionSchema, managedMemberInspectionSchema, -} from "../domain/managed/managedArtifact.js"; +} from "../../../src/domain/managed/managedArtifact.js"; import { managedTokenScopesMatch, readManagedInspectionEvidence, -} from "../domain/managed/managedInspectionEvidence.js"; -import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; +} from "../../../src/domain/managed/managedInspectionEvidence.js"; +import { compareUnicodeCodePoints } from "../../../src/domain/unicodeCodePointOrder.js"; import type { FixtureClaimContext, FixtureClaimExpectation, diff --git a/src/evaluation/AgentEvaluationReport.ts b/tests/evaluation/model-evals/AgentEvaluationReport.ts similarity index 100% rename from src/evaluation/AgentEvaluationReport.ts rename to tests/evaluation/model-evals/AgentEvaluationReport.ts diff --git a/src/evaluation/AgentEvaluationScenarios.ts b/tests/evaluation/model-evals/AgentEvaluationScenarios.ts similarity index 100% rename from src/evaluation/AgentEvaluationScenarios.ts rename to tests/evaluation/model-evals/AgentEvaluationScenarios.ts diff --git a/src/evaluation/CodexAgentEval.ts b/tests/evaluation/model-evals/CodexAgentEval.ts similarity index 98% rename from src/evaluation/CodexAgentEval.ts rename to tests/evaluation/model-evals/CodexAgentEval.ts index 8dfc14df22..64cd50b2a3 100644 --- a/src/evaluation/CodexAgentEval.ts +++ b/tests/evaluation/model-evals/CodexAgentEval.ts @@ -5,8 +5,8 @@ import { type KnownAnswerAssessment, type ParsedCodexToolCall, } from "./KnownAnswerEvaluation.js"; -import { canonicalJson } from "../domain/comparisonSemantics.js"; -import { TOOL_CONTRACTS } from "../contracts/toolContracts.js"; +import { canonicalJson } from "../../../src/domain/comparisonSemantics.js"; +import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; /** One REA MCP invocation observed in a Codex JSONL transcript. */ interface CodexMcpCall { diff --git a/src/evaluation/KnownAnswerEvaluation.ts b/tests/evaluation/model-evals/KnownAnswerEvaluation.ts similarity index 98% rename from src/evaluation/KnownAnswerEvaluation.ts rename to tests/evaluation/model-evals/KnownAnswerEvaluation.ts index 6b51d4284f..38d86e2c6f 100644 --- a/src/evaluation/KnownAnswerEvaluation.ts +++ b/tests/evaluation/model-evals/KnownAnswerEvaluation.ts @@ -6,9 +6,12 @@ import { evidenceAuthoritySchema, evidenceSchema, type Evidence, -} from "../domain/evidence.js"; -import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; -import { canonicalJson } from "../domain/comparisonSemantics.js"; +} from "../../../src/domain/evidence.js"; +import { + jsonValueSchema, + type JsonValue, +} from "../../../src/domain/jsonValue.js"; +import { canonicalJson } from "../../../src/domain/comparisonSemantics.js"; /** Only authenticated, successful, non-conflicting transcript deliveries. */ export interface FixtureClaimContext { diff --git a/tests/evaluation/model-evals/agentEvaluationReport.test.ts b/tests/evaluation/model-evals/agentEvaluationReport.test.ts index 562fd591ea..db403720ad 100644 --- a/tests/evaluation/model-evals/agentEvaluationReport.test.ts +++ b/tests/evaluation/model-evals/agentEvaluationReport.test.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest"; import { agentEvaluationPassed, summarizeFactualCorrectness, -} from "../../../src/evaluation/AgentEvaluationReport.js"; +} from "./AgentEvaluationReport.js"; const routingSuccess = { exitCode: 0, diff --git a/tests/evaluation/model-evals/agentEvaluationScenarios.test.ts b/tests/evaluation/model-evals/agentEvaluationScenarios.test.ts index eab15fdf6c..6c3af8392c 100644 --- a/tests/evaluation/model-evals/agentEvaluationScenarios.test.ts +++ b/tests/evaluation/model-evals/agentEvaluationScenarios.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "vitest"; -import { evaluateCodexEvents } from "../../../src/evaluation/CodexAgentEval.js"; -import { agentEvaluationPassed } from "../../../src/evaluation/AgentEvaluationReport.js"; +import { evaluateCodexEvents } from "./CodexAgentEval.js"; +import { agentEvaluationPassed } from "./AgentEvaluationReport.js"; import { MISSING_TARGET_SCENARIO, nativeProviderScenario, -} from "../../../src/evaluation/AgentEvaluationScenarios.js"; +} from "./AgentEvaluationScenarios.js"; const call = (id: string, tool: string, errorCode?: string) => ({ type: "item.completed", diff --git a/tests/evaluation/model-evals/codexAgentEval.test.ts b/tests/evaluation/model-evals/codexAgentEval.test.ts index b72bf2ff42..e4fe0d6051 100644 --- a/tests/evaluation/model-evals/codexAgentEval.test.ts +++ b/tests/evaluation/model-evals/codexAgentEval.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { createEvidence } from "../../../src/domain/evidence.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; -import { evaluateCodexEvents } from "../../../src/evaluation/CodexAgentEval.js"; +import { evaluateCodexEvents } from "./CodexAgentEval.js"; const observation = ( operation = "inspect_managed_artifact", diff --git a/tests/evaluation/model-evals/knownAnswerEvaluation.test.ts b/tests/evaluation/model-evals/knownAnswerEvaluation.test.ts index a9a4cc32b2..67b6945aeb 100644 --- a/tests/evaluation/model-evals/knownAnswerEvaluation.test.ts +++ b/tests/evaluation/model-evals/knownAnswerEvaluation.test.ts @@ -4,18 +4,18 @@ import { describe, expect, it } from "vitest"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { createEvidence, type Evidence } from "../../../src/domain/evidence.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; -import { evaluateCodexEvents } from "../../../src/evaluation/CodexAgentEval.js"; +import { evaluateCodexEvents } from "./CodexAgentEval.js"; import { agentFixtureAnswerInstructions, agentFixtureClaims, -} from "../../../src/evaluation/AgentEvaluationFixtures.js"; +} from "./AgentEvaluationFixtures.js"; import { inspectManagedArtifactBytes } from "../../../src/dotnet/ManagedArtifactInspector.js"; import { inspectManagedMembersBytes } from "../../../src/dotnet/ManagedMemberInspector.js"; import { buildManagedPeFixture, managedPeFixtureTarget, } from "../../../src/dotnet/ManagedPe.fixture.js"; -import type { FixtureClaimExpectation } from "../../../src/evaluation/KnownAnswerEvaluation.js"; +import type { FixtureClaimExpectation } from "./KnownAnswerEvaluation.js"; import { ToolResultDelivery } from "../../../src/server/toolResult.js"; const delivery = new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE); diff --git a/tests/fixtures/binaryDiagnostics/peImports.ts b/tests/fixtures/binaryDiagnostics/peImports.ts new file mode 100644 index 0000000000..bdbdce47b2 --- /dev/null +++ b/tests/fixtures/binaryDiagnostics/peImports.ts @@ -0,0 +1,314 @@ +const SECTION_RVA = 0x1000; +const SECTION_OFFSET = 0x400; +const SECTION_BYTES = 0x800; +const HEADER_BYTES = 0x400; +const SPLIT_GAP_BYTES = 0x200; + +/** File offset of an RVA in an unsplit fixture image. */ +export const peImportFixtureOffset = (rva: number): number => + rva - SECTION_RVA + SECTION_OFFSET; + +export type PeImportFixtureFunction = + | { readonly name: string; readonly hint?: number } + | { readonly ordinal: number }; + +export interface PeImportFixtureDll { + readonly name: string; + readonly functions: readonly PeImportFixtureFunction[]; +} + +/** + * Minimal PE32/PE32+ image whose `.idata` section holds IMAGE_IMPORT_DESCRIPTOR + * and optional IMAGE_DELAYLOAD_DESCRIPTOR tables at RVA 0x1000. + * + * `splitAt` keeps the RVA layout but moves section bytes from that section + * offset onward into a second section at a later, discontiguous file offset, + * filling the vacated file bytes with 0xcc. + */ +export const peImportImage = ( + options: { + readonly bits?: 32 | 64; + readonly dlls?: readonly PeImportFixtureDll[]; + readonly delayDlls?: readonly PeImportFixtureDll[]; + readonly delayAttributes?: number; + readonly importDirectory?: boolean; + readonly delayDirectory?: boolean; + readonly sectionBytes?: number; + readonly splitAt?: number; + } = {}, +): Buffer => { + const sectionBytes = options.sectionBytes ?? SECTION_BYTES; + const bits = options.bits ?? 64; + const dlls = options.dlls ?? [ + { + name: "KERNEL32.dll", + functions: [ + { name: "GetProcAddress", hint: 1 }, + { name: "ExitProcess", hint: 2 }, + ], + }, + ]; + const delayDlls = options.delayDlls ?? []; + const bytes = Buffer.alloc(SECTION_OFFSET + sectionBytes); + const wide = bits === 64; + const thunkSize = wide ? 8 : 4; + bytes.write("MZ", 0, "ascii"); + bytes.writeUInt32LE(128, 60); + bytes.write("PE\0\0", 128, "ascii"); + bytes.writeUInt16LE(wide ? 0x8664 : 0x14c, 132); + bytes.writeUInt16LE(1, 134); + const directoriesAt = wide ? 112 : 96; + const optionalSize = directoriesAt + 16 * 8; + bytes.writeUInt16LE(optionalSize, 148); + const optional = 152; + bytes.writeUInt16LE(wide ? 0x20b : 0x10b, optional); + if (wide) bytes.writeBigUInt64LE(0x180000000n, optional + 24); + else bytes.writeUInt32LE(0x10000000, optional + 28); + bytes.writeUInt32LE(16, optional + directoriesAt - 4); + bytes.writeUInt32LE(HEADER_BYTES, optional + 60); + const section = optional + optionalSize; + bytes.write(".idata", section, "ascii"); + bytes.writeUInt32LE(sectionBytes, section + 8); + bytes.writeUInt32LE(SECTION_RVA, section + 12); + bytes.writeUInt32LE(sectionBytes, section + 16); + bytes.writeUInt32LE(SECTION_OFFSET, section + 20); + bytes.writeUInt32LE(0x40000040, section + 36); + + const at = (rva: number) => rva - SECTION_RVA + SECTION_OFFSET; + const writeThunk = (rva: number, value: bigint): void => { + if (wide) bytes.writeBigUInt64LE(value, at(rva)); + else bytes.writeUInt32LE(Number(value), at(rva)); + }; + let cursor = SECTION_RVA; + const place = (size: number): number => { + const rva = cursor; + cursor += size; + return rva; + }; + + const importCount = options.importDirectory === false ? 0 : dlls.length; + const delayCount = + options.delayDirectory === false || delayDlls.length === 0 + ? 0 + : delayDlls.length; + const importTable = importCount === 0 ? 0 : place((importCount + 1) * 20); + const delayTable = delayCount === 0 ? 0 : place((delayCount + 1) * 32); + + if (options.importDirectory !== false) { + bytes.writeUInt32LE(importTable, optional + directoriesAt + 8); + bytes.writeUInt32LE((importCount + 1) * 20, optional + directoriesAt + 12); + } + if (delayCount > 0) { + bytes.writeUInt32LE(delayTable, optional + directoriesAt + 13 * 8); + bytes.writeUInt32LE( + (delayCount + 1) * 32, + optional + directoriesAt + 13 * 8 + 4, + ); + } + + const writeFunctions = ( + functions: readonly PeImportFixtureFunction[], + ): { readonly ilt: number; readonly iat: number } => { + const ilt = place((functions.length + 1) * thunkSize); + const iat = place((functions.length + 1) * thunkSize); + functions.forEach((entry, index) => { + let thunk: bigint; + if ("ordinal" in entry) { + thunk = + (wide ? 0x8000000000000000n : 0x80000000n) | BigInt(entry.ordinal); + } else { + const hintName = place(2 + entry.name.length + 1); + bytes.writeUInt16LE(entry.hint ?? 0, at(hintName)); + bytes.write(`${entry.name}\0`, at(hintName) + 2, "latin1"); + thunk = BigInt(hintName); + } + writeThunk(ilt + index * thunkSize, thunk); + writeThunk(iat + index * thunkSize, thunk); + }); + return { ilt, iat }; + }; + + dlls.slice(0, importCount).forEach((dll, index) => { + const { ilt, iat } = writeFunctions(dll.functions); + const nameRva = place(dll.name.length + 1); + bytes.write(`${dll.name}\0`, at(nameRva), "latin1"); + const descriptor = at(importTable + index * 20); + bytes.writeUInt32LE(ilt, descriptor); + bytes.writeUInt32LE(nameRva, descriptor + 12); + bytes.writeUInt32LE(iat, descriptor + 16); + }); + + delayDlls.slice(0, delayCount).forEach((dll, index) => { + const { ilt, iat } = writeFunctions(dll.functions); + const nameRva = place(dll.name.length + 1); + bytes.write(`${dll.name}\0`, at(nameRva), "latin1"); + const descriptor = at(delayTable + index * 32); + bytes.writeUInt32LE(options.delayAttributes ?? 1, descriptor); + bytes.writeUInt32LE(nameRva, descriptor + 4); + bytes.writeUInt32LE(iat, descriptor + 12); + bytes.writeUInt32LE(ilt, descriptor + 16); + }); + + if (options.splitAt === undefined) return bytes; + const head = options.splitAt; + const tail = sectionBytes - head; + const moved = SECTION_OFFSET + sectionBytes + SPLIT_GAP_BYTES; + const split = Buffer.alloc(moved + tail); + bytes.copy(split, 0, 0, SECTION_OFFSET + head); + split.fill(0xcc, SECTION_OFFSET + head, moved); + bytes.copy(split, moved, SECTION_OFFSET + head); + split.writeUInt16LE(2, 134); + split.writeUInt32LE(head, section + 8); + split.writeUInt32LE(head, section + 16); + const second = section + 40; + split.write(".idata2", second, "ascii"); + split.writeUInt32LE(tail, second + 8); + split.writeUInt32LE(SECTION_RVA + head, second + 12); + split.writeUInt32LE(tail, second + 16); + split.writeUInt32LE(moved, second + 20); + split.writeUInt32LE(0x40000040, second + 36); + return split; +}; + +/** PE image whose optional header admits no import data directory. */ +export const peWithoutImportTableFixture = (bits: 32 | 64 = 64): Buffer => + peImportImage({ bits, importDirectory: false, delayDlls: [] }); + +/** Add a file-backed PE32+ range while preserving the ordinary low-RVA payload. */ +const withHighImportRange = ( + source: Buffer, + rva: number, + data: Buffer, +): Buffer => { + const offset = 0x1000; + const bytes = Buffer.alloc(offset + data.length); + source.copy(bytes); + data.copy(bytes, offset); + bytes.writeUInt16LE(2, 134); + const section = 152 + 240 + 40; + bytes.write(".high", section, "ascii"); + bytes.writeUInt32LE(data.length, section + 8); + bytes.writeUInt32LE(rva, section + 12); + bytes.writeUInt32LE(data.length, section + 16); + bytes.writeUInt32LE(offset, section + 20); + bytes.writeUInt32LE(0x40000040, section + 36); + return bytes; +}; + +/** Directory descriptor/terminator whose mapped range crosses the uint32 RVA end. */ +export const peImportDirectoryAtRva = ( + kind: "standard" | "delay", + rva: number, +): Buffer => { + const source = peImportImage({ + delayDlls: + kind === "delay" + ? [{ name: "ADVAPI32.dll", functions: [{ name: "RegOpenKeyExW" }] }] + : [], + }); + const width = kind === "standard" ? 20 : 32; + const directory = 152 + 112 + (kind === "standard" ? 1 : 13) * 8; + const oldOffset = peImportFixtureOffset(source.readUInt32LE(directory)); + const bytes = withHighImportRange( + source, + rva, + source.subarray(oldOffset, oldOffset + 2 * width), + ); + bytes.writeUInt32LE(rva, directory); + return bytes; +}; + +/** A directory starts in a gap, or its first descriptor is followed by a gap. */ +export const peImportDirectoryMappingStops = ( + kind: "standard" | "delay", + retained: 0 | 1, +): Buffer => { + const bytes = peImportDirectoryAtRva(kind, 0x40000000); + const section = 152 + 240 + 40; + const size = retained * (kind === "standard" ? 20 : 32); + bytes.writeUInt32LE(size, section + 8); + bytes.writeUInt32LE(size, section + 16); + return bytes; +}; + +/** A DLL name whose NUL lies outside the uint32 RVA space. */ +export const peImportNameAtRva = (rva: number): Buffer => { + const bytes = withHighImportRange( + peImportImage(), + rva, + Buffer.from("KERNEL32.dll\0", "latin1"), + ); + bytes.writeUInt32LE(rva, peImportFixtureOffset(0x1000) + 12); + return bytes; +}; + +/** Later import section behind an empty or suspicious first section header. */ +export const peImportLeadingSectionImage = (suspicious: boolean): Buffer => { + const bytes = peImportImage(); + const section = 152 + 240; + bytes.copy(bytes, section + 40, section, section + 40); + bytes.fill(0, section, section + 40); + bytes.writeUInt16LE(2, 134); + if (suspicious) { + bytes.writeUInt32LE(0x20000000, section + 8); + bytes.writeUInt32LE(0x20000000, section + 12); + bytes.writeUInt32LE(0xfffffe00, section + 20); + } + return bytes; +}; + +/** Independent PE32+ mappings with imports in the last section, for capacity checks. */ +export const peImportManySectionsImage = ( + sectionCount: number, + functionCount: number, +): Buffer => { + const rawSize = 0x20000; + const original = peImportImage({ + sectionBytes: rawSize, + dlls: [ + { + name: "KERNEL32.dll", + functions: Array.from({ length: functionCount }, (_, i) => ({ + name: `Function${String(i).padStart(4, "0")}`, + })), + }, + ], + }); + const optional = 152; + const sectionStart = optional + 240; + const headerBytes = Math.ceil((sectionStart + sectionCount * 40) / 512) * 512; + const finalRaw = headerBytes + (sectionCount - 1) * 512; + const firstRva = Math.ceil(headerBytes / 0x1000) * 0x1000; + const finalRva = firstRva + (sectionCount - 1) * 0x1000; + const delta = finalRva - SECTION_RVA; + const bytes = Buffer.alloc(finalRaw + rawSize); + original.copy(bytes, 0, 0, sectionStart); + bytes.writeUInt16LE(sectionCount, 134); + bytes.writeUInt32LE(0x1000, optional + 32); + bytes.writeUInt32LE(512, optional + 36); + bytes.writeUInt32LE(headerBytes, optional + 60); + bytes.writeUInt32LE(finalRva, optional + 112 + 8); + bytes.writeUInt32LE(finalRva + rawSize, optional + 56); + for (let i = 0; i < sectionCount; i++) { + const at = sectionStart + i * 40; + const size = i === sectionCount - 1 ? rawSize : 512; + bytes.write(i === sectionCount - 1 ? ".idata" : ".dummy", at, "ascii"); + bytes.writeUInt32LE(size, at + 8); + bytes.writeUInt32LE(firstRva + i * 0x1000, at + 12); + bytes.writeUInt32LE(size, at + 16); + bytes.writeUInt32LE(headerBytes + i * 512, at + 20); + bytes.writeUInt32LE(0x40000040, at + 36); + } + const data = Buffer.from(original.subarray(SECTION_OFFSET)); + const ilt = data.readUInt32LE(0); + const iat = data.readUInt32LE(16); + for (const field of [0, 12, 16]) + data.writeUInt32LE(data.readUInt32LE(field) + delta, field); + for (const table of [ilt, iat]) + for (let i = 0; i < functionCount; i++) { + const at = table - SECTION_RVA + i * 8; + data.writeBigUInt64LE(data.readBigUInt64LE(at) + BigInt(delta), at); + } + data.copy(bytes, finalRaw); + return bytes; +}; diff --git a/tests/fixtures/cliJsonProcess.ts b/tests/fixtures/cliJsonProcess.ts deleted file mode 100644 index c66c82d4e9..0000000000 --- a/tests/fixtures/cliJsonProcess.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; - -const execute = promisify(execFile); - -/** Run the built CLI and parse its JSON stdout, including a failing command's. */ -export const runCliJson = async ( - arguments_: readonly string[], - environment: NodeJS.ProcessEnv = process.env, -): Promise => { - try { - const { stdout } = await execute( - process.execPath, - ["scripts/rea.mjs", ...arguments_], - { - cwd: process.cwd(), - env: environment, - maxBuffer: 16 * 1_024 * 1_024, - }, - ); - return JSON.parse(stdout); - } catch (cause: unknown) { - if ( - typeof cause === "object" && - cause !== null && - "stdout" in cause && - typeof cause.stdout === "string" - ) - return JSON.parse(cause.stdout); - throw cause; - } -}; diff --git a/tests/process-global/cutterDescriptorLifetime.test.ts b/tests/process-global/cutterDescriptorLifetime.test.ts index 99f5b4cd51..86e4091e95 100644 --- a/tests/process-global/cutterDescriptorLifetime.test.ts +++ b/tests/process-global/cutterDescriptorLifetime.test.ts @@ -38,7 +38,7 @@ import { AnalysisCancelledError } from "../../src/domain/analysisErrorCore.js"; import { AnalysisError } from "../../src/domain/analysisErrorBase.js"; import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; -import { runCliJson } from "../fixtures/cliJsonProcess.js"; +import { cliTest } from "../support/cli/cliFixture.js"; beforeEach(() => { fault.root = ""; @@ -74,19 +74,25 @@ const descriptor = async (root: string, index: number) => { return path; }; -it.skipIf(process.platform === "win32")( +cliTest.skipIf(process.platform === "win32")( "public CLI skips a named-pipe descriptor without waiting for a writer", - async () => { + async ({ cli }) => { const root = await createTestTempDirectory("rea-cutter-fifo-"); const fifo = join( root, "cutter-1-00000000-0000-4000-8000-000000000001.json", ); await promisify(execFile)("mkfifo", [fifo]); - const result = await runCliJson(["list-cutter-sessions", "--json"], { - ...process.env, - REA_CUTTER_BRIDGE_DIR: root, + const resultInvocation = await cli.run({ + arguments: ["list-cutter-sessions", "--json"], + environment: { + ...process.env, + REA_CUTTER_BRIDGE_DIR: root, + }, }); + expect(resultInvocation.exitCode, resultInvocation.stderr).toBe(0); + expect(resultInvocation.signal).toBeNull(); + const result = resultInvocation.json; expect(result).toMatchObject({ sessions: [], discovery_status: "no_live_bridge_found", diff --git a/tests/process-global/dylibFileLifetime.test.ts b/tests/process-global/dylibFileLifetime.test.ts index 1cb37d37f2..91d3bf6894 100644 --- a/tests/process-global/dylibFileLifetime.test.ts +++ b/tests/process-global/dylibFileLifetime.test.ts @@ -125,6 +125,23 @@ it("preserves a dyld read failure when the same read observes cancellation", asy expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); }); +it("retains the original dyld I/O cause through provider translation", async () => { + const { client } = await fixture(); + const cause = Object.assign(new Error("controlled dyld EIO"), { + code: "EIO", + path: fault.path, + }); + fault.readFailure = cause; + const result = await client.execute("trace_dylib_resolution", { + roots: ["Main"], + }); + if (result.ok) throw new Error("Expected failed dyld read"); + expect(result.error).toMatchObject({ reason: "io" }); + expect(result.error.cause).toBe(cause); + expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); + await expect(client.close()).resolves.toMatchObject({ ok: true }); +}); + it("observes cancellation during the final dyld digest close", async () => { const { trace } = await fixture(); fault.abortClose = 2; @@ -382,15 +399,31 @@ it("waits for an in-flight final dyld close before completing provider close", a } }); -it("keeps post-admission pathname disappearance as an integrity failure through the provider", async () => { - const { client } = await fixture(); - fault.onRead = async () => { - fault.onRead = undefined; - await rename(fault.path, `${fault.path}.old`); - }; - const result = await client.execute("trace_dylib_resolution", { - roots: ["Main"], - }); - expect(result).toMatchObject({ ok: false, error: { reason: "integrity" } }); - expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); -}); +it.each(["image", "digest"] as const)( + "retains the path and filesystem cause when the pathname disappears during the dyld %s read", + async (stage) => { + const { client } = await fixture(); + fault.onRead = async () => { + if (fault.handles.length !== (stage === "image" ? 1 : 2)) return; + fault.onRead = undefined; + await rename(fault.path, `${fault.path}.old`); + }; + const result = await client.execute("trace_dylib_resolution", { + roots: ["Main"], + }); + expect(result).toMatchObject({ + ok: false, + error: { + reason: "integrity", + detail: expect.stringContaining(fault.path), + cause: { + reason: "integrity", + cause: { code: "ENOENT", path: fault.path }, + }, + }, + }); + expect(fault.handles).toHaveLength(stage === "image" ? 1 : 2); + expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); + await expect(client.close()).resolves.toMatchObject({ ok: true }); + }, +); diff --git a/tests/process-global/stableArtifactConsumers.test.ts b/tests/process-global/stableArtifactConsumers.test.ts index ef6205e1b4..5ef18a45a1 100644 --- a/tests/process-global/stableArtifactConsumers.test.ts +++ b/tests/process-global/stableArtifactConsumers.test.ts @@ -97,10 +97,12 @@ import { PeResourcesProvider } from "../../src/native/pe/PeResourcesProvider.js" import { Il2cppMetadataProvider } from "../../src/il2cpp/Il2cppMetadataProvider.js"; import { ElfNotesProvider } from "../../src/native/elf/ElfNotesProvider.js"; import { PeCertificatesProvider } from "../../src/native/pe/PeCertificatesProvider.js"; +import { PeImportsProvider } from "../../src/native/pe/PeImportsProvider.js"; import { NativeExportsProvider } from "../../src/native/exports/NativeExportsProvider.js"; import { BeamProvider } from "../../src/erlang/BeamProvider.js"; import { inspectElfNotesInputSchema } from "../../src/domain/native/elfNotes.js"; import { inspectPeCertificatesInputSchema } from "../../src/domain/native/peCertificates.js"; +import { inspectPeImportsInputSchema } from "../../src/domain/native/peImports.js"; import { inspectNativeExportsInputSchema } from "../../src/domain/native/nativeExports.js"; import { inspectErlangBeamInputSchema } from "../../src/domain/erlang/beam.js"; import { ModelArtifactProvider } from "../../src/model/ModelArtifactProvider.js"; @@ -855,6 +857,7 @@ const stableArtifactProviders = [ ...staticProviders, ["ELF notes", () => new ElfNotesProvider()], ["PE certificates", () => new PeCertificatesProvider()], + ["PE imports", () => new PeImportsProvider()], ["native exports", () => new NativeExportsProvider()], ["Python pyc", () => new PythonPycProvider()], ] as const; @@ -885,6 +888,11 @@ const inspectFileFailure = ( inspectPeCertificatesInputSchema.parse({ path }), options, ); + if (provider instanceof PeImportsProvider) + return provider.inspect( + inspectPeImportsInputSchema.parse({ path }), + options, + ); if (provider instanceof NativeExportsProvider) return provider.inspect( inspectNativeExportsInputSchema.parse({ path }), diff --git a/tests/support/browser/analyzeWebBundleFixture.ts b/tests/support/browser/analyzeWebBundleFixture.ts new file mode 100644 index 0000000000..db1443f908 --- /dev/null +++ b/tests/support/browser/analyzeWebBundleFixture.ts @@ -0,0 +1,23 @@ +import type { WebPageInspection } from "../../../src/domain/browserObservationSchemas.js"; +import type { WebBundleAnalysis } from "../../../src/domain/webBundleAnalysis.js"; +import { analyzeCapturedWebScript } from "../../../src/domain/webBundleAnalyzer.js"; +import { buildCapturedWebBundleAnalysis } from "../../../src/domain/webBundleAnalyzerResult.js"; +import { isIncludedScript } from "../../../src/domain/webBundleAnalyzerState.js"; + +/** Compose the production script parser and result merger for semantic fixtures. */ +export const analyzeWebBundleFixture = ( + inspection: WebPageInspection, + sourceMaps: WebBundleAnalysis["observations"]["source_maps"] = { + status: "not_requested", + requested: 0, + processed: 0, + items: [], + }, +): WebBundleAnalysis => { + const sourceScripts = inspection.scripts.items.filter(isIncludedScript); + return buildCapturedWebBundleAnalysis( + inspection, + sourceMaps, + sourceScripts.map(analyzeCapturedWebScript), + ); +}; diff --git a/tests/support/cli/cliCommandSession.ts b/tests/support/cli/cliCommandSession.ts index 1805836054..96ed81b521 100644 --- a/tests/support/cli/cliCommandSession.ts +++ b/tests/support/cli/cliCommandSession.ts @@ -61,12 +61,23 @@ interface CliCommandSessionDependencies { readonly commandLimit?: number; /** Resident-set bytes, reported by the worker, that start a new process. */ readonly residentLimitBytes?: number; + /** Optional caller-selected startup and command deadlines; unset by default. */ + readonly startTimeoutMs?: number; + readonly requestTimeoutMs?: number; + /** Grace for EOF cleanup and each signal before escalation. */ + readonly shutdownGraceMs?: number; } interface PendingCommand { readonly id: number; readonly resolve: (result: CliCommandResult) => void; readonly reject: (error: unknown) => void; + readonly timer: ReturnType | undefined; +} + +interface RetiredWorker { + readonly child: ChildProcessWithoutNullStreams; + attempt: Promise | undefined; } type SessionResponse = z.infer; @@ -78,6 +89,11 @@ const positiveLimit = (value: number | undefined, fallback: number): number => ? value : fallback; +const selectedTimeout = (value: number | undefined): number | undefined => + value !== undefined && Number.isSafeInteger(value) && value > 0 + ? value + : undefined; + const envKey = (environment: Readonly>): string => JSON.stringify( Object.keys(environment) @@ -110,13 +126,17 @@ const signalChild = ( const stopChild = async ( child: ChildProcessWithoutNullStreams, + graceMs: number, ): Promise => { if (child.exitCode !== null || child.signalCode !== null) return; + // EOF lets the worker await closeCliJavaScriptResources before exiting. + if (!child.stdin.writableEnded) child.stdin.end(); + if (await waitForExit(child, graceMs)) return; signalChild(child, "SIGTERM"); - if (await waitForExit(child, SHUTDOWN_GRACE_MS)) return; + if (await waitForExit(child, graceMs)) return; if (child.exitCode !== null || child.signalCode !== null) return; signalChild(child, "SIGKILL"); - if (await waitForExit(child, SHUTDOWN_GRACE_MS)) return; + if (await waitForExit(child, graceMs)) return; throw new Error( `shared CLI process ${String(child.pid)} did not exit after SIGKILL`, ); @@ -157,11 +177,14 @@ class SharedCliProcess implements CliCommandSession { readonly #commandLimit: number; readonly #residentLimitBytes: number; + readonly #startTimeoutMs: number | undefined; + readonly #requestTimeoutMs: number | undefined; + readonly #shutdownGraceMs: number; #child: ChildProcessWithoutNullStreams | undefined; #childEnvKey: string | undefined; #commands = 0; #residentBytes: number | undefined; - #retiring: Promise | undefined; + #retiring: RetiredWorker | undefined; #closed = false; #closing: Promise | undefined; #starting: Promise | undefined; @@ -181,6 +204,12 @@ class SharedCliProcess implements CliCommandSession { dependencies.residentLimitBytes, RESIDENT_REUSE_LIMIT_BYTES, ); + this.#startTimeoutMs = selectedTimeout(dependencies.startTimeoutMs); + this.#requestTimeoutMs = selectedTimeout(dependencies.requestTimeoutMs); + this.#shutdownGraceMs = positiveLimit( + dependencies.shutdownGraceMs, + SHUTDOWN_GRACE_MS, + ); } run(request: CliCommandRequest): Promise { @@ -207,45 +236,83 @@ class SharedCliProcess implements CliCommandSession { close(): Promise { this.#closed = true; if (this.#closing !== undefined) return this.#closing; - const attempt = this.#shutdown(); + let attempt: Promise; + attempt = this.#shutdown().catch((error: unknown) => { + if (this.#closing === attempt) this.#closing = undefined; + throw error; + }); this.#closing = attempt; return attempt; } async #shutdown(): Promise { - try { - const owner = this.#child; - if (owner !== undefined) await stopChild(owner); - const retiring = this.#retiring; - if (retiring !== undefined) await retiring; - } catch (error) { - this.#closing = undefined; - throw error; - } const pendingStart = this.#starting; + this.#failReady(closedError()); + this.#failPending(closedError()); + const owner = this.#child; + if (owner !== undefined) this.#retire(owner); + await this.#finishRetirement(); if (pendingStart !== undefined) await pendingStart.catch(() => undefined); } async #invoke(request: CliCommandRequest): Promise { if (this.#closed) throw closedError(); - try { - return await this.#run(request); - } finally { + await this.#finishRetirement(); + const current = this.#child; + if ( + !this.#reusable(current, envKey(request.environment)) && + current !== undefined + ) { + this.#retire(current); await this.#finishRetirement(); } + if (this.#closed) throw closedError(); + let result: CliCommandResult; + try { + if (this.#child === undefined) await this.#start(request.environment); + result = await this.#run(request); + } catch (primary) { + try { + await this.#finishRetirement(); + } catch (cleanup) { + const primaryReason = + primary instanceof Error ? primary.message : String(primary); + const cleanupReason = + cleanup instanceof Error ? cleanup.message : String(cleanup); + throw new AggregateError( + [primary, cleanup], + `${primaryReason}; shared CLI cleanup failed: ${cleanupReason}`, + { cause: primary }, + ); + } + throw primary; + } + await this.#finishRetirement(); + return result; } async #run(request: CliCommandRequest): Promise { - await this.#ensure(request.environment); const owner = this.#child; if (this.#closed || owner === undefined) throw closedError(); const id = this.#nextId; this.#nextId += 1; const result = new Promise((resolve, reject) => { - this.#pending = { id, resolve, reject }; + const timer = + this.#requestTimeoutMs === undefined + ? undefined + : setTimeout(() => { + this.#failTransport( + owner, + new Error( + `Shared CLI command did not respond within ${String(this.#requestTimeoutMs)} ms`, + ), + ); + }, this.#requestTimeoutMs); + timer?.unref(); + this.#pending = { id, resolve, reject, timer }; }); try { - await writeChild( + void writeChild( owner, encodeCliSessionFrame({ id, @@ -253,7 +320,9 @@ class SharedCliProcess implements CliCommandSession { cwd: request.cwd, env: request.environment, }), - ); + ).catch((error: unknown) => { + this.#failTransport(owner, error); + }); } catch (error) { this.#failTransport(owner, error); } @@ -263,11 +332,14 @@ class SharedCliProcess implements CliCommandSession { async #finishRetirement(): Promise { const retiring = this.#retiring; if (retiring === undefined) return; - try { - await retiring; - } finally { - if (this.#retiring === retiring) this.#retiring = undefined; - } + retiring.attempt ??= stopChild( + retiring.child, + this.#shutdownGraceMs, + ).finally(() => { + retiring.attempt = undefined; + }); + await retiring.attempt; + if (this.#retiring === retiring) this.#retiring = undefined; } #reusable( @@ -289,16 +361,6 @@ class SharedCliProcess implements CliCommandSession { ); } - async #ensure(environment: Readonly>): Promise { - if (this.#closed) throw closedError(); - const key = envKey(environment); - const current = this.#child; - if (this.#reusable(current, key)) return; - if (current !== undefined) await stopChild(current); - if (this.#closed) throw closedError(); - await this.#start(environment); - } - #start(environment: Readonly>): Promise { if (this.#closed) return Promise.reject(closedError()); const started = this.#spawnWorker(environment); @@ -311,12 +373,25 @@ class SharedCliProcess implements CliCommandSession { this.#rejectReady = reject; }); this.#starting = ready; + const timer = + this.#startTimeoutMs === undefined + ? undefined + : setTimeout(() => { + this.#failTransport( + started, + new Error( + `Shared CLI worker did not become ready within ${String(this.#startTimeoutMs)} ms`, + ), + ); + }, this.#startTimeoutMs); + timer?.unref(); this.#watch(started); if (this.#closed) { - signalChild(started, "SIGTERM"); this.#failReady(closedError()); + this.#retire(started); } return ready.finally(() => { + clearTimeout(timer); if (this.#starting === ready) this.#starting = undefined; }); } @@ -378,6 +453,7 @@ class SharedCliProcess implements CliCommandSession { return; } this.#pending = undefined; + clearTimeout(current.timer); if (message.fatal === true) { this.#retire(owner); } else { @@ -394,7 +470,7 @@ class SharedCliProcess implements CliCommandSession { } this.#commands = 0; this.#residentBytes = undefined; - this.#retiring = stopChild(owner); + this.#retiring ??= { child: owner, attempt: undefined }; } #onClose( @@ -427,6 +503,7 @@ class SharedCliProcess implements CliCommandSession { #failPending(error: unknown): void { const current = this.#pending; this.#pending = undefined; + clearTimeout(current?.timer); current?.reject(error); } diff --git a/tests/support/cli/cliCommandSessionWorker.mjs b/tests/support/cli/cliCommandSessionWorker.mjs index c98b6aa3ba..c75e3a33da 100644 --- a/tests/support/cli/cliCommandSessionWorker.mjs +++ b/tests/support/cli/cliCommandSessionWorker.mjs @@ -1,8 +1,9 @@ import { Writable } from "node:stream"; import { access } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { prepareCliProcessEnvironment } from "../../../scripts/lib/cli-process-environment.mjs"; import { createCliSessionFrameReader, encodeCliSessionFrame, @@ -19,23 +20,6 @@ const { default: packageJson } = await import( { with: { type: "json" } } ); -const prepareCliProcess = () => { - process.env.REA_PACKAGE_VERSION = packageJson.version; - // Windows home lookup throws for an empty USERPROFILE. Dependencies resolve - // home during import, before REA's own resolver can apply its account fallback. - if (process.platform === "win32" && process.env.USERPROFILE === "") { - delete process.env.USERPROFILE; - } - if ( - process.platform === "android" && - process.env.PLAYWRIGHT_BROWSERS_PATH === undefined - ) { - const cacheRoot = - process.env.XDG_CACHE_HOME ?? join(process.env.HOME ?? ".", ".cache"); - process.env.PLAYWRIGHT_BROWSERS_PATH = join(cacheRoot, "ms-playwright"); - } -}; - const runtimeFiles = [ "dist/cli-runtime/cli.js", "dist/cli-runtime/cliOutput.js", @@ -43,7 +27,11 @@ const runtimeFiles = [ "dist/cli-runtime/cliResultOutput.js", ]; -prepareCliProcess(); +prepareCliProcessEnvironment( + process.env, + process.platform, + packageJson.version, +); if (!(await compiledRuntimeExists(runtimeFiles))) { process.stderr.write( `REA's compiled runtime is missing. Run \`npm ci && npm run build:cached\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`, @@ -118,7 +106,11 @@ async function handle(value) { } for (const key of Object.keys(process.env)) delete process.env[key]; Object.assign(process.env, request.env); - prepareCliProcess(); + prepareCliProcessEnvironment( + process.env, + process.platform, + packageJson.version, + ); process.chdir(request.cwd); const result = await runOneShot(request.arguments); await writeFrame({ diff --git a/tests/support/cli/compiledCliSession.ts b/tests/support/cli/compiledCliSession.ts deleted file mode 100644 index 086efa6806..0000000000 --- a/tests/support/cli/compiledCliSession.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; -import { fileURLToPath } from "node:url"; - -import { z } from "zod"; - -import { waitForExit } from "../process/processFixture.js"; - -const START_TIMEOUT_MS = 20_000; -const REQUEST_TIMEOUT_MS = 20_000; -const STOP_TIMEOUT_MS = 1_000; -const STDERR_TAIL_CHARS = 4_000; -const workerPath = fileURLToPath( - new URL("./compiledCliWorker.mjs", import.meta.url), -); -const readyMessage = z.object({ ready: z.literal(true) }).strict(); -const responseMessage = z - .object({ - id: z.number(), - exitCode: z.number(), - stdout: z.string(), - error: z.string().optional(), - }) - .strict(); - -type CliResponse = z.infer; - -interface PendingResponse { - readonly resolve: (response: CliResponse) => void; - readonly reject: (cause: unknown) => void; -} - -interface OwnedStop { - readonly child: ChildProcessWithoutNullStreams; - attempt: Promise | undefined; -} - -interface SessionState { - child: ChildProcessWithoutNullStreams | undefined; - starting: Promise | undefined; - buffer: string; - nextId: number; - readonly pending: Map; - readonly stopping: OwnedStop[]; - queue: Promise; - stderr: string; - closed: boolean; - closing: Promise | undefined; - settleStart: ((cause?: Error) => void) | undefined; - ready: boolean; - readonly requestTimeoutMs: number; - readonly stopTimeoutMs: number; - readonly spawnWorker: () => ChildProcessWithoutNullStreams; -} - -/** One warm compiled CLI process serving production JSON commands. */ -export interface CompiledCliSession { - run(args: readonly string[]): Promise; - close(): Promise; -} - -/** Limits and process factory for session tests. Production callers omit them. */ -export interface CompiledCliSessionOptions { - readonly spawnWorker?: () => ChildProcessWithoutNullStreams; - readonly requestTimeoutMs?: number; - readonly stopTimeoutMs?: number; -} - -/** Start a session that loads the compiled CLI once and reuses that process. */ -export const startCompiledCliSession = ( - options: CompiledCliSessionOptions = {}, -): CompiledCliSession => { - const state = createSessionState(options); - return { - run: (args) => enqueue(state, () => dispatch(state, args)), - close: () => closeSession(state), - }; -}; - -const positiveMilliseconds = ( - value: number | undefined, - fallback: number, -): number => - value !== undefined && Number.isSafeInteger(value) && value > 0 - ? value - : fallback; - -const defaultSpawnWorker = (): ChildProcessWithoutNullStreams => - spawn(process.execPath, [workerPath], { - cwd: process.cwd(), - env: { ...process.env, REA_LOG_LEVEL: "silent" }, - stdio: ["pipe", "pipe", "pipe"], - }); - -const createSessionState = ( - options: CompiledCliSessionOptions, -): SessionState => ({ - child: undefined, - starting: undefined, - buffer: "", - nextId: 1, - pending: new Map(), - stopping: [], - queue: Promise.resolve(), - stderr: "", - closed: false, - closing: undefined, - settleStart: undefined, - ready: false, - requestTimeoutMs: positiveMilliseconds( - options.requestTimeoutMs, - REQUEST_TIMEOUT_MS, - ), - stopTimeoutMs: positiveMilliseconds(options.stopTimeoutMs, STOP_TIMEOUT_MS), - spawnWorker: options.spawnWorker ?? defaultSpawnWorker, -}); - -const enqueue = ( - state: SessionState, - task: () => Promise, -): Promise => { - const run = state.queue.then(task); - state.queue = run.then( - () => undefined, - () => undefined, - ); - return run; -}; - -const dispatch = async ( - state: SessionState, - args: readonly string[], -): Promise => { - await ensureStarted(state); - const child = state.child; - if (child === undefined) - throw new Error("Compiled CLI session is not running"); - const id = state.nextId; - state.nextId += 1; - const response = await request(state, child, id, args); - if (response.exitCode !== 0) throw new Error(failureText(state, response)); - return JSON.parse(response.stdout) as unknown; -}; - -const request = ( - state: SessionState, - child: ChildProcessWithoutNullStreams, - id: number, - args: readonly string[], -): Promise => - new Promise((resolve, reject) => { - let settled = false; - let timer: ReturnType | undefined; - const settle = (finish: () => void): void => { - if (settled) return; - settled = true; - clearTimeout(timer); - state.pending.delete(id); - finish(); - }; - timer = setTimeout(() => { - settle(() => { - reject( - new Error( - `Compiled CLI command did not respond within ${String(state.requestTimeoutMs)} ms`, - ), - ); - retireChild(state, child); - }); - }, state.requestTimeoutMs); - timer.unref(); - state.pending.set(id, { - resolve: (response) => { - settle(() => { - resolve(response); - }); - }, - reject: (cause) => { - settle(() => { - reject(cause); - }); - }, - }); - child.stdin.write(`${JSON.stringify({ id, args })}\n`, (cause) => { - if (cause === undefined || cause === null) return; - settle(() => { - reject(cause); - }); - }); - }); - -const failureText = (state: SessionState, response: CliResponse): string => { - const detail = response.error ?? response.stdout.slice(0, 500); - const stderr = state.stderr.trim(); - return stderr.length === 0 - ? `Compiled CLI command failed with status ${String(response.exitCode)}: ${detail}` - : `Compiled CLI command failed with status ${String(response.exitCode)}: ${detail}\n${stderr}`; -}; - -const ensureStarted = async (state: SessionState): Promise => { - if (state.closed) throw sessionClosed(); - await releaseRetired(state); - if (state.closed) throw sessionClosed(); - if (state.ready && state.child !== undefined) return; - state.starting ??= launch(state); - await state.starting; - if (state.closed) throw sessionClosed(); -}; - -const launch = (state: SessionState): Promise => - new Promise((resolve, reject) => { - const child = state.spawnWorker(); - state.child = child; - let settled = false; - let timer: ReturnType | undefined; - const finish = (cause?: Error): void => { - if (settled) return; - settled = true; - clearTimeout(timer); - if (state.settleStart === finish) state.settleStart = undefined; - if (cause === undefined) resolve(); - else reject(cause); - }; - timer = setTimeout(() => { - signalChild(child, "SIGKILL"); - finish( - new Error( - `Compiled CLI session did not start within ${String(START_TIMEOUT_MS)} ms\n${state.stderr}`, - ), - ); - }, START_TIMEOUT_MS); - timer.unref(); - state.settleStart = finish; - const fail = (cause: unknown): void => { - finish(cause instanceof Error ? cause : new Error(String(cause))); - }; - const currentChild = (): boolean => state.child === child; - child.once("error", fail); - child.once("exit", (code, signal) => { - if (!currentChild()) return; - state.child = undefined; - state.ready = false; - state.starting = undefined; - state.buffer = ""; - const cause = new Error( - `Compiled CLI session exited (${String(code)}, ${String(signal)})\n${state.stderr}`, - ); - fail(cause); - rejectPending(state, cause); - }); - child.stderr.setEncoding("utf8"); - child.stderr.on("data", (chunk: string) => { - if (!currentChild()) return; - state.stderr = `${state.stderr}${chunk}`.slice(-STDERR_TAIL_CHARS); - }); - child.stdout.setEncoding("utf8"); - child.stdout.on("data", (chunk: string) => { - if (!currentChild()) return; - state.buffer += chunk; - drainLines(state, () => { - state.ready = true; - finish(); - }); - }); - }); - -const hasExited = (child: ChildProcessWithoutNullStreams): boolean => - child.exitCode !== null || child.signalCode !== null; - -const signalChild = ( - child: ChildProcessWithoutNullStreams, - signal: NodeJS.Signals, -): void => { - try { - child.kill(signal); - } catch (error) { - const code = - error instanceof Error && "code" in error ? error.code : undefined; - if (code !== "ESRCH") throw error; - } -}; - -const detachChild = ( - state: SessionState, - child: ChildProcessWithoutNullStreams, -): void => { - if (state.child !== child) return; - state.child = undefined; - state.ready = false; - state.starting = undefined; - state.buffer = ""; -}; - -const stopOwned = (state: SessionState, entry: OwnedStop): Promise => { - entry.attempt ??= (async () => { - if (hasExited(entry.child)) return true; - signalChild(entry.child, "SIGKILL"); - return waitForExit(entry.child, state.stopTimeoutMs); - })().finally(() => { - entry.attempt = undefined; - }); - return entry.attempt; -}; - -const trackRetired = ( - state: SessionState, - child: ChildProcessWithoutNullStreams, -): OwnedStop => { - const existing = state.stopping.find((entry) => entry.child === child); - if (existing !== undefined) return existing; - const entry: OwnedStop = { child, attempt: undefined }; - state.stopping.push(entry); - if (!hasExited(child)) void stopOwned(state, entry).catch(() => undefined); - return entry; -}; - -const releaseRetired = async (state: SessionState): Promise => { - let failed = false; - for (const entry of [...state.stopping]) { - if (hasExited(entry.child)) continue; - const stopped = await stopOwned(state, entry).catch(() => false); - if (!stopped || !hasExited(entry.child)) failed = true; - } - const retained = state.stopping.filter((entry) => !hasExited(entry.child)); - state.stopping.length = 0; - state.stopping.push(...retained); - if (failed) - throw new Error("Compiled CLI session stayed running after SIGKILL"); -}; - -const retireChild = ( - state: SessionState, - child: ChildProcessWithoutNullStreams, -): void => { - detachChild(state, child); - if (hasExited(child)) return; - trackRetired(state, child); -}; - -const drainLines = (state: SessionState, onReady: () => void): void => { - let newline = state.buffer.indexOf("\n"); - while (newline !== -1) { - const line = state.buffer.slice(0, newline); - state.buffer = state.buffer.slice(newline + 1); - if (line.length > 0) acceptLine(state, line, onReady); - newline = state.buffer.indexOf("\n"); - } -}; - -const acceptLine = ( - state: SessionState, - line: string, - onReady: () => void, -): void => { - let message: unknown; - try { - message = JSON.parse(line) as unknown; - } catch (cause) { - rejectPending( - state, - cause instanceof Error ? cause : new Error(String(cause)), - ); - return; - } - if (readyMessage.safeParse(message).success) { - onReady(); - return; - } - const parsed = responseMessage.safeParse(message); - if (!parsed.success) { - rejectPending( - state, - new Error("Compiled CLI session returned an unexpected message"), - ); - return; - } - const pending = state.pending.get(parsed.data.id); - state.pending.delete(parsed.data.id); - if (pending === undefined) { - rejectPending( - state, - new Error( - `Compiled CLI session returned unexpected id ${String(parsed.data.id)}`, - ), - ); - return; - } - pending.resolve(parsed.data); -}; - -const rejectPending = (state: SessionState, cause: Error): void => { - for (const pending of state.pending.values()) pending.reject(cause); - state.pending.clear(); -}; - -const closeSession = (state: SessionState): Promise => { - state.closed = true; - if (state.closing !== undefined) return state.closing; - let attempt: Promise; - attempt = shutdown(state).catch((error: unknown) => { - if (state.closing === attempt) state.closing = undefined; - throw error; - }); - state.closing = attempt; - return attempt; -}; - -const shutdown = async (state: SessionState): Promise => { - const child = state.child; - let forced = false; - const stillRunning: OwnedStop[] = []; - if (child !== undefined) { - rejectPending(state, sessionClosed()); - state.settleStart?.(sessionClosed()); - detachChild(state, child); - const stopped = await stopGracefully(state, child); - if (stopped === "forced") forced = true; - if (stopped === "stuck" && !hasExited(child)) - stillRunning.push({ child, attempt: undefined }); - } - try { - await releaseRetired(state); - } finally { - state.stopping.push(...stillRunning); - } - if (state.stopping.length > 0) - throw new Error("Compiled CLI session stayed running after SIGKILL"); - if (forced) - throw new Error("Compiled CLI session did not exit after stdin closed"); -}; - -const stopGracefully = async ( - state: SessionState, - child: ChildProcessWithoutNullStreams, -): Promise<"clean" | "forced" | "stuck"> => { - child.stdin.end(); - if (await waitForExit(child, state.stopTimeoutMs)) return "clean"; - signalChild(child, "SIGTERM"); - if (await waitForExit(child, state.stopTimeoutMs)) return "forced"; - signalChild(child, "SIGKILL"); - if (await waitForExit(child, state.stopTimeoutMs)) return "forced"; - return "stuck"; -}; - -const sessionClosed = (): Error => new Error("Compiled CLI session is closed"); diff --git a/tests/support/cli/compiledCliWorker.mjs b/tests/support/cli/compiledCliWorker.mjs deleted file mode 100644 index fc16840989..0000000000 --- a/tests/support/cli/compiledCliWorker.mjs +++ /dev/null @@ -1,151 +0,0 @@ -import { Writable } from "node:stream"; -import { createInterface } from "node:readline"; - -const { closeCliJavaScriptResources, createCli } = - await import("../../../dist/cli-runtime/cli.js"); -const { createCliResultOutput } = - await import("../../../dist/cli-runtime/cliResultOutput.js"); -const { - cliConfigurationError, - normalizeCliArguments, - renderCliOutputArgumentError, - renderEmptyFilteredCliOutput, - parseCliOutputArguments, -} = await import("../../../dist/cli-runtime/cliOutput.js"); - -const MAX_STDOUT_CHARS = 16 * 1024 * 1024; - -const writeMessage = (message) => { - process.stdout.write(`${JSON.stringify(message)}\n`); -}; - -// One CLI instance serves every command. A fresh instance per command retains -// its command registrations after the call returns; production exits instead. -const outputParts = []; -let outputCharacters = 0; -const destination = new Writable({ - write(chunk, _encoding, callback) { - const text = Buffer.isBuffer(chunk) - ? chunk.toString("utf8") - : String(chunk); - outputCharacters += text.length; - if (outputCharacters > MAX_STDOUT_CHARS) { - callback(new Error("Compiled CLI stdout exceeded 16 MiB")); - return; - } - outputParts.push(text); - callback(); - }, -}); -Object.defineProperty(destination, "isTTY", { value: false }); -const resultOutput = createCliResultOutput( - parseCliOutputArguments(["--json"]), - destination, -); -const cli = createCli(process.env, resultOutput); - -const resetOutput = () => { - outputParts.length = 0; - outputCharacters = 0; -}; - -const runCommand = async (args) => { - const previousExitCode = process.exitCode; - process.exitCode = undefined; - resetOutput(); - try { - const cliArgs = normalizeCliArguments(args); - const outputOptions = parseCliOutputArguments(cliArgs); - const outputArguments = outputOptions.validation; - if (!outputArguments.ok) - return { - exitCode: 1, - stdout: renderCliOutputArgumentError(outputArguments), - }; - const configurationError = cliConfigurationError(process.env); - if (configurationError !== undefined) - return { exitCode: 1, stdout: "", error: configurationError }; - - let wroteOutput = false; - let explicitExit; - resultOutput?.begin(outputOptions); - try { - await cli.serve(cliArgs, { - stdout: (output) => { - if (resultOutput?.handled) return; - if (output.length === 0) return; - wroteOutput = true; - if (resultOutput?.captureSdkOutput(output)) return; - destination.write(output); - }, - exit: (code) => { - explicitExit = code; - }, - }); - } catch (cause) { - return { - exitCode: 1, - stdout: outputParts.join(""), - error: - cause instanceof Error - ? (cause.stack ?? cause.message) - : String(cause), - }; - } - if (!wroteOutput && !resultOutput?.handled) { - const empty = renderEmptyFilteredCliOutput(outputOptions); - if (empty !== undefined) destination.write(empty); - } - await resultOutput?.finishSdkOutput( - explicitExit ?? - (typeof process.exitCode === "number" ? process.exitCode : 0), - ); - // logCliCommand reports some failures only through process.exitCode. - return { - exitCode: - explicitExit ?? - (typeof process.exitCode === "number" ? process.exitCode : 0), - stdout: outputParts.join(""), - }; - } finally { - process.exitCode = previousExitCode; - } -}; - -const acceptRequest = async (line) => { - let request; - try { - request = JSON.parse(line); - } catch (cause) { - writeMessage({ - id: -1, - exitCode: 1, - stdout: "", - error: cause instanceof Error ? cause.message : String(cause), - }); - return; - } - const args = request?.args; - if ( - typeof request?.id !== "number" || - !Array.isArray(args) || - args.some((argument) => typeof argument !== "string") - ) { - writeMessage({ - id: typeof request?.id === "number" ? request.id : -1, - exitCode: 1, - stdout: "", - error: "Compiled CLI session expected an id and string arguments", - }); - return; - } - writeMessage({ id: request.id, ...(await runCommand(args)) }); -}; - -const lines = createInterface({ input: process.stdin, crlfDelay: Infinity }); -writeMessage({ ready: true }); -for await (const line of lines) { - if (line.trim().length === 0) continue; - await acceptRequest(line); -} -await closeCliJavaScriptResources(); diff --git a/vitest.config.ts b/vitest.config.ts index 6f733c9874..92519037a8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -66,10 +66,10 @@ const PROJECT_POLICIES = { // CLI tests create fresh command instances or subprocesses and own their // cleanup. Reuse the immutable graph; module mocks belong in process-global. isolate: false, - // Coverage of the shared command/schema graph exhausts the ordinary - // 384 MiB budget. The preloaded CLI/ADB control completes with 512 MiB; - // keep both profiles bounded with the same concurrency. - execArgv: [`--max-old-space-size=${COVERAGE_ENABLED ? "512" : "384"}`], + // The shared source/contract graph also needs transient schema/help space. + // Ordinary CI exhausted 384 MiB after earlier files warmed that graph. + // Use the measured 512 MiB budget with the same bounded concurrency. + execArgv: ["--max-old-space-size=512"], }, "process-boundary": { pool: "forks" as const,