diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md
index de9f6b7c1..f7e44e17a 100644
--- a/.agents/skills/reverse-engineer-anything/SKILL.md
+++ b/.agents/skills/reverse-engineer-anything/SKILL.md
@@ -31,6 +31,49 @@ Diagnose the observed blocker, continue useful supported work, and resume the
original task after repair. Client approval, shell PATH, registration and engine
availability have different remedies; avoid repeating setup for all of them.
+1. Diagnose without changing REA or client configuration. `npx` may download
+ the package and write to npm's cache. For Codex, run
+ `npx -y rea-agents@latest doctor --client codex --json`. Substitute the current
+ supported client: `claude_code`, `claude_desktop`, `codex`, `cursor`,
+ `gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`, `copilot_cli`,
+ `commandcode`, `vscode`, `grok_build`, `omp`, or `grok_bot`. `grok_bot` has no local
+ file registration: doctor reports the Grok Bot chat step, and setup does not
+ write `mcp.json`. If the client is unknown, use `doctor --json` and
+ inspect its registration results before choosing a setup scope.
+2. Distinguish the reason. Missing, malformed, or stale registration needs a
+ scoped configuration repair. An aligned registration with no tools in the
+ active session needs a restart/reconnection; doctor cannot prove that the
+ current agent has connected. A missing provider affects only tasks requiring
+ that provider: static JavaScript inspection needs neither Hopper nor Ghidra,
+ and Android inspection has separate bring-your-own JADX/Java prerequisites.
+3. For a configuration repair, prepare the read-only plan:
+ `npx -y rea-agents@latest setup --client codex --dry-run --json`.
+ Use the current client's ID, show its exact proposed paths, backups, and
+ changes, and obtain approval before setup writes configuration or installs
+ Hopper. Setup normally installs the matching bundled skill too; include that
+ replacement in the reviewed plan. After approval, apply the same scope with
+ `npx -y rea-agents@latest setup --client codex --yes`. Add `--install-hopper`
+ only if that separate installation was needed and explicitly approved.
+4. Restart/reconnect the affected agent. Verify that REA tools actually appear
+ in the session, then resume the original investigation. If they remain
+ absent, inspect the client's MCP launch error rather than repeating setup.
+
+REA setup never installs or upgrades Node.js, npm, Homebrew, Java, Ghidra, IDA,
+JADX, Binwalk, or Unblob. Use existing prerequisites; do not install unrelated
+software to repair
+MCP registration. For an unsupported client, use manual stdio registration
+with a version-pinned `rea-agents` package or continue through the CLI.
+
+A concrete CLI fallback for an operator-supplied JavaScript tree or ASAR is:
+
+```bash
+npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
+```
+
+No MCP registration or native engine is required. Read the returned Evidence,
+graph, limitations, and unknowns with the same care as an MCP result; the CLI
+returns the Evidence record directly. This fallback does not establish that
+MCP is configured. Native CLI tasks still require their selected engine.
Prefer an existing `rea` CLI. When none is installed and REA package acquisition
is within the authorized scope, use `npx -y rea-agents@latest` in its place.
Configuration repair starts with a read-only setup plan; show paths, backups and
@@ -122,6 +165,14 @@ explicit path or endpoint and do not need it.
absent metadata does not prove a binary is not Go, and unparsed text records
remain explicit. See the
[Go binary guide](https://github.com/morluto/rea/blob/main/docs/go-binaries.md).
+- Embedded cargo-auditable Rust dependency metadata: `inspect_rust_binary`
+ with the explicit local ELF, PE, or thin Mach-O file. This built-in
+ target-free inspection requires no Rust installation or disassembler. Preserve
+ package order, dependency indices, and the section file range. A missing
+ `.dep-v0` section is null metadata, not proof the binary has no dependencies.
+ A rustc commit hash is an inference from a read-only `/rustc/<40-hex>/` path.
+ See the
+ [Rust binary guide](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
- Explicit local SQLite database snapshot: `inspect_sqlite_database`. Inspect
with Node.js 24.x >=24.15 or 26+ providing native SQLite limits. Read schema
diff --git a/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md b/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md
index c651287f3..539bfb6b2 100644
--- a/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md
+++ b/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md
@@ -65,3 +65,7 @@ Use `inspect_analysis_view` with `view: {kind: "api-surface"}` on retained
method, origin and path template. Format matches are inferences, not handler
identity or schemas. Group pages cite parent record ordinals; consult record
coverage and exclusions before claiming completeness. No requests are replayed.
+When the connected schema advertises `include_query_parameter_names`, select
+it in the view for distinct observed names with original record ordinals. Query
+values are omitted; the names establish neither required parameters nor a
+complete server schema. Keep the original parent Evidence for exact URLs.
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 3e185a8a2..960e46094 100644
--- a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md
+++ b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md
@@ -11,6 +11,18 @@ unparsed module-text line. A missing record is an unknown, not proof of a
non-Go binary; observed metadata does not authenticate its claims. Function,
source and type recovery remain a separate native-analysis question.
+## Rust dependency metadata
+
+Use `inspect_rust_binary` for an explicit local ELF, PE, or thin Mach-O when
+the question concerns the cargo-auditable crate list embedded in `.dep-v0`
+(Mach-O: `__DATA,.dep-v0`). No `open_binary`, Rust installation, or target
+execution is required. The CLI equivalent is `inspect-rust-binary PATH`.
+Preserve artifact SHA-256, package order, dependency indices, and the section
+file range. A missing section is null metadata, not proof the binary has no
+dependencies. An omitted package kind stays null. A rustc commit hash is
+inferred from a read-only `/rustc/<40-hex>/` path and does not by itself
+identify the compiler. See [Rust binaries](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
+
## SQLite snapshots
Use `inspect_sqlite_database` with an explicit local database snapshot for its
diff --git a/.github/actions/npm-cache/action.yml b/.github/actions/npm-cache/action.yml
new file mode 100644
index 000000000..1af22da2b
--- /dev/null
+++ b/.github/actions/npm-cache/action.yml
@@ -0,0 +1,42 @@
+name: Cache npm package downloads
+description: Restore npm's download cache and save it only from trusted default-branch events.
+outputs:
+ can-save:
+ description: Whether this run may update the shared npm download cache.
+ value: ${{ steps.context.outputs.can-save }}
+ path:
+ description: The npm download-cache directory.
+ value: ${{ steps.context.outputs.path }}
+runs:
+ using: composite
+ steps:
+ - id: context
+ shell: bash
+ env:
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ LOCKFILES_HASH: ${{ hashFiles('package-lock.json', 'npm-shrinkwrap.json', '.npmrc') }}
+ run: |
+ cache_path="$(npm config get cache)"
+ runtime_arch="$(node -p 'process.arch')"
+ can_save=false
+ if [[ -n "${DEFAULT_BRANCH}" && "${GITHUB_REF}" == "refs/heads/${DEFAULT_BRANCH}" && ( "${GITHUB_EVENT_NAME}" == "push" || "${GITHUB_EVENT_NAME}" == "schedule" || "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ) ]]; then
+ can_save=true
+ fi
+ cache_key="npm-download-${RUNNER_OS}-${runtime_arch}-${LOCKFILES_HASH}"
+ {
+ printf 'can-save=%s\n' "${can_save}"
+ printf 'path=%s\n' "${cache_path}"
+ printf 'key=%s\n' "${cache_key}"
+ } >> "${GITHUB_OUTPUT}"
+ - name: Restore and save npm package downloads
+ if: steps.context.outputs.can-save == 'true'
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: ${{ steps.context.outputs.path }}
+ key: ${{ steps.context.outputs.key }}
+ - name: Restore npm package downloads without saving
+ if: steps.context.outputs.can-save != 'true'
+ uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: ${{ steps.context.outputs.path }}
+ key: ${{ steps.context.outputs.key }}
diff --git a/.github/workflows/ci-timings.yml b/.github/workflows/ci-timings.yml
index 73a9ba4b9..74f27af32 100644
--- a/.github/workflows/ci-timings.yml
+++ b/.github/workflows/ci-timings.yml
@@ -12,12 +12,13 @@ concurrency:
permissions:
contents: read
actions: read
+ pull-requests: read
jobs:
report:
name: Collect recent completed workflow attempts
runs-on: ubuntu-latest
- timeout-minutes: 5
+ timeout-minutes: 10
steps:
# Consume only API data and execute batch code from the trusted default branch.
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
@@ -27,6 +28,18 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
+ - name: Cache npm package downloads
+ id: npm-cache
+ continue-on-error: true
+ uses: ./.github/actions/npm-cache
+ - name: Diagnose missing and startup-failed PR CI
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ github.token }}
+ CI_REPOSITORY: ${{ github.repository }}
+ CI_REPOSITORY_ID: ${{ github.repository_id }}
+ CI_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ run: node scripts/ci/pr-ci-health.mjs
- name: Record queue, execution, and matched step timing
continue-on-error: true
env:
@@ -44,3 +57,28 @@ jobs:
if-no-files-found: error
overwrite: true
retention-days: 14
+ - name: Install dependencies for static failure attribution
+ id: failure-deps
+ if: ${{ !cancelled() && hashFiles('.cache/ci-job-timings/batch-manifest.json') != '' }}
+ continue-on-error: true
+ run: npm ci
+ - name: Attribute supported static failures against main
+ if: ${{ !cancelled() && steps.failure-deps.outcome == 'success' && hashFiles('.cache/ci-job-timings/batch-manifest.json') != '' }}
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ github.token }}
+ CI_REPOSITORY: ${{ github.repository }}
+ CI_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ run: node scripts/ci/failure-report.mjs
+ - name: Upload CI health and failure diagnostics
+ if: ${{ !cancelled() }}
+ continue-on-error: true
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: ci-health-diagnostics
+ path: |
+ .cache/ci-pr-health/
+ .cache/ci-failures/
+ include-hidden-files: true
+ if-no-files-found: warn
+ retention-days: 14
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8b47fcf89..b35b6a6e1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,6 +42,7 @@ jobs:
tier: ${{ steps.plan.outputs.tier }}
qualification_run_id: ${{ steps.qualification.outputs.run_id }}
code: ${{ steps.plan.outputs.code }}
+ reverse_tools: ${{ steps.plan.outputs.reverse_tools }}
sqlite: ${{ steps.plan.outputs.sqlite }}
runtime: ${{ steps.plan.outputs.runtime }}
package: ${{ steps.plan.outputs.package }}
@@ -122,11 +123,28 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
+ - name: Restore static task cache
+ if: needs.changes.outputs.code == 'true'
+ id: static-cache
+ uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: .turbo/cache/
+ key: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-${{ github.sha }}
+ # Turbo validates each task's inputs after restoring this snapshot.
+ restore-keys: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-
- name: Validate workflow syntax and expressions
if: needs.changes.outputs.workflows == 'true'
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 -shellcheck= -pyflakes=
+ - name: Record static task source and execution identity
+ continue-on-error: true
+ if: needs.changes.outputs.code == 'true'
+ env:
+ CI_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ run: npm run ci:static-context
- if: needs.changes.outputs.code == 'true'
run: npm run check:ci
- if: needs.changes.outputs.code == 'true'
@@ -136,6 +154,15 @@ jobs:
run: npm run test:python
- if: needs.changes.outputs.code != 'true'
run: npm run format:check
+ - name: Save validated static task cache
+ if: >-
+ success() && needs.changes.outputs.code == 'true' &&
+ steps.npm-cache.outputs.can-save == 'true' &&
+ steps.static-cache.outputs.cache-hit != 'true'
+ uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: .turbo/cache/
+ key: ${{ steps.static-cache.outputs.cache-primary-key }}
- name: Upload complete static diagnostics
if: always() && needs.changes.outputs.code == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
@@ -146,6 +173,34 @@ jobs:
if-no-files-found: ignore
retention-days: 7
+ test-source:
+ name: Source tests
+ needs: changes
+ if: needs.changes.outputs.code == 'true' && needs.changes.outputs.tier == 'fast'
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version-file: .nvmrc
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
+ - run: npm ci
+ - name: Run source tests without waiting for build
+ run: npm run test:fast -- --reporter=default --reporter=blob
+ env:
+ VITEST_BLOB_LABEL: source
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ if: ${{ !cancelled() }}
+ with:
+ name: vitest-report-source
+ path: .vitest/blob/*
+ include-hidden-files: true
+ if-no-files-found: error
+ retention-days: 7
+
test-shard:
name: Test shard (${{ matrix.shard }})
needs: [changes, build]
@@ -153,15 +208,21 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
- fail-fast: false
+ # Failed PRs release the remaining matrix slots; qualification retains
+ # every shard's diagnostics and coverage. Passing PRs run the full suite.
+ fail-fast: ${{ github.event_name == 'pull_request' && needs.changes.outputs.tier == 'fast' }}
matrix:
- shard: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6]
+ # Fast feedback spends the same six test jobs on one source group and
+ # five built groups; full qualification retains six complete shards.
+ shard: ${{ fromJSON(needs.changes.outputs.tier == 'fast' && '["1/5","2/5","3/5","4/5","5/5"]' || '["1/6","2/6","3/6","4/6","5/6","6/6"]') }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
@@ -172,21 +233,42 @@ jobs:
run: npm run test:ci:shard:run -- --shard=${{ matrix.shard }}
env:
REA_CI_TEST_HISTORY: .cache/ci-test-history/test-results.json
- - name: Run complete portable suite for fast feedback
+ - name: Run built-dependent tests for fast feedback
if: needs.changes.outputs.tier == 'fast'
- run: npm run test:run -- --reporter=default --reporter=blob --shard=${{ matrix.shard }}
+ run: npm run test:built -- --reporter=default --reporter=blob --shard=${{ matrix.shard }}
env:
+ VITEST_BLOB_LABEL: built
REA_CI_TEST_HISTORY: .cache/ci-test-history/test-results.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ !cancelled() }}
with:
- name: vitest-report-${{ strategy.job-index }}
+ name: vitest-report-built-${{ strategy.job-index }}
path: .vitest/blob/*
include-hidden-files: true
if-no-files-found: error
overwrite: true
retention-days: 7
+ reverse-tools-quality:
+ name: Reverse tools coverage and mutation
+ needs: [changes, static]
+ if: needs.changes.outputs.reverse_tools == 'true'
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version-file: .nvmrc
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
+ - run: npm ci
+ - name: Run reverse tools tests with coverage
+ run: npm run test:coverage:reverse-tools
+ - name: Report reverse tools mutation results
+ run: npm run test:mutation:reverse-tools
+
build:
name: Build package
needs: changes
@@ -212,7 +294,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- name: Reuse portable build outputs across runs
uses: ./.github/actions/portable-build-cache
@@ -271,7 +355,7 @@ jobs:
test:
name: Portable tests
- needs: [changes, test-shard]
+ needs: [changes, test-source, test-shard]
if: ${{ !cancelled() && needs.changes.outputs.tier != 'reused' }}
runs-on: ubuntu-latest
timeout-minutes: 10
@@ -286,13 +370,18 @@ jobs:
if: needs.changes.outputs.code == 'true' && success()
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ if: needs.changes.outputs.code == 'true' && success()
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- if: needs.changes.outputs.code == 'true' && success()
run: npm ci
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: >-
!cancelled() && needs.changes.outputs.code == 'true' &&
- (needs.test-shard.result == 'success' || needs.test-shard.result == 'failure')
+ (needs.test-shard.result == 'success' || needs.test-shard.result == 'failure' ||
+ (needs.changes.outputs.tier == 'fast' &&
+ (needs.test-source.result == 'success' || needs.test-source.result == 'failure')))
with:
path: .vitest/blob
pattern: vitest-report-*
@@ -320,7 +409,8 @@ jobs:
if: >-
!cancelled() && needs.changes.outputs.code == 'true' &&
needs.changes.outputs.tier == 'fast' &&
- (needs.test-shard.result == 'success' || needs.test-shard.result == 'failure')
+ (needs.test-shard.result == 'success' || needs.test-shard.result == 'failure' ||
+ needs.test-source.result == 'success' || needs.test-source.result == 'failure')
run: >-
npm exec -- vitest --merge-reports
--reporter=default
@@ -333,6 +423,8 @@ jobs:
if: needs.changes.outputs.code == 'true' && !cancelled() && hashFiles('test-timings.json') != ''
env:
SHARD_RESULT: ${{ needs.test-shard.result }}
+ SOURCE_REQUIRED: ${{ needs.changes.outputs.tier == 'fast' }}
+ SOURCE_RESULT: ${{ needs.test-source.result }}
MERGE_RESULT: ${{ needs.changes.outputs.tier == 'full' && steps.merge.outcome || steps.merge-fast.outcome }}
CI_VERIFY_TEST_INVENTORY: "true"
CI_TEST_SELECTION: ${{ needs.changes.outputs.test_selection }}
@@ -365,7 +457,13 @@ jobs:
env:
CODE_REQUIRED: ${{ needs.changes.outputs.code }}
SHARD_RESULT: ${{ needs.test-shard.result }}
- run: test "${CODE_REQUIRED}" != "true" || test "${SHARD_RESULT}" = "success"
+ SOURCE_REQUIRED: ${{ needs.changes.outputs.tier == 'fast' }}
+ SOURCE_RESULT: ${{ needs.test-source.result }}
+ run: |
+ if [ "${CODE_REQUIRED}" = "true" ]; then
+ test "${SHARD_RESULT}" = "success" || exit 1
+ test "${SOURCE_REQUIRED}" != "true" || test "${SOURCE_RESULT}" = "success"
+ fi
conformance-fixtures:
name: Source-built conformance fixtures
@@ -450,7 +548,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- name: Verify native runtime identity
env:
EXPECTED_CI_PLATFORM: ${{ matrix.platform }}
@@ -472,11 +572,6 @@ jobs:
- run: npm run verify:windows-native:artifact
- name: Verify NativeAOT diagnostic applicability on this host
run: npm run test:focused -- tests/evaluation/package/nativeAotDiagnostics.test.ts
- - name: Resolve npm cache before verifier HOME isolation
- id: npm-cache
- run: |
- cache_path="$(npm config get cache)"
- echo "path=${cache_path}" >> "${GITHUB_OUTPUT}"
- run: npm run verify:package
env:
npm_config_cache: ${{ steps.npm-cache.outputs.path }}
@@ -506,7 +601,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
@@ -554,7 +651,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- name: Run the public CLI/MCP reconstruction journey
env:
@@ -583,7 +682,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
@@ -703,7 +804,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- run: npm run docs:check
- name: Generation leaves authored files unchanged
@@ -792,7 +895,9 @@ jobs:
changes,
static,
build,
+ reverse-tools-quality,
test-shard,
+ test-source,
sqlite-runtime,
test,
conformance-fixtures,
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 6b8a05b46..baa6ba785 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -23,6 +23,8 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- run: npm run docs:build
diff --git a/.github/workflows/real-binary-layout.yml b/.github/workflows/real-binary-layout.yml
index 0218543f0..06fcc290a 100644
--- a/.github/workflows/real-binary-layout.yml
+++ b/.github/workflows/real-binary-layout.yml
@@ -32,7 +32,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
diff --git a/.github/workflows/real-browser.yml b/.github/workflows/real-browser.yml
index 3c3fe955a..aa7e6ebb2 100644
--- a/.github/workflows/real-browser.yml
+++ b/.github/workflows/real-browser.yml
@@ -25,7 +25,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
diff --git a/.github/workflows/real-cutter.yml b/.github/workflows/real-cutter.yml
new file mode 100644
index 000000000..91121f207
--- /dev/null
+++ b/.github/workflows/real-cutter.yml
@@ -0,0 +1,76 @@
+name: Real upstream Cutter verification
+
+on:
+ workflow_dispatch:
+ repository_dispatch:
+ types: [real-cutter-upstream]
+
+permissions:
+ contents: read
+
+jobs:
+ windows-native-artifact:
+ name: Build Windows bridge security boundary
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version-file: .nvmrc
+ - name: Install the cross compiler used by the Windows native lane
+ run: |
+ sudo apt-get update
+ sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
+ - run: npm run build:windows-native
+ - uses: actions/upload-artifact@v4
+ with:
+ name: cutter-windows-native
+ path: native/windows/build/
+ if-no-files-found: error
+
+ cutter-upstream:
+ name: Cutter upstream smoke (${{ matrix.platform }})
+ needs: windows-native-artifact
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux
+ runner: [self-hosted, cutter-upstream, linux]
+ - platform: macOS
+ runner: [self-hosted, cutter-upstream, macOS]
+ - platform: Windows
+ runner: [self-hosted, cutter-upstream, Windows]
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
+ with:
+ node-version-file: .nvmrc
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
+ - run: npm ci
+ - if: matrix.platform == 'Windows'
+ uses: actions/download-artifact@v4
+ with:
+ name: cutter-windows-native
+ path: native/windows/build/
+ - if: matrix.platform == 'Windows'
+ run: npm run verify:windows-native:artifact
+ - if: matrix.platform == 'Windows'
+ run: npm run build:cached
+ - if: matrix.platform == 'Windows'
+ run: npm run verify:windows-native
+ - name: Verify host and real Cutter bridge
+ env:
+ EXPECTED_PLATFORM: ${{ matrix.platform }}
+ run: npm run verify:cutter
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: cutter-upstream-${{ matrix.platform }}
+ path: cutter-smoke.json
+ if-no-files-found: ignore
+ retention-days: 30
diff --git a/.github/workflows/real-evm-interface.yml b/.github/workflows/real-evm-interface.yml
index 8588a269f..3a47b8f90 100644
--- a/.github/workflows/real-evm-interface.yml
+++ b/.github/workflows/real-evm-interface.yml
@@ -26,7 +26,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
diff --git a/.github/workflows/real-go-binary.yml b/.github/workflows/real-go-binary.yml
index c7d74b001..d99622067 100644
--- a/.github/workflows/real-go-binary.yml
+++ b/.github/workflows/real-go-binary.yml
@@ -48,7 +48,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.26.1"
diff --git a/.github/workflows/real-hopper-linux.yml b/.github/workflows/real-hopper-linux.yml
index d5e5f8c33..417af2ca5 100644
--- a/.github/workflows/real-hopper-linux.yml
+++ b/.github/workflows/real-hopper-linux.yml
@@ -19,7 +19,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- name: Pack CLI for setup verification
shell: bash
@@ -40,7 +42,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- name: Install packed CLI in isolation
shell: bash
diff --git a/.github/workflows/real-hopper.yml b/.github/workflows/real-hopper.yml
index b3ceeb7d9..da1c60c9f 100644
--- a/.github/workflows/real-hopper.yml
+++ b/.github/workflows/real-hopper.yml
@@ -19,7 +19,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- name: Install packed CLI in isolation
shell: bash
diff --git a/.github/workflows/real-javascript-recovery.yml b/.github/workflows/real-javascript-recovery.yml
index a5043b62e..786d4a2d4 100644
--- a/.github/workflows/real-javascript-recovery.yml
+++ b/.github/workflows/real-javascript-recovery.yml
@@ -27,7 +27,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
diff --git a/.github/workflows/real-packet-capture.yml b/.github/workflows/real-packet-capture.yml
index 8daebfd8a..12cb3d23d 100644
--- a/.github/workflows/real-packet-capture.yml
+++ b/.github/workflows/real-packet-capture.yml
@@ -47,7 +47,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: sudo apt-get update && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y tshark util-linux python3
- run: npm ci
- run: npm run build:cached
diff --git a/.github/workflows/real-recorded-crash.yml b/.github/workflows/real-recorded-crash.yml
index 0463df27d..58cf90aed 100644
--- a/.github/workflows/real-recorded-crash.yml
+++ b/.github/workflows/real-recorded-crash.yml
@@ -29,7 +29,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
diff --git a/.github/workflows/real-signal-recording.yml b/.github/workflows/real-signal-recording.yml
index 5c1380e2d..1ddc2d5e0 100644
--- a/.github/workflows/real-signal-recording.yml
+++ b/.github/workflows/real-signal-recording.yml
@@ -40,7 +40,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: sudo apt-get update && sudo apt-get install -y sigrok-cli util-linux python3
- run: npm ci
- run: npm run build:cached
diff --git a/.github/workflows/real-web-network-captures.yml b/.github/workflows/real-web-network-captures.yml
index 3ee59d2c8..7901114f0 100644
--- a/.github/workflows/real-web-network-captures.yml
+++ b/.github/workflows/real-web-network-captures.yml
@@ -26,7 +26,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- name: Use pinned package manager
run: npm install --global npm@11.16.0
- run: npm ci
diff --git a/.github/workflows/real-web-runtime.yml b/.github/workflows/real-web-runtime.yml
index f5b9f201a..0ceb9b057 100644
--- a/.github/workflows/real-web-runtime.yml
+++ b/.github/workflows/real-web-runtime.yml
@@ -21,12 +21,14 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- - run: npm run verify:browser:runtime
+ - run: env -u DBUS_SESSION_BUS_ADDRESS npm run verify:browser:runtime
env:
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
REA_BROWSER_NO_SANDBOX: "true"
diff --git a/.github/workflows/real-web-source-map.yml b/.github/workflows/real-web-source-map.yml
index 2a1e65dc0..eb1ecde34 100644
--- a/.github/workflows/real-web-source-map.yml
+++ b/.github/workflows/real-web-source-map.yml
@@ -21,7 +21,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
diff --git a/.github/workflows/sqlite-runtime.yml b/.github/workflows/sqlite-runtime.yml
index fc379d1dc..1ea6964b7 100644
--- a/.github/workflows/sqlite-runtime.yml
+++ b/.github/workflows/sqlite-runtime.yml
@@ -27,7 +27,9 @@ jobs:
with:
node-version: ${{ matrix.node }}
check-latest: true
- cache: npm
+ - name: Cache npm package downloads
+ id: npm-cache
+ uses: ./.github/actions/npm-cache
- run: npm ci
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
diff --git a/.gitignore b/.gitignore
index 0d006e13f..e4dc40814 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,8 @@ codedb.snapshot
htmlcov/
.coverage
coverage/
+.stryker-tmp/
+reports/mutation/
.vitest/
.vitest-reports/
test-results.xml
diff --git a/README.md b/README.md
index 53a86c5fc..0088640d0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
**English** · [简体中文](docs/readme/README_zh.md) · [繁體中文](docs/readme/README_zh-TW.md) · [日本語](docs/readme/README_ja.md) · [한국어](docs/readme/README_ko.md) · [Türkçe](docs/readme/README_tr.md) · [Русский](docs/readme/README_ru.md) · [Tiếng Việt](docs/readme/README_vi.md) · [ไทย](docs/readme/README_th.md) · [Deutsch](docs/readme/README_de.md) · [Español](docs/readme/README_es.md) · [Français](docs/readme/README_fr.md) · [Українська](docs/readme/README_uk.md) · [Polski](docs/readme/README_pl.md) · [Português (Brasil)](docs/readme/README_pt-BR.md) · [العربية](docs/readme/README_ar.md) · [فارسی](docs/readme/README_fa.md) · [Bahasa Indonesia](docs/readme/README_id.md) · [Italiano](docs/readme/README_it.md)
+· [Ελληνικά](docs/readme/README_el.md)
# REA: Reverse Engineer Anything
@@ -23,6 +24,12 @@
npx rea-agents setup
+> [!IMPORTANT]
+> Get REA from [morluto/rea](https://github.com/morluto/rea),
+> [rea.tools](https://rea.tools/), or the [rea-agents npm package](https://www.npmjs.com/package/rea-agents).
+> Follow the installation and release links from these official sources. A similar
+> repository name or copied README does not establish that a download is official.
+
@@ -155,6 +162,7 @@ Additional tools and host support depend on the target:
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://rea.tools/guides/native/) |
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
+| 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) |
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
@@ -179,6 +187,11 @@ Static JavaScript and .NET inspection read the supplied files without running
the application. Runtime capture runs or interacts with the selected target
using your user permissions; each runtime guide describes its effects.
+For offline native-binary inspection, REA also supports caller-supplied GNU
+objdump and Rizin. See the [reverse-engineering tools guide](docs/reverse-engineering-tools.md)
+for provider setup, CLI commands, and Evidence limitations. REA does not install
+or update either provider.
+
Native formats and host support vary by provider. See
diff --git a/bridge/cutter/rea_bridge.py b/bridge/cutter/rea_bridge.py
new file mode 100644
index 000000000..473f1d5fe
--- /dev/null
+++ b/bridge/cutter/rea_bridge.py
@@ -0,0 +1,816 @@
+"""REA's experimental local bridge plugin for Cutter.
+
+Install this file in Cutter's user plugin directory. The bridge is REA-owned
+IPC, not an upstream Cutter transport. It binds to loopback and requires a
+per-process random token stored in a user-only descriptor file.
+"""
+
+from __future__ import annotations
+
+import json
+import hashlib
+import hmac
+import os
+import secrets
+import socket
+import stat
+import tempfile
+import threading
+import time
+import uuid
+from pathlib import Path
+from typing import Any
+
+if os.name == "nt":
+ import ctypes
+ from ctypes import wintypes
+
+import cutter
+
+try:
+ from PySide6.QtCore import QObject, Signal
+except ImportError:
+ from PySide2.QtCore import QObject, Signal
+
+
+MAX_FRAME_BYTES = 16 * 1024 * 1024
+SOCKET_TIMEOUT_SECONDS = 20
+AUTH_FRAME_TIMEOUT_SECONDS = 1
+MAX_DESCRIPTOR_BYTES = 64 * 1024
+MAX_COMPLETION_RECEIPTS = 64
+
+
+def _windows_api() -> tuple[Any, Any]:
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
+ advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
+ kernel32.GetCurrentProcess.restype = wintypes.HANDLE
+ kernel32.LocalFree.argtypes = [wintypes.HLOCAL]
+ kernel32.LocalFree.restype = wintypes.HLOCAL
+ kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
+ kernel32.CloseHandle.restype = wintypes.BOOL
+ kernel32.GetFileAttributesW.argtypes = [wintypes.LPCWSTR]
+ kernel32.GetFileAttributesW.restype = wintypes.DWORD
+ kernel32.CreateDirectoryW.argtypes = [wintypes.LPCWSTR, wintypes.LPVOID]
+ kernel32.CreateDirectoryW.restype = wintypes.BOOL
+ kernel32.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD,
+ wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE]
+ kernel32.CreateFileW.restype = wintypes.HANDLE
+ kernel32.WriteFile.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD,
+ ctypes.POINTER(wintypes.DWORD), wintypes.LPVOID]
+ kernel32.WriteFile.restype = wintypes.BOOL
+ kernel32.FlushFileBuffers.argtypes = [wintypes.HANDLE]
+ kernel32.FlushFileBuffers.restype = wintypes.BOOL
+ kernel32.MoveFileExW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD]
+ kernel32.MoveFileExW.restype = wintypes.BOOL
+ kernel32.DeleteFileW.argtypes = [wintypes.LPCWSTR]
+ kernel32.DeleteFileW.restype = wintypes.BOOL
+ advapi32.OpenProcessToken.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)]
+ advapi32.OpenProcessToken.restype = wintypes.BOOL
+ advapi32.GetTokenInformation.argtypes = [wintypes.HANDLE, ctypes.c_int, wintypes.LPVOID,
+ wintypes.DWORD, ctypes.POINTER(wintypes.DWORD)]
+ advapi32.GetTokenInformation.restype = wintypes.BOOL
+ advapi32.ConvertSidToStringSidW.argtypes = [wintypes.LPVOID, ctypes.POINTER(wintypes.LPWSTR)]
+ advapi32.ConvertSidToStringSidW.restype = wintypes.BOOL
+ advapi32.GetNamedSecurityInfoW.argtypes = [wintypes.LPWSTR, ctypes.c_int, wintypes.DWORD,
+ ctypes.POINTER(wintypes.LPVOID), ctypes.POINTER(wintypes.LPVOID),
+ ctypes.POINTER(wintypes.LPVOID), ctypes.POINTER(wintypes.LPVOID),
+ ctypes.POINTER(wintypes.LPVOID)]
+ advapi32.GetNamedSecurityInfoW.restype = wintypes.DWORD
+ advapi32.GetSecurityDescriptorControl.argtypes = [wintypes.LPVOID, ctypes.POINTER(wintypes.WORD),
+ ctypes.POINTER(wintypes.DWORD)]
+ advapi32.GetSecurityDescriptorControl.restype = wintypes.BOOL
+ advapi32.EqualSid.argtypes = [wintypes.LPVOID, wintypes.LPVOID]
+ advapi32.EqualSid.restype = wintypes.BOOL
+ advapi32.GetAclInformation.argtypes = [wintypes.LPVOID, wintypes.LPVOID, wintypes.DWORD, ctypes.c_int]
+ advapi32.GetAclInformation.restype = wintypes.BOOL
+ advapi32.GetAce.argtypes = [wintypes.LPVOID, wintypes.DWORD, ctypes.POINTER(wintypes.LPVOID)]
+ advapi32.GetAce.restype = wintypes.BOOL
+ advapi32.IsValidSid.argtypes = [wintypes.LPVOID]
+ advapi32.IsValidSid.restype = wintypes.BOOL
+ advapi32.GetLengthSid.argtypes = [wintypes.LPVOID]
+ advapi32.GetLengthSid.restype = wintypes.DWORD
+ advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.argtypes = [
+ wintypes.LPCWSTR, wintypes.DWORD, ctypes.POINTER(wintypes.LPVOID), ctypes.POINTER(wintypes.DWORD),
+ ]
+ advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.restype = wintypes.BOOL
+ return kernel32, advapi32
+
+
+def _windows_user_sid_text() -> str:
+ kernel32, advapi32 = _windows_api()
+ token = wintypes.HANDLE()
+ if not advapi32.OpenProcessToken(kernel32.GetCurrentProcess(), 0x0008, ctypes.byref(token)):
+ raise ctypes.WinError(ctypes.get_last_error())
+ try:
+ required = wintypes.DWORD()
+ advapi32.GetTokenInformation(token, 1, None, 0, ctypes.byref(required))
+ if required.value == 0:
+ raise ctypes.WinError(ctypes.get_last_error())
+ buffer = ctypes.create_string_buffer(required.value)
+ if not advapi32.GetTokenInformation(token, 1, buffer, required, ctypes.byref(required)):
+ raise ctypes.WinError(ctypes.get_last_error())
+
+ class SID_AND_ATTRIBUTES(ctypes.Structure):
+ _fields_ = [("Sid", wintypes.LPVOID), ("Attributes", wintypes.DWORD)]
+
+ class TOKEN_USER(ctypes.Structure):
+ _fields_ = [("User", SID_AND_ATTRIBUTES)]
+
+ sid = ctypes.cast(buffer, ctypes.POINTER(TOKEN_USER)).contents.User.Sid
+ text = wintypes.LPWSTR()
+ if not advapi32.ConvertSidToStringSidW(sid, ctypes.byref(text)):
+ raise ctypes.WinError(ctypes.get_last_error())
+ try:
+ return text.value
+ finally:
+ kernel32.LocalFree(text)
+ finally:
+ kernel32.CloseHandle(token)
+
+
+def _verify_windows_private_path(path: Path, directory: bool) -> None:
+ kernel32, advapi32 = _windows_api()
+ attributes = kernel32.GetFileAttributesW(str(path))
+ if attributes == 0xFFFFFFFF:
+ raise ctypes.WinError(ctypes.get_last_error())
+ if attributes & 0x400 or bool(attributes & 0x10) != directory:
+ raise RuntimeError("Cutter bridge path is a reparse point or has the wrong object type")
+
+ class ACL_SIZE_INFORMATION(ctypes.Structure):
+ _fields_ = [("AceCount", wintypes.DWORD), ("AclBytesInUse", wintypes.DWORD), ("AclBytesFree", wintypes.DWORD)]
+
+ class ACE_HEADER(ctypes.Structure):
+ _fields_ = [("AceType", wintypes.BYTE), ("AceFlags", wintypes.BYTE), ("AceSize", wintypes.WORD)]
+
+ class SID_HEADER(ctypes.Structure):
+ _fields_ = [("Revision", wintypes.BYTE), ("SubAuthorityCount", wintypes.BYTE),
+ ("IdentifierAuthority", wintypes.BYTE * 6)]
+
+ class ACCESS_ALLOWED_ACE(ctypes.Structure):
+ _fields_ = [("Header", ACE_HEADER), ("Mask", wintypes.DWORD), ("SidStart", wintypes.DWORD)]
+
+ owner = wintypes.LPVOID()
+ dacl = wintypes.LPVOID()
+ descriptor = wintypes.LPVOID()
+ result = advapi32.GetNamedSecurityInfoW(
+ str(path), 1, 0x1 | 0x4, ctypes.byref(owner), None, ctypes.byref(dacl), None, ctypes.byref(descriptor),
+ )
+ if result != 0:
+ raise ctypes.WinError(result)
+ try:
+ control = wintypes.WORD()
+ revision = wintypes.DWORD()
+ if not advapi32.GetSecurityDescriptorControl(descriptor, ctypes.byref(control), ctypes.byref(revision)):
+ raise ctypes.WinError(ctypes.get_last_error())
+ if not control.value & 0x1000 or not dacl:
+ raise RuntimeError("Cutter bridge ACL must be present and protected from inheritance")
+
+ token = wintypes.HANDLE()
+ if not advapi32.OpenProcessToken(kernel32.GetCurrentProcess(), 0x0008, ctypes.byref(token)):
+ raise ctypes.WinError(ctypes.get_last_error())
+ try:
+ required = wintypes.DWORD()
+ advapi32.GetTokenInformation(token, 1, None, 0, ctypes.byref(required))
+ token_user = ctypes.create_string_buffer(required.value)
+ if not advapi32.GetTokenInformation(token, 1, token_user, required, ctypes.byref(required)):
+ raise ctypes.WinError(ctypes.get_last_error())
+
+ class SID_AND_ATTRIBUTES(ctypes.Structure):
+ _fields_ = [("Sid", wintypes.LPVOID), ("Attributes", wintypes.DWORD)]
+
+ class TOKEN_USER(ctypes.Structure):
+ _fields_ = [("User", SID_AND_ATTRIBUTES)]
+
+ current_sid = ctypes.cast(token_user, ctypes.POINTER(TOKEN_USER)).contents.User.Sid
+ if not advapi32.EqualSid(owner, current_sid):
+ raise RuntimeError("Cutter bridge path owner does not match the current user")
+ finally:
+ kernel32.CloseHandle(token)
+
+ info = ACL_SIZE_INFORMATION()
+ if not advapi32.GetAclInformation(dacl, ctypes.byref(info), ctypes.sizeof(info), 2) or info.AceCount != 2:
+ raise RuntimeError("Cutter bridge ACL must allow only the current user and SYSTEM")
+ expected_user = _windows_user_sid_text()
+ user_allowed = False
+ system_allowed = False
+ for index in range(info.AceCount):
+ ace_pointer = wintypes.LPVOID()
+ if not advapi32.GetAce(dacl, index, ctypes.byref(ace_pointer)):
+ raise ctypes.WinError(ctypes.get_last_error())
+ header = ctypes.cast(ace_pointer, ctypes.POINTER(ACE_HEADER)).contents
+ if header.AceType != 0 or header.AceSize < ctypes.sizeof(ACE_HEADER) + ctypes.sizeof(wintypes.DWORD) + ctypes.sizeof(SID_HEADER):
+ raise RuntimeError("Cutter bridge ACL contains a malformed or unsupported ACE")
+ ace_address = ctypes.cast(ace_pointer, ctypes.c_void_p).value
+ if ace_address is None:
+ raise RuntimeError("Cutter bridge ACL returned a null ACE")
+ sid_pointer = ctypes.c_void_p(ace_address + ctypes.sizeof(ACE_HEADER) + ctypes.sizeof(wintypes.DWORD))
+ sid_header = ctypes.cast(sid_pointer, ctypes.POINTER(SID_HEADER)).contents
+ sid_size = ctypes.sizeof(SID_HEADER) + sid_header.SubAuthorityCount * ctypes.sizeof(wintypes.DWORD)
+ if sid_size > header.AceSize - ctypes.sizeof(ACE_HEADER) - ctypes.sizeof(wintypes.DWORD) or not advapi32.IsValidSid(sid_pointer) or advapi32.GetLengthSid(sid_pointer) != sid_size:
+ raise RuntimeError("Cutter bridge ACL contains an invalid SID")
+ ace = ctypes.cast(ace_pointer, ctypes.POINTER(ACCESS_ALLOWED_ACE)).contents
+ sid_text = wintypes.LPWSTR()
+ if ace.Mask != 0x001F01FF or ace.Header.AceFlags != 0x03:
+ raise RuntimeError("Cutter bridge ACL contains an unsupported ACE")
+ if not advapi32.ConvertSidToStringSidW(sid_pointer, ctypes.byref(sid_text)):
+ raise ctypes.WinError(ctypes.get_last_error())
+ try:
+ if sid_text.value == expected_user:
+ user_allowed = True
+ elif sid_text.value == "S-1-5-18":
+ system_allowed = True
+ else:
+ raise RuntimeError("Cutter bridge ACL allows an unrelated principal")
+ finally:
+ kernel32.LocalFree(sid_text)
+ if not user_allowed or not system_allowed:
+ raise RuntimeError("Cutter bridge ACL must allow only the current user and SYSTEM")
+ finally:
+ kernel32.LocalFree(descriptor)
+
+
+def _windows_security_attributes() -> tuple[Any, Any]:
+ kernel32, advapi32 = _windows_api()
+ sid = _windows_user_sid_text()
+ sddl = f"O:{sid}G:{sid}D:P(A;OICI;FA;;;{sid})(A;OICI;FA;;;SY)"
+ descriptor = wintypes.LPVOID()
+ if not advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl, 1, ctypes.byref(descriptor), None):
+ raise ctypes.WinError(ctypes.get_last_error())
+
+ class SECURITY_ATTRIBUTES(ctypes.Structure):
+ _fields_ = [("nLength", wintypes.DWORD), ("lpSecurityDescriptor", wintypes.LPVOID), ("bInheritHandle", wintypes.BOOL)]
+
+ attributes = SECURITY_ATTRIBUTES(ctypes.sizeof(SECURITY_ATTRIBUTES), descriptor, False)
+ return kernel32, (descriptor, attributes)
+
+
+def _ensure_windows_bridge_directory(directory: Path) -> None:
+ directory.parent.mkdir(parents=True, exist_ok=True)
+ if not directory.exists():
+ kernel32, (descriptor, attributes) = _windows_security_attributes()
+ try:
+ if not kernel32.CreateDirectoryW(str(directory), ctypes.byref(attributes)):
+ error = ctypes.get_last_error()
+ if error != 183:
+ raise ctypes.WinError(error)
+ finally:
+ kernel32.LocalFree(descriptor)
+ _verify_windows_private_path(directory, True)
+
+
+def _ensure_posix_bridge_directory(directory: Path) -> None:
+ directory.mkdir(mode=0o700, parents=True, exist_ok=True)
+ if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"):
+ raise RuntimeError("Cutter bridge directory requires no-follow directory handles")
+ descriptor = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
+ try:
+ directory_info = os.fstat(descriptor)
+ if directory_info.st_uid != os.getuid():
+ raise RuntimeError("Cutter bridge directory must be owned by the current user")
+ os.fchmod(descriptor, 0o700)
+ directory_info = os.fstat(descriptor)
+ if stat.S_IMODE(directory_info.st_mode) != 0o700:
+ raise RuntimeError("Cutter bridge directory must have mode 0700")
+ finally:
+ os.close(descriptor)
+
+
+def _write_windows_descriptor(directory: Path, destination: Path, payload: bytes) -> None:
+ if len(payload) > MAX_DESCRIPTOR_BYTES:
+ raise RuntimeError("Cutter bridge descriptor exceeds its byte limit")
+ kernel32, (security_descriptor, attributes) = _windows_security_attributes()
+ temporary = directory / f".{destination.name}.{uuid.uuid4().hex}.tmp"
+ handle = wintypes.HANDLE()
+ try:
+ handle = kernel32.CreateFileW(str(temporary), 0x40000000 | 0x00020000 | 0x00040000,
+ 0x1 | 0x2 | 0x4, ctypes.byref(attributes), 1, 0x80, None)
+ if handle == wintypes.HANDLE(-1).value:
+ raise ctypes.WinError(ctypes.get_last_error())
+ _verify_windows_private_path(temporary, False)
+ if payload:
+ data = ctypes.create_string_buffer(payload)
+ written = wintypes.DWORD()
+ if not kernel32.WriteFile(handle, data, len(payload), ctypes.byref(written), None) or written.value != len(payload):
+ raise ctypes.WinError(ctypes.get_last_error())
+ if not kernel32.FlushFileBuffers(handle):
+ raise ctypes.WinError(ctypes.get_last_error())
+ kernel32.CloseHandle(handle)
+ handle = wintypes.HANDLE()
+ if not kernel32.MoveFileExW(str(temporary), str(destination), 0x1 | 0x8):
+ raise ctypes.WinError(ctypes.get_last_error())
+ finally:
+ if handle and handle != wintypes.HANDLE(-1).value:
+ kernel32.CloseHandle(handle)
+ kernel32.DeleteFileW(str(temporary))
+ kernel32.LocalFree(security_descriptor)
+
+
+def _bridge_directory() -> Path:
+ configured = os.environ.get("REA_CUTTER_BRIDGE_DIR")
+ if configured:
+ return Path(configured).expanduser()
+ local_app_data = os.environ.get("LOCALAPPDATA")
+ if os.name == "nt" and local_app_data:
+ return Path(local_app_data) / "REA" / "CutterBridge"
+ return Path.home() / ".cache" / "rea" / "cutter-bridge"
+
+
+class _RequestDispatcher(QObject):
+ dispatch = Signal(object, object)
+
+ def __init__(self) -> None:
+ super().__init__()
+ self.dispatch.connect(self._handle)
+ self.generation = 0
+ self.completed_commands: dict[str, str] = {}
+ self._receipt_lock = threading.Lock()
+ self.current_file = self._read_current_file()
+ try:
+ self.cutter_version = str(cutter.core().getVersionInformation())
+ except Exception:
+ self.cutter_version = None
+
+ @staticmethod
+ def _read_current_file() -> str | None:
+ try:
+ value = cutter.core().currentlyOpenFile()
+ return str(value) if value else None
+ except Exception:
+ return None
+
+ def _handle(self, request: dict[str, Any], reply: dict[str, Any]) -> None:
+ kind = request.get("kind")
+ request_id = request.get("request_id")
+ if kind in ("command", "ack"):
+ try:
+ valid_request_id = (
+ isinstance(request_id, str)
+ and str(uuid.UUID(request_id)) == request_id.lower()
+ )
+ except (ValueError, AttributeError):
+ valid_request_id = False
+ if not valid_request_id:
+ reply.update({
+ "ok": False,
+ "error": "invalid-command-request-id",
+ "execution_state": "not_started",
+ })
+ request["_event"].set()
+ return
+ if kind == "ack":
+ with self._receipt_lock:
+ acknowledged = self.completed_commands.pop(request_id, None) is not None
+ reply.update({"ok": True, "acknowledged": acknowledged})
+ request["_event"].set()
+ return
+ if request["_cancelled"].is_set():
+ reply.update({
+ "ok": False,
+ "error": "request-cancelled-before-execution",
+ "execution_state": "not_started",
+ })
+ if kind == "command":
+ self._record_command_completion(request_id, "not_started")
+ request["_event"].set()
+ return
+ self._refresh_document_identity()
+ if kind == "status":
+ with self._receipt_lock:
+ completed_commands = [
+ {"request_id": receipt_id, "execution_state": state}
+ for receipt_id, state in self.completed_commands.items()
+ ]
+ reply.update({
+ "ok": True,
+ "session_id": request.get("session_id"),
+ "current_file": self.current_file,
+ "document_generation": self.generation,
+ "cutter_version": self.cutter_version,
+ "identity_status": "partial",
+ "completed_commands": completed_commands,
+ })
+ request["_event"].set()
+ return
+ if kind != "command":
+ reply.update({"ok": False, "error": "unsupported-request-kind"})
+ request["_event"].set()
+ return
+ if request.get("expected_generation") != self.generation:
+ reply.update({
+ "ok": False,
+ "error": "active-document-generation-changed",
+ "execution_state": "not_started",
+ })
+ self._record_command_completion(request_id, "not_started")
+ request["_event"].set()
+ return
+ command = request.get("command")
+ if not isinstance(command, str) or not command.strip():
+ reply.update({
+ "ok": False,
+ "error": "invalid-command",
+ "execution_state": "not_started",
+ })
+ self._record_command_completion(request_id, "not_started")
+ request["_event"].set()
+ return
+ reply["execution_state"] = "running"
+ try:
+ output = cutter.cmdj(command) if request.get("json") is True else cutter.cmd(command)
+ command_error = None
+ except Exception as error:
+ output = None
+ command_error = str(error)
+ self._refresh_document_identity()
+ if command_error is None:
+ reply.update({
+ "ok": True,
+ "output": output,
+ "current_file": self.current_file,
+ "document_generation": self.generation,
+ "cutter_version": self.cutter_version,
+ "identity_status": "partial",
+ "execution_state": "complete",
+ })
+ self._record_command_completion(request_id, "complete")
+ else:
+ reply.update({
+ "ok": False,
+ "error": command_error,
+ "current_file": self.current_file,
+ "document_generation": self.generation,
+ "cutter_version": self.cutter_version,
+ "identity_status": "partial",
+ "execution_state": "unknown",
+ "message": "The command raised an exception; it may have produced partial effects. Do not retry automatically.",
+ })
+ self._record_command_completion(request_id, "failed")
+ request["_event"].set()
+
+ def _record_command_completion(
+ self, request_id: str, execution_state: str
+ ) -> None:
+ with self._receipt_lock:
+ if request_id in self.completed_commands or len(self.completed_commands) < MAX_COMPLETION_RECEIPTS:
+ self.completed_commands[request_id] = execution_state
+
+ def can_accept_command(self) -> bool:
+ with self._receipt_lock:
+ return len(self.completed_commands) < MAX_COMPLETION_RECEIPTS
+
+ def _refresh_document_identity(self) -> None:
+ current_file = self._read_current_file()
+ if current_file != self.current_file:
+ self.current_file = current_file
+ self.generation += 1
+
+
+class _BridgeServer(threading.Thread):
+ def __init__(self, dispatcher: _RequestDispatcher) -> None:
+ super().__init__(name="rea-cutter-bridge", daemon=True)
+ self.dispatcher = dispatcher
+ self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.server.bind(("127.0.0.1", 0))
+ self.server.listen(8)
+ self.server.settimeout(0.5)
+ self.port = self.server.getsockname()[1]
+ self.session_id = str(uuid.uuid4())
+ self.token = secrets.token_urlsafe(32)
+ self.poisoned = False
+ self.connection_ordinal = 0
+ self.directory = _bridge_directory()
+ self.descriptor = self.directory / f"cutter-{os.getpid()}-{self.session_id}.json"
+
+ def publish(self) -> None:
+ if os.name == "nt":
+ _ensure_windows_bridge_directory(self.directory)
+ else:
+ _ensure_posix_bridge_directory(self.directory)
+ if os.name == "nt" and (self.directory.is_symlink() or not self.directory.is_dir()):
+ raise RuntimeError("Cutter bridge directory must be a real directory")
+ payload = {
+ "session_id": self.session_id,
+ "pid": os.getpid(),
+ "host": "127.0.0.1",
+ "port": self.port,
+ "token": self.token,
+ "document_generation": self.dispatcher.generation,
+ "current_file": self.dispatcher.current_file,
+ "cutter_version": self.dispatcher.cutter_version,
+ "identity_status": "partial",
+ }
+ if os.name == "nt":
+ _write_windows_descriptor(self.directory, self.descriptor, json.dumps(payload).encode("utf-8"))
+ return
+ descriptor, temporary_path = tempfile.mkstemp(prefix=f".{self.descriptor.name}.", dir=self.directory)
+ try:
+ os.fchmod(descriptor, 0o600)
+ descriptor_info = os.fstat(descriptor)
+ if descriptor_info.st_uid != os.getuid() or stat.S_IMODE(descriptor_info.st_mode) & 0o077:
+ raise RuntimeError("Cutter bridge descriptor must be owned by the current user with mode 0600")
+ with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
+ stream.write(json.dumps(payload))
+ stream.flush()
+ os.fsync(stream.fileno())
+ os.replace(temporary_path, self.descriptor)
+ except Exception:
+ try:
+ os.close(descriptor)
+ except OSError:
+ pass
+ try:
+ os.unlink(temporary_path)
+ except OSError:
+ pass
+ raise
+
+ def run(self) -> None:
+ try:
+ self.publish()
+ except Exception:
+ self.server.close()
+ return
+ while True:
+ try:
+ connection, _ = self.server.accept()
+ except socket.timeout:
+ if getattr(self, "stopping", False):
+ break
+ continue
+ except OSError:
+ break
+ self.connection_ordinal += 1
+ connection_ordinal = self.connection_ordinal
+ with connection:
+ connection.settimeout(AUTH_FRAME_TIMEOUT_SECONDS)
+ authenticated = False
+ try:
+ hello = self._receive_frame(connection)
+ if not isinstance(hello, dict) or hello.get("kind") != "rea-auth-v2" or not isinstance(hello.get("nonce"), str) or len(hello["nonce"]) != 64:
+ self._send(connection, {"ok": False, "error": "authentication-failed"})
+ continue
+ client_nonce = hello["nonce"]
+ server_nonce = f"{connection_ordinal:016x}{secrets.token_hex(24)}"
+ server_proof = hmac.new(self.token.encode(), f"rea-cutter-server-v2:{self.session_id}:{client_nonce}:{server_nonce}".encode(), hashlib.sha256).hexdigest()
+ self._send(connection, {"session_id": self.session_id, "nonce": server_nonce, "proof": server_proof})
+ authenticated_frame = self._receive_frame(connection)
+ request_bytes = authenticated_frame.get("request") if isinstance(authenticated_frame, dict) else None
+ expected_proof = hmac.new(self.token.encode(), f"rea-cutter-client-v2:{self.session_id}:{client_nonce}:{server_nonce}:{request_bytes}".encode(), hashlib.sha256).hexdigest()
+ if not isinstance(request_bytes, str) or not isinstance(authenticated_frame.get("proof"), str) or not hmac.compare_digest(authenticated_frame["proof"], expected_proof):
+ self._send(connection, {"ok": False, "error": "authentication-failed"})
+ continue
+ request = json.loads(request_bytes)
+ if not isinstance(request, dict) or request.get("session_id") != self.session_id:
+ self._send(connection, {"ok": False, "error": "authentication-failed"})
+ continue
+ authenticated = True
+ connection.settimeout(SOCKET_TIMEOUT_SECONDS)
+ if self.poisoned and request.get("kind") not in ("status", "ack"):
+ self._send_authenticated(connection, {
+ "ok": False,
+ "error": "session-poisoned-after-ui-timeout",
+ "execution_state": "not_started",
+ }, client_nonce, server_nonce)
+ continue
+ if (
+ request.get("kind") == "command"
+ and not self.dispatcher.can_accept_command()
+ ):
+ self._send_authenticated(connection, {
+ "ok": False,
+ "error": "completion-receipt-capacity-exceeded",
+ "execution_state": "not_started",
+ }, client_nonce, server_nonce)
+ continue
+ request["session_id"] = self.session_id
+ event = threading.Event()
+ cancelled = threading.Event()
+ reply: dict[str, Any] = {}
+ request["_event"] = event
+ request["_cancelled"] = cancelled
+ self.dispatcher.dispatch.emit(request, reply)
+ if not event.wait(SOCKET_TIMEOUT_SECONDS):
+ cancelled.set()
+ self.poisoned = True
+ self._send_authenticated(connection, {
+ "ok": False,
+ "error": "ui-thread-request-timeout",
+ "execution_state": "unknown",
+ "message": "The UI operation may still complete after this timeout; restart Cutter before issuing further commands.",
+ }, client_nonce, server_nonce)
+ else:
+ if request.get("kind") == "status" and reply.get("ok") is True:
+ reply["completed_connection"] = f"{connection_ordinal - 1:016x}"
+ self._send_authenticated(connection, reply, client_nonce, server_nonce)
+ except socket.timeout:
+ if authenticated:
+ self._send_authenticated(connection, {
+ "ok": False,
+ "error": "request-timeout",
+ "execution_state": "unknown",
+ "message": "The command may have produced partial or persistent effects. Do not retry automatically.",
+ }, client_nonce, server_nonce)
+ except Exception as error:
+ self._send(connection, {"ok": False, "error": str(error)})
+ self.server.close()
+ try:
+ self.descriptor.unlink(missing_ok=True)
+ except OSError:
+ pass
+
+ @staticmethod
+ def _receive_frame(connection: socket.socket) -> dict[str, Any]:
+ data = bytearray()
+ deadline = time.monotonic() + AUTH_FRAME_TIMEOUT_SECONDS
+ while b"\n" not in data:
+ if len(data) >= MAX_FRAME_BYTES:
+ raise ValueError("frame-limit-exceeded")
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ raise socket.timeout("authentication frame timed out")
+ connection.settimeout(remaining)
+ part = connection.recv(min(65536, MAX_FRAME_BYTES + 1 - len(data)))
+ if not part:
+ raise ValueError("incomplete authentication frame")
+ data.extend(part)
+ line, remainder = bytes(data).split(b"\n", 1)
+ if len(line) > MAX_FRAME_BYTES or remainder:
+ raise ValueError("invalid framed request")
+ value = json.loads(line)
+ return value if isinstance(value, dict) else {}
+
+ def _send_authenticated(self, connection: socket.socket, value: dict[str, Any], client_nonce: str, server_nonce: str) -> None:
+ encoded = self._encode_authenticated_response(value, client_nonce, server_nonce)
+ if encoded is None:
+ value = self._truncated_response(value)
+ encoded = self._encode_authenticated_response(value, client_nonce, server_nonce)
+ if encoded is None:
+ # This fixed-size fallback is checked through the same signed framing path.
+ value = {
+ "ok": False,
+ "error": "response-limit-exceeded",
+ "execution_state": value.get("execution_state", "unknown"),
+ "output": None,
+ "output_truncated": True,
+ "message": "The result exceeded the bridge frame limit; command completion state is preserved.",
+ }
+ encoded = self._encode_authenticated_response(value, client_nonce, server_nonce)
+ if encoded is None:
+ return
+ try:
+ connection.sendall(encoded)
+ except OSError:
+ pass
+
+ def _encode_authenticated_response(self, value: dict[str, Any], client_nonce: str, server_nonce: str) -> bytes | None:
+ if not _within_response_limit(value):
+ return None
+ prefix = b'{"response":"'
+ suffix = b'","proof":"' + (b"0" * 64) + b'"}\n'
+ frame_size = len(prefix) + len(suffix)
+ response_parts: list[bytes] = []
+ escaped_parts: list[bytes] = []
+ for fragment in json.JSONEncoder(ensure_ascii=False, separators=(",", ":")).iterencode(value):
+ raw = fragment.encode("utf-8")
+ escaped = json.dumps(fragment, ensure_ascii=False)[1:-1].encode("utf-8")
+ frame_size += len(escaped)
+ if frame_size > MAX_FRAME_BYTES:
+ return None
+ response_parts.append(raw)
+ escaped_parts.append(escaped)
+ proof = hmac.new(self.token.encode(), digestmod=hashlib.sha256)
+ proof.update(f"rea-cutter-response-v2:{self.session_id}:{client_nonce}:{server_nonce}:".encode())
+ for part in response_parts:
+ proof.update(part)
+ return prefix + b"".join(escaped_parts) + b'","proof":"' + proof.hexdigest().encode() + b'"}\n'
+
+ @staticmethod
+ def _truncated_response(value: dict[str, Any]) -> dict[str, Any]:
+ response: dict[str, Any] = {
+ "ok": value.get("ok") is True,
+ "output": None,
+ "execution_state": value.get("execution_state", "unknown"),
+ "output_truncated": True,
+ "message": "The command result exceeded the bridge frame limit; completion state is preserved and output was omitted.",
+ }
+ for key in ("current_file", "document_generation", "cutter_version", "identity_status"):
+ if key in value:
+ response[key] = value[key]
+ if value.get("ok") is not True and isinstance(value.get("error"), str):
+ response["error"] = value["error"]
+ return response
+
+ @staticmethod
+ def _send(connection: socket.socket, value: dict[str, Any]) -> None:
+ if not _within_response_limit(value):
+ value = {
+ "ok": False,
+ "error": "response-limit-exceeded",
+ "execution_state": value.get("execution_state", "unknown"),
+ "output_truncated": True,
+ "message": "The command completed or may have completed, but its result exceeded the bridge response limit. Do not retry automatically.",
+ }
+ parts: list[bytes] = []
+ encoded_size = 0
+ for part in json.JSONEncoder(ensure_ascii=False).iterencode(value):
+ encoded_part = part.encode("utf-8")
+ encoded_size += len(encoded_part)
+ if encoded_size > MAX_FRAME_BYTES:
+ fallback = {
+ "ok": False,
+ "error": "response-limit-exceeded",
+ "execution_state": value.get("execution_state", "unknown"),
+ "output_truncated": True,
+ "message": "The command completed or may have completed, but its result exceeded the bridge response limit. Do not retry automatically.",
+ }
+ parts = [json.dumps(fallback, separators=(",", ":")).encode("utf-8")]
+ break
+ parts.append(encoded_part)
+ encoded = b"".join(parts)
+ try:
+ connection.sendall(encoded + b"\n")
+ except OSError:
+ # A disconnected caller must not terminate the bridge thread or
+ # prevent later commands from being served.
+ pass
+
+ def stop(self) -> None:
+ self.stopping = True
+ try:
+ self.server.close()
+ except OSError:
+ pass
+
+
+class ReaCutterPlugin(cutter.CutterPlugin):
+ name = "REA Local Bridge"
+ description = "Local REA analysis bridge for the active Cutter session"
+ version = "1.0"
+ author = "REA"
+
+ def __init__(self) -> None:
+ super().__init__()
+ self._dispatcher: _RequestDispatcher | None = None
+ self._bridge: _BridgeServer | None = None
+
+ def setupPlugin(self) -> None:
+ return
+
+ def setupInterface(self, main: Any) -> None:
+ self._dispatcher = _RequestDispatcher()
+ self._bridge = _BridgeServer(self._dispatcher)
+ self._bridge.start()
+
+ def terminate(self) -> None:
+ if self._bridge is not None:
+ self._bridge.stop()
+ self._bridge.join(timeout=2)
+ self._bridge = None
+ self._dispatcher = None
+
+
+def create_cutter_plugin() -> ReaCutterPlugin:
+ return ReaCutterPlugin()
+
+
+def _within_response_limit(value: Any) -> bool:
+ pending = [value]
+ seen: set[int] = set()
+ size = 0
+ while pending:
+ current = pending.pop()
+ if isinstance(current, str):
+ size += 2
+ if current.isascii():
+ size += len(current)
+ size += current.count('"') + current.count("\\")
+ size += sum(5 for character in current if ord(character) < 0x20)
+ else:
+ size += 4 * len(current)
+ size += current.count('"') + current.count("\\")
+ size += sum(5 for character in current if ord(character) < 0x20)
+ elif isinstance(current, dict):
+ identity = id(current)
+ if identity in seen:
+ return False
+ seen.add(identity)
+ size += 2 + 4 * len(current)
+ if size > MAX_FRAME_BYTES:
+ return False
+ pending.extend(current.keys())
+ pending.extend(current.values())
+ elif isinstance(current, (list, tuple)):
+ identity = id(current)
+ if identity in seen:
+ return False
+ seen.add(identity)
+ size += 2 + 2 * len(current)
+ if size > MAX_FRAME_BYTES:
+ return False
+ pending.extend(current)
+ else:
+ size += 16
+ if size > MAX_FRAME_BYTES:
+ return False
+ return True
diff --git a/bridge/ghidra/ReaGhidraBridge.java b/bridge/ghidra/ReaGhidraBridge.java
index 84e809ccb..e184dc206 100644
--- a/bridge/ghidra/ReaGhidraBridge.java
+++ b/bridge/ghidra/ReaGhidraBridge.java
@@ -81,6 +81,7 @@ import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.listing.InstructionIterator;
+import ghidra.program.model.listing.Listing;
import ghidra.program.model.listing.Variable;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.mem.MemoryBlockSourceInfo;
@@ -405,7 +406,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
case "xrefs" -> xrefs(request.params);
case "analyze_function" -> {
FunctionAnalysisObservation observation = analyzeFunctionObservation(request.params);
- yield functionObservation(observation.dossier(), observation.limitations());
+ JsonObject result = functionObservation(observation.dossier(), observation.limitations());
+ result.add("relationship_coverage", observation.relationshipCoverage());
+ yield result;
}
case "set_address_name", "set_addresses_names" -> {
if (!descriptor.transport.equals("unix-socket"))
@@ -1016,12 +1019,19 @@ public final class ReaGhidraBridge extends HeadlessScript {
for (Function item : ordered) {
result.add(canonicalAddress(item.getEntryPoint()));
}
- return functionObservation(
- result,
- noReturnCallLimitations(
+ if (callers) {
+ JsonObject observation = functionObservation(result, noReturnCallLimitations(
currentProgram.getListing().getInstructions(function.getBody(), true)
- )
+ ));
+ observation.add("relationship_coverage", callRelationshipCoverage(null,
+ unrepresentedIncomingCalls(function, observed)));
+ monitor.checkCancelled();
+ return observation;
+ }
+ CallBoundaryObservation calls = callBoundaryObservation(
+ function, currentProgram.getListing().getInstructions(function.getBody(), true), observed, false, null
);
+ return callRelationshipObservation(result, calls);
}
private JsonObject procedureInfo(JsonObject params) throws Exception {
@@ -1071,18 +1081,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
result.addProperty("direction", direction);
result.add("references", edges);
result.addProperty("reference_kinds_available", true);
- JsonArray unresolvedCalls = new JsonArray();
- if (direction.equals("outgoing")) for (Instruction instruction : scanInstructions(function, Integer.MAX_VALUE).instructions) {
- if (!instruction.getFlowType().isCall()) continue;
- boolean resolved = false;
- for (Reference reference : instruction.getReferencesFrom()) if (reference.getReferenceType().isCall()) resolved = true;
- if (!resolved) {
- JsonObject site = new JsonObject();
- site.addProperty("address", canonicalAddress(instruction.getAddress()));
- site.addProperty("reason", "Instruction decoder identifies a call but ReferenceManager supplies no call destination");
- unresolvedCalls.add(site);
- }
- }
+ JsonArray unresolvedCalls = direction.equals("outgoing")
+ ? callBoundaryObservation(
+ function, currentProgram.getListing().getInstructions(function.getBody(), true), null, true, references
+ ).unresolvedCalls() : new JsonArray();
result.add("unresolved_calls", unresolvedCalls);
return result;
}
@@ -1490,19 +1492,28 @@ public final class ReaGhidraBridge extends HeadlessScript {
InstructionScan scan = scanInstructions(function, Integer.MAX_VALUE);
DecompileResults decompilation = decompile(function);
String pseudocode = decompiledText(decompilation);
+ JsonObject availability = new JsonObject();
+ availability.add("pseudocode", facetAvailability(
+ pseudocode != null,
+ function.isExternal() ? "Ghidra does not decompile external functions."
+ : function.getBody().isEmpty() ? "Ghidra has no function body to decompile."
+ : "Ghidra completed decompilation without returning pseudocode."
+ ));
if (pseudocode == null) {
pseudocode = "";
}
- List
incomingReferences = collectReferences(function, "incoming")
- .stream()
+ List bodyIncoming = collectReferences(function, "incoming");
+ List incomingReferences = bodyIncoming.stream()
.filter(reference -> !function.getBody().contains(reference.getFromAddress()))
.toList();
List outgoingReferences = collectReferences(function, "outgoing");
JsonArray incoming = referenceEdges(incomingReferences);
JsonArray outgoing = referenceEdges(outgoingReferences);
- JsonArray comments = comments(scan.instructions);
- JsonArray callers = procedureIdentities(function.getCallingFunctions(monitor));
- JsonArray callees = procedureIdentities(function.getCalledFunctions(monitor));
+ JsonArray comments = comments(function);
+ Set callerFunctions = function.getCallingFunctions(monitor);
+ JsonArray callers = procedureIdentities(callerFunctions);
+ Set calleeFunctions = function.getCalledFunctions(monitor);
+ JsonArray callees = procedureIdentities(calleeFunctions);
JsonArray referencedStrings = referencedStrings(outgoingReferences);
JsonArray referencedNames = referencedNames(outgoingReferences);
JsonArray blocks = basicBlockValues(function);
@@ -1536,6 +1547,15 @@ public final class ReaGhidraBridge extends HeadlessScript {
nativeApiBoundary(function, decompilation)
);
result.add("native_value_flow", nativeValueFlow(decompilation));
+ // Successful full queries distinguish collected empty sets from missing facts.
+ // Availability does not claim that Ghidra resolved every runtime relationship.
+ for (String facet : List.of("comments", "callers", "callees", "references", "strings_names", "cfg")) {
+ availability.add(facet, facetAvailability(true, null));
+ }
+ availability.add("assembly", facetAvailability(
+ !scan.truncated(), "The function instruction scan was truncated."
+ ));
+ result.add("facet_availability", availability);
JsonArray limitations = new JsonArray();
limitations.add(
"Unresolved computed or indirect flows without target addresses are not represented as reference edges."
@@ -1563,8 +1583,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
"Swift String literal decoding is implemented for AArch64 register pairs only; no Swift literals were decoded for this architecture."
);
}
- List boundaryLimitations = noReturnCallLimitations(scan.instructions);
- for (String limitation : boundaryLimitations) {
+ CallBoundaryObservation calls = callBoundaryObservation(
+ function, scan.instructions, calleeFunctions, true, outgoingReferences);
+ result.add("unresolved_calls", calls.unresolvedCalls());
+ for (String limitation : calls.limitations()) {
limitations.add(limitation);
}
if (decompilation != null && decompilation.getErrorMessage() != null &&
@@ -1572,7 +1594,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
limitations.add("Ghidra decompiler diagnostic: " + decompilation.getErrorMessage());
}
result.add("limitations", limitations);
- return new FunctionAnalysisObservation(result, boundaryLimitations);
+ monitor.checkCancelled();
+ int missingCallers = 0;
+ for (Reference reference : bodyIncoming) {
+ monitor.checkCancelled();
+ if (unrepresentedCaller(reference, callerFunctions)) missingCallers++;
+ }
+ return new FunctionAnalysisObservation(result, calls.limitations(),
+ callRelationshipCoverage(calls, missingCallers));
}
private JsonObject nativeValueFlow(DecompileResults decompilation) throws Exception {
@@ -3276,54 +3305,202 @@ public final class ReaGhidraBridge extends HeadlessScript {
return result;
}
+ private static JsonObject facetAvailability(boolean available, String reason) {
+ JsonObject result = new JsonObject();
+ result.addProperty("available", available);
+ if (!available) result.addProperty("reason", reason);
+ return result;
+ }
+
+ private JsonObject callRelationshipObservation(JsonElement value, CallBoundaryObservation calls)
+ throws Exception {
+ monitor.checkCancelled();
+ JsonObject result = functionObservation(value, calls.limitations());
+ result.add("relationship_coverage", callRelationshipCoverage(calls, null));
+ return result;
+ }
+
+ private static JsonObject callRelationshipCoverage(
+ CallBoundaryObservation calls, Integer missingCallers) {
+ JsonObject coverage = completeResolvedCallCoverage();
+ Integer omitted = calls == null ? null : calls.unrepresentedCalleeCalls();
+ JsonArray directions = new JsonArray();
+ if (missingCallers != null && missingCallers > 0) directions.add("callers");
+ String callerLimitation = missingCallers != null && missingCallers > 0
+ ? "Ghidra's caller inventory omits sources of " + missingCallers +
+ " call reference(s) into this function's body." : null;
+ if ((calls != null && omitted == null) || (calls == null && missingCallers == null)) {
+ coverage.addProperty("status", "unknown");
+ String reason = calls == null ? "No call inventory was examined."
+ : "The callee inventory was not examined alongside decoded call targets.";
+ coverage.addProperty("reason", reason + (callerLimitation == null ? "" : " " + callerLimitation));
+ if (calls == null && missingCallers == null) directions.add("callers");
+ directions.add("callees");
+ coverage.add("limited_directions", directions);
+ return coverage;
+ }
+ JsonArray limitations = new JsonArray();
+ if (callerLimitation != null) limitations.add(callerLimitation);
+ if (omitted != null && omitted > 0) {
+ limitations.add("Ghidra's callee inventory omits targets of " + omitted +
+ " call reference(s) or decoded call(s) without destinations.");
+ directions.add("callees");
+ }
+ if (!limitations.isEmpty()) {
+ coverage.addProperty("status", "partial");
+ coverage.add("limitations", limitations);
+ coverage.add("limited_directions", directions);
+ }
+ return coverage;
+ }
+
+ private static JsonObject completeResolvedCallCoverage() {
+ JsonObject coverage = new JsonObject();
+ coverage.addProperty("status", "complete");
+ JsonArray diagnostics = new JsonArray();
+ diagnostics.add(
+ "Only resolved Ghidra call references are returned; unresolved computed or indirect calls remain unknown, while function classifications distinguish thunks and externals."
+ );
+ coverage.add("diagnostics", diagnostics);
+ return coverage;
+ }
+
+ private boolean unrepresentedCaller(Reference reference, Set callers) {
+ if (!reference.getReferenceType().isCall()) return false;
+ Function caller = containingFunction(reference.getFromAddress());
+ return caller == null || !callers.contains(caller);
+ }
+
+ private int unrepresentedIncomingCalls(Function function, Set callers)
+ throws Exception {
+ int[] omitted = { 0 };
+ visitReferences(function, "incoming", reference -> {
+ if (unrepresentedCaller(reference, callers)) omitted[0]++;
+ });
+ return omitted[0];
+ }
+
private List noReturnCallLimitations(Iterable instructions)
throws Exception {
List limitations = new ArrayList<>();
for (Instruction instruction : instructions) {
monitor.checkCancelled();
- if (!instruction.getFlowType().isCall() || !instruction.getFlowType().isTerminal()) {
- continue;
- }
- Address[] destinations = instruction.getFlows();
- StringBuilder limitation = new StringBuilder();
- limitation.append("Ghidra Listing reports a terminal call at ")
- .append(canonicalAddress(instruction.getAddress()));
- if (destinations.length == 0) {
- limitation.append(" with no resolved callee; its no-return basis is unknown.");
- }
- else {
- limitation.append(" to ");
- for (int index = 0; index < destinations.length; index++) {
- if (index > 0) limitation.append(", ");
- Address destination = destinations[index];
- limitation.append(canonicalAddress(destination));
- Function target = currentProgram.getFunctionManager().getFunctionAt(destination);
- if (target == null) {
- limitation.append(" (callee identity unknown)");
- }
- else {
- limitation.append(" ").append(procedureName(target));
- limitation.append(" (external=").append(target.isExternal());
- limitation.append(", thunk=").append(target.isThunk());
- limitation.append(", hasNoReturn=").append(target.hasNoReturn()).append(")");
- }
- }
- limitation.append("; fallthrough is excluded by Ghidra's flow model.");
- Function target = destinations.length == 1
- ? currentProgram.getFunctionManager().getFunctionAt(destinations[0])
- : null;
- if (target != null && target.hasNoReturn()) {
- limitation.append(" This records Ghidra's FunctionManager flag, not an independent verification that the callee cannot return.");
- }
- else {
- limitation.append(" The reason for the terminal flow is not established by this observation.");
- }
- }
- limitations.add(limitation.toString());
+ String limitation = terminalCallLimitation(instruction);
+ if (limitation != null) limitations.add(limitation);
}
return limitations;
}
+ private CallBoundaryObservation callBoundaryObservation(
+ Function function, Iterable instructions, Set returnedCallees,
+ boolean includeUnresolvedSites, List observedReferences)
+ throws Exception {
+ List limitations = new ArrayList<>();
+ CallBoundaryReferences calls = new CallBoundaryReferences(returnedCallees, includeUnresolvedSites);
+ if (observedReferences == null) visitReferences(function, "outgoing", calls::observe);
+ else for (Reference reference : observedReferences) {
+ monitor.checkCancelled();
+ calls.observe(reference);
+ }
+ for (Instruction instruction : instructions) {
+ monitor.checkCancelled();
+ String limitation = terminalCallLimitation(instruction);
+ if (limitation != null) limitations.add(limitation);
+ if (!instruction.getFlowType().isCall()) continue;
+ boolean destination = false;
+ for (Reference reference : instruction.getReferencesFrom()) {
+ if (!reference.getReferenceType().isCall()) continue;
+ destination = true;
+ break;
+ }
+ if (!destination) {
+ calls.unrepresented++;
+ calls.unresolved(instruction.getAddress(),
+ "Instruction decoder identifies a call but ReferenceManager supplies no call destination");
+ }
+ }
+ return new CallBoundaryObservation(List.copyOf(limitations),
+ returnedCallees == null ? null : calls.unrepresented, calls.unresolvedCalls());
+ }
+
+ private final class CallBoundaryReferences {
+ private final Set returnedCallees;
+ private final TreeMap unresolved;
+ private int unrepresented;
+
+ CallBoundaryReferences(Set returnedCallees, boolean includeUnresolvedSites) {
+ this.returnedCallees = returnedCallees;
+ unresolved = includeUnresolvedSites ? new TreeMap<>() : null;
+ }
+
+ void observe(Reference reference) {
+ if (!reference.getReferenceType().isCall()) return;
+ // Reference identity follows the same containing-function owner as referenceEdge.
+ Function target = containingFunction(reference.getToAddress());
+ if (returnedCallees != null && (target == null || !returnedCallees.contains(target)))
+ unrepresented++;
+ if (target == null) unresolved(reference.getFromAddress(),
+ "A call destination has no known containing Ghidra procedure");
+ }
+
+ void unresolved(Address address, String reason) {
+ if (unresolved != null) unresolved.put(address, reason);
+ }
+
+ JsonArray unresolvedCalls() {
+ if (unresolved == null) return null;
+ JsonArray result = new JsonArray();
+ for (var item : unresolved.entrySet()) {
+ JsonObject site = new JsonObject();
+ site.addProperty("address", canonicalAddress(item.getKey()));
+ site.addProperty("reason", item.getValue());
+ result.add(site);
+ }
+ return result;
+ }
+ }
+
+ private String terminalCallLimitation(Instruction instruction) {
+ if (!instruction.getFlowType().isCall() || !instruction.getFlowType().isTerminal())
+ return null;
+ Address[] destinations = instruction.getFlows();
+ StringBuilder limitation = new StringBuilder();
+ limitation.append("Ghidra Listing reports a terminal call at ")
+ .append(canonicalAddress(instruction.getAddress()));
+ if (destinations.length == 0) {
+ limitation.append(" with no resolved callee; its no-return basis is unknown.");
+ }
+ else {
+ limitation.append(" to ");
+ for (int index = 0; index < destinations.length; index++) {
+ if (index > 0) limitation.append(", ");
+ Address destination = destinations[index];
+ limitation.append(canonicalAddress(destination));
+ Function target = currentProgram.getFunctionManager().getFunctionAt(destination);
+ if (target == null) {
+ limitation.append(" (callee identity unknown)");
+ }
+ else {
+ limitation.append(" ").append(procedureName(target));
+ limitation.append(" (external=").append(target.isExternal());
+ limitation.append(", thunk=").append(target.isThunk());
+ limitation.append(", hasNoReturn=").append(target.hasNoReturn()).append(")");
+ }
+ }
+ limitation.append("; fallthrough is excluded by Ghidra's flow model.");
+ Function target = destinations.length == 1
+ ? currentProgram.getFunctionManager().getFunctionAt(destinations[0])
+ : null;
+ if (target != null && target.hasNoReturn()) {
+ limitation.append(" This records Ghidra's FunctionManager flag, not an independent verification that the callee cannot return.");
+ }
+ else {
+ limitation.append(" The reason for the terminal flow is not established by this observation.");
+ }
+ }
+ return limitation.toString();
+ }
+
private static String renderAssembly(List instructions) {
return String.join("\n", renderAssemblyLines(instructions));
}
@@ -3347,15 +3524,27 @@ public final class ReaGhidraBridge extends HeadlessScript {
private List collectReferences(
Function function,
String direction) throws Exception {
+ TreeMap observed = new TreeMap<>();
+ visitReferences(function, direction, reference -> addReference(observed, reference));
+ List result = new ArrayList<>(observed.values());
+ result.sort(REFERENCE_ORDER);
+ return result;
+ }
+
+ @FunctionalInterface
+ private interface ReferenceVisitor {
+ void accept(Reference reference) throws Exception;
+ }
+
+ private void visitReferences(Function function, String direction, ReferenceVisitor visitor)
+ throws Exception {
// References can target instruction interiors or originate in embedded
// data. Use actual body ownership, including an external's exact entry.
- AddressSet scope = new AddressSet(function.getBody());
- scope.add(function.getEntryPoint());
+ AddressSet scope = functionObservationScope(function);
boolean outgoing = direction.equals("outgoing");
AddressIterator addresses = outgoing
? currentProgram.getReferenceManager().getReferenceSourceIterator(scope, true)
: currentProgram.getReferenceManager().getReferenceDestinationIterator(scope, true);
- TreeMap observed = new TreeMap<>();
while (addresses.hasNext()) {
monitor.checkCancelled();
Address address = addresses.next();
@@ -3363,7 +3552,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
for (Reference reference : currentProgram.getReferenceManager()
.getReferencesFrom(address)) {
monitor.checkCancelled();
- addReference(observed, reference);
+ if (!reference.isEntryPointReference()) visitor.accept(reference);
}
}
else {
@@ -3371,19 +3560,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
.getReferencesTo(address);
while (iterator.hasNext()) {
monitor.checkCancelled();
- addReference(observed, iterator.next());
+ Reference reference = iterator.next();
+ if (!reference.isEntryPointReference()) visitor.accept(reference);
}
}
}
- List result = new ArrayList<>(observed.values());
- result.sort(REFERENCE_ORDER);
- return result;
}
private static void addReference(TreeMap observed, Reference reference) {
- if (reference.isEntryPointReference()) {
- return;
- }
String key = canonicalAddress(reference.getFromAddress()) + "\u0000" +
canonicalAddress(reference.getToAddress()) + "\u0000" +
reference.getReferenceType().getName() + "\u0000" +
@@ -3392,6 +3576,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
observed.putIfAbsent(key, reference);
}
+ private static AddressSet functionObservationScope(Function function) {
+ AddressSet scope = new AddressSet(function.getBody());
+ scope.add(function.getEntryPoint());
+ return scope;
+ }
+
private JsonArray referenceEdges(List references) {
JsonArray result = new JsonArray();
for (Reference reference : references) {
@@ -3545,23 +3735,28 @@ public final class ReaGhidraBridge extends HeadlessScript {
return result;
}
- private static JsonArray comments(List instructions) {
+ private JsonArray comments(Function function) throws Exception {
+ Listing listing = currentProgram.getListing();
+ AddressSet scope = functionObservationScope(function);
+ AddressIterator addresses = listing.getCommentAddressIterator(scope, true);
TreeMap observed = new TreeMap<>();
- for (Instruction instruction : instructions) {
+ while (addresses.hasNext()) {
+ monitor.checkCancelled();
+ Address address = addresses.next();
for (CommentType type : CommentType.values()) {
- String text = instruction.getComment(type);
+ String text = listing.getComment(type, address);
if (text == null || text.isEmpty()) {
continue;
}
String kind = type == CommentType.EOL ? "inline" : "comment";
JsonObject item = new JsonObject();
- item.addProperty("address", canonicalAddress(instruction.getAddress()));
+ item.addProperty("address", canonicalAddress(address));
item.addProperty("kind", kind);
item.addProperty("text", text);
- String key = canonicalAddress(instruction.getAddress()) + "\u0000" +
+ String key = canonicalAddress(address) + "\u0000" +
kind + "\u0000" + type.name() + "\u0000" + text;
observed.put(key, item);
- }
+ }
}
JsonArray result = new JsonArray();
observed.values().forEach(result::add);
@@ -4497,7 +4692,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
private record InventoryItem(Address address, String value, JsonObject facts) {}
private record AddressNameRequest(String inputAddress, Address address, String name) {}
private record FunctionEntry(Function function, InventoryItem item) {}
- private record FunctionAnalysisObservation(JsonObject dossier, List limitations) {}
+ private record FunctionAnalysisObservation(JsonObject dossier, List limitations, JsonObject relationshipCoverage) {}
+ private record CallBoundaryObservation(List limitations, Integer unrepresentedCalleeCalls, JsonArray unresolvedCalls) {}
private record SwiftStringLiteralDecode(JsonArray literals, JsonArray omissions) {}
private record SwiftLiteralLayout(
String representation,
diff --git a/bridge/hopper_bridge.py b/bridge/hopper_bridge.py
index 92a473c06..4188fe0da 100644
--- a/bridge/hopper_bridge.py
+++ b/bridge/hopper_bridge.py
@@ -644,7 +644,7 @@ def _analyze_function(document, params):
"end": _hex(_basic_block_end(procedure, block)),
"successors": sorted(set(successors), key=lambda value: int(value, 16)),
})
- pseudo = _pseudocode(document, procedure) or ""
+ pseudo = _pseudocode(document, procedure)
assembly_lines = _assembly(procedure).splitlines()
callers = sorted((_procedure_identity(item) for item in procedure.getAllCallerProcedures()), key=lambda item: int(item["address"], 16))
callees = sorted((_procedure_identity(item) for item in procedure.getAllCalleeProcedures()), key=lambda item: int(item["address"], 16))
@@ -706,7 +706,7 @@ def _analyze_function(document, params):
"signature": procedure.signatureString(),
"locals": _procedure_locals(procedure),
},
- "pseudocode": pseudo,
+ "pseudocode": pseudo or "",
"assembly": assembly_lines,
"comments": comments,
"callers": callers, "callees": callees,
@@ -718,6 +718,12 @@ def _analyze_function(document, params):
"basic_blocks": blocks,
"native_api": None,
"native_value_flow": None,
+ "facet_availability": {
+ "pseudocode": {"available": True} if pseudo is not None else _unavailable("Hopper did not supply pseudocode."),
+ **{facet: {"available": True} for facet in (
+ "assembly", "comments", "callers", "callees", "references", "strings_names", "cfg"
+ )},
+ },
"limitations": [
"Native CallReference classifications are observed where available; detailed reference flags remain unknown.",
"Hopper's public Python API does not expose equivalent external or thunk classification in this dossier.",
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 5d7fcc2f3..dd562d813 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -29,6 +29,7 @@ export default defineConfig({
items: [
{ text: "Native binaries", link: "/native-investigation" },
{ text: "Go build metadata", link: "/go-binaries" },
+ { text: "Rust dependencies", link: "/rust-binaries" },
{
text: "JavaScript and Electron",
link: "/javascript-artifact-reconstruction",
@@ -62,6 +63,7 @@ export default defineConfig({
{ text: "Ghidra DOS analysis", link: "/ghidra-dos" },
{ text: "Ghidra raw binary", link: "/raw-binary" },
{ text: "Ghidra NativeAOT", link: "/ghidra-nativeaot" },
+ { text: "Ghidra PowerPC", link: "/ghidra-ppc" },
{ text: "Ghidra semantics", link: "/ghidra-provider" },
],
},
diff --git a/docs/android-analysis.md b/docs/android-analysis.md
index 50087f464..668bcbffb 100644
--- a/docs/android-analysis.md
+++ b/docs/android-analysis.md
@@ -33,10 +33,40 @@ unverified.
## Supply tools
Use a **full JDK 17 or newer** (including its compiler module) and
-**jadx-headless-mcp 0.7.1**. REA runs its packaged Java metadata bridge in
-source-file mode against the unmodified engine JAR. REA does not install Java,
-an Android SDK, JADX, a device service or an emulator. Set the path in the shell
-or MCP server environment:
+[**jadx-headless-mcp 0.7.1**](https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar).
+Verify the downloaded JAR's SHA-256 against
+[REA's pinned release metadata](https://github.com/morluto/rea/blob/main/src/android/JadxRelease.ts)
+before using it. The following download and verification commands work for npm
+package consumers as well as repository checkouts; the repository-only
+`npm run fixtures:android` helper is not included in the npm package.
+
+On macOS or Linux, choose a destination and verify the pinned digest:
+
+```sh
+mkdir -p "$HOME/.local/share/rea/android" &&
+cd "$HOME/.local/share/rea/android" &&
+curl -fL -o jadx-headless-mcp-0.7.1-all.jar https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar &&
+if command -v sha256sum >/dev/null; then
+ echo '6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75 jadx-headless-mcp-0.7.1-all.jar' | sha256sum -c -
+else
+ echo '6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75 jadx-headless-mcp-0.7.1-all.jar' | shasum -a 256 -c -
+fi &&
+export REA_JADX_MCP_JAR="$PWD/jadx-headless-mcp-0.7.1-all.jar"
+```
+
+On Windows, run the equivalent in PowerShell:
+
+```powershell
+$jar = Join-Path $HOME '.local\share\rea\android\jadx-headless-mcp-0.7.1-all.jar'
+New-Item -ItemType Directory -Force (Split-Path $jar) | Out-Null
+Invoke-WebRequest -Uri 'https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar' -OutFile $jar
+if ((Get-FileHash -Algorithm SHA256 $jar).Hash.ToLowerInvariant() -ne '6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75') { throw 'JADX JAR SHA-256 does not match REA pinned metadata' }
+$env:REA_JADX_MCP_JAR = $jar
+```
+
+REA runs its packaged Java metadata bridge in source-file mode against the
+unmodified engine JAR. REA does not install Java, an Android SDK, JADX, a device
+service or an emulator. Set the path in the shell or MCP server environment:
```sh
export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid
index 753c57024..90dd41ab1 100644
--- a/docs/architecture.mermaid
+++ b/docs/architecture.mermaid
@@ -38,6 +38,10 @@ graph TD
JS_CODEC["src/javascript/sourceMaps/ pinned upstream map codec owned heap-limited Node process, complete point evidence"]
INSPECTOR["src/inspector/ passive V8 Inspector discovery and capture script and target locations"]
DOTNET["src/dotnet/ Managed static metadata provider PE/CLI metadata inventory, members, IL"]
+ OBJDUMP["src/objdump/ GNU objdump command profile"]
+ RIZIN["src/rizin/ Static command profile and persistent debugger sessions"]
+ GDB["src/gdb/ Persistent GDB/MI sessions owned process lifecycle and target state"]
+
FIRMWARE["src/firmware/ Binwalk / Unblob adapters owned processes, report normalization, private extraction"]
ANDROID["src/android/ Static APK adapter owned SDK stdio, pinned headless JADX, normalization"]
REFERENCE["src/reference/ Filesystem reference-source reader bounded traversal, validation, errors"]
@@ -50,7 +54,7 @@ graph TD
end
subgraph "Contracts Layer"
- CONTRACTS["src/contracts/ Caller-visible schemas for analysis, managed, browser, Electron, application, and session tools"]
+ CONTRACTS["src/contracts/ Caller-visible schemas for analysis, reverse engineering, managed, browser, Electron, application, and session tools"]
ERRORS["src/contracts/errorSchemas.ts Shared analysis error schemas CLI diagnostics, generated error catalog"]
end
@@ -122,6 +126,14 @@ graph TD
INSPECTOR --> DOMAIN
APP --> DOTNET
DOTNET --> DOMAIN
+ COMPOSITION --> OBJDUMP
+ OBJDUMP --> DOMAIN
+ COMPOSITION --> RIZIN
+ RIZIN --> DOMAIN
+ CLI --> GDB
+ SERVER --> GDB
+ GDB --> PROCESS
+ GDB --> DOMAIN
HOPPER --> HOPPER_BRIDGE
GHIDRA --> GHIDRA_BRIDGE
HOPPER --> PROCESS
diff --git a/docs/browser-observation.md b/docs/browser-observation.md
index 72dde900b..879d01520 100644
--- a/docs/browser-observation.md
+++ b/docs/browser-observation.md
@@ -243,6 +243,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
+- Unavailable or malformed storage inventories mark `storage_keys` unavailable, even without fingerprints. Comparisons suppress inferred key additions and removals while preserving modifications proven by complete fingerprints on both sides. Previously exported captures are not retroactively corrected; recapture them to obtain these coverage markers.
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Script metadata admission bounds retained identity and source-map strings to 8 MiB and 50,000 records. When a source-map URL cannot fit, the script identity remains and source-map coverage reports `resource_budget_exhausted`; rejected script observations are counted without retaining their IDs. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
Frames, resources, scripts, events, and workers outside the selected origin scope are excluded. An explicit `allowed_origins` list can include additional origins; excluded target details are counted without being exposed.
diff --git a/docs/cli.md b/docs/cli.md
index c43ec2181..c801bf006 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -83,6 +83,18 @@ The shared CLI/MCP workflow reports compiler version, modules, replacements,
build settings, original bytes and SHA-256. See [Go binaries](go-binaries.md)
for supported containers and absent or malformed metadata.
+Inspect an embedded cargo-auditable Rust dependency list without installing
+Rust or configuring a disassembler:
+
+```bash
+rea inspect-rust-binary /absolute/path/to/program --json
+```
+
+The same workflow reports package names, versions, sources, the root package,
+dependency edges, and the section's file range. A missing `.dep-v0` section is
+null metadata, not proof that the binary has no dependencies. See
+[Rust binaries](rust-binaries.md).
+
Configure [Hopper or Ghidra](installation.md#hopper), or the
[IDA adapter](ida-provider.md), before analyzing a native target. Substitute
your target, search text and function name or address in these examples:
diff --git a/docs/client-configuration.md b/docs/client-configuration.md
index 409bc7f19..d03984ec8 100644
--- a/docs/client-configuration.md
+++ b/docs/client-configuration.md
@@ -265,6 +265,21 @@ The user-level MCP paths follow the editor's VS Code layout. Trae's official
documentation describes the raw JSON editor without specifying those paths;
loading this registration in Trae remains unverified.
+## WorkBuddy
+
+For [WorkBuddy](https://www.workbuddy.cn/docs/workbuddy/From-Beginner-to-Expert-Guide/Function-Description/MCP-Guide), setup writes `mcpServers.rea` into the
+global `~/.workbuddy/mcp.json` (a standard `mcpServers` JSON file at the user
+home root on every platform). Entries carry `command`, `args`, and `env`;
+stdio is the default transport. WorkBuddy discovers personal skills from
+`~/.workbuddy/skills` (`%userprofile%\.workbuddy\skills` on Windows), so
+setup installs its skill copy there.
+
+The global MCP path is a plain JSON file; setup preserves every other server
+and top-level key when it upserts `rea`. WorkBuddy also reads a project-level
+`/.workbuddy/mcp.json`, which setup does not touch: it registers the
+user-level file only. Loading this registration and real REA tool calls
+through WorkBuddy are verified.
+
## ZCode
For [ZCode](https://zcode.z.ai/en/docs/mcp-services), setup registers `rea` in the nested `mcp.servers` table of the
diff --git a/docs/frida-instrumentation.md b/docs/frida-instrumentation.md
index c93c76465..718af30c2 100644
--- a/docs/frida-instrumentation.md
+++ b/docs/frida-instrumentation.md
@@ -6,8 +6,8 @@ For remote devices, pass a host with an optional port (`host[:port]`; bracket IP
MCP exposes `list_frida_devices`, `list_frida_processes`, `start_frida_session`, `load_frida_script`, `resume_frida_session`, `unload_frida_script`, `frida_session_status`, and `close_frida_session`. A spawned process starts paused so scripts can load first. Closing unloads session scripts and detaches. If a spawn remains paused, REA resumes it only after script cleanup or session detach confirms the instrumentation is released; if both cleanup paths fail, the target stays paused and the cleanup error is retained. REA never kills the target. MCP sessions end with the server process. The one-shot CLI command `instrument-with-frida` accepts one inline script or script file, observes messages for a bounded duration, and returns Evidence with the source hash and captured messages.
-Failed startup cleanup retains acquired Frida handles, including a paused spawn that has not attached, for later cleanup retries. Its remote connection stays leased until session detach and target resume complete. CLI commands await provider cleanup before returning; MCP retains these resources for cleanup when its server closes. A failed cleanup remains reported rather than treating an unregistered session as fully released.
+Failed startup cleanup retains acquired Frida handles, including a paused spawn that has not attached, for later cleanup retries. Its remote connection stays leased until session detach and target resume complete. CLI commands await provider cleanup before returning; MCP retains these resources for cleanup when its server closes. Server shutdown stops admitting new Frida operations and waits for admitted startup, discovery, and queued work before releasing their resources. A failed cleanup remains reported and can be retried by closing the server again, rather than treating an unregistered session as fully released.
-Frida scripts execute inside the selected target with that process's memory, filesystem, and network authority. Message capture is bounded to 16 MiB and reports truncation. Script source is not stored in Evidence; its selected path and SHA-256 digest are. Evidence retains non-authentication target selection such as device or endpoint, mode, program or PID, and spawn arguments. Authentication values remain excluded. Results report the observed session and messages, while unsupported platform metadata remains unknown. A request cancelled before a Frida operation starts has no provider effects. If cancellation arrives during a native Frida call, REA waits for that call to return, then closes any session it created and returns the observed session as a partial observation. Cancellation Evidence separately reports whether a target resume was observed, whether instrumentation cleanup completed, and that final target liveness remains unknown. REA does not kill the target. Remote discovery retains its observations and reports a separate cleanup error if the temporary connection cannot be removed.
+Frida scripts execute inside the selected target with that process's memory, filesystem, and network authority. Message capture is bounded to 16 MiB and reports truncation. Script source is not stored in Evidence; its selected path and SHA-256 digest are. Evidence retains non-authentication target selection such as device or endpoint, mode, program or PID, and spawn arguments. Authentication values remain excluded. Results report the observed session and messages, while unsupported platform metadata remains unknown. A request cancelled before a Frida operation starts has no provider effects. If cancellation arrives during a native Frida call, REA waits for that call to return, then closes any session it created and returns the observed session as a partial observation. A cancelled script load on an existing session releases only its new script before queued operations can run; the session and previously loaded scripts remain available. Once the provider commits a successful load, later request cancellation does not unload that script; use `unload_frida_script` to release it. If script cleanup fails, the script remains owned and a paused target cannot resume until the pending instrumentation is released. Queued script loads, resumes, and unloads check cancellation before dispatching native effects. Cancellation Evidence separately reports whether a target resume was observed, whether instrumentation cleanup completed, and that final target liveness remains unknown. REA does not kill the target. Remote discovery retains its observations and reports a separate cleanup error if the temporary connection cannot be removed.
The [Frida Core API](https://frida.re/docs/frida-core-api/) documents device management, sessions, and script loading; the [JavaScript API](https://frida.re/docs/javascript-api/) documents target-side instrumentation. [Luma](https://luma.frida.re/) is Frida's standalone GUI; REA does not automate or control it. `npm run verify:frida` requires a Linux host with the optional Frida binding and uses a short-lived local Node.js fixture created by the verifier. Add `-- --address HOST:PORT --pid PID` to run the separate remote attach lane against a caller-selected target. Mock tests do not establish real device/server compatibility.
diff --git a/docs/ghidra-ppc.md b/docs/ghidra-ppc.md
new file mode 100644
index 000000000..034158378
--- /dev/null
+++ b/docs/ghidra-ppc.md
@@ -0,0 +1,90 @@
+# PowerPC ELF analysis through Ghidra
+
+This lane extends native ELF analysis to 32-bit big-endian PowerPC; it does
+not execute PowerPC code. It is the first milestone of
+[#1741](https://github.com/morluto/rea/issues/1741). GameCube/Wii DOL
+executables, PPC64 and other PowerPC variants are separate follow-ups.
+
+## Admission boundary
+
+ELF `EM_PPC` (20) is retained as the provider-neutral `powerpc` family. The
+target also retains class, byte order, `e_type` and raw `e_flags`. Recognition
+does not mean every provider or PowerPC variant is supported. `EM_PPC64` (21)
+is not recognized as this family.
+
+The Ghidra lane admits ELF32 `ET_EXEC`, big endian. ELF64, little-endian,
+relocatable and shared objects are refused with a specific unsupported
+constraint. `EF_PPC_EMB` declares an embedded variant and is refused; the
+remaining `e_flags` bits only declare relocation conventions and do not change
+instruction decoding.
+
+## Bound language
+
+Ghidra's ELF loader selects `PowerPC:BE:32:e500:default` for a plain `EM_PPC`
+header with `e_flags` 0. e500 is the embedded Book-E variant whose SPE unit
+replaces the classic floating-point unit, so it misinterprets ordinary
+750-family code. Nothing in the header distinguishes the two.
+
+The lane therefore imports with `PowerPC:BE:32:default` and compiler spec
+`default`, commits that choice in the analysis profile as
+`language_selection: "provider-pinned-v1"`, and checks the language Ghidra
+reports after import. A different language set through
+`REA_GHIDRA_LANGUAGE_ID` (and optionally `REA_GHIDRA_COMPILER_SPEC_ID`)
+replaces the lane default and is admitted against the installed language
+catalog as usual; for example `PowerPC:BE:32:e500` for genuine e500 code.
+
+PowerPC header facts are committed in the analysis profile so interpretation
+changes invalidate profile-bound snapshots. The support-lane identity is
+versioned independently of file identity. Evidence, lifecycle outputs and saved
+snapshots preserve the `powerpc` family. Hopper and IDA adapters do not
+implicitly gain PowerPC support. Windows Ghidra P0 remains PE x86/x86-64 only.
+
+## Use
+
+With a caller-installed Ghidra and compatible JDK on a supported Linux/macOS host:
+
+```bash
+rea analyze /absolute/path/to/ppc.elf --provider ghidra --json
+rea function /absolute/path/to/ppc.elf function_name --provider ghidra --json
+```
+
+MCP uses the existing tools, including `open_binary` with an absolute path and
+`provider_id: "ghidra"`. No PowerPC-specific tool catalog or emulator is
+introduced.
+
+## Verification
+
+The source-owned freestanding fixture is `tests/conformance/c/ppc.c`. The
+optional cross-target lane requires a caller-supplied Clang with the PowerPC
+target, LLD, GNU `readelf`, Ghidra and its compatible JDK; ordinary host-native
+checks do not acquire these tools. `REA_PPC_CLANG` selects Clang and
+`REA_PPC_READELF` selects GNU readelf (for example
+`/opt/homebrew/opt/binutils/bin/readelf` on macOS). These are test-time
+prerequisites, not REA runtime providers.
+
+```bash
+GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra npm run verify:ghidra:ppc
+```
+
+The lane compiles the fixture for a 750-family core (`-mcpu=750`) and never
+executes it. GNU readelf independently supplies header facts and symbol
+addresses, compared with the production REA target resolver before provider
+startup. Its version, bounded command output and selected tool path are
+retained in the verification report. The parser deliberately recognizes only
+this fixture's GNU output, rejecting missing, ambiguous or unexpected fields
+rather than substituting defaults.
+
+Existing public MCP operations check Ghidra's **observed** language and
+compiler specification and loaded-file hashes; an e500 import fails the lane.
+The source-owned `ppc-probe.S` supplies fixed `addi` and conditional-branch
+encodings: verification checks bytes, length, immediate and direct destination,
+independent of decompiler variable names. Global and string bytes are checked at
+independently reported symbol addresses.
+
+CLI/MCP discovery and decompilation, direct calls, target/profile/Evidence
+identity, snapshot round-trip and final process cleanup remain covered.
+Pseudocode presence is a liveness check, not proof of semantic equivalence.
+Instruction decoding and reader agreement do not establish runtime or complete
+floating-point semantics. Parser/profile tests are narrower checks, not
+substitutes for this real-provider run. Report the actual lanes run and leave
+unexecuted verification pending.
diff --git a/docs/index.md b/docs/index.md
index 732ad9f05..9e0912dd1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -49,7 +49,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
| Task | Guides |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.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), [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), [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) |
diff --git a/docs/installation.md b/docs/installation.md
index a39471585..38c4d7966 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -288,11 +288,12 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
| Qoder | `qoder` |
| Factory Droid | `droid` |
| Trae | `trae` |
+| WorkBuddy | `workbuddy` |
| ZCode | `zcode` |
| DeepSeek Harness | `dsh` |
-`dsh` and `kimi` require a release containing them. The registry check above does
-not cover them.
+`dsh`, `kimi`, and `workbuddy` require a release containing them. The registry
+check above does not cover them.
See [client configuration and troubleshooting](client-configuration.md) for
configuration paths, profiles, trust policies, and schema-limit workarounds.
@@ -493,7 +494,13 @@ copying license secrets into logs, or killing unrelated Hopper processes.
REA connects to an existing Ghidra installation on Linux x64/arm64, macOS x64/arm64,
or experimental Windows x64 P0.
It accepts Ghidra 12.1.x and the 64-bit full JDK declared by that installation's
-`application.java.min` and `application.java.max`. Current 12.1 releases require
+`application.java.min` and `application.java.max`. The JDK's reported platform must also
+map to the same native decompiler directory as the host: a JDK running under
+emulation (for example x86_64 under Rosetta on Apple silicon) or reporting a
+non-host `os.name` fails the doctor
+even when it is an otherwise accepted 64-bit full JDK. Select a matching JDK
+through `JAVA_HOME`; adding another decompiler in the JVM-selected directory
+does not satisfy this host-validated admission rule. Current 12.1 releases require
JDK 21 or newer and set no maximum. The bridge is verified with Ghidra 12.1.4
and JDK 21. Each installation must include the native decompiler for the host
architecture in `Ghidra/Features/Decompiler/os//` or the corresponding
@@ -556,6 +563,11 @@ registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagno
Doctor validates the platform, architecture, Ghidra 12.1.x application version,
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, the installation's
Java major range, 64-bit JDK bitness, and the presence of `javac`/`javac.exe`.
+It also checks that the reported JVM `os.arch` and `os.name` map to the same native decompiler
+directory validated for the host; unrecognized reported architectures or a
+non-host operating system are refused. LaunchSupport overrides of either
+property, including through helper-environment JVM option variables, are
+rejected at launch.
When Java is found through `PATH`, setup records its observed JDK home so GUI
MCP clients do not depend on an incidental shell path. Setup shows every exact
environment entry in its plan, writes only after approval, and never downloads,
@@ -764,6 +776,9 @@ errors expose Error names, messages, and available codes under
truncation markers. Darwin ownership-helper compilation failures also retain
the compiler outcome, exit code or signal, and bounded stderr; a timed-out
compiler remains distinguishable from a compiler that exited with an error.
+The helper compile deadline defaults to 120,000 ms; set
+`REA_SWIFTC_COMPILE_TIMEOUT_MS` to an integer between 1 and 2,147,483,647 when
+a busy host needs longer.
Other rejection values retain their primitive value or an explicit type.
Shutdown warnings include the failure
kind, message, and diagnostics, while process and temporary-project cleanup
diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md
index 1d1a579e6..05782b4a2 100644
--- a/docs/managed-code-analysis.md
+++ b/docs/managed-code-analysis.md
@@ -139,14 +139,40 @@ The result preserves the requested names in `selection.type_names` and reports
unmatched names separately. If metadata names are unreadable, limitations retain
that uncertainty. With no selection, `selection` is null and whole-assembly
inspection keeps its existing resource policy. Selection restricts types,
-fields, methods, call edges and field accesses; member references remain an
-assembly-wide inventory, so output size is not guaranteed to be small.
+fields, methods, call edges, field accesses and user strings; member references
+remain an assembly-wide inventory, so output size is not guaranteed to be small.
Coverage describes the selected members. Comparisons leave members outside the
opposite observation's selection unknown, and application graphs report their
selected scope as partial. Neither makes an absence claim about the whole
assembly. Older member Evidence without `selection` remains readable.
+### User strings
+
+`ldstr` anchors keep their build-local `0x70` tokens. `user_strings.literals`
+lists each distinct token loaded by an inspected method body once, in token
+order, with its `#US` file offset and decoded value. Values keep their stored
+UTF-16 code units, including unpaired surrogates. Token `0x70000000` names the
+heap's empty entry at offset 0 and reads as `""`; any other item there is
+malformed. The terminal flag is checked against ECMA-335 II.24.2.4, and the
+length prefix against the width II.23.2 assigns. A difference is reported with
+its byte offset as a user-string issue while preserving the readable literal.
+Toolchain conventions can differ; this conformance observation does not
+establish runtime rejection. A missing `#US` stream is one stream-level issue;
+a malformed item is reported against its token. User strings carry their own
+coverage: any of these makes `user_strings.state` partial and lists the issue
+in `user_strings.issues`, while the member `coverage` and `metadata.status` are
+unaffected, and readable literals remain. The state is also partial when an
+inspected method's CIL was not fully decoded (a malformed exception section
+alone does not count) or a method that may belong to the inspected types was
+not inspected (its row could not be read, or its type's name could not be read
+under a `type_names` selection), because `ldstr` instructions there were not
+observed. Normalized CIL digests do not include decoded values.
+`compare_managed_members` does not decode or compare literals: an unchanged
+method does not establish unchanged strings, and the comparison says so in its
+limitations. Older member Evidence without `user_strings` reads as `null`: not
+examined.
+
### Edge target declaring types
`member_refs[].parent_name`, `call_edges[].target_declaring_type`, and
@@ -183,21 +209,34 @@ native/runtime questions retain their own authority and coverage.
Names and semantic labels do not establish durable member identity. Comparison
preserves observed differences, structural inferences, and unknowns separately.
-Cross-version matching first prefers exact CIL/signature identity. When that
-does not match, it pairs an exact declared type, method name, and raw signature
-before trying structural body shape. The exact-signature key uses names only
-as part of that full tuple; names alone never select a pair. Duplicate tuples
-remain ambiguous rather than being paired by token order. Fields follow the
-same exact tiers. The raw signature is exact whether or not REA decoded it, so
-undecoded signatures pair by that tuple too, but they never enter structural
-rounds. An unmatched member is therefore reported as `unknown`, not added or
-removed, when its own signature was not decoded or when the other side has an
-unpaired member, one-sided or ambiguous, with the same declared type and name
-whose signature was not decoded.
+Cross-version matching first pairs members of the same declared type and name:
+methods by exact CIL/signature identity, then by raw signature, then by
+structural body shape; fields by raw signature. Exact CIL/signature identity
+requires equal raw CIL digests and decoded fingerprints. Equal decoded
+fingerprints alone cannot establish an exact match: they omit switch targets
+and collapse distinct NaN encodings. Only then may a remap pair a
+renamed or moved member across declared types or names: a method by exact
+CIL/signature identity or structural body shape, a field by exact decoded
+signature. A remap never takes a member whose own declared type and name remain
+unpaired on the other side, so a member is never taken from its own changed
+counterpart. A member whose declaring type is unresolved could belong to any
+type, so it pairs by name only when no other member shares its exact
+CIL/signature key, never by structural shape alone, and any remap group it
+joins stays ambiguous. The exact-signature key uses names only as part of that
+full tuple; names alone never select a pair. Duplicate tuples remain ambiguous
+rather than being paired by token order. The raw signature is exact whether or
+not REA decoded it, so undecoded signatures pair by that tuple too, but they
+never enter structural rounds. An unmatched member is therefore reported as
+`unknown`, not added or removed, when its own signature was not decoded or when
+the other side has an unpaired member, one-sided or ambiguous, with the same
+declared type and name whose signature was not decoded.
For a matched method, an unavailable or partial body makes body-shape facets
-unknown while preserving observed signature differences. Structural identity
-compares normalized signatures, the limited decoded-CIL tuple fingerprint,
+unknown while preserving observed signature differences. For complete bodies,
+a difference in either the raw CIL digest or the decoded fingerprint is reported
+in the `cil` dimension, including reordered switch targets with equal decoded
+fingerprints. Structural identity compares normalized signatures, the limited
+decoded-CIL tuple fingerprint,
constants, and bounded shape context. It reports all candidates at the winning
score and remains ambiguous when the evidence does not distinguish them. The
digest does not itself remap metadata tokens.
@@ -334,8 +373,8 @@ Opcode names retain short/long forms and prefix opcodes. This makes the value a
reproducible decoded-tuple fingerprint, not a canonical semantic CIL identity:
it does not resolve tokens across MVIDs, does not commit full control flow or
exception semantics, and does not replace the raw CIL digest. Cross-build
-comparison uses the value only as one unique-only structural tier and reports
-ambiguity when that evidence is insufficient.
+comparison requires equal raw CIL digests as well for exact CIL/signature
+matches and reports ambiguity when structural evidence is insufficient.
For example, a decoded `nop; ret` body serializes exactly as
`[["nop","none",null],["ret","none",null]]` and has digest
diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md
index 876a30f6d..557c98ab4 100644
--- a/docs/mcp-contracts.md
+++ b/docs/mcp-contracts.md
@@ -21,6 +21,31 @@ directly to `compare_functions`, and `inspect_artifact` Evidence to
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
retained session history or an explicit bundle for transfer.
+Artifact inventories declare `coverage.scope` (`root` or `container-entries`)
+and `coverage.status` (`complete` or `unknown`) independently of diagnostic
+wording. Comparison requires matching scopes, complete graph members and complete
+traversal coverage before concluding equality. A fully observed root without
+container children can be `unchanged`; its scope notice stays in `limitations`
+without creating a gap. Content and executable metadata changes still produce
+root deltas. Missing graph members yield `truncated`; unknown traversal coverage
+or unavailable occurrence hashes retain uncertainty. Integrity mismatches remain
+contradictions. Static artifact equality does not establish unchanged runtime
+behavior.
+
+Exhausted traversals with deliberately omitted filesystem entries retain
+`unknown` coverage and identify each omitted path/type in `limitations`; FIFO,
+socket and device entries are not opened. Case-distinct archive members remain
+separate observations. Their case-insensitive materialization diagnostic does
+not reduce traversal coverage when both spellings were read.
+
+Current output also retains `coverage_status` (`complete` or `partial`) for existing clients, derived from the scoped traversal status. The owning parser normalizes retained `coverage_status` records to scoped coverage and rejects conflicting declarations.
+
+The coverage field is optional when reading older retained Evidence. The owning
+inventory parser recognizes historical REA scope notices for compatibility;
+unfamiliar legacy limitations remain conservative. Typed records do not derive
+coverage from limitation text. Pages from one inventory must carry the same
+coverage declaration.
+
Function dossiers can declare per-facet coverage in the optional
`normalized_result.facet_availability` map. Each supplied facet is either
`{ "available": true }` or `{ "available": false, "reason": "..." }`.
@@ -33,7 +58,13 @@ side declares a required facet unavailable. An empty legacy facet without a
coverage declaration is also unknown. A producer that actually observed an
empty facet can declare it available so observed absence remains comparable.
Call comparison additionally retains the Evidence relationship coverage
-checks. IDA declares the facets its selected adapter cannot provide; an empty
+checks. Ghidra and Hopper declare their observed dossier facets and complete
+provider-observed caller/callee sets, including empty sets. Missing decompiler
+output remains unavailable; complete call coverage does not claim every runtime
+target. Hopper's unavailable reference-kind metadata and reported unresolved
+calls still prevent equality conclusions for those dimensions. Analysis profiles
+version these declarations so older snapshots are not reused as current results.
+IDA declares the facets its selected adapter cannot provide; an empty
unsupported array does not prove observed absence.
Evidence and bundle imports reject unsupported fields in their envelopes,
identities, manifests and other fixed metadata before authentication. Imports
@@ -474,6 +505,7 @@ These deadlines have different owners:
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
| Hopper startup | REA allows 120,000 ms by default; `REA_HOPPER_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. Startup failure is reported by the first provider query. |
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid supplied values fail configuration validation. Startup failure is reported by the first provider query. |
+| Darwin swiftc helper | REA allows 120,000 ms by default for ownership-helper compilation; `REA_SWIFTC_COMPILE_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. |
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
For an already connected client using the pinned SDK, request options are the
diff --git a/docs/readme/README_ar.md b/docs/readme/README_ar.md
index 8df2522bc..64e963c91 100644
--- a/docs/readme/README_ar.md
+++ b/docs/readme/README_ar.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: هندسة عكسية لأي شيء
@@ -19,10 +20,14 @@
**[الموقع](https://rea.tools/ar/) · [الأدلة](https://rea.tools/ar/guides/) · [دراسات الحالة](https://rea.tools/ar/showcase/)**
-[البدء السريع](#البدء-السريع) · [كيف يعمل REA](#كيف-يعمل-rea) · [ما الذي يمكنك تحليله](#ما-الذي-يمكنك-تحليله) · [دراسات الحالة](#دراسات-الحالة) · [الأسئلة الشائعة](#الأسئلة-الشائعة) · [التوثيق](#التوثيق)
+[البدء السريع](#البدء-السريع) · [كيف يعمل REA](#كيف-يعمل-rea) · [ما الذي يمكنك تحليله](#ما-الذي-يمكنك-تحليله) · [دراسات الحالة](#دراسات-الحالة) · [الأسئلة الشائعة](#الأسئلة-الشائعة) · [أدوات GNU objdump وRizin](../reverse-engineering-tools.md) · [التوثيق](#التوثيق)
npx rea-agents setup
+> [!IMPORTANT]
+> المصادر الرسمية لـ REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> استخدم روابط التثبيت والإصدارات المنشورة في هذه المصادر الرسمية. تشابه اسم المستودع أو نسخ ملف README لا يثبت أن التنزيل رسمي.
+
@@ -135,6 +140,7 @@ npx rea-agents@latest setup
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/ar/guides/native/) |
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
+| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/ar/guides/javascript/) |
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/ar/guides/browser/) |
diff --git a/docs/readme/README_de.md b/docs/readme/README_de.md
index 2dbb1d41a..aee634f06 100644
--- a/docs/readme/README_de.md
+++ b/docs/readme/README_de.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Alles reverse-engineeren
@@ -19,10 +20,14 @@
**[Website](https://rea.tools/de/) · [Anleitungen](https://rea.tools/de/guides/) · [Fallstudien](https://rea.tools/de/showcase/)**
-[Schnellstart](#schnellstart) · [So funktioniert REA](#so-funktioniert-rea) · [Was du analysieren kannst](#was-du-analysieren-kannst) · [Fallstudien](#fallstudien) · [Häufige Fragen](#häufige-fragen) · [Dokumentation](#dokumentation)
+[Schnellstart](#schnellstart) · [So funktioniert REA](#so-funktioniert-rea) · [Was du analysieren kannst](#was-du-analysieren-kannst) · [Fallstudien](#fallstudien) · [Häufige Fragen](#häufige-fragen) · [GNU-objdump- und Rizin-Tools](../reverse-engineering-tools.md) · [Dokumentation](#dokumentation)
npx rea-agents setup
+> [!IMPORTANT]
+> Offizielle Quellen für REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Folge den Installations- und Release-Links aus diesen offiziellen Quellen. Ein ähnlicher Repository-Name oder eine kopierte README belegt nicht, dass ein Download offiziell ist.
+
@@ -135,6 +140,7 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](https://rea.tools/de/guides/native/) |
| 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) |
| 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) |
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](../recorded-crashes.md) |
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](https://rea.tools/de/guides/javascript/) |
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/de/guides/browser/) |
diff --git a/docs/readme/README_el.md b/docs/readme/README_el.md
new file mode 100644
index 000000000..699a3ebdc
--- /dev/null
+++ b/docs/readme/README_el.md
@@ -0,0 +1,316 @@
+
+
+[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md) · **Ελληνικά**
+
+# REA: Reverse Engineer Anything
+
+### Ένα MCP για αντίστροφη μηχανική δυαδικών αρχείων, εφαρμογών και της συμπεριφοράς τους κατά την εκτέλεση.
+
+**Βρήκες μια λειτουργία που σου αρέσει; Μάθε πώς λειτουργεί, μέχρι το επίπεδο του δυαδικού κώδικα.**
+
+[](https://www.npmjs.com/package/rea-agents)
+[](../mcp-contracts.md#generated-catalog)
+[](#τι-μπορείς-να-αναλύσεις)
+[](https://skills.sh/morluto/rea/reverse-engineer-anything)
+[](../../LICENSE)
+[](https://discord.gg/GkcryMnJDM)
+
+
+
+**[Ιστότοπος](https://rea.tools/) · [Οδηγοί](https://rea.tools/guides/) · [Μελέτες περίπτωσης](https://rea.tools/showcase/)**
+
+[Γρήγορη εκκίνηση](#γρήγορη-εκκίνηση) · [Πώς λειτουργεί το REA](#πώς-λειτουργεί-το-rea) · [Τι μπορείς να αναλύσεις](#τι-μπορείς-να-αναλύσεις) · [Μελέτες περίπτωσης](#μελέτες-περίπτωσης) · [Συχνές ερωτήσεις](#συχνές-ερωτήσεις) · [Τεκμηρίωση](#τεκμηρίωση)
+
+
npx rea-agents setup
+
+> [!IMPORTANT]
+> Κατέβασε το REA από το [morluto/rea](https://github.com/morluto/rea),
+> το [rea.tools](https://rea.tools/) ή το [πακέτο rea-agents στο npm](https://www.npmjs.com/package/rea-agents).
+> Ακολούθησε τους συνδέσμους εγκατάστασης και εκδόσεων από αυτές τις επίσημες πηγές.
+> Ένα παρόμοιο όνομα αποθετηρίου ή ένα αντιγραμμένο README δεν αποδεικνύει ότι
+> ένα αρχείο προς λήψη προέρχεται από επίσημη πηγή.
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+Είδες σε μια εφαρμογή μια λειτουργία που θα ήθελες στο δικό σου προϊόν; Ζήτησε από τον πράκτορά σου (agent) να τη διερευνήσει με το REA. Μπορεί να εξετάσει την εφαρμογή χωρίς τον πηγαίο κώδικά της, να εξηγήσει πώς λειτουργεί η συγκεκριμένη δυνατότητα, να παρουσιάσει τα τεκμήρια και να υλοποιήσει μια αντίστοιχη έκδοση για το έργο σου.
+
+Το REA συνδέει τον πράκτορά σου με εργαλεία εξέτασης δυαδικών αρχείων με κώδικα μηχανής, εφαρμογών JavaScript και Electron, assemblies .NET και ιστοτόπων. Μπορείς να χρησιμοποιήσεις τα ίδια εργαλεία και από το τερματικό. Η ανάλυση εκτελείται τοπικά και τα αποτελέσματα περιλαμβάνουν τα τεκμήρια και τους περιορισμούς στους οποίους βασίζεται κάθε συμπέρασμα.
+
+Η διαδικασία εγκατάστασης καταχωρίζει το REA στον agent σου και εγκαθιστά τις αντίστοιχες οδηγίες εργασίας. Για ανάλυση κώδικα μηχανής μπορείς να χρησιμοποιήσεις μια υπάρχουσα εγκατάσταση του Hopper, του Ghidra ή του IDA. Η διαδικασία εγκατάστασης μπορεί επίσης να εγκαταστήσει το Hopper, εφόσον το εγκρίνεις. Η στατική ανάλυση JavaScript δεν χρειάζεται μηχανή ανάλυσης κώδικα μηχανής.
+
+> **[Επισκέψου τον ιστότοπο του REA](https://rea.tools/)** για οδηγίες εγκατάστασης, εικονογραφημένους οδηγούς και πραγματικές μελέτες περίπτωσης.
+
+## Γρήγορη εκκίνηση
+
+### Ρύθμιση του agent σου
+
+Αφού εγκαταστήσεις το Node.js και το npm, εκτέλεσε:
+
+```bash
+npx rea-agents setup
+```
+
+Επίλεξε τους agents σου, έλεγξε τις προτεινόμενες αλλαγές και ενέκρινέ τες. Η διαδικασία προσθέτει τον MCP server του REA και τις αντίστοιχες οδηγίες εργασίας, κρατώντας αντίγραφα ασφαλείας των υφιστάμενων ρυθμίσεων. Στη συνέχεια, επανεκκίνησε τον agent σου.
+
+Υποστηρίζονται τα Claude Code, Codex, Cursor, Gemini CLI, Grok Build και [άλλοι agents](../installation.md#supported-agents). Για τη ρύθμιση των παρόχων ανάλυσης και τη μη αυτόματη καταχώριση MCP, δες τον οδηγό [εγκατάστασης και ρύθμισης](../installation.md).
+
+### Ζήτησε από τον agent σου
+
+```text
+Διερεύνησε πώς λειτουργεί η αναζήτηση στην εφαρμογή Notes, δείξε μου τα τεκμήρια και υλοποίησε μια παρόμοια λειτουργία για το έργο μου.
+```
+
+Αντικατάστησε το Notes με την εφαρμογή που θέλεις να εξετάσεις και προσδιόρισε τη λειτουργία που θέλεις να κατανοήσεις.
+
+### Χρήση από το τερματικό
+
+Εξέτασε τον αποσυμπιεσμένο φάκελο μιας εφαρμογής JavaScript/Electron ή ένα αρχείο ASAR:
+
+```bash
+npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
+```
+
+Το αποτέλεσμα περιλαμβάνει modules, εισαγωγές, τα όρια μεταξύ των μερών της εφαρμογής Electron και τα αντίστοιχα τεκμήρια. Αντικατάστησε τη διαδρομή με τη δική σου, για παράδειγμα `"D:/apps/example"` στα Windows.
+
+Για να εγκαταστήσεις την εντολή `rea` για τακτική χρήση:
+
+```bash
+npm install --global rea-agents
+rea --help
+```
+
+Για ανάλυση κώδικα μηχανής, ρύθμισε πρώτα έναν πάροχο ανάλυσης. Ο [οδηγός CLI και Evidence](../cli.md) περιγράφει τις σχετικές εντολές, την επιλογή παρόχου, τα στιγμιότυπα και τη χρήση σε scripts.
+
+### Ενημέρωση του REA
+
+Το REA εξελίσσεται γρήγορα και οι νέες εκδόσεις περιλαμβάνουν συχνές διορθώσεις σφαλμάτων. Διατήρησε την εγκατάστασή σου ενημερωμένη.
+
+Για CLI εγκατεστημένο με `npm install --global` ή `bun add --global`:
+
+```bash
+rea update
+```
+
+Οι ενημερώσεις καθολικής εγκατάστασης με Bun απαιτούν Bun 1.2.17 ή νεότερο. Αν χρησιμοποιείς παλαιότερη έκδοση, αναβάθμισε το Bun χειροκίνητα πριν εκτελέσεις το `rea update`.
+
+Για να ανανεώσεις τις καταχωρίσεις των agents και το skill, εκτέλεσε την εντολή ρύθμισης που εμφανίζει η ενημέρωση.
+
+Αν χρησιμοποιείς `npx`, ενημέρωσε τη ρύθμιση του agent με:
+
+```bash
+npx rea-agents@latest setup
+```
+
+Έλεγξε τις αλλαγές και επανεκκίνησε τον agent σου. Για μεμονωμένες εντολές CLI, χρησιμοποίησε `npx rea-agents@latest` και στη συνέχεια την εντολή που θέλεις. Με Bun, χρησιμοποίησε `bunx rea-agents@latest` αντί για `npx rea-agents@latest`.
+
+## Πώς λειτουργεί το REA
+
+Ο agent σου καλεί το REA μέσω MCP για να εξετάσει τον στόχο και να ακολουθήσει τον σχετικό κώδικα. Το REA επιστρέφει ευρήματα μαζί με τα τεκμήριά τους. Ο agent τα χρησιμοποιεί για να θέσει πρόσθετες ερωτήσεις, να εξηγήσει τη συμπεριφορά ή να γράψει και να δοκιμάσει μια υλοποίηση. Οι εντολές CLI χρησιμοποιούν τις ίδιες ροές εργασίας.
+
+
+
+[Άνοιγμα του διαγράμματος σε πλήρες μέγεθος](../../website/public/assets/figures/rea-investigation-flow.svg).
+
+
+
+## Τι μπορείς να αναλύσεις
+
+Το REA απαιτεί Node.js 22.x (>=22.19), 24.x (>=24.11) ή 26+, καθώς και npm. Τα πρόσθετα εργαλεία και τα υποστηριζόμενα συστήματα εξαρτώνται από τον στόχο:
+
+| Στόχος | Τι επιστρέφει το REA | Απαιτήσεις και οδηγός |
+| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
+| Δυαδικά αρχεία με κώδικα μηχανής | Ψευδοκώδικα, κώδικα assembly, συμβολοσειρές, σύμβολα, κλήσεις και αναφορές | Hopper, Ghidra ή IDA· [ανάλυση κώδικα μηχανής](https://rea.tools/guides/native/) |
+| Δομή ELF χωρίς εκτέλεση | Ενότητες, τμήματα, αρχικά σύμβολα/εγγραφές μετεγκατάστασης και ενδείξεις πιθανών μέτρων προστασίας από στατική ανάλυση | pwntools που παρέχει ο χρήστης σε Linux x64· [διαγνωστικά δυαδικών αρχείων](../binary-diagnostics.md) |
+| Δυαδικά αρχεία Go | Ενσωματωμένη έκδοση μεταγλωττιστή, modules, αντικαταστάσεις και ρυθμίσεις μεταγλώττισης | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· δεν απαιτείται εγκατάσταση του Go |
+| Δυαδικά αρχεία Rust | Ενσωματωμένα ονόματα, εκδόσεις, πηγές και σχέσεις εξάρτησης των crates από το cargo-auditable | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· [δυαδικά αρχεία Rust](../rust-binaries.md) |
+| Αρχείο WASM | Έλεγχο εγκυρότητας, ενότητες, ακριβείς μορφές εισαγωγών/εξαγωγών σε WAT και αποκωδικοποιημένο WAT | WABT 1.0.42 που παρέχει ο χρήστης· `inspect_wasm_artifact` |
+| Bytecode EVM | Selectors επιλογής συνάρτησης, μετατοπίσεις σε bytes, εκτιμώμενα ορίσματα και δυνατότητα μεταβολής της κατάστασης | Τοπική είσοδος σε ακατέργαστα bytes ή δεκαεξαδική μορφή σε Linux x64· [οδηγός EVM χωρίς εκτέλεση](../evm-bytecode.md) |
+| Βάσεις δεδομένων SQLite | Πίνακες, στήλες, ευρετήρια, όψεις, triggers και επιλεγμένες γραμμές κανονικών πινάκων | Τοπικό στιγμιότυπο βάσης δεδομένων/WAL· Node.js 24.x >=24.15 ή 26+ με εγγενή όρια SQLite |
+| Καταγεγραμμένες καταρρεύσεις στο Linux | Ακατέργαστες εγγραφές notes, καταχωρητές/σήματα κάθε καταγεγραμμένου νήματος και προαιρετικές πιθανές αντιστοιχίσεις μνήμης | pwntools που παρέχει ο χρήστης σε Linux x64· προαιρετικά GDB/pwndbg· [καταγεγραμμένες καταρρεύσεις](../recorded-crashes.md) |
+| JavaScript / Electron | Modules, εισαγωγές, source maps, διαδρομές, IPC και σχέσεις με πρόσθετα κώδικα μηχανής | Node.js και npm· [ανάλυση εφαρμογών](https://rea.tools/guides/javascript/) |
+| Ιστότοποι | Δομή σελίδων, scripts, παρατηρήσεις δικτύου και ζητούμενα στιγμιότυπα οθόνης | Πρόγραμμα περιήγησης της οικογένειας Chrome· [ανάλυση μέσω προγράμματος περιήγησης](https://rea.tools/guides/browser/) |
+| Αποθηκευμένες καταγραφές δικτύου | Αιτήματα, αποκρίσεις, διαθέσιμα δεδομένα φορτίου και θέσεις στην πηγή | HAR ή SAZ του Fiddler Everywhere· mitmdump σε Linux για την εγγενή μορφή mitmproxy· [οδηγός καταγραφών](../web-network-captures.md) |
+| Assemblies .NET | Μεταδεδομένα, εντολές CIL, δηλωμένες εξαρτήσεις από κώδικα μηχανής και συγκρίσεις builds | Καμία πρόσθετη απαίτηση (μόνο στατική ανάλυση)· [οδηγός διαχειριζόμενου κώδικα](../managed-code-analysis.md) |
+| APK Android | Δηλώσεις manifest, κλάσεις, απομεταγλωττισμένες μέθοδοι και αναφορές | JADX χωρίς γραφικό περιβάλλον και πλήρες JDK σε Linux/macOS/Windows x64· [οδηγός Android](../android-analysis.md) |
+| Συσκευές Android | Συσκευές, πακέτα, διεργασίες, logs, dumps, στιγμιότυπα οθόνης και μεταφορές APK/αρχείων | adb που παρέχει ο χρήστης (εξομοιωτής ή φυσική συσκευή)· [οδηγός ADB](../adb-device-analysis.md) |
+| Έργα JEB | Μονάδες έργου από τη μηχανή ανάλυσης, ψηφιακά αποτυπώματα αρχείων και ψευδοκώδικα τύπων/μεθόδων | Client JEB που έχει ξεκινήσει ο χρήστης και παρέχει MCP, επιλεγμένος μέσω REA_JEB_MCP_URL· [οδηγός JEB](../jeb-analysis.md) |
+| Πόροι Android | Αποκωδικοποιημένο manifest, πίνακες συμβολοσειρών, τοπικές ρυθμίσεις και στοιχεία έκδοσης | Launcher του Apktool που παρέχει ο χρήστης· [οδηγός πόρων](../apktool-resource-analysis.md) |
+| Εφαρμογές Flutter | Hash του στιγμιότυπου Dart, ψηφιακά αποτυπώματα βιβλιοθηκών ανά ABI και στοιχεία προέλευσης του build της μηχανής | Καμία πρόσθετη απαίτηση (στατική ανάλυση APK)· [οδηγός Flutter](../flutter-analysis.md) |
+| Firmware | Περιοχές, αποτελέσματα εξαγωγής και παραπομπές για ανάλυση κώδικα μηχανής | Binwalk / Unblob σε Linux· [οδηγός firmware](../firmware-analysis.md) |
+| Καταγεγραμμένα πακέτα | Επιλεγμένα πεδία, επαναλαμβανόμενες τιμές, φίλτρα και κανόνες decode-as | Linux: TShark 4.x + prlimit· [οδηγός καταγραφών](../packet-captures.md) |
+| Σύνοψη πακέτων | Τερματικά σημεία, DNS, TLS SNI, κατανομή πρωτοκόλλων και αιτήματα HTTP | tshark και capinfos που παρέχει ο χρήστης (επαληθευμένα σε Linux)· [οδηγός pcap](../pcap-analysis.md) |
+| Καταγεγραμμένα λογικά σήματα | Σχολιασμούς αποκωδικοποιητή, εύρη δειγμάτων και διαγνωστικά | Linux: sigrok-cli 0.7.2 + prlimit· [καταγραφές σημάτων](../signal-recordings.md) |
+| Πακέτα και πόροι | Καταλόγους αρχείων, ψηφιακά αποτυπώματα, plists, δομή bundles της Apple και εξαγόμενους πόρους | [Οδηγός αρχείων και JavaScript](../javascript-artifact-reconstruction.md), [εφαρμογές Apple](../apple-application-analysis.md) |
+| Συμπεριφορά διεργασιών | Έξοδο τερματικού, αλληλεπιδράσεις, παρατηρήσεις τερματισμού και συστήματος αρχείων, καθώς και συγκρίσεις εκτελέσεων | Linux/macOS με εγγενές PTY· [καταγραφή διεργασιών](../process-capture.md) |
+
+Για εξέταση δυαδικών αρχείων με κώδικα μηχανής χωρίς εκτέλεση, το REA υποστηρίζει
+επίσης GNU objdump και Rizin που παρέχει ο χρήστης. Δες τον
+[οδηγό εργαλείων αντίστροφης μηχανικής](../reverse-engineering-tools.md)
+για τη ρύθμιση των παρόχων, τις εντολές CLI και τους περιορισμούς των Evidence.
+Το REA δεν εγκαθιστά ούτε ενημερώνει κανέναν από τους δύο παρόχους.
+
+Η στατική εξέταση JavaScript και .NET διαβάζει τα αρχεία που παρέχονται χωρίς να εκτελεί την εφαρμογή. Η καταγραφή κατά την εκτέλεση εκτελεί τον επιλεγμένο στόχο ή αλληλεπιδρά μαζί του με τα δικαιώματα του χρήστη σου. Κάθε σχετικός οδηγός περιγράφει τις επιπτώσεις της.
+
+
+
+Οι μορφές κώδικα μηχανής και τα υποστηριζόμενα συστήματα διαφέρουν ανά πάροχο ανάλυσης. Δες τη [ρύθμιση Hopper και Ghidra](../installation.md#hopper), τον [οδηγό IDA](../ida-provider.md) (που περιλαμβάνει προαιρετική γέφυρα MCP για κώδικα μηχανής) και την [πειραματική υποστήριξη Ghidra στα Windows](../windows-ghidra-p0.md). Το Ghidra υποστηρίζει επίσης [ανάλυση DOS 16 bit](../ghidra-dos.md). Για μεγάλα δυαδικά αρχεία, αύξησε το χρονικό όριο εκκίνησης του παρόχου με `REA_GHIDRA_STARTUP_TIMEOUT_MS` ή `REA_HOPPER_STARTUP_TIMEOUT_MS`. Για την επιλογή παρόχου, δες τον [οδηγό CLI](../cli.md#choose-a-provider). Έλεγξε τη [διαθεσιμότητα ανά έκδοση](../installation.md#released-package-and-main) για λειτουργίες που προστέθηκαν μετά την τελευταία έκδοση στο npm.
+
+## Μελέτες περίπτωσης
+
+[](https://rea.tools/showcase/)
+
+### DX-Ball: ανακατασκευή υπολογισμού στερεοφωνικής τοποθέτησης ήχου
+
+Ακολούθησε μια κλήση ήχου μέχρι τη βοηθητική συνάρτηση που μετατρέπει τη θέση σε στερεοφωνική τοποθέτηση, εξέτασε τις εντολές και μετέτρεψε τον ελλιπή ψευδοκώδικα σε C. Η ανακατασκευή περνά 3.205 δοκιμές με τον αρχικό κώδικα x86 και αναπαράγει και τα 63 bytes της μεταγλωττισμένης συνάρτησης.
+
+[Διάβασε τη μελέτη περίπτωσης](https://rea.tools/showcase/dx-ball/) · [Αποθετήριο ανακατασκευής](https://github.com/N0zoM1z0/dx-ball)
+
+### Notion: ιχνηλάτηση της γέφυρας προχείρου στο Electron
+
+Βρες το API προχείρου του renderer, ακολούθησέ το μέσω του preload και του IPC μέχρι την κύρια διεργασία και εξέτασε τη μορφή εμπλουτισμένου περιεχομένου του προχείρου.
+
+[Διάβασε τη μελέτη περίπτωσης](https://rea.tools/showcase/notion/)
+
+### TH04: ανάκτηση υπολογισμού δακτυλίου βλημάτων σε DOS
+
+Εξέτασε τις εντολές 16 bit του αρχικού παιχνιδιού για PC-98, ανέκτησε τους υπολογισμούς σταθερών γωνιών και γωνιών στόχευσης και σύγκρινε τον ανακατασκευασμένο κώδικα C++ με την έξοδο του μεταγλωττιστή της εποχής.
+
+[Διάβασε τη μελέτη περίπτωσης](https://rea.tools/showcase/th04/) · [Αποθετήριο ανακατασκευής](https://github.com/N0zoM1z0/th04)
+
+Αν χρησιμοποίησες το REA για κάτι ενδιαφέρον, θα θέλαμε να το δούμε. Μοιράσου τη μελέτη σου σε ένα [issue](https://github.com/morluto/rea/issues) ή [pull request](https://github.com/morluto/rea/pulls), αναφέροντας τον στόχο, το ερώτημά σου, πώς βοήθησε το REA και τι ανακάλυψες.
+
+## Συχνές ερωτήσεις
+
+
+Ποιοι agents μπορούν να χρησιμοποιήσουν το REA;
+
+Κάθε agent που υποστηρίζει τοπικούς MCP servers. Η διαδικασία εγκατάστασης ρυθμίζει τους [υποστηριζόμενους agents](../installation.md#supported-agents). Οι υπόλοιποι clients μπορούν να χρησιμοποιήσουν τη [μη αυτόματη καταχώριση MCP](../installation.md#mcp-registry).
+
+
+
+
+Χρειάζομαι Hopper, Ghidra ή IDA;
+
+Η εις βάθος ανάλυση κώδικα μηχανής χρησιμοποιεί ένα από αυτά. Η στατική εξέταση JavaScript και .NET λειτουργεί χωρίς μηχανή ανάλυσης κώδικα μηχανής. Η διαδικασία εγκατάστασης μπορεί να εγκαταστήσει το Hopper μετά την έγκρισή σου· το Ghidra και το IDA χρησιμοποιούν τις υπάρχουσες εγκαταστάσεις σου. Δες τη [ρύθμιση παρόχων ανάλυσης](../installation.md#hopper).
+
+
+
+
+Πρέπει να ξεκινήσω πρώτα το Hopper;
+
+Το REA ξεκινά το Hopper όταν το χρειάζεται κάποια λειτουργία. Στο macOS, κατά την πρώτη εκκίνηση μπορεί να εμφανιστεί παράθυρο που ζητά να επιλέξεις τη δοκιμαστική λειτουργία ή να ενεργοποιήσεις την άδειά σου. Ολοκλήρωσε αυτό το βήμα στην ίδια συνεδρία χρήστη πριν από την πρώτη λειτουργία CLI/MCP ή εργασία χωρίς επίβλεψη. Άνοιξε το Hopper χειροκίνητα αν χρειαστεί. Δες την [εκκίνηση και αντιμετώπιση προβλημάτων του Hopper](../installation.md#launcher-paths-and-troubleshooting).
+
+
+
+
+Τι προσφέρει η εγκατάσταση του skill από το skills.sh;
+
+Το skill παρέχει οδηγίες διερεύνησης για τον agent σου. Χρησιμοποίησε `npx rea-agents setup` για να καταχωρίσεις τον MCP server του REA και να εγκαταστήσεις τις αντίστοιχες οδηγίες. Στη συνέχεια, επανεκκίνησε τον agent σου. Δες την [εγκατάσταση μόνο του skill](../installation.md#skill-only-installation).
+
+
+
+
+Τι κώδικα επιστρέφει το REA;
+
+Η ανάλυση κώδικα μηχανής επιστρέφει ψευδοκώδικα και assembly. Η ανάλυση JavaScript/Electron ανακτά modules και τις σχέσεις τους. Ο agent σου χρησιμοποιεί αυτά τα ευρήματα για να γράψει και να δοκιμάσει μια υλοποίηση. Οι [μελέτες περίπτωσης](#μελέτες-περίπτωσης) παρουσιάζουν ολοκληρωμένα παραδείγματα.
+
+
+
+
+Το REA ανεβάζει την εφαρμογή μου στο διαδίκτυο;
+
+Το REA αναλύει τους στόχους τοπικά. Ο agent σου λαμβάνει τα αποτελέσματα των εργαλείων και ο πάροχος του μοντέλου του έχει τη δική του πολιτική δεδομένων.
+
+
+
+
+Τι να κάνω αν συναντήσω κάποιο σφάλμα;
+
+Κάνε πρώτα ενημέρωση· μια πρόσφατη έκδοση μπορεί να το έχει ήδη διορθώσει.
+
+Για CLI εγκατεστημένο με `npm install --global` ή `bun add --global`:
+
+```bash
+rea update
+```
+
+Για ρύθμιση agent μέσω `npx`:
+
+```bash
+npx rea-agents@latest setup
+```
+
+Αν χρησιμοποιείς agent, ολοκλήρωσε την [ανανέωση της ρύθμισης](#ενημέρωση-του-rea) και επανεκκίνησέ τον. Δοκίμασε ξανά την ίδια εργασία. Αν το πρόβλημα παραμένει, [άνοιξε ένα issue](https://github.com/morluto/rea/issues) με την έκδοση του REA, τον τύπο του στόχου, τα βήματα αναπαραγωγής και την έξοδο του σφάλματος.
+
+
+
+## Τεκμηρίωση
+
+Ξεκίνα από τους [οδηγούς με ολοκληρωμένα παραδείγματα](https://rea.tools/guides/) στον ιστότοπο. Για τις ακριβείς επιλογές, τις προϋποθέσεις και τις προδιαγραφές των αποτελεσμάτων:
+
+- [Εγκατάσταση και ρύθμιση](../installation.md): καταχώριση agents, ρύθμιση παρόχων ανάλυσης, ενημερώσεις και απεγκατάσταση.
+- [Έλεγχος ετοιμότητας και αντιμετώπιση προβλημάτων](../installation.md#check-readiness-for-your-task): διάγνωση ενός agent ή μιας μηχανής ανάλυσης.
+- [CLI και Evidence](../cli.md): εντολές, επιλογή παρόχου, στιγμιότυπα, εισαγωγή/εξαγωγή και κωδικοί εξόδου.
+- [Προδιαγραφές MCP](../mcp-contracts.md) και [prompts για agents](../mcp-prompts.md): αποτελέσματα εργαλείων, συνεδρίες και καθοδηγούμενες διερευνήσεις.
+- [Κατάλογος εργαλείων](../mcp-contracts.md#generated-catalog): αυτόματα παραγόμενος κατάλογος εργαλείων, παρόχων και εντολών CLI.
+- [Οδικός χάρτης](../roadmap.md): προγραμματισμένες εργασίες και παρακολούθηση της ανάπτυξης δυνατοτήτων.
+
+Ανάφερε ευπάθειες ακολουθώντας το [SECURITY.md](../../SECURITY.md).
+
+## Ιστορικό αστεριών
+
+🎉 **180.000 αστέρια στο GitHub — σας ευχαριστούμε!**
+
+Ευχαριστούμε όλους όσοι χρησιμοποιούν το REA, αναφέρουν σφάλματα, ζητούν νέες λειτουργίες, δοκιμάζουν builds και συνεισφέρουν διορθώσεις.
+
+
+
+
+
+
+
+
+
+## Αποποίηση ευθύνης
+
+Δες το [DISCLAIMER.md](../../DISCLAIMER.md) για οδηγίες υπεύθυνης χρήσης.
+
+Η θέση του έργου σχετικά με την παράνομη ή μη εξουσιοδοτημένη χρήση ισχύει για όλες τις εκδόσεις του REA, τα κανάλια διανομής και τις πλατφόρμες. Κάθε αντίγραφο διατηρεί τις άδειες που το διέπουν.
+
+Το REA προορίζεται για νόμιμη έρευνα, εκπαίδευση, διαλειτουργικότητα, διατήρηση λογισμικού και ανάλυση λογισμικού που σου ανήκει ή για το οποίο έχεις άδεια εξέτασης.
+
+## Συνεισφορά
+
+Θα χαρούμε να βοηθήσεις στο REA! [Άνοιξε ένα issue](https://github.com/morluto/rea/issues) για να αναφέρεις ένα σφάλμα ή να προτείνεις μια λειτουργία, ή [στείλε ένα pull request](https://github.com/morluto/rea/pulls) για να βελτιώσεις τον κώδικα ή την τεκμηρίωση.
+
+Δες το [CONTRIBUTING.md](../../CONTRIBUTING.md) για τη ρύθμιση του περιβάλλοντος ανάπτυξης και τους ελέγχους, τον [οδηγό δοκιμών](../testing.md) για τις διαδικασίες επαλήθευσης και τον [χάρτη αρχιτεκτονικής](../architecture.mermaid) για τη δομή του έργου.
+
+## Άδεια χρήσης
+
+[MIT](../../LICENSE)
+
+Ο κώδικας που έχει δημιουργηθεί για το REA διατίθεται με την άδεια MIT. Το υλικό τρίτων διατηρεί τις δικές του άδειες και δηλώσεις πνευματικών δικαιωμάτων. Δες τις [δηλώσεις για υλικό τρίτων](../../THIRD_PARTY_NOTICES.md) για τον κατάλογο εξαρτήσεων, τα διατηρημένα κείμενα αδειών και τους όρους των εξωτερικών εργαλείων.
+
+**Πρέπει να αποκτήσεις νόμιμα τα εμπορικά εργαλεία και να έχεις δικαίωμα χρήσης τους**, συμπεριλαμβανομένων των IDA Pro / Hex-Rays decompilers, Hopper και JEB, σύμφωνα με τις αντίστοιχες άδειές τους. Το REA δεν παρέχει άδεια εργαλείου, κλειδί ή δικαίωμα ενεργοποίησης. Οι δοκιμαστικές εκδόσεις και τα demos εξακολουθούν να υπόκεινται στους όρους του προμηθευτή, συμπεριλαμβανομένων των περιορισμών χρήστη, λειτουργιών, αυτοματοποίησης και κοινοποίησης αποτελεσμάτων. Δεν επιτρέπονται σπασμένες εκδόσεις, κλεμμένα κλειδιά, μη εξουσιοδοτημένη κοινοποίηση κλειδιών ή παράκαμψη περιορισμών αδειοδότησης. Αυτές οι οδηγίες δεν τροποποιούν την άδεια MIT ή τις άδειες τρίτων.
diff --git a/docs/readme/README_es.md b/docs/readme/README_es.md
index d09be25e2..07f2517ea 100644
--- a/docs/readme/README_es.md
+++ b/docs/readme/README_es.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Ingeniería inversa de cualquier cosa
@@ -19,10 +20,14 @@
**[Sitio web](https://rea.tools/es/) · [Guías](https://rea.tools/es/guides/) · [Casos prácticos](https://rea.tools/es/showcase/)**
-[Inicio rápido](#inicio-rápido) · [Cómo funciona REA](#cómo-funciona-rea) · [Qué puedes analizar](#qué-puedes-analizar) · [Casos prácticos](#casos-prácticos) · [Preguntas frecuentes](#preguntas-frecuentes) · [Documentación](#documentación)
+[Inicio rápido](#inicio-rápido) · [Cómo funciona REA](#cómo-funciona-rea) · [Qué puedes analizar](#qué-puedes-analizar) · [Casos prácticos](#casos-prácticos) · [Preguntas frecuentes](#preguntas-frecuentes) · [Herramientas GNU objdump y Rizin](../reverse-engineering-tools.md) · [Documentación](#documentación)
npx rea-agents setup
+> [!IMPORTANT]
+> Fuentes oficiales de REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Usa los enlaces de instalación y de versiones publicados en estas fuentes oficiales. Un nombre de repositorio parecido o un README copiado no demuestra que una descarga sea oficial.
+
@@ -135,6 +140,7 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](https://rea.tools/es/guides/native/) |
| 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) |
| 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) |
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](../recorded-crashes.md) |
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](https://rea.tools/es/guides/javascript/) |
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](https://rea.tools/es/guides/browser/) |
diff --git a/docs/readme/README_fa.md b/docs/readme/README_fa.md
index b24e4380c..bd129ded1 100644
--- a/docs/readme/README_fa.md
+++ b/docs/readme/README_fa.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی** · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: مهندسی معکوس هر چیزی
@@ -19,10 +20,14 @@
**[وبسایت](https://rea.tools/fa/) · [راهنماها](https://rea.tools/fa/guides/) · [نمونههای عملی](https://rea.tools/fa/showcase/)**
-[شروع سریع](#شروع-سریع) · [REA چگونه کار میکند؟](#rea-چگونه-کار-میکند) · [چه چیزهایی را میتوان تحلیل کرد؟](#چه-چیزهایی-را-میتوان-تحلیل-کرد) · [نمونههای عملی](#نمونههای-عملی) · [پرسشهای متداول](#پرسشهای-متداول) · [مستندات](#مستندات)
+[شروع سریع](#شروع-سریع) · [REA چگونه کار میکند؟](#rea-چگونه-کار-میکند) · [چه چیزهایی را میتوان تحلیل کرد؟](#چه-چیزهایی-را-میتوان-تحلیل-کرد) · [نمونههای عملی](#نمونههای-عملی) · [پرسشهای متداول](#پرسشهای-متداول) · [ابزارهای GNU objdump و Rizin](../reverse-engineering-tools.md) · [مستندات](#مستندات)
npx rea-agents setup
+> [!IMPORTANT]
+> منابع رسمی REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> از پیوندهای نصب و انتشار در این منابع رسمی استفاده کنید. نام مشابه مخزن یا README کپیشده ثابت نمیکند که فایل دانلودی رسمی است.
+
@@ -127,6 +132,7 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، بههمراه np
| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/fa/guides/native/) |
| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
| بایتکد EVM | selectorهای dispatch، offsetهای بایتی، آرگومانهای استنباطشده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
+| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](../recorded-crashes.md) |
| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/fa/guides/javascript/) |
| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/fa/guides/browser/) |
diff --git a/docs/readme/README_fr.md b/docs/readme/README_fr.md
index e6278ac93..255b15a9a 100644
--- a/docs/readme/README_fr.md
+++ b/docs/readme/README_fr.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Français** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA : rétro-ingénierie de tout
@@ -19,10 +20,14 @@
**[Site web](https://rea.tools/fr/) · [Guides](https://rea.tools/fr/guides/) · [Études de cas](https://rea.tools/fr/showcase/)**
-[Démarrage rapide](#démarrage-rapide) · [Comment fonctionne REA](#comment-fonctionne-rea) · [Ce que vous pouvez analyser](#ce-que-vous-pouvez-analyser) · [Études de cas](#études-de-cas) · [Questions fréquentes](#questions-fréquentes) · [Documentation](#documentation)
+[Démarrage rapide](#démarrage-rapide) · [Comment fonctionne REA](#comment-fonctionne-rea) · [Ce que vous pouvez analyser](#ce-que-vous-pouvez-analyser) · [Études de cas](#études-de-cas) · [Questions fréquentes](#questions-fréquentes) · [Outils GNU objdump et Rizin](../reverse-engineering-tools.md) · [Documentation](#documentation)
npx rea-agents setup
+> [!IMPORTANT]
+> Sources officielles de REA : [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Utilisez les liens d’installation et de publication de ces sources officielles. Un nom de dépôt similaire ou un README copié ne prouve pas qu’un téléchargement est officiel.
+
@@ -139,6 +144,7 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
| 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) |
| 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) |
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](../recorded-crashes.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/fr/guides/javascript/) |
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/fr/guides/browser/) |
diff --git a/docs/readme/README_id.md b/docs/readme/README_id.md
index fba947fb2..713cfc0c0 100644
--- a/docs/readme/README_id.md
+++ b/docs/readme/README_id.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · **Bahasa Indonesia** · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Rekayasa Balik Apa Pun
@@ -19,10 +20,14 @@
**[Situs Web](https://rea.tools/id/) · [Panduan](https://rea.tools/id/guides/) · [Contoh Penggunaan](https://rea.tools/id/showcase/)**
-[Mulai Cepat](#mulai-cepat) · [Cara Kerja REA](#cara-kerja-rea) · [Hal yang Dapat Anda Analisis](#hal-yang-dapat-anda-analisis) · [Contoh Penggunaan](#contoh-penggunaan) · [FAQ](#faq) · [Dokumentasi](#dokumentasi)
+[Mulai Cepat](#mulai-cepat) · [Cara Kerja REA](#cara-kerja-rea) · [Hal yang Dapat Anda Analisis](#hal-yang-dapat-anda-analisis) · [Contoh Penggunaan](#contoh-penggunaan) · [FAQ](#faq) · [Alat GNU objdump dan Rizin](../reverse-engineering-tools.md) · [Dokumentasi](#dokumentasi)
npx rea-agents setup
+> [!IMPORTANT]
+> Sumber resmi REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Gunakan tautan instalasi dan rilis dari sumber resmi ini. Nama repositori yang mirip atau README yang disalin tidak membuktikan bahwa unduhan tersebut resmi.
+
@@ -142,6 +147,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
| 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) |
| 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) |
| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](../recorded-crashes.md) |
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](https://rea.tools/id/guides/javascript/) |
| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](https://rea.tools/id/guides/browser/) |
diff --git a/docs/readme/README_it.md b/docs/readme/README_it.md
index b7e7c6100..d9b73e923 100644
--- a/docs/readme/README_it.md
+++ b/docs/readme/README_it.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · **Italiano**
+· [Ελληνικά](README_el.md)
# REA: Reverse Engineer Anything
@@ -19,10 +20,14 @@
**[Sito web](https://rea.tools/it/) · [Guide](https://rea.tools/it/guides/) · [Showcase](https://rea.tools/it/showcase/)**
-[Avvio rapido](#avvio-rapido) · [Come funziona REA](#come-funziona-rea) · [Cosa puoi analizzare](#cosa-puoi-analizzare) · [Showcase](#showcase) · [FAQ](#faq) · [Documentazione](#documentazione)
+[Avvio rapido](#avvio-rapido) · [Come funziona REA](#come-funziona-rea) · [Cosa puoi analizzare](#cosa-puoi-analizzare) · [Showcase](#showcase) · [FAQ](#faq) · [Strumenti GNU objdump e Rizin](../reverse-engineering-tools.md) · [Documentazione](#documentazione)
npx rea-agents setup
+> [!IMPORTANT]
+> Fonti ufficiali di REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Segui i link di installazione e delle versioni pubblicate da queste fonti ufficiali. Un nome di repository simile o un README copiato non dimostra che un download sia ufficiale.
+
@@ -139,6 +144,7 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](../binary-diagnostics.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) |
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](../recorded-crashes.md) |
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](https://rea.tools/it/guides/javascript/) |
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](https://rea.tools/it/guides/browser/) |
diff --git a/docs/readme/README_ja.md b/docs/readme/README_ja.md
index c9d7981ba..afad1f488 100644
--- a/docs/readme/README_ja.md
+++ b/docs/readme/README_ja.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA:あらゆるものをリバースエンジニアリング
@@ -19,10 +20,14 @@
**[ウェブサイト](https://rea.tools/ja/) · [ガイド](https://rea.tools/ja/guides/) · [事例](https://rea.tools/ja/showcase/)**
-[クイックスタート](#クイックスタート) · [REA の仕組み](#rea-の仕組み) · [分析できる対象](#分析できる対象) · [事例](#事例) · [よくある質問](#よくある質問) · [ドキュメント](#ドキュメント)
+[クイックスタート](#クイックスタート) · [REA の仕組み](#rea-の仕組み) · [分析できる対象](#分析できる対象) · [事例](#事例) · [よくある質問](#よくある質問) · [GNU objdump・Rizin ツール](../reverse-engineering-tools.md) · [ドキュメント](#ドキュメント)
npx rea-agents setup
+> [!IMPORTANT]
+> REA の公式配布元: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> インストールとリリースのリンクは、これらの公式配布元からたどってください。似たリポジトリ名やコピーされた README だけでは、公式の配布物とは確認できません。
+
@@ -135,6 +140,7 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/ja/guides/native/) |
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
+| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/ja/guides/javascript/) |
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/ja/guides/browser/) |
diff --git a/docs/readme/README_ko.md b/docs/readme/README_ko.md
index 26db0d824..1c1133254 100644
--- a/docs/readme/README_ko.md
+++ b/docs/readme/README_ko.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: 무엇이든 리버스 엔지니어링
@@ -19,10 +20,14 @@
**[웹사이트](https://rea.tools/ko/) · [가이드](https://rea.tools/ko/guides/) · [사례](https://rea.tools/ko/showcase/)**
-[빠른 시작](#빠른-시작) · [REA의 작동 방식](#rea의-작동-방식) · [분석할 수 있는 대상](#분석할-수-있는-대상) · [사례](#사례) · [자주 묻는 질문](#자주-묻는-질문) · [문서](#문서)
+[빠른 시작](#빠른-시작) · [REA의 작동 방식](#rea의-작동-방식) · [분석할 수 있는 대상](#분석할-수-있는-대상) · [사례](#사례) · [자주 묻는 질문](#자주-묻는-질문) · [GNU objdump 및 Rizin 도구](../reverse-engineering-tools.md) · [문서](#문서)
npx rea-agents setup
+> [!IMPORTANT]
+> REA 공식 배포처: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> 이 공식 배포처에서 제공하는 설치 및 릴리스 링크를 이용하세요. 비슷한 저장소 이름이나 복사된 README만으로는 공식 배포 파일인지 확인할 수 없습니다.
+
@@ -135,6 +140,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/ko/guides/native/) |
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
+| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](../recorded-crashes.md) |
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/ko/guides/javascript/) |
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/ko/guides/browser/) |
diff --git a/docs/readme/README_pl.md b/docs/readme/README_pl.md
index 48200087d..c49c1ec05 100644
--- a/docs/readme/README_pl.md
+++ b/docs/readme/README_pl.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Reverse Engineer Anything
@@ -19,10 +20,14 @@
**[Strona internetowa](https://rea.tools/pl/) · [Przewodniki](https://rea.tools/pl/guides/) · [Przykłady zastosowań](https://rea.tools/pl/showcase/)**
-[Szybki start](#szybki-start) · [Jak działa REA](#jak-działa-rea) · [Co możesz analizować](#co-możesz-analizować) · [Przykłady zastosowań](#przykłady-zastosowań) · [FAQ](#faq) · [Dokumentacja](#dokumentacja)
+[Szybki start](#szybki-start) · [Jak działa REA](#jak-działa-rea) · [Co możesz analizować](#co-możesz-analizować) · [Przykłady zastosowań](#przykłady-zastosowań) · [FAQ](#faq) · [Narzędzia GNU objdump i Rizin](../reverse-engineering-tools.md) · [Dokumentacja](#dokumentacja)
npx rea-agents setup
+> [!IMPORTANT]
+> Oficjalne źródła REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Korzystaj z odnośników do instalacji i wydań w tych oficjalnych źródłach. Podobna nazwa repozytorium lub skopiowany README nie potwierdzają, że pobierany plik jest oficjalny.
+
@@ -135,6 +140,7 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](https://rea.tools/pl/guides/native/) |
| 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) |
| 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) |
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](../recorded-crashes.md) |
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](https://rea.tools/pl/guides/javascript/) |
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](https://rea.tools/pl/guides/browser/) |
diff --git a/docs/readme/README_pt-BR.md b/docs/readme/README_pt-BR.md
index 6c3f34e35..ec7ecd29c 100644
--- a/docs/readme/README_pt-BR.md
+++ b/docs/readme/README_pt-BR.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Faça engenharia reversa de qualquer coisa
@@ -19,10 +20,14 @@
**[Site](https://rea.tools/pt-BR/) · [Guias](https://rea.tools/pt-BR/guides/) · [Casos de uso](https://rea.tools/pt-BR/showcase/)**
-[Início rápido](#início-rápido) · [Como o REA funciona](#como-o-rea-funciona) · [O que você pode analisar](#o-que-você-pode-analisar) · [Casos de uso](#casos-de-uso) · [Perguntas frequentes](#perguntas-frequentes) · [Documentação](#documentação)
+[Início rápido](#início-rápido) · [Como o REA funciona](#como-o-rea-funciona) · [O que você pode analisar](#o-que-você-pode-analisar) · [Casos de uso](#casos-de-uso) · [Perguntas frequentes](#perguntas-frequentes) · [Ferramentas GNU objdump e Rizin](../reverse-engineering-tools.md) · [Documentação](#documentação)
npx rea-agents setup
+> [!IMPORTANT]
+> Fontes oficiais do REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Use os links de instalação e de versões dessas fontes oficiais. Um nome de repositório parecido ou um README copiado não comprova que um download seja oficial.
+
@@ -135,6 +140,7 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](https://rea.tools/pt-BR/guides/native/) |
| 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) |
| 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) |
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](../recorded-crashes.md) |
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](https://rea.tools/pt-BR/guides/javascript/) |
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](https://rea.tools/pt-BR/guides/browser/) |
diff --git a/docs/readme/README_ru.md b/docs/readme/README_ru.md
index 744e26f6c..136344e9f 100644
--- a/docs/readme/README_ru.md
+++ b/docs/readme/README_ru.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Реверс-инжиниринг чего угодно
@@ -19,10 +20,14 @@
**[Сайт](https://rea.tools/ru/) · [Руководства](https://rea.tools/ru/guides/) · [Примеры](https://rea.tools/ru/showcase/)**
-[Быстрый старт](#быстрый-старт) · [Как работает REA](#как-работает-rea) · [Что можно анализировать](#что-можно-анализировать) · [Примеры](#примеры) · [Частые вопросы](#частые-вопросы) · [Документация](#документация)
+[Быстрый старт](#быстрый-старт) · [Как работает REA](#как-работает-rea) · [Что можно анализировать](#что-можно-анализировать) · [Примеры](#примеры) · [Частые вопросы](#частые-вопросы) · [Инструменты GNU objdump и Rizin](../reverse-engineering-tools.md) · [Документация](#документация)
npx rea-agents setup
+> [!IMPORTANT]
+> Официальные источники REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Используйте ссылки на установку и выпуски из этих официальных источников. Похожее название репозитория или скопированный README не подтверждают, что скачиваемый файл является официальным.
+
@@ -135,6 +140,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/ru/guides/native/) |
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
+| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/ru/guides/javascript/) |
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/ru/guides/browser/) |
diff --git a/docs/readme/README_th.md b/docs/readme/README_th.md
index 734b28c86..f758ca421 100644
--- a/docs/readme/README_th.md
+++ b/docs/readme/README_th.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Reverse Engineer Anything
@@ -19,10 +20,14 @@
**[เว็บไซต์](https://rea.tools/th/) · [คู่มือ](https://rea.tools/th/guides/) · [ตัวอย่างการใช้งาน](https://rea.tools/th/showcase/)**
-[เริ่มต้นอย่างรวดเร็ว](#เริ่มต้นอย่างรวดเร็ว) · [REA ทำงานอย่างไร](#rea-ทำงานอย่างไร) · [สิ่งที่วิเคราะห์ได้](#สิ่งที่วิเคราะห์ได้) · [ตัวอย่างการใช้งาน](#ตัวอย่างการใช้งาน) · [คำถามที่พบบ่อย](#คำถามที่พบบ่อย) · [เอกสาร](#เอกสาร)
+[เริ่มต้นอย่างรวดเร็ว](#เริ่มต้นอย่างรวดเร็ว) · [REA ทำงานอย่างไร](#rea-ทำงานอย่างไร) · [สิ่งที่วิเคราะห์ได้](#สิ่งที่วิเคราะห์ได้) · [ตัวอย่างการใช้งาน](#ตัวอย่างการใช้งาน) · [คำถามที่พบบ่อย](#คำถามที่พบบ่อย) · [เครื่องมือ GNU objdump และ Rizin](../reverse-engineering-tools.md) · [เอกสาร](#เอกสาร)
npx rea-agents setup
+> [!IMPORTANT]
+> แหล่งทางการของ REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> โปรดใช้ลิงก์ติดตั้งและรุ่นเผยแพร่จากแหล่งทางการเหล่านี้ ชื่อที่เก็บโค้ดที่คล้ายกันหรือ README ที่คัดลอกมาไม่ได้ยืนยันว่าไฟล์ดาวน์โหลดเป็นของทางการ
+
@@ -135,6 +140,7 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/th/guides/native/) |
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
+| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/th/guides/javascript/) |
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/th/guides/browser/) |
diff --git a/docs/readme/README_tr.md b/docs/readme/README_tr.md
index 152c81819..5b4de00d5 100644
--- a/docs/readme/README_tr.md
+++ b/docs/readme/README_tr.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Her şeyi tersine mühendislikle inceleyin
@@ -19,10 +20,14 @@
**[Web sitesi](https://rea.tools/tr/) · [Rehberler](https://rea.tools/tr/guides/) · [Örnek çalışmalar](https://rea.tools/tr/showcase/)**
-[Hızlı başlangıç](#hızlı-başlangıç) · [REA nasıl çalışır](#rea-nasıl-çalışır) · [Neleri analiz edebilirsiniz](#neleri-analiz-edebilirsiniz) · [Örnek çalışmalar](#örnek-çalışmalar) · [Sık sorulan sorular](#sık-sorulan-sorular) · [Belgeler](#belgeler)
+[Hızlı başlangıç](#hızlı-başlangıç) · [REA nasıl çalışır](#rea-nasıl-çalışır) · [Neleri analiz edebilirsiniz](#neleri-analiz-edebilirsiniz) · [Örnek çalışmalar](#örnek-çalışmalar) · [Sık sorulan sorular](#sık-sorulan-sorular) · [GNU objdump ve Rizin araçları](../reverse-engineering-tools.md) · [Belgeler](#belgeler)
npx rea-agents setup
+> [!IMPORTANT]
+> REA’nın resmî kaynakları: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Kurulum ve sürüm bağlantılarını bu resmî kaynaklardan takip edin. Benzer bir depo adı veya kopyalanmış bir README, indirilen dosyanın resmî olduğunu kanıtlamaz.
+
@@ -137,6 +142,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/tr/guides/native/) |
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) |
| 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) |
| 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` |
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
| SQLite veritabanları | Tablolar, sütunlar, index'ler, view'lar, trigger'lar ve seçilen sıradan tablo satırları | Yerel database/WAL snapshot'ı; native SQLite sınırlarıyla Node.js 24.x >=24.15 veya 26+ |
diff --git a/docs/readme/README_uk.md b/docs/readme/README_uk.md
index 5c77d80b0..3eebfeeab 100644
--- a/docs/readme/README_uk.md
+++ b/docs/readme/README_uk.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Реверс-інжиніринг будь-чого
@@ -19,10 +20,14 @@
**[Сайт](https://rea.tools/uk/) · [Посібники](https://rea.tools/uk/guides/) · [Приклади](https://rea.tools/uk/showcase/)**
-[Швидкий старт](#швидкий-старт) · [Як працює REA](#як-працює-rea) · [Що можна аналізувати](#що-можна-аналізувати) · [Приклади](#приклади) · [Поширені запитання](#поширені-запитання) · [Документація](#документація)
+[Швидкий старт](#швидкий-старт) · [Як працює REA](#як-працює-rea) · [Що можна аналізувати](#що-можна-аналізувати) · [Приклади](#приклади) · [Поширені запитання](#поширені-запитання) · [Інструменти GNU objdump і Rizin](../reverse-engineering-tools.md) · [Документація](#документація)
npx rea-agents setup
+> [!IMPORTANT]
+> Офіційні джерела REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Користуйтеся посиланнями на встановлення та випуски з цих офіційних джерел. Схожа назва репозиторію або скопійований README не підтверджують, що завантажуваний файл є офіційним.
+
@@ -135,6 +140,7 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/uk/guides/native/) |
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
+| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/uk/guides/javascript/) |
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/uk/guides/browser/) |
diff --git a/docs/readme/README_vi.md b/docs/readme/README_vi.md
index 3d2062da6..e5879c872 100644
--- a/docs/readme/README_vi.md
+++ b/docs/readme/README_vi.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA: Kỹ thuật dịch ngược cho mọi thứ
@@ -19,10 +20,14 @@
**[Trang web](https://rea.tools/vi/) · [Hướng dẫn](https://rea.tools/vi/guides/) · [Các nghiên cứu điển hình](https://rea.tools/vi/showcase/)**
-[Bắt đầu nhanh](#bắt-đầu-nhanh) · [Cách REA hoạt động](#cách-rea-hoạt-động) · [Những gì bạn có thể phân tích](#những-gì-bạn-có-thể-phân-tích) · [Các nghiên cứu điển hình](#các-nghiên-cứu-điển-hình) · [Câu hỏi thường gặp](#câu-hỏi-thường-gặp) · [Tài liệu](#tài-liệu)
+[Bắt đầu nhanh](#bắt-đầu-nhanh) · [Cách REA hoạt động](#cách-rea-hoạt-động) · [Những gì bạn có thể phân tích](#những-gì-bạn-có-thể-phân-tích) · [Các nghiên cứu điển hình](#các-nghiên-cứu-điển-hình) · [Câu hỏi thường gặp](#câu-hỏi-thường-gặp) · [Công cụ GNU objdump và Rizin](../reverse-engineering-tools.md) · [Tài liệu](#tài-liệu)
npx rea-agents setup
+> [!IMPORTANT]
+> Các nguồn chính thức của REA: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> Hãy dùng các liên kết cài đặt và bản phát hành từ những nguồn chính thức này. Tên kho mã tương tự hoặc README được sao chép không chứng minh rằng tệp tải xuống là bản chính thức.
+
@@ -135,6 +140,7 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](https://rea.tools/vi/guides/native/) |
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
| 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) |
| 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) |
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](https://rea.tools/vi/guides/javascript/) |
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](https://rea.tools/vi/guides/browser/) |
diff --git a/docs/readme/README_zh-TW.md b/docs/readme/README_zh-TW.md
index 1ddbbd26e..86ad30c82 100644
--- a/docs/readme/README_zh-TW.md
+++ b/docs/readme/README_zh-TW.md
@@ -1,6 +1,7 @@
[English](../../README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA:逆向一切
@@ -19,10 +20,14 @@
**[網站](https://rea.tools/zh-TW/) · [指南](https://rea.tools/zh-TW/guides/) · [案例](https://rea.tools/zh-TW/showcase/)**
-[快速開始](#快速開始) · [REA 的工作原理](#rea-的工作原理) · [可以分析什麼](#可以分析什麼) · [案例](#案例) · [常見問題](#常見問題) · [文件](#文件)
+[快速開始](#快速開始) · [REA 的工作原理](#rea-的工作原理) · [可以分析什麼](#可以分析什麼) · [案例](#案例) · [常見問題](#常見問題) · [GNU objdump 與 Rizin 工具](../reverse-engineering-tools.md) · [文件](#文件)
npx rea-agents setup
+> [!IMPORTANT]
+> REA 官方來源: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> 請透過這些官方來源提供的安裝與發布連結取得 REA。相似的儲存庫名稱或複製的 README 不能證明下載內容來自官方。
+
@@ -139,6 +144,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| Go 二進位檔案 | 內嵌編譯器版本、模組、替換項和建置設定 | 內建靜態 ELF/PE/thin Mach-O 檢查;無需安裝 Go |
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
+| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/zh-TW/guides/javascript/) |
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/zh-TW/guides/browser/) |
diff --git a/docs/readme/README_zh.md b/docs/readme/README_zh.md
index 6e70fd3c3..304b89389 100644
--- a/docs/readme/README_zh.md
+++ b/docs/readme/README_zh.md
@@ -1,6 +1,7 @@
[English](../../README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md) · [Bahasa Indonesia](README_id.md) · [Italiano](README_it.md)
+· [Ελληνικά](README_el.md)
# REA:逆向一切
@@ -19,10 +20,14 @@
**[网站](https://rea.tools/zh-CN/) · [指南](https://rea.tools/zh-CN/guides/) · [案例](https://rea.tools/zh-CN/showcase/)**
-[快速开始](#快速开始) · [REA 的工作原理](#rea-的工作原理) · [可以分析什么](#可以分析什么) · [案例](#案例) · [常见问题](#常见问题) · [文档](#文档)
+[快速开始](#快速开始) · [REA 的工作原理](#rea-的工作原理) · [可以分析什么](#可以分析什么) · [案例](#案例) · [常见问题](#常见问题) · [GNU objdump 和 Rizin 工具](../reverse-engineering-tools.md) · [文档](#文档)
npx rea-agents setup
+> [!IMPORTANT]
+> REA 官方来源: [morluto/rea](https://github.com/morluto/rea), [rea.tools](https://rea.tools/), [rea-agents](https://www.npmjs.com/package/rea-agents) (npm).
+> 请通过这些官方来源提供的安装和发布链接获取 REA。相似的仓库名称或复制的 README 不能证明下载内容来自官方。
+
@@ -139,6 +144,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
+| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/zh-CN/guides/javascript/) |
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/zh-CN/guides/browser/) |
diff --git a/docs/reverse-engineering-tools.md b/docs/reverse-engineering-tools.md
new file mode 100644
index 000000000..124c92dc0
--- /dev/null
+++ b/docs/reverse-engineering-tools.md
@@ -0,0 +1,81 @@
+# Reverse-engineering tool integrations
+
+REA can call caller-supplied GNU objdump, GDB, and Rizin installations. REA does not install or update these tools. Static objdump/Rizin workflows use the same services from CLI and MCP. GDB's persistent MCP sessions and ephemeral CLI command are available only on Linux. Persistent debugger sessions exist only for the lifetime of the REA MCP server process.
+
+In a shared MCP server, unrestricted Rizin and Cutter commands, GDB console
+commands, Rizin debugger start/close, and GDB debugger close retire any active
+binary analysis session after the operation. This prevents a fresh analysis
+provider from being rebound automatically while command effects may still be
+writing. After the command and any background writers have stopped, call
+`open_binary` explicitly before continuing analysis. A successful command
+response alone does not prove that background writers have stopped. An active
+Ghidra analysis session must be closed explicitly before an external mutation
+can run.
+
+## GNU objdump
+
+`inspect_with_objdump` and `rea inspect-with-objdump` capture stdout and stderr as inline Evidence. Artifact inspection Evidence records SHA-256 digests before and after the provider call and whether those observed digests differ; this cannot prove the exact bytes consumed if the artifact changes during execution. Choose one artifact operation: `file_headers`, `section_headers`, `symbols`, `relocations`, `dwarf`, `disassemble`, or `disassemble_all`. `disassemble` maps to `-d`; `disassemble_all` maps to `-D` and has different symbol-boundary semantics. The pathless `inspect_with_objdump {"operation":"architectures"}` MCP call or `rea list-objdump-architectures` CLI command maps to `-i` and produces subjectless provider-capability Evidence describing BFD targets compiled into the installed binary, not a guarantee that a target is supported.
+
+For DWARF, REA does not follow local debug links by default. Set `follow_debug_links` to `true` to pass `-WK`; external debug-file identities are not bound to Evidence when following links is enabled. The CLI option is `--follow-debug-links true`. REA clears `DEBUGINFOD_URLS` in the objdump child environment to disable debuginfod network access on GNU binutils versions that do not support `-WE`. The complete raw output is authoritative; REA does not normalize it into a version-independent disassembly schema.
+
+```sh
+rea inspect-with-objdump ./program disassemble
+rea inspect-with-objdump ./program dwarf --follow-debug-links false
+```
+
+Captured stdout and stderr are retained up to a 16 MiB combined limit; `output_truncated` marks output beyond that limit.
+
+## GDB
+
+Persistent GDB sessions, including the MCP workflow and one-shot `rea debug-with-gdb` CLI, are available only on Linux. REA verifies a live inferior's output descriptors through `/proc` before detaching it; it does not claim equivalent descriptor verification on macOS. Windows is also unsupported because owned Job Object cleanup can terminate debugger inferiors. On supported Linux hosts, `start_gdb_session`, `gdb_console`, `gdb_session_status`, and `close_gdb_session` manage a persistent GDB/MI session in MCP. The one-shot CLI requires a caller-selected durable TTY: `rea debug-with-gdb --inferior-tty /dev/pts/7 'file ./program'`. The TTY must remain available after GDB exits so a detached inferior can continue writing. MCP starts GDB with `--nx --quiet --interpreter=mi3`, retries initialization with MI2 if MI3 initialization returns an error and cleanup succeeds, waits for its prompt, and disables auto-loading. Pass optional `inferior_tty` to select the terminal that future inferior runs use; REA configures it before the session becomes ready and checks the exact value with GDB/MI. `gdb_session_status` reports the selected path, whether REA still considers the configuration verified, and whether invalid UTF-8 has been observed in MI bytes (`mi_invalid_utf8`) or stderr (`stderr_invalid_utf8`). Commands that can change the inferior TTY or redirect a run revoke that verification permanently for the session, so close retains GDB ownership if a live inferior remains. Closing a session with one live inferior and a verified TTY automatically detaches the target, which can resume execution; MCP clients receive destructive-operation annotations for this behavior. The console remains unrestricted and can run shell commands, change target state, access files, and use network in the REA process user's security context. Raw MI records and console/target/log streams are returned with Evidence; command output is retained up to 16 MiB and explicitly marked when truncated.
+
+Before a normal close, REA checks for live GDB thread groups. If one remains and a durable `inferior_tty` is verified, REA asks GDB to detach it and confirms the detach before stopping the debugger. Without that sink, select and apply a disposition to each inferior through `gdb_console` (for example, detach or kill as appropriate), then retry close. REA never infers target ownership from a PID or path. When the MCP server shuts down, REA terminates only its directly owned GDB process after confirming no inferior or a sink-backed detach; otherwise it retains the process and its output streams for retry.
+
+```text
+start_gdb_session {"inferior_tty":"/dev/pts/7"}
+gdb_console {"session_id":"…","command":"file ./program"}
+gdb_console {"session_id":"…","command":"info files"}
+gdb_session_status {"session_id":"…"}
+close_gdb_session {"session_id":"…"}
+```
+
+## Rizin
+
+`start_rizin_debug_session` and `rea debug-with-rizin` require an explicit `inferior_tty` on Linux. Rizin launches the target with stdout and stderr routed to that caller-selected TTY and `dbg.exitkills=false`; the target can therefore continue running and writing to the TTY after REA closes the debugger. This is a visible target output effect, and the caller is responsible for selecting a suitable terminal that remains available. Before closing, REA serializes after the active command, parses the structured `olj` descriptor list, detaches only a unique `ptrace://` descriptor, confirms its absence, reads back `dbg.exitkills=false`, and verifies `/proc/
/fd/1` and `/proc//fd/2` still identify the selected TTY by device and inode. Any malformed or ambiguous readback keeps the Rizin session owned for retry. Persistent Rizin debugger sessions are unavailable outside Linux because this verification depends on `/proc`.
+
+Persistent command Evidence retains the caller-selected and resolved startup paths in `parameters.launch_provenance`. These paths describe launch provenance, not the current target after unrestricted commands. `rizin_debug_session_status.output_invalid_utf8` reports whether lossy frame decoding has been observed during the session. Command Evidence sets `output_invalid_utf8` only when loss was observed in frames collected for that command, including retained partial output; earlier startup or command frames do not mark later valid output as lossy.
+
+```sh
+rea debug-with-rizin --inferior-tty /dev/pts/7 ./program dr
+```
+
+`execute_rizin_command` and `rea execute-rizin-command` run one selected command against a local artifact. The one-shot profile uses `-N -q -c`, suppressing user settings and scripts while leaving plugins available. Commands and plugins run with the REA process user's security context and may access files, the network, or perform other actions. REA preserves raw output and does not claim that command-specific JSON has one universal schema.
+
+```sh
+rea execute-rizin-command ./program iI
+```
+
+Captured stdout and stderr are retained up to a 16 MiB combined limit; `output_truncated` marks output beyond that limit.
+
+## Cutter bridge
+
+Cutter commands use the local REA Python bridge and authenticated loopback messages.
+The client and plugin must support the current v2 handshake; older peers are
+rejected before dispatch because they cannot preserve cross-caller recovery proof.
+The client checks the final encoded request against the bridge's 16 MiB frame
+limit before dispatch; an oversized command does not retain a cleanup owner.
+Discovery admits at most 256 descriptor candidates and reads and probes at most
+eight concurrently. A larger directory reports `bridge_directory_capacity_exceeded`.
+
+Terminal receipts are recovered before another command, including from a new CLI
+invocation. A signed status response also confirms completion of older bridge
+connections after its UI dispatch, preserving recovery proof for a caller whose
+receipt another caller has acknowledged. Neither receipt recovery nor a status
+probe retries the original command. Failed acknowledgements remain pending and
+block new commands until they can be retried successfully.
+
+## Configuration and coverage
+
+The optional `REA_OBJDUMP_COMMAND`, `REA_GDB_COMMAND`, and `REA_RIZIN_COMMAND` values select executable names or paths; otherwise REA searches `PATH`. Values are passed directly to process spawning, not expanded as shell command lines. Provider versions and capabilities remain unknown until observed from the installed tool and verified in the corresponding real-provider lane.
+
+Provider capture limits are separate from the MCP response budget. A result that fits the provider limit may still exceed the default 10 MiB MCP response budget and return a transport `resource_constraint`. Configure `REA_MCP_MAX_RESPONSE_BYTES` to match a larger client receive buffer when that client supports one; otherwise use retained Evidence/export or complete CLI JSON output. See [MCP response limits](./mcp-contracts.md#tool-results).
diff --git a/docs/rust-binaries.md b/docs/rust-binaries.md
new file mode 100644
index 000000000..d7cb7a750
--- /dev/null
+++ b/docs/rust-binaries.md
@@ -0,0 +1,67 @@
+# Rust binary dependency metadata
+
+`inspect_rust_binary` / `inspect-rust-binary` answers which crate names,
+versions, sources, and dependency edges are embedded by cargo-auditable in one
+selected local executable or library. It reads the file without running it and
+requires no Rust installation, active binary target, Hopper, Ghidra, or IDA.
+
+```bash
+rea inspect-rust-binary /absolute/path/to/application --json
+```
+
+The CLI resolves relative paths against the caller's working directory. MCP
+requires an absolute filesystem path and preserves that exact selection:
+
+```json
+{
+ "name": "inspect_rust_binary",
+ "arguments": { "path": "/artifacts/application" }
+}
+```
+
+## Observations
+
+The inline Evidence result contains the artifact path, SHA-256, byte length,
+container format, architecture, word size, and byte order. When a `.dep-v0`
+section is present, `auditable_metadata` contains:
+
+- `section_name`, `.dep-v0` for ELF and PE or `__DATA,.dep-v0` for thin Mach-O;
+- `file_offset` and `section_bytes`, the exact file range of the compressed section;
+- `compressed_bytes` and `uncompressed_bytes`;
+- `format_version`, the embedded integer, or `null` when the field is omitted;
+- `packages`, in the section's original order, with `name`, `version`, `source`,
+ `kind`, `dependencies`, and `root`;
+- `root_package_index`, the single package marked `root`, or `null` when none is marked;
+- `dependency_edges`, pairs of package indices in package-then-dependency order;
+- `cyclic_dependency_indices`, sorted unique indices that participate in a cycle.
+
+An omitted `kind` stays `null`. The embedded format treats that omission as
+runtime, and that default is not stored in the section. An omitted dependency
+list is an empty array. An omitted `root` flag is false.
+
+`rustc_commit` is present only when a `/rustc/<40-hex>/` path is found in
+read-only data. Its `evidence_kind` is `inferred`. The hash is not a claim that
+that compiler produced the binary. Several distinct hashes leave a single
+commit unknown.
+
+## Limits and failures
+
+The selected file is limited to 256 MiB. A dependency section is limited to 1
+MiB compressed. Inflated JSON is limited to the smaller of 8 MiB and 64 times
+the compressed size. The complete report is limited to 8 MiB. Empty sections,
+truncated or trailing zlib bytes, invalid JSON, unknown fields, dependency
+indices outside the package list, more than one root package, and compressed
+dependency sections are typed failures. A cycle is reported and is not walked.
+
+A missing section returns success with `auditable_metadata: null` and a
+limitation. Absence of `.dep-v0` does not mean the binary has no dependencies.
+Embedded names and versions are file content. They do not authenticate the
+packages or the compiler.
+
+Universal Mach-O containers are unsupported. Select a thin slice first. ELF,
+PE, and thin Mach-O are the supported containers.
+
+Package and dependency counts are checked against the minimum required JSON
+representation before graph allocation or schema cloning. Oversized graphs
+report the output byte budget; the compiler-path capacity reports
+`maximum_rustc_hashes` in hashes, independently of byte limits.
diff --git a/docs/testing.md b/docs/testing.md
index 213c48e44..aa8d2163f 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -262,8 +262,33 @@ CI owns coverage; aggregate and domain/contract thresholds are maintained in
`vitest.config.ts`. They are glob-specific and never updated automatically.
Coverage does not replace named boundary or real-provider scenarios.
-CI runs six native Vitest shards without retries. Each shard emits a blob
-report; the merge job produces JUnit and JSON diagnostics, a module timing
+The `reverse-tools` commands run the focused tests and report coverage and
+mutation results without an additional percentage requirement. Test failures,
+invalid mutation configuration, and runner failures still fail the check.
+Mutation targets cover reviewed production behavior, including signal-terminated
+commands, Cutter descriptor identity, debugger cleanup ownership, GDB command
+exit and cancellation, and Rizin startup, timeout Evidence, and process exit.
+A second exhaustive pass runs the focused Cutter and Rizin timeout semantics tests against every compilable
+selected mutant to avoid relying on per-test mutation coverage associations;
+Stryker reports compile-error mutants separately because those cannot execute
+tests. Both passes resolve named syntax owners, scoped declarations, guards,
+callbacks, and reviewed expressions from the current TypeScript source; no
+hand-maintained line ranges select mutations. Missing, ambiguous, or malformed
+source targets stop configuration before mutation generation. The selections
+retain their review scope rather than expanding to whole source files. The
+focused suite also exercises affected MCP routes and the Python Cutter bridge.
+Contract, integration, and real-provider lanes complement these focused tests;
+simulated providers do not establish real-provider behavior.
+
+Fast CI starts the source-only projects after classification, independently of
+build and static checks, and runs five built-dependent shards after the build.
+The six test jobs still select every portable test. Full qualification keeps six
+complete Vitest shards with coverage. Neither tier retries tests. Canonical
+project ownership lives in `scripts/lib/test-projects.mjs`, shared by Vitest,
+CI group execution, and development-test preparation. A test importing compiled
+verifier code belongs to the built group even if its directory is conformance.
+Source and built blobs have separate labels to prevent merge collisions.
+Each test job emits a blob report; the merge job produces JUnit and JSON diagnostics, a module timing
report, plus aggregate coverage during full qualification. It retains available
reports even when a shard fails, uploads available diagnostics, and writes the
slowest files to the workflow summary. Partial reports are marked diagnostic
@@ -298,6 +323,30 @@ portable test shard. These limits leave headroom for cold setup and full coverag
while preventing a hung job from holding the active main run for hours. They do
not bound runner queue waiting. Timeouts still fail the required gate.
+The separate `CI timings` workflow also diagnoses missing, startup-failed,
+queued/running, approval-required, canceled, and failed PR CI through bounded,
+read-only API scans. Incomplete pagination or changing identity stays unknown.
+It never closes/reopens PRs, reruns workflows, posts comments, or changes gates.
+The timing manifest retains each run's event and head branch, including explicit
+unknowns, so PR runs cannot consume the eligible-main comparison budget.
+Its static failure report compares supported lint/duplication diagnostics with
+failed main runs using exact source, ancestry, task hashes, and toolchain proof.
+A match describes those diagnostics only; it does not establish that every
+failure in a job came from main. Duplication reports retain the actual checker
+exit result, so passing duplication checks preserve lint failures and caller
+thresholds remain owned by the producer. Missing checker outcomes leave
+those duplication diagnostics unclassified. Missing historical receipts remain unknown.
+The reporter executes only trusted default-branch code and parses whitelisted
+JSON artifact members without executing artifact content. Reports and limits
+are retained in `ci-health-diagnostics`; its ten-minute job limit covers the
+existing four-minute timing collection plus the bounded diagnostic stages.
+
+Shard-count reduction remains deferred: the available successful timing sample
+covers an older inventory, and file phase weights are not job wall time. The
+one-source/five-built split retains six test jobs; it makes no hosted speedup
+claim. Current complete timings and completed candidate hosted runs are needed
+before enabling a smaller adaptive matrix.
+
The separate `CI timings` workflow batches recent completed CI and Release runs
hourly or on manual dispatch. It uploads `ci-job-timings-batch` with raw API pages and
a normalized point-in-time report for each collected workflow attempt, plus a
@@ -346,6 +395,17 @@ that explicit lane, not the routine iteration requirement.
## Real-toolchain verification lanes
+Linux Rizin debugger ownership changes require a separate real-provider gate on
+Rizin 0.7.4. Run
+`npm run test:focused -- tests/boundary/providers/RizinInferiorTty.test.ts`.
+This test verifies the caller-selected TTY against the actual inferior's file
+descriptors, confirms `dbg.exitkills=false`, detaches through the `olj`/`o-`
+protocol, and checks that the target survives debugger close and writes to the
+selected TTY. The suite is included in the reverse-tools Vitest and mutation
+typecheck selections; its real-provider assertions skip when Rizin 0.7.4 is
+unavailable. Run the focused command above on a host with that provider for the
+explicit real-provider verification gate.
+
Each real-toolchain command must require only the host tools needed to prove
its stated claim. Use a host-native fixture for host/provider acceptance, and
place optional cross-target formats or platform-specific runners in separate
@@ -887,8 +947,13 @@ successful ancestor push run of this CI workflow, rather than the preceding
push: changes from failed or cancelled intermediate runs remain in scope.
Missing, untrusted, or non-ancestor history selects every lane. The lookup is
bounded to the last 100 successful push runs; lookup failures broaden selection.
-Every code change still runs all six portable shards, without coverage
-instrumentation in this fast tier. This deliberately avoids relying on an
+Every code change still selects all six portable shards, without coverage
+instrumentation in this fast tier. Ordinary PR matrices cancel remaining
+shards after the first failure; passing PRs execute the complete suite. Main,
+`ci:full`, scheduled, manual, and release qualification retain every shard's
+results after a failure. Available failed-shard reports remain diagnostic only:
+failed, cancelled, skipped, or missing selected shards cannot pass the gate or
+publish successful timing history. This deliberately avoids relying on an
import graph for generated files, filesystem inputs, subprocesses, or bridges.
Scheduled qualification is admitted hourly at minute 23. A forced full run at
@@ -916,10 +981,11 @@ classification job records its selection and broad-fallback reasons in the run
summary. Representative Git-diff cases live in
`tests/boundary/filesystem/ciChangeScope.test.ts`.
-`CI required` always runs and checks classification and every selected lane,
+`CI required` checks classification and every selected lane,
rejecting failed, cancelled, or unexpectedly skipped work. It rejects missing or
unknown tiers and requires every canonical scope to be explicitly `true` or
`false`; incomplete classification cannot silently omit verification lanes.
+The aggregate remains cancellable so superseded runs release their slots.
Branch protection
should require this aggregate, `Static checks`, `Portable tests`, and
`Conventional PR title`. Title validation runs separately on PR opening,
@@ -968,7 +1034,7 @@ filename cases.
POSIX package verification resolves the runner's configured npm cache before
creating disposable client homes and passes that location to its child installs.
-This reuses the download cache restored by `setup-node` while keeping client
+This reuses the download cache restored by the shared npm-cache action while keeping client
configuration isolated. Installs retain their existing registry and integrity
checks.
@@ -998,7 +1064,24 @@ thresholds remain enforced in full qualification.
Missing or unusable history falls back to Vitest's default assignment. Only
timing data is reused; live provider results are never cached. Turbo outputs are
restored by task hash, with trusted default-branch runs as cache writers and PRs
-as restore-only consumers. npm caches hold downloads, not `node_modules`.
+as restore-only consumers. The static job also restores its Turbo snapshot;
+each task revalidates its own input hash before reusing outputs or diagnostics.
+The Node pin and npm install settings are global hash inputs. Knip includes its ignore rules and VitePress
+entry, so changes to those inputs cannot reuse an older successful check.
+
+The shared npm-cache action caches downloads, not `node_modules`, and every
+consumer still runs `npm ci`. Keys cover the runner OS, actual Node architecture,
+root lockfiles, and `.npmrc`. Only push, schedule, and manual events on the
+repository's default branch may save shared npm caches; PRs restore only.
+Static snapshots use the same write authority and are saved only after the
+static job's validation succeeds. Main warms the new cache namespace after
+merge; a cold cache or changed lockfile falls back to the normal install.
+This prevents each PR from saving another branch-scoped copy of the same
+download cache. It does not remove runner assignment waits.
+
+Portable owner-based test selection remains observational. Its reports compare
+proposed omissions with the full run; incomplete or skipped observations do not
+establish that fewer tests would preserve regression detection.
The eight automatic browser, binary-layout, EVM, JavaScript-recovery,
recorded-crash, and web provider lanes download `rea-build-cache` from their own
diff --git a/docs/wasm-artifacts.md b/docs/wasm-artifacts.md
index 17b6c77f9..12d4da85b 100644
--- a/docs/wasm-artifacts.md
+++ b/docs/wasm-artifacts.md
@@ -61,9 +61,24 @@ The tool profile preserves upstream release/source identity, resolved executable
paths, executable digests, exact banners and arguments. Executable digests are
checked again after each command. Caller-supplied tools are trusted programs;
version and digest recording are not publisher authentication or an OS sandbox.
-Each subprocess has a 30-second deadline and 32 MiB combined output budget;
-truncation fails. All commands use existing owned-process cancellation and
-cleanup. Workspace cleanup failures remain explicit errors with resource paths.
+Each subprocess has a 30-second deadline and 32 MiB combined output budget.
+Inspection output past the budget fails with `resource_constraint`; an oversized
+`--version` probe still reports `unreadable_output`. Both streams share the
+budget, so a stream that starts after the other has filled it retains nothing.
+The captured output keeps up to the first 4,096 bytes that each stream retained,
+each stream's retained byte count, and the exit code and signal. Retained text
+can end in U+FFFD where the budget cut a multi-byte character.
+
+These excerpts bound the returned diagnostic text.
+[PR #2117](https://github.com/morluto/rea/pull/2117) reports elapsed time, CPU
+time, and peak process-family memory, including WABT children, for warm Linux
+x64 WABT 1.0.42 CLI workflows; those costs did not change outside measurement
+noise. Agent token usage and truly cold workflows remain
+unmeasured. The underlying error cause still retains the full captured
+subprocess snapshot, so the excerpt does not establish lower retained memory
+or faster analysis.
+All commands use existing owned-process cancellation and cleanup. Workspace
+cleanup failures remain explicit errors with resource paths.
WAT is a decoded representation, not original source or proof of runtime
behavior. Custom annotations can preserve payloads, but WAT does not establish original
diff --git a/docs/web-network-captures.md b/docs/web-network-captures.md
index c447c4783..2d5bdcae1 100644
--- a/docs/web-network-captures.md
+++ b/docs/web-network-captures.md
@@ -224,6 +224,22 @@ are 0 and 50. The view groups request lines by method, WHATWG URL origin and
path template, sorted by origin, template and method. Query strings, fragments,
credentials, headers and bodies are not copied into groups.
+Select `include_query_parameter_names: true` in the view to include distinct
+observed query names per group. The default and explicit `false` omit the
+`query_parameter_names` field. Selected groups include an array of
+`{name, record_ordinals}` entries, sorted by Unicode code point; each name's
+ordinals follow the parent's selected record order and occur once per record.
+For `/products?category=books&page=1` and
+`/products?category=games&sort=price`, names are `category`, `page` and `sort`.
+Query values and fragments are not copied into this inventory.
+
+Names use WHATWG `URLSearchParams` form-query decoding: literal `+` becomes
+space, `%2B` becomes `+`, percent escapes decode as UTF-8, and empty names are
+retained. Empty query pairs are ignored. Original URLs stay in parent Evidence;
+name decoding does not normalize the recorded paths used for grouping.
+The inventory describes observed names, without inferring required parameters,
+value types or a complete API schema. Sensitive-parent refusals still apply.
+
| Rule | Exact segment format | Placeholder |
| ---------- | ---------------------------------------------------- | ------------ |
| `numeric` | ASCII digits only | `{numeric}` |
@@ -269,13 +285,16 @@ and validation scratch by walking the parent without cloning it. Inline requests
borrow the Evidence value through CLI/MCP envelope validation; the application
admits the entire portable envelope before authenticated parsing or hashing.
The envelope clone, hash scratch and capture clone stay charged during grouping.
-Temporary URL/path/MIME work and retained state must
+Temporary URL/path/MIME/query work and retained state must
fit together within a 96 MiB projection representation budget. A whole group or
merge is admitted before allocating its retained sets, map entries or array
values. Fixed group storage, distinct text, exclusions and limitations count as
well as matched segments. Per-record reservations are released after each
record; repeated values do not accumulate another retained charge. Only the
requested page is materialized as output groups.
+Opt-in query projection reserves parser tuples and the per-record name set
+before decoding, then admits distinct retained names and source ordinals before
+changing a group. Repeated names within a request retain that ordinal once.
The representation budget uses conservative storage and worst-case JSON-escaping
allowances; it is not an enforced V8 heap or process-memory limit. It is separate
diff --git a/knip.json b/knip.json
index 72d093397..5ed82edf0 100644
--- a/knip.json
+++ b/knip.json
@@ -9,6 +9,7 @@
"src/application/RealHopperAssertions.ts",
"scripts/verify/fridaBindingsCompatibility.ts",
"scripts/verify/termux/browser.mjs",
+ "scripts/verify-real-cutter.mjs",
"scripts/verify-real-ghidra-noreturn.mjs",
"tests/processOwnershipRunner.ts"
],
@@ -17,7 +18,12 @@
"entry": ["docs/.vitepress/config.ts"]
},
"ignoreBinaries": ["mkfifo"],
- "ignoreDependencies": ["electron", "esbuild", "webpack"],
+ "ignoreDependencies": [
+ "@stryker-mutator/command-runner",
+ "electron",
+ "esbuild",
+ "webpack"
+ ],
"ignoreExportsUsedInFile": true,
"rules": {
"files": "error",
diff --git a/licenses/npm-dependencies.txt b/licenses/npm-dependencies.txt
index 05fc2f173..794d5287e 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: 587; runtime: 175.
+Locked package entries: 719; runtime: 175.
Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
@algolia/abtesting@1.25.0 | development | MIT
@@ -95,21 +95,236 @@ Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
Package: https://www.npmjs.com/package/@algolia/requester-node-http/v/5.59.0
Archive: https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.59.0.tgz
Integrity: sha512-YYHLEs5rC6oRTFwT7bJaKBR9NSFzikeVHGAT1ffATmUQzR8XqyhHXjoieAUoR7fBU6I5cfy1FVeh5GoRgriuzA==
-@babel/helper-string-parser@8.0.0 | runtime | MIT
+@babel/code-frame@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/code-frame
+ Package: https://www.npmjs.com/package/@babel/code-frame/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.6.tgz
+ Integrity: sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==
+@babel/compat-data@8.0.5 | development | MIT
+ Lockfile path: node_modules/@babel/compat-data
+ Package: https://www.npmjs.com/package/@babel/compat-data/v/8.0.5
+ Archive: https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.5.tgz
+ Integrity: sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==
+@babel/core@8.0.7 | development | MIT
+ Lockfile path: node_modules/@babel/core
+ Package: https://www.npmjs.com/package/@babel/core/v/8.0.7
+ Archive: https://registry.npmjs.org/@babel/core/-/core-8.0.7.tgz
+ Integrity: sha512-ZIsZWBG6Y1oa+8fkQVp0xeCdjCHBkXFCCIWrmg9SvejVvfcVQgKKRmGrBDWfM7rilhvnRoEuaqa5dkK7KB3pHA==
+@babel/parser@8.0.7 | development | MIT
+ Lockfile path: node_modules/@babel/core/node_modules/@babel/parser
+ Package: https://www.npmjs.com/package/@babel/parser/v/8.0.7
+ Archive: https://registry.npmjs.org/@babel/parser/-/parser-8.0.7.tgz
+ Integrity: sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==
+@babel/types@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/core/node_modules/@babel/types
+ Package: https://www.npmjs.com/package/@babel/types/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz
+ Integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==
+obug@3.0.0 | development | MIT
+ Lockfile path: node_modules/@babel/core/node_modules/obug
+ Package: https://www.npmjs.com/package/obug/v/3.0.0
+ Archive: https://registry.npmjs.org/obug/-/obug-3.0.0.tgz
+ Integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==
+@babel/generator@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/generator
+ Package: https://www.npmjs.com/package/@babel/generator/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/generator/-/generator-8.0.6.tgz
+ Integrity: sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==
+@babel/parser@8.0.7 | development | MIT
+ Lockfile path: node_modules/@babel/generator/node_modules/@babel/parser
+ Package: https://www.npmjs.com/package/@babel/parser/v/8.0.7
+ Archive: https://registry.npmjs.org/@babel/parser/-/parser-8.0.7.tgz
+ Integrity: sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==
+@babel/types@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/generator/node_modules/@babel/types
+ Package: https://www.npmjs.com/package/@babel/types/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz
+ Integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==
+@babel/helper-annotate-as-pure@8.0.0 | development | MIT
+ Lockfile path: node_modules/@babel/helper-annotate-as-pure
+ Package: https://www.npmjs.com/package/@babel/helper-annotate-as-pure/v/8.0.0
+ Archive: https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-8.0.0.tgz
+ Integrity: sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==
+@babel/helper-compilation-targets@8.0.7 | development | MIT
+ Lockfile path: node_modules/@babel/helper-compilation-targets
+ Package: https://www.npmjs.com/package/@babel/helper-compilation-targets/v/8.0.7
+ Archive: https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.7.tgz
+ Integrity: sha512-ilAZCdOOlOVA7dQbaFNw2f/59p6GW33NxTopc3DvEE3efp3ZXTrGX/yZScaKw8/vd9bq31Ag0EcXiLw32bAgJg==
+@babel/helper-create-class-features-plugin@8.0.7 | development | MIT
+ Lockfile path: node_modules/@babel/helper-create-class-features-plugin
+ Package: https://www.npmjs.com/package/@babel/helper-create-class-features-plugin/v/8.0.7
+ Archive: https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-8.0.7.tgz
+ Integrity: sha512-7ug3KrOhNVPPxFFLTel0/pMzimKzU/vTlNJKVL/dKguCpmNJZjjibyFbWg+Qs8QHmiIZPNuZ2UNnDGdHWGEpOg==
+@babel/helper-globals@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/helper-globals
+ Package: https://www.npmjs.com/package/@babel/helper-globals/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.6.tgz
+ Integrity: sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==
+@babel/helper-member-expression-to-functions@8.0.5 | development | MIT
+ Lockfile path: node_modules/@babel/helper-member-expression-to-functions
+ Package: https://www.npmjs.com/package/@babel/helper-member-expression-to-functions/v/8.0.5
+ Archive: https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-8.0.5.tgz
+ Integrity: sha512-GLe05QD98BkNFTkjaqeqF9QSRoHLKrrB4tpoplyQuuPrQJ72rtmkM4wvqJLX/sjZPqkbK5MUYSsuUTqdJoOVjA==
+@babel/types@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types
+ Package: https://www.npmjs.com/package/@babel/types/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz
+ Integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==
+@babel/helper-module-imports@8.0.0 | development | MIT
+ Lockfile path: node_modules/@babel/helper-module-imports
+ Package: https://www.npmjs.com/package/@babel/helper-module-imports/v/8.0.0
+ Archive: https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-8.0.0.tgz
+ Integrity: sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==
+@babel/helper-module-transforms@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/helper-module-transforms
+ Package: https://www.npmjs.com/package/@babel/helper-module-transforms/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-8.0.6.tgz
+ Integrity: sha512-YUi+3X0cb0v97xKhuqkLdrwQtP6XXv1eVbhLRCFyMwG481UskbMQk4mjH7K/Jg3oJ1KLskvFwwq4kF/QnABkWQ==
+@babel/helper-optimise-call-expression@8.0.0 | development | MIT
+ Lockfile path: node_modules/@babel/helper-optimise-call-expression
+ Package: https://www.npmjs.com/package/@babel/helper-optimise-call-expression/v/8.0.0
+ Archive: https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-8.0.0.tgz
+ Integrity: sha512-3W6satvtPuCUkUx63S2jMoW9EQNYkADgs1HTfufmL7gCmAulHMKupA/12WNz4A0GMMFn/YnWWwqOT9IZrJHQjg==
+@babel/helper-plugin-utils@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/helper-plugin-utils
+ Package: https://www.npmjs.com/package/@babel/helper-plugin-utils/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-8.0.1.tgz
+ Integrity: sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==
+@babel/helper-replace-supers@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/helper-replace-supers
+ Package: https://www.npmjs.com/package/@babel/helper-replace-supers/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-8.0.1.tgz
+ Integrity: sha512-B1SZADIcy3tmH8CmWvj4SHi/oAPom4UL3uknTc2QRNsPVLFk/sPnZvQL/8kj7Y5omvjMqie0vklvs6XM4OLW5Q==
+@babel/helper-skip-transparent-expression-wrappers@8.0.0 | development | MIT
+ Lockfile path: node_modules/@babel/helper-skip-transparent-expression-wrappers
+ Package: https://www.npmjs.com/package/@babel/helper-skip-transparent-expression-wrappers/v/8.0.0
+ Archive: https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-8.0.0.tgz
+ Integrity: sha512-xmCA9kP3IhySsqhzwIdWGlDN/1A4cCKNBO/uwZx/3YzmDoMePwno2Q5/Bq0q+tYaKbeF940YiKV/kaW8Mzvpjw==
+@babel/helper-string-parser@8.0.6 | runtime | MIT
Lockfile path: node_modules/@babel/helper-string-parser
- Package: https://www.npmjs.com/package/@babel/helper-string-parser/v/8.0.0
- Archive: https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz
- Integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==
-@babel/helper-validator-identifier@8.0.4 | runtime | MIT
+ Package: https://www.npmjs.com/package/@babel/helper-string-parser/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.6.tgz
+ Integrity: sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==
+@babel/helper-validator-identifier@8.0.6 | runtime | MIT
Lockfile path: node_modules/@babel/helper-validator-identifier
- Package: https://www.npmjs.com/package/@babel/helper-validator-identifier/v/8.0.4
- Archive: https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz
- Integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==
+ Package: https://www.npmjs.com/package/@babel/helper-validator-identifier/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz
+ Integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==
+@babel/helper-validator-option@8.0.0 | development | MIT
+ Lockfile path: node_modules/@babel/helper-validator-option
+ Package: https://www.npmjs.com/package/@babel/helper-validator-option/v/8.0.0
+ Archive: https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz
+ Integrity: sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==
+@babel/helpers@8.0.7 | development | MIT
+ Lockfile path: node_modules/@babel/helpers
+ Package: https://www.npmjs.com/package/@babel/helpers/v/8.0.7
+ Archive: https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.7.tgz
+ Integrity: sha512-5dMj5kFwS2AY7Dt4PZp4px3c5ThMbbEa7tZfuHTncCGSFYq8uoA1ufDE1IS/lYpQedE+3+GQ3cPZUiplIu195w==
+@babel/types@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/helpers/node_modules/@babel/types
+ Package: https://www.npmjs.com/package/@babel/types/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz
+ Integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==
@babel/parser@8.0.4 | runtime | MIT
Lockfile path: node_modules/@babel/parser
Package: https://www.npmjs.com/package/@babel/parser/v/8.0.4
Archive: https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz
Integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==
+@babel/plugin-proposal-decorators@8.0.2 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-proposal-decorators
+ Package: https://www.npmjs.com/package/@babel/plugin-proposal-decorators/v/8.0.2
+ Archive: https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-8.0.2.tgz
+ Integrity: sha512-+C6O6KKXU7BBq1GNaIkFJxrALUVGRcr+WeWm4OcuRl3h+l/CmNfcTLMrT2Lm3uvGBimBH/8pEBRrXJFLoO67Gg==
+@babel/plugin-syntax-decorators@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-syntax-decorators
+ Package: https://www.npmjs.com/package/@babel/plugin-syntax-decorators/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-8.0.1.tgz
+ Integrity: sha512-NI+0S/6MvR6GlcQFwjDZ+WIc2qvG6TXN534lYs9llNldwW4b7Dh6KTtk030FA0xWdYGs4t1lWo+OEWN8wGB+Nw==
+@babel/plugin-syntax-jsx@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-syntax-jsx
+ Package: https://www.npmjs.com/package/@babel/plugin-syntax-jsx/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-8.0.1.tgz
+ Integrity: sha512-n0jtCOxEovhU7METqSQjcZO9pX53nu9uNIjMS+hEt+Nt9jA7oOZoBIgbCxhhASmF6T6rPDGge5UAvh6Z4eFz/g==
+@babel/plugin-syntax-typescript@8.0.3 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-syntax-typescript
+ Package: https://www.npmjs.com/package/@babel/plugin-syntax-typescript/v/8.0.3
+ Archive: https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-8.0.3.tgz
+ Integrity: sha512-jmTPwps7oSQSZaV1SxkQ3C12UWyufGysGc5OzDpZzvPAIX4mO7dJT3hoqkWVrSImvkcMiknir1iLN1SNV/CZzg==
+@babel/plugin-transform-destructuring@8.0.5 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-transform-destructuring
+ Package: https://www.npmjs.com/package/@babel/plugin-transform-destructuring/v/8.0.5
+ Archive: https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-8.0.5.tgz
+ Integrity: sha512-evjxc5fGvpXG2WMGSalpX8IJZVumLIDgk5r5eRhvXRDqFX/GPWq1BeRfJalPCcwJfrR5A/wvegUhx9W5Nr7/mg==
+@babel/plugin-transform-explicit-resource-management@8.0.5 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-transform-explicit-resource-management
+ Package: https://www.npmjs.com/package/@babel/plugin-transform-explicit-resource-management/v/8.0.5
+ Archive: https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-8.0.5.tgz
+ Integrity: sha512-Vr20jk/ZxRGu30o0jYvcKl2Pf0tiyr8H9q2ZGxYD6yjOpv9DdP6//I85q0cSOdADZhsu/ZZoApjGVCfl4xnVJw==
+@babel/plugin-transform-modules-commonjs@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-transform-modules-commonjs
+ Package: https://www.npmjs.com/package/@babel/plugin-transform-modules-commonjs/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-8.0.1.tgz
+ Integrity: sha512-PMuzulWrrzFNmY3lXSk/tV9NRb7y0eZZLJY4UEo2TKszroxvUZHAPPi+T9FDyrQhod+TQA+t+8/QYaaMpiEuhA==
+@babel/plugin-transform-react-display-name@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-transform-react-display-name
+ Package: https://www.npmjs.com/package/@babel/plugin-transform-react-display-name/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-8.0.1.tgz
+ Integrity: sha512-soLishXlkyu6jcICPyO3HEP7A3GCzKEnn7XfvYrImuWEOwFAz93qShmWSYPf5ww0ZkO4By0zsN2bVIDF54fSdA==
+@babel/plugin-transform-react-jsx@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-transform-react-jsx
+ Package: https://www.npmjs.com/package/@babel/plugin-transform-react-jsx/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-8.0.1.tgz
+ Integrity: sha512-NgkoF7Uq+30TmOPDdNUimT0Nta02uVjqJRFNlVWKrbOCu/CkzfHa4aMnIs0lMpkMmZmWA1e42Va+F04i/pY1zw==
+@babel/plugin-transform-react-jsx-development@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-transform-react-jsx-development
+ Package: https://www.npmjs.com/package/@babel/plugin-transform-react-jsx-development/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-8.0.1.tgz
+ Integrity: sha512-Hb+HUZpV9KFHjm+F+P3aLDMi8QXU9l3ROCQv20z18Me2sGyW5nNNR5YTevNlgHvCpFek3BnAwhDGq/BRndXViw==
+@babel/plugin-transform-react-pure-annotations@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-transform-react-pure-annotations
+ Package: https://www.npmjs.com/package/@babel/plugin-transform-react-pure-annotations/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-8.0.1.tgz
+ Integrity: sha512-7/8UwU8hoPBurXa9tUiTTC8aACTRy5tCqLUtqikHp2eGiWoEB57AduOdbQ71OOMTEvawKrGhv3WfzkDpI+/oSg==
+@babel/plugin-transform-typescript@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/plugin-transform-typescript
+ Package: https://www.npmjs.com/package/@babel/plugin-transform-typescript/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-8.0.6.tgz
+ Integrity: sha512-WF3yRlQtClzScvQbWct00ix63+w3DPiyU0WmCBU4YqK539n3VvJFElElpevoD8wfu6953CbiNoUZBi2B7/5jXw==
+@babel/preset-react@8.0.1 | development | MIT
+ Lockfile path: node_modules/@babel/preset-react
+ Package: https://www.npmjs.com/package/@babel/preset-react/v/8.0.1
+ Archive: https://registry.npmjs.org/@babel/preset-react/-/preset-react-8.0.1.tgz
+ Integrity: sha512-jrFuPp/pTddFZbtmWhdLNAYc6UMcpboeUPnw0BBrm4nOmcAko/1TRcFi1PzWCeOFRU+VaSiKmat87W1HvR7mIg==
+@babel/preset-typescript@8.0.7 | development | MIT
+ Lockfile path: node_modules/@babel/preset-typescript
+ Package: https://www.npmjs.com/package/@babel/preset-typescript/v/8.0.7
+ Archive: https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-8.0.7.tgz
+ Integrity: sha512-sUFLFphAG6YNjCnJpSKpyoNxkfyeM2KNuUjJJN0R6dZusMcK2V3Fg11MVHPFvb37AZeDNocc+xM748vdh9z82Q==
+@babel/template@8.0.0 | development | MIT
+ Lockfile path: node_modules/@babel/template
+ Package: https://www.npmjs.com/package/@babel/template/v/8.0.0
+ Archive: https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz
+ Integrity: sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==
+@babel/traverse@8.0.7 | development | MIT
+ Lockfile path: node_modules/@babel/traverse
+ Package: https://www.npmjs.com/package/@babel/traverse/v/8.0.7
+ Archive: https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.7.tgz
+ Integrity: sha512-bVV0brinsTWgc6ptSIoQo3GbITtl0VZAqEUJbHHcyfDPEu+q/fEvlYEa+6H85HoK8IdksKBFtKJbJFZ6Kff+zg==
+@babel/parser@8.0.7 | development | MIT
+ Lockfile path: node_modules/@babel/traverse/node_modules/@babel/parser
+ Package: https://www.npmjs.com/package/@babel/parser/v/8.0.7
+ Archive: https://registry.npmjs.org/@babel/parser/-/parser-8.0.7.tgz
+ Integrity: sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==
+@babel/types@8.0.6 | development | MIT
+ Lockfile path: node_modules/@babel/traverse/node_modules/@babel/types
+ Package: https://www.npmjs.com/package/@babel/types/v/8.0.6
+ Archive: https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz
+ Integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==
+obug@3.0.0 | development | MIT
+ Lockfile path: node_modules/@babel/traverse/node_modules/obug
+ Package: https://www.npmjs.com/package/obug/v/3.0.0
+ Archive: https://registry.npmjs.org/obug/-/obug-3.0.0.tgz
+ Integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==
@babel/types@8.0.4 | runtime | MIT
Lockfile path: node_modules/@babel/types
Package: https://www.npmjs.com/package/@babel/types/v/8.0.4
@@ -190,6 +405,91 @@ commander@13.1.0 | runtime | MIT
Package: https://www.npmjs.com/package/@iconify/types/v/2.0.0
Archive: https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz
Integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==
+@inquirer/ansi@2.0.9 | development | MIT
+ Lockfile path: node_modules/@inquirer/ansi
+ Package: https://www.npmjs.com/package/@inquirer/ansi/v/2.0.9
+ Archive: https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.9.tgz
+ Integrity: sha512-0Ywt3S8tqM6TWUcsRlBFQ1x+X3/o8kktX0ZiKAnByYb9GFDknnOhse1PVtfiD2BXPI8ar+BWx9PV9R0lQIcygw==
+@inquirer/checkbox@5.2.6 | development | MIT
+ Lockfile path: node_modules/@inquirer/checkbox
+ Package: https://www.npmjs.com/package/@inquirer/checkbox/v/5.2.6
+ Archive: https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.6.tgz
+ Integrity: sha512-XkOardciF3tow2NUlY6/Qjk1QqYa+6WpmVLTpxXl97Uv4erczDEZ+xg1j6LqBf0ekcRSUiICQA4nQXHHkVKQ9w==
+@inquirer/confirm@6.3.3 | development | MIT
+ Lockfile path: node_modules/@inquirer/confirm
+ Package: https://www.npmjs.com/package/@inquirer/confirm/v/6.3.3
+ Archive: https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.3.tgz
+ Integrity: sha512-ImOBgpl/sJTFp8naeSkNu4S0Sf8F8pxv3/CUx+LoBAdQvCVqFz6rSB2A/oZYsJge3+yphbEDxEkfaRNYq1OU0A==
+@inquirer/core@12.0.4 | development | MIT
+ Lockfile path: node_modules/@inquirer/core
+ Package: https://www.npmjs.com/package/@inquirer/core/v/12.0.4
+ Archive: https://registry.npmjs.org/@inquirer/core/-/core-12.0.4.tgz
+ Integrity: sha512-M5fB67y7jPObI45vhD82RskWwCSm0scOvenV/I6K8tIi2gfr9DXqQqJ+iw+Wi6SgapikdOiJibYzz1gW47fsDg==
+@inquirer/editor@5.3.4 | development | MIT
+ Lockfile path: node_modules/@inquirer/editor
+ Package: https://www.npmjs.com/package/@inquirer/editor/v/5.3.4
+ Archive: https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.4.tgz
+ Integrity: sha512-imzxQDfw26NpcJAubMj/dE5RjU9Mp80IcT+FzsD6YoxXmL8L1QYnAFdCNv7ccGWh//73+0YP+1kfhwnExdl2VQ==
+@inquirer/expand@5.1.6 | development | MIT
+ Lockfile path: node_modules/@inquirer/expand
+ Package: https://www.npmjs.com/package/@inquirer/expand/v/5.1.6
+ Archive: https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.6.tgz
+ Integrity: sha512-JfiLKi7qGuHmcQO/DqR10UFEp054Y1FXZaszP5ZEfHhgHeuFoT5nNFQIXFhed/5/p07SbsV2tIJFwG/dg5+NmQ==
+@inquirer/external-editor@3.0.6 | development | MIT
+ Lockfile path: node_modules/@inquirer/external-editor
+ Package: https://www.npmjs.com/package/@inquirer/external-editor/v/3.0.6
+ Archive: https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.6.tgz
+ Integrity: sha512-rWmw6WG8JZ7LMn/srXEylYGhL6EXXJeROE7/4x4GREh6NgzqJBDbIgpNGsYoLPfobeb4VZtEYzPGnT5EFNYhiQ==
+@inquirer/figures@2.0.9 | development | MIT
+ Lockfile path: node_modules/@inquirer/figures
+ Package: https://www.npmjs.com/package/@inquirer/figures/v/2.0.9
+ Archive: https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz
+ Integrity: sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==
+@inquirer/input@5.1.7 | development | MIT
+ Lockfile path: node_modules/@inquirer/input
+ Package: https://www.npmjs.com/package/@inquirer/input/v/5.1.7
+ Archive: https://registry.npmjs.org/@inquirer/input/-/input-5.1.7.tgz
+ Integrity: sha512-Icrs9tuMBCxGI+lFYNO71VFOnqGO8IIrLIGqnTGh07X8jZ2la9QLAYxXcikHZQjjVhIZUtU2xyH89DVN/pPhVg==
+@inquirer/number@4.2.4 | development | MIT
+ Lockfile path: node_modules/@inquirer/number
+ Package: https://www.npmjs.com/package/@inquirer/number/v/4.2.4
+ Archive: https://registry.npmjs.org/@inquirer/number/-/number-4.2.4.tgz
+ Integrity: sha512-C6g80U4U2+w2Vxa1CB0MN3a2WnQIfzLSUG0lduO3Au9GsirJsKATZkOwZPK+vZ1BlAlz+/Z5ParfQCS9E0svmg==
+@inquirer/password@5.2.3 | development | MIT
+ Lockfile path: node_modules/@inquirer/password
+ Package: https://www.npmjs.com/package/@inquirer/password/v/5.2.3
+ Archive: https://registry.npmjs.org/@inquirer/password/-/password-5.2.3.tgz
+ Integrity: sha512-1Uo2qfI2K3wXbfunMpvwAzoZPIUf5iEETwDGqRVbswoorXGqYQJ/bfIJFeLj+pxjq+e2UFPNOmx+fsIl4Urigw==
+@inquirer/prompts@8.7.3 | development | MIT
+ Lockfile path: node_modules/@inquirer/prompts
+ Package: https://www.npmjs.com/package/@inquirer/prompts/v/8.7.3
+ Archive: https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.3.tgz
+ Integrity: sha512-DHK20kO+H5k/XcJQpHQUpVQbMYHHhZchlGWhZVMUV4WIuRuBW5744hipOtriE1vMqPLgnIyXFMHWKkIbTepa4Q==
+@inquirer/rawlist@5.3.6 | development | MIT
+ Lockfile path: node_modules/@inquirer/rawlist
+ Package: https://www.npmjs.com/package/@inquirer/rawlist/v/5.3.6
+ Archive: https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.6.tgz
+ Integrity: sha512-2exExhQ5xTBqfN/rRlZkvM1Gm3b0eiLcWe2NqxWxe7qYmnU4M3j/n9TwzfPUb+Q7dAAz9TmBtDWWdF3jbp4ZLA==
+@inquirer/search@4.3.4 | development | MIT
+ Lockfile path: node_modules/@inquirer/search
+ Package: https://www.npmjs.com/package/@inquirer/search/v/4.3.4
+ Archive: https://registry.npmjs.org/@inquirer/search/-/search-4.3.4.tgz
+ Integrity: sha512-xsk0UeFP04jsbEeFMzghjUaH2aHXyt+WykSajQzq/8qyBNHsgGYHea/sFwv5T08kW7Sp+KDX8aF124zPUIs1Ww==
+@inquirer/select@5.2.6 | development | MIT
+ Lockfile path: node_modules/@inquirer/select
+ Package: https://www.npmjs.com/package/@inquirer/select/v/5.2.6
+ Archive: https://registry.npmjs.org/@inquirer/select/-/select-5.2.6.tgz
+ Integrity: sha512-jFu7CFghlr0vpi/326a4wz6BGkJH9X3H/iIu+ahyV2Ja52ktqQE3fC9BYP47JKeSPQYGUv5xquE2Vp9PbepZ3g==
+@inquirer/type@4.2.0 | development | MIT
+ Lockfile path: node_modules/@inquirer/type
+ Package: https://www.npmjs.com/package/@inquirer/type/v/4.2.0
+ Archive: https://registry.npmjs.org/@inquirer/type/-/type-4.2.0.tgz
+ Integrity: sha512-2rnHX8RPo4glJYqnFc/v3nQeIhDcjyRRyaT7MGcfL/8xv/8K03SXYoA2tX06WVSKywWOE/ncbG3dtuzLjR5TCQ==
+@jridgewell/gen-mapping@0.4.0-beta.0 | development | MIT
+ Lockfile path: node_modules/@jridgewell/gen-mapping
+ Package: https://www.npmjs.com/package/@jridgewell/gen-mapping/v/0.4.0-beta.0
+ Archive: https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.4.0-beta.0.tgz
+ Integrity: sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==
@jridgewell/resolve-uri@3.1.2 | runtime | MIT
Lockfile path: node_modules/@jridgewell/resolve-uri
Package: https://www.npmjs.com/package/@jridgewell/resolve-uri/v/3.1.2
@@ -865,6 +1165,11 @@ commander@13.1.0 | runtime | MIT
Package: https://www.npmjs.com/package/@scalar/openapi-types/v/0.8.0
Archive: https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.8.0.tgz
Integrity: sha512-WmaxVSfvY5K/TwcG2B2TU1WOe1As1uc2s7myswtP6dBlcjU3hM08SApxv/jmyGaCE8t4gO5BBhmHY4pDUfmr2g==
+@sec-ant/readable-stream@0.4.1 | development | MIT
+ Lockfile path: node_modules/@sec-ant/readable-stream
+ Package: https://www.npmjs.com/package/@sec-ant/readable-stream/v/0.4.1
+ Archive: https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz
+ Integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==
@shikijs/core@2.5.0 | development | MIT
Lockfile path: node_modules/@shikijs/core
Package: https://www.npmjs.com/package/@shikijs/core/v/2.5.0
@@ -905,6 +1210,41 @@ commander@13.1.0 | runtime | MIT
Package: https://www.npmjs.com/package/@shikijs/vscode-textmate/v/10.0.2
Archive: https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz
Integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
+@sindresorhus/merge-streams@4.0.0 | development | MIT
+ Lockfile path: node_modules/@sindresorhus/merge-streams
+ Package: https://www.npmjs.com/package/@sindresorhus/merge-streams/v/4.0.0
+ Archive: https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz
+ Integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==
+@stryker-mutator/api@10.0.0 | development | Apache-2.0
+ Lockfile path: node_modules/@stryker-mutator/api
+ Package: https://www.npmjs.com/package/@stryker-mutator/api/v/10.0.0
+ Archive: https://registry.npmjs.org/@stryker-mutator/api/-/api-10.0.0.tgz
+ Integrity: sha512-ZtAJ0ZT3MVRCWJTBE2h90XB/6E+4lifHYtcTyNG6nU2nLekPgTo4gD5esjX6Okxo1b/JB4jJzyxYB54fwKAoJw==
+@stryker-mutator/core@10.0.0 | development | Apache-2.0
+ Lockfile path: node_modules/@stryker-mutator/core
+ Package: https://www.npmjs.com/package/@stryker-mutator/core/v/10.0.0
+ Archive: https://registry.npmjs.org/@stryker-mutator/core/-/core-10.0.0.tgz
+ Integrity: sha512-ZvMsRyaXQQ5e6Thcid9pkuODv6Fn9E3nrBQJUap+hcJuGJ4unm26afo3m6YKSjn8kinyxJ/3TXf0cTWRDaTxVw==
+@stryker-mutator/instrumenter@10.0.0 | development | Apache-2.0
+ Lockfile path: node_modules/@stryker-mutator/instrumenter
+ Package: https://www.npmjs.com/package/@stryker-mutator/instrumenter/v/10.0.0
+ Archive: https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-10.0.0.tgz
+ Integrity: sha512-B7Wmn1KlEWyFeOz6D6oGvQGRfi5Xw3VemG6dEKvFQp4qLvxD9Mf4kcZghfxffgnYwXd3bFgqXsJ+ZGlhdfIOrQ==
+@stryker-mutator/typescript-checker@10.0.0 | development | Apache-2.0
+ Lockfile path: node_modules/@stryker-mutator/typescript-checker
+ Package: https://www.npmjs.com/package/@stryker-mutator/typescript-checker/v/10.0.0
+ Archive: https://registry.npmjs.org/@stryker-mutator/typescript-checker/-/typescript-checker-10.0.0.tgz
+ Integrity: sha512-GLvC0LVd8RJ5SxR3eFcgb7DFKmw07DYtPXs942z6Qh7l6p7YBnFyd7lPw7nvA9UKEYDd22embrFmLlyC1XJjXw==
+@stryker-mutator/util@10.0.0 | development | Apache-2.0
+ Lockfile path: node_modules/@stryker-mutator/util
+ Package: https://www.npmjs.com/package/@stryker-mutator/util/v/10.0.0
+ Archive: https://registry.npmjs.org/@stryker-mutator/util/-/util-10.0.0.tgz
+ Integrity: sha512-LzOpHiJaCp2ABQgnPMlrQQcsK43bd5Vo/2FGL78aN62yDoeRQ+4j3tzeuXxK5OAHdC3fUz6TDoy4IsoAuLAd3w==
+@stryker-mutator/vitest-runner@10.0.0 | development | Apache-2.0
+ Lockfile path: node_modules/@stryker-mutator/vitest-runner
+ Package: https://www.npmjs.com/package/@stryker-mutator/vitest-runner/v/10.0.0
+ Archive: https://registry.npmjs.org/@stryker-mutator/vitest-runner/-/vitest-runner-10.0.0.tgz
+ Integrity: sha512-SHK2/vfvRUpiz7jXPnQMBnr6zLdm69DK03Mo5mPhaZWcRSygrKUqYsPqWsXsK+5ySHzlMTfCyFK5NQ/X9sJFFw==
@toon-format/toon@2.3.1 | runtime | MIT
Lockfile path: node_modules/@toon-format/toon
Package: https://www.npmjs.com/package/@toon-format/toon/v/2.3.1
@@ -965,11 +1305,21 @@ commander@13.1.0 | runtime | MIT
Package: https://www.npmjs.com/package/@types/estree/v/1.0.9
Archive: https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz
Integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==
+@types/gensync@1.0.5 | development | MIT
+ Lockfile path: node_modules/@types/gensync
+ Package: https://www.npmjs.com/package/@types/gensync/v/1.0.5
+ Archive: https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz
+ Integrity: sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==
@types/hast@3.0.5 | development | MIT
Lockfile path: node_modules/@types/hast
Package: https://www.npmjs.com/package/@types/hast/v/3.0.5
Archive: https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz
Integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==
+@types/jsesc@2.5.1 | development | MIT
+ Lockfile path: node_modules/@types/jsesc
+ Package: https://www.npmjs.com/package/@types/jsesc/v/2.5.1
+ Archive: https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz
+ Integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==
@types/linkify-it@5.0.0 | development | MIT
Lockfile path: node_modules/@types/linkify-it
Package: https://www.npmjs.com/package/@types/linkify-it/v/5.0.0
@@ -1325,6 +1675,11 @@ algoliasearch@5.59.0 | development | MIT
Package: https://www.npmjs.com/package/algoliasearch/v/5.59.0
Archive: https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.59.0.tgz
Integrity: sha512-wUXzaeI7B526W4y1gFg3lcxgDZ67XSgRJIiellYWOas/pLpO7rOtxm9Gr2E/C8aiSDXIgx1q5TdSsvK67Uakqw==
+angular-html-parser@10.11.0 | development | MIT
+ Lockfile path: node_modules/angular-html-parser
+ Package: https://www.npmjs.com/package/angular-html-parser/v/10.11.0
+ Archive: https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.11.0.tgz
+ Integrity: sha512-3vERzJ65UFDr3C7uozLJwsNcQS3FS784dSh583oDgDTTZMgXe3/pdyXgKndxiP5R2lvYRfW6gSl145Hnyf2OFA==
ansi-escapes@7.3.0 | development | MIT
Lockfile path: node_modules/ansi-escapes
Package: https://www.npmjs.com/package/ansi-escapes/v/7.3.0
@@ -1375,6 +1730,11 @@ base64-js@1.5.1 | runtime | MIT
Package: https://www.npmjs.com/package/base64-js/v/1.5.1
Archive: https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz
Integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+baseline-browser-mapping@2.11.28 | development | Apache-2.0
+ Lockfile path: node_modules/baseline-browser-mapping
+ 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==
bindings@1.5.0 | runtime, optional | MIT
Lockfile path: node_modules/bindings
Package: https://www.npmjs.com/package/bindings/v/1.5.0
@@ -1405,6 +1765,11 @@ brace-expansion@5.0.12 | runtime | MIT
Package: https://www.npmjs.com/package/brace-expansion/v/5.0.12
Archive: https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz
Integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==
+browserslist@4.29.3 | development | MIT
+ Lockfile path: node_modules/browserslist
+ Package: https://www.npmjs.com/package/browserslist/v/4.29.3
+ Archive: https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz
+ Integrity: sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==
buffer@6.0.3 | runtime | MIT
Lockfile path: node_modules/buffer
Package: https://www.npmjs.com/package/buffer/v/6.0.3
@@ -1425,6 +1790,11 @@ 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==
+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
+ Archive: https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz
+ Integrity: sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==
canonicalize@3.0.0 | runtime | Apache-2.0
Lockfile path: node_modules/canonicalize
Package: https://www.npmjs.com/package/canonicalize/v/3.0.0
@@ -1445,6 +1815,11 @@ chai@6.3.0 | development | MIT
Package: https://www.npmjs.com/package/chai/v/6.3.0
Archive: https://registry.npmjs.org/chai/-/chai-6.3.0.tgz
Integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==
+chalk@5.6.2 | development | MIT
+ Lockfile path: node_modules/chalk
+ Package: https://www.npmjs.com/package/chalk/v/5.6.2
+ Archive: https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz
+ Integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==
character-entities-html4@2.1.0 | development | MIT
Lockfile path: node_modules/character-entities-html4
Package: https://www.npmjs.com/package/character-entities-html4/v/2.1.0
@@ -1455,6 +1830,11 @@ character-entities-legacy@3.0.0 | development | MIT
Package: https://www.npmjs.com/package/character-entities-legacy/v/3.0.0
Archive: https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz
Integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==
+chardet@2.2.0 | development | MIT
+ Lockfile path: node_modules/chardet
+ Package: https://www.npmjs.com/package/chardet/v/2.2.0
+ Archive: https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz
+ Integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==
chownr@1.1.4 | runtime, optional | ISC
Lockfile path: node_modules/chownr
Package: https://www.npmjs.com/package/chownr/v/1.1.4
@@ -1475,6 +1855,11 @@ cli-truncate@5.2.0 | development | MIT
Package: https://www.npmjs.com/package/cli-truncate/v/5.2.0
Archive: https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz
Integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==
+cli-width@4.1.0 | development | ISC
+ Lockfile path: node_modules/cli-width
+ 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==
colorette@2.0.20 | development | MIT
Lockfile path: node_modules/colorette
Package: https://www.npmjs.com/package/colorette/v/2.0.20
@@ -1490,6 +1875,11 @@ 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==
+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
+ Archive: https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz
+ Integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
copy-anything@4.1.5 | development | MIT
Lockfile path: node_modules/copy-anything
Package: https://www.npmjs.com/package/copy-anything/v/4.1.5
@@ -1530,6 +1920,11 @@ dequal@2.0.3 | development | MIT
Package: https://www.npmjs.com/package/dequal/v/2.0.3
Archive: https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz
Integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+des.js@1.1.0 | development | MIT
+ Lockfile path: node_modules/des.js
+ 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-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
@@ -1545,6 +1940,11 @@ diff@9.0.0 | runtime | BSD-3-Clause
Package: https://www.npmjs.com/package/diff/v/9.0.0
Archive: https://registry.npmjs.org/diff/-/diff-9.0.0.tgz
Integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==
+diff-match-patch@1.0.5 | development | Apache-2.0
+ Lockfile path: node_modules/diff-match-patch
+ Package: https://www.npmjs.com/package/diff-match-patch/v/1.0.5
+ Archive: https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz
+ Integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
diff3@0.0.3 | runtime | MIT
Lockfile path: node_modules/diff3
Package: https://www.npmjs.com/package/diff3/v/0.0.3
@@ -1555,6 +1955,11 @@ dunder-proto@1.0.1 | runtime | MIT
Package: https://www.npmjs.com/package/dunder-proto/v/1.0.1
Archive: https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz
Integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+electron-to-chromium@1.5.452 | development | ISC
+ Lockfile path: node_modules/electron-to-chromium
+ Package: https://www.npmjs.com/package/electron-to-chromium/v/1.5.452
+ Archive: https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.452.tgz
+ Integrity: sha512-cVvk1YBSj6571VmV6hdJTqj94Y79xSu8cj6Y9t6ae4Y4431z8/7EJFvu3rVp90xhfuv9BpPKUmaLfTo4+FOGBg==
emoji-regex@10.6.0 | development | MIT
Lockfile path: node_modules/emoji-regex
Package: https://www.npmjs.com/package/emoji-regex/v/10.6.0
@@ -1565,6 +1970,11 @@ emoji-regex-xs@1.0.0 | development | MIT
Package: https://www.npmjs.com/package/emoji-regex-xs/v/1.0.0
Archive: https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz
Integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==
+empathic@2.1.0 | development | MIT
+ Lockfile path: node_modules/empathic
+ Package: https://www.npmjs.com/package/empathic/v/2.1.0
+ Archive: https://registry.npmjs.org/empathic/-/empathic-2.1.0.tgz
+ Integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==
end-of-stream@1.4.5 | runtime, optional | MIT
Lockfile path: node_modules/end-of-stream
Package: https://www.npmjs.com/package/end-of-stream/v/1.4.5
@@ -1600,6 +2010,11 @@ es-object-atoms@1.1.2 | runtime | MIT
Package: https://www.npmjs.com/package/es-object-atoms/v/1.1.2
Archive: https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz
Integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==
+escalade@3.2.0 | development | MIT
+ Lockfile path: node_modules/escalade
+ 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==
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
@@ -1635,6 +2050,11 @@ evmole@0.9.3 | runtime | MIT
Package: https://www.npmjs.com/package/evmole/v/0.9.3
Archive: https://registry.npmjs.org/evmole/-/evmole-0.9.3.tgz
Integrity: sha512-UhO5HMAoEPCupSxvBJPlm/L0Qk/KVJ/OJo3r4EyhXADIEHqU5aTkAzsBfzk4lXGjqxVnaYbh1GPSmZtmSHFnQQ==
+execa@9.6.1 | development | MIT
+ Lockfile path: node_modules/execa
+ Package: https://www.npmjs.com/package/execa/v/9.6.1
+ Archive: https://registry.npmjs.org/execa/-/execa-9.6.1.tgz
+ Integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==
expand-template@2.0.3 | runtime, optional | (MIT OR WTFPL)
Lockfile path: node_modules/expand-template
Package: https://www.npmjs.com/package/expand-template/v/2.0.3
@@ -1685,11 +2105,21 @@ fdir@6.5.0 | development | MIT
Package: https://www.npmjs.com/package/fdir/v/6.5.0
Archive: https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz
Integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
+figures@6.1.0 | development | MIT
+ Lockfile path: node_modules/figures
+ Package: https://www.npmjs.com/package/figures/v/6.1.0
+ Archive: https://registry.npmjs.org/figures/-/figures-6.1.0.tgz
+ Integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==
file-uri-to-path@1.0.0 | runtime, optional | MIT
Lockfile path: node_modules/file-uri-to-path
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==
+flru@1.0.2 | development | MIT
+ Lockfile path: node_modules/flru
+ Package: https://www.npmjs.com/package/flru/v/1.0.2
+ Archive: https://registry.npmjs.org/flru/-/flru-1.0.2.tgz
+ Integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==
focus-trap@7.8.0 | development | MIT
Lockfile path: node_modules/focus-trap
Package: https://www.npmjs.com/package/focus-trap/v/7.8.0
@@ -1725,6 +2155,11 @@ function-bind@1.1.2 | runtime | MIT
Package: https://www.npmjs.com/package/function-bind/v/1.1.2
Archive: https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz
Integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+gensync@1.0.0-beta.2 | development | MIT
+ Lockfile path: node_modules/gensync
+ 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-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
@@ -1740,6 +2175,11 @@ get-proto@1.0.1 | runtime | MIT
Package: https://www.npmjs.com/package/get-proto/v/1.0.1
Archive: https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz
Integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+get-stream@9.0.1 | development | MIT
+ Lockfile path: node_modules/get-stream
+ Package: https://www.npmjs.com/package/get-stream/v/9.0.1
+ Archive: https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz
+ Integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==
get-tsconfig@4.14.3 | development | MIT
Lockfile path: node_modules/get-tsconfig
Package: https://www.npmjs.com/package/get-tsconfig/v/4.14.3
@@ -1805,11 +2245,21 @@ html-void-elements@3.0.0 | development | MIT
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==
+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
+ Archive: https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz
+ Integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==
husky@9.1.7 | development | MIT
Lockfile path: node_modules/husky
Package: https://www.npmjs.com/package/husky/v/9.1.7
Archive: https://registry.npmjs.org/husky/-/husky-9.1.7.tgz
Integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==
+iconv-lite@0.7.3 | development | MIT
+ Lockfile path: node_modules/iconv-lite
+ Package: https://www.npmjs.com/package/iconv-lite/v/0.7.3
+ Archive: https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz
+ Integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==
ieee754@1.2.1 | runtime | BSD-3-Clause
Lockfile path: node_modules/ieee754
Package: https://www.npmjs.com/package/ieee754/v/1.2.1
@@ -1820,6 +2270,11 @@ ignore@7.0.6 | runtime | MIT
Package: https://www.npmjs.com/package/ignore/v/7.0.6
Archive: https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz
Integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==
+import-meta-resolve@4.2.0 | development | MIT
+ Lockfile path: node_modules/import-meta-resolve
+ Package: https://www.npmjs.com/package/import-meta-resolve/v/4.2.0
+ Archive: https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz
+ Integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==
imurmurhash@0.1.4 | runtime | MIT
Lockfile path: node_modules/imurmurhash
Package: https://www.npmjs.com/package/imurmurhash/v/0.1.4
@@ -1855,11 +2310,26 @@ is-fullwidth-code-point@5.1.0 | development | MIT
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-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
+ Archive: https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz
+ Integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
+is-stream@4.0.1 | development | MIT
+ Lockfile path: node_modules/is-stream
+ Package: https://www.npmjs.com/package/is-stream/v/4.0.1
+ Archive: https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz
+ Integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==
is-typed-array@1.1.15 | runtime | MIT
Lockfile path: node_modules/is-typed-array
Package: https://www.npmjs.com/package/is-typed-array/v/1.1.15
Archive: https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz
Integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==
+is-unicode-supported@2.1.0 | development | MIT
+ Lockfile path: node_modules/is-unicode-supported
+ Package: https://www.npmjs.com/package/is-unicode-supported/v/2.1.0
+ Archive: https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz
+ Integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==
isarray@2.0.5 | runtime | MIT
Lockfile path: node_modules/isarray
Package: https://www.npmjs.com/package/isarray/v/2.0.5
@@ -1890,6 +2360,11 @@ jose@6.2.3 | runtime | MIT
Package: https://www.npmjs.com/package/jose/v/6.2.3
Archive: https://registry.npmjs.org/jose/-/jose-6.2.3.tgz
Integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==
+js-md4@0.3.2 | development | MIT
+ Lockfile path: node_modules/js-md4
+ Package: https://www.npmjs.com/package/js-md4/v/0.3.2
+ Archive: https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz
+ Integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==
js-tokens@10.0.0 | development | MIT
Lockfile path: node_modules/js-tokens
Package: https://www.npmjs.com/package/js-tokens/v/10.0.0
@@ -1930,11 +2405,26 @@ 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==
+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-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
+ Archive: https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.8.1.tgz
+ Integrity: sha512-BmN42JKn6FI36pcMMzHCXQhpKNEfbeqgSIcUYHKnifq0/4Ey6PDinovwUOgajT7NsgUtpWdDXaCWlpI4LHXN3g==
json-schema-traverse@1.0.0 | runtime | MIT
Lockfile path: node_modules/json-schema-traverse
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==
+json5@2.2.3 | development | MIT
+ Lockfile path: node_modules/json5
+ Package: https://www.npmjs.com/package/json5/v/2.2.3
+ Archive: https://registry.npmjs.org/json5/-/json5-2.2.3.tgz
+ Integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jsonc-parser@3.3.1 | runtime | MIT
Lockfile path: node_modules/jsonc-parser
Package: https://www.npmjs.com/package/jsonc-parser/v/3.3.1
@@ -2020,6 +2510,11 @@ 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==
+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==
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
@@ -2120,6 +2615,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==
+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
+ Archive: https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz
+ Integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimatch@10.2.5 | runtime | BlueOak-1.0.0
Lockfile path: node_modules/minimatch
Package: https://www.npmjs.com/package/minimatch/v/10.2.5
@@ -2155,6 +2655,31 @@ 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==
+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
+ Archive: https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz
+ Integrity: sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==
+mutation-testing-elements@3.8.4 | development | Apache-2.0
+ Lockfile path: node_modules/mutation-testing-elements
+ Package: https://www.npmjs.com/package/mutation-testing-elements/v/3.8.4
+ Archive: https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.8.4.tgz
+ Integrity: sha512-5CF1SNa7at5ZH33vEr+21wNebTSrtNIVvnzaUlxortHajOrIPaSLczIWvg6sI/fsExekQ7jookwf2cHftkckqQ==
+mutation-testing-metrics@3.8.4 | development | Apache-2.0
+ Lockfile path: node_modules/mutation-testing-metrics
+ Package: https://www.npmjs.com/package/mutation-testing-metrics/v/3.8.4
+ Archive: https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.8.4.tgz
+ Integrity: sha512-DZcmndJBH6nrNs3tpiB3OcMVq9KkG2cHCpJSnDxSxPwi9qrafRmoec40xjhkbzOoX1n7/4UkDqg5tIj4A6nvCw==
+mutation-testing-report-schema@3.8.4 | development | Apache-2.0
+ Lockfile path: node_modules/mutation-testing-report-schema
+ Package: https://www.npmjs.com/package/mutation-testing-report-schema/v/3.8.4
+ Archive: https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.8.4.tgz
+ Integrity: sha512-s4G71R6Lt/PpZ0cqeglIcgyBdzLM8E+SeCHZAPg1wkSsPtRBa4XfPzAozYKdiJk/TLbNEEb7En9t0/bveuPuxA==
+mute-stream@3.0.0 | development | ISC
+ Lockfile path: node_modules/mute-stream
+ Package: https://www.npmjs.com/package/mute-stream/v/3.0.0
+ Archive: https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz
+ Integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==
nanoid@3.3.20 | development | MIT
Lockfile path: node_modules/nanoid
Package: https://www.npmjs.com/package/nanoid/v/3.3.20
@@ -2170,6 +2695,26 @@ node-abi@3.98.0 | runtime, optional | MIT
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-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==
+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
+ Archive: https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz
+ Integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==
+path-key@4.0.0 | development | MIT
+ Lockfile path: node_modules/npm-run-path/node_modules/path-key
+ 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==
+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
+ Archive: https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz
+ Integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
obug@2.2.1 | development | MIT
Lockfile path: node_modules/obug
Package: https://www.npmjs.com/package/obug/v/2.2.1
@@ -2230,6 +2775,11 @@ 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-ms@4.0.0 | development | MIT
+ Lockfile path: node_modules/parse-ms
+ Package: https://www.npmjs.com/package/parse-ms/v/4.0.0
+ Archive: https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz
+ Integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==
parse5@8.0.1 | runtime | MIT
Lockfile path: node_modules/parse5
Package: https://www.npmjs.com/package/parse5/v/8.0.1
@@ -2320,6 +2870,11 @@ prebuild-install@7.1.3 | runtime, optional | MIT
Package: https://www.npmjs.com/package/prebuild-install/v/7.1.3
Archive: https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz
Integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==
+pretty-ms@9.3.1 | development | MIT
+ Lockfile path: node_modules/pretty-ms
+ Package: https://www.npmjs.com/package/pretty-ms/v/9.3.1
+ Archive: https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz
+ Integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==
process@0.11.10 | runtime | MIT
Lockfile path: node_modules/process
Package: https://www.npmjs.com/package/process/v/0.11.10
@@ -2330,6 +2885,11 @@ process-warning@5.0.0 | runtime | MIT
Package: https://www.npmjs.com/package/process-warning/v/5.0.0
Archive: https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz
Integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==
+progress@2.0.3 | development | MIT
+ Lockfile path: node_modules/progress
+ Package: https://www.npmjs.com/package/progress/v/2.0.3
+ Archive: https://registry.npmjs.org/progress/-/progress-2.0.3.tgz
+ Integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
property-information@7.2.0 | development | MIT
Lockfile path: node_modules/property-information
Package: https://www.npmjs.com/package/property-information/v/7.2.0
@@ -2345,6 +2905,11 @@ pure-rand@8.4.2 | development | MIT
Package: https://www.npmjs.com/package/pure-rand/v/8.4.2
Archive: https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz
Integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==
+qs@6.15.1 | development | BSD-3-Clause
+ Lockfile path: node_modules/qs
+ Package: https://www.npmjs.com/package/qs/v/6.15.1
+ Archive: https://registry.npmjs.org/qs/-/qs-6.15.1.tgz
+ Integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==
quick-format-unescaped@4.0.4 | runtime | MIT
Lockfile path: node_modules/quick-format-unescaped
Package: https://www.npmjs.com/package/quick-format-unescaped/v/4.0.4
@@ -2415,6 +2980,11 @@ rollup@4.64.1 | development | MIT
Package: https://www.npmjs.com/package/rollup/v/4.64.1
Archive: https://registry.npmjs.org/rollup/-/rollup-4.64.1.tgz
Integrity: sha512-7s4V+3J8ux75WNj9qwSJnrWmlCmZ2YHr2fojtHy5RiIMvhJv6DGe7HKKz7L6G1sq1Eg1fLC8SjYtAtDis1F4CQ==
+rxjs@7.8.2 | development | Apache-2.0
+ Lockfile path: node_modules/rxjs
+ Package: https://www.npmjs.com/package/rxjs/v/7.8.2
+ Archive: https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz
+ Integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==
safe-buffer@5.2.1 | runtime | MIT
Lockfile path: node_modules/safe-buffer
Package: https://www.npmjs.com/package/safe-buffer/v/5.2.1
@@ -2425,6 +2995,11 @@ safe-stable-stringify@2.5.0 | runtime | MIT
Package: https://www.npmjs.com/package/safe-stable-stringify/v/2.5.0
Archive: https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz
Integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
+safer-buffer@2.1.2 | development | MIT
+ Lockfile path: node_modules/safer-buffer
+ Package: https://www.npmjs.com/package/safer-buffer/v/2.1.2
+ Archive: https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz
+ Integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
search-insights@2.17.3 | development | MIT
Lockfile path: node_modules/search-insights
Package: https://www.npmjs.com/package/search-insights/v/2.17.3
@@ -2460,6 +3035,26 @@ shiki@2.5.0 | development | MIT
Package: https://www.npmjs.com/package/shiki/v/2.5.0
Archive: https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz
Integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==
+side-channel@1.1.1 | development | MIT
+ Lockfile path: node_modules/side-channel
+ Package: https://www.npmjs.com/package/side-channel/v/1.1.1
+ Archive: https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz
+ Integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==
+side-channel-list@1.0.1 | development | MIT
+ Lockfile path: node_modules/side-channel-list
+ Package: https://www.npmjs.com/package/side-channel-list/v/1.0.1
+ Archive: https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz
+ Integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==
+side-channel-map@1.0.1 | development | MIT
+ Lockfile path: node_modules/side-channel-map
+ Package: https://www.npmjs.com/package/side-channel-map/v/1.0.1
+ Archive: https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz
+ Integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
+side-channel-weakmap@1.0.2 | development | MIT
+ Lockfile path: node_modules/side-channel-weakmap
+ Package: https://www.npmjs.com/package/side-channel-weakmap/v/1.0.2
+ Archive: https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz
+ Integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
signal-exit@4.1.0 | runtime | ISC
Lockfile path: node_modules/signal-exit
Package: https://www.npmjs.com/package/signal-exit/v/4.1.0
@@ -2495,6 +3090,11 @@ sonic-boom@4.2.1 | runtime | MIT
Package: https://www.npmjs.com/package/sonic-boom/v/4.2.1
Archive: https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz
Integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==
+source-map@0.7.6 | development | BSD-3-Clause
+ Lockfile path: node_modules/source-map
+ Package: https://www.npmjs.com/package/source-map/v/0.7.6
+ Archive: https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz
+ Integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==
source-map-js@1.2.2 | development | BSD-3-Clause
Lockfile path: node_modules/source-map-js
Package: https://www.npmjs.com/package/source-map-js/v/1.2.2
@@ -2555,6 +3155,11 @@ strip-ansi@7.2.0 | development | MIT
Package: https://www.npmjs.com/package/strip-ansi/v/7.2.0
Archive: https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz
Integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==
+strip-final-newline@4.0.0 | development | MIT
+ Lockfile path: node_modules/strip-final-newline
+ 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-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
@@ -2625,16 +3230,26 @@ tokenx@1.3.0 | runtime | MIT
Package: https://www.npmjs.com/package/tokenx/v/1.3.0
Archive: https://registry.npmjs.org/tokenx/-/tokenx-1.3.0.tgz
Integrity: sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ==
+tree-kill@1.2.2 | development | MIT
+ Lockfile path: node_modules/tree-kill
+ Package: https://www.npmjs.com/package/tree-kill/v/1.2.2
+ Archive: https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz
+ Integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
trim-lines@3.0.1 | development | MIT
Lockfile path: node_modules/trim-lines
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==
-tslib@2.8.1 | development, optional | 0BSD
+tslib@2.8.1 | development | 0BSD
Lockfile path: node_modules/tslib
Package: https://www.npmjs.com/package/tslib/v/2.8.1
Archive: https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz
Integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+tunnel@0.0.6 | development | MIT
+ Lockfile path: node_modules/tunnel
+ Package: https://www.npmjs.com/package/tunnel/v/0.0.6
+ Archive: https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz
+ Integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
tunnel-agent@0.6.0 | runtime, optional | Apache-2.0
Lockfile path: node_modules/tunnel-agent
Package: https://www.npmjs.com/package/tunnel-agent/v/0.6.0
@@ -2650,6 +3265,16 @@ typed-array-buffer@1.0.3 | runtime | MIT
Package: https://www.npmjs.com/package/typed-array-buffer/v/1.0.3
Archive: https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz
Integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==
+typed-inject@5.0.0 | development | Apache-2.0
+ Lockfile path: node_modules/typed-inject
+ Package: https://www.npmjs.com/package/typed-inject/v/5.0.0
+ Archive: https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz
+ Integrity: sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==
+typed-rest-client@2.3.1 | development | MIT
+ Lockfile path: node_modules/typed-rest-client
+ Package: https://www.npmjs.com/package/typed-rest-client/v/2.3.1
+ Archive: https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz
+ Integrity: sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==
typescript@7.0.2 | development | Apache-2.0
Lockfile path: node_modules/typescript
Package: https://www.npmjs.com/package/typescript/v/7.0.2
@@ -2660,16 +3285,31 @@ typescript@5.9.3 | development | Apache-2.0
Package: https://www.npmjs.com/package/typescript/v/5.9.3
Archive: https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz
Integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
+typescript@6.0.3 | development | Apache-2.0
+ Lockfile path: node_modules/typescript6
+ 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==
unbash@4.0.12 | development | ISC
Lockfile path: node_modules/unbash
Package: https://www.npmjs.com/package/unbash/v/4.0.12
Archive: https://registry.npmjs.org/unbash/-/unbash-4.0.12.tgz
Integrity: sha512-PnKvRX+Fbxl/usVlTr/31tXIOVAH+8OdC9qaxUQcvurwEnoc3Vk/ipi2Ce8IjMxzK9byfSGdsUBjqqJGc1o5lw==
+underscore@1.13.8 | development | MIT
+ Lockfile path: node_modules/underscore
+ Package: https://www.npmjs.com/package/underscore/v/1.13.8
+ Archive: https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz
+ Integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==
undici-types@8.9.0 | development | MIT
Lockfile path: node_modules/undici-types
Package: https://www.npmjs.com/package/undici-types/v/8.9.0
Archive: https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz
Integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==
+unicorn-magic@0.3.0 | development | MIT
+ Lockfile path: node_modules/unicorn-magic
+ Package: https://www.npmjs.com/package/unicorn-magic/v/0.3.0
+ Archive: https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz
+ Integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==
unist-util-is@6.0.1 | development | MIT
Lockfile path: node_modules/unist-util-is
Package: https://www.npmjs.com/package/unist-util-is/v/6.0.1
@@ -2695,11 +3335,21 @@ 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==
+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
+ Archive: https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz
+ Integrity: sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==
util-deprecate@1.0.2 | runtime, optional | MIT
Lockfile path: node_modules/util-deprecate
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==
+verkit@0.5.0 | development | MIT
+ Lockfile path: node_modules/verkit
+ Package: https://www.npmjs.com/package/verkit/v/0.5.0
+ Archive: https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz
+ Integrity: sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==
vfile@6.0.3 | development | MIT
Lockfile path: node_modules/vfile
Package: https://www.npmjs.com/package/vfile/v/6.0.3
@@ -2880,6 +3530,11 @@ walk-up-path@4.0.0 | development | ISC
Package: https://www.npmjs.com/package/walk-up-path/v/4.0.0
Archive: https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz
Integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==
+weapon-regex@2.0.6 | development | Apache-2.0
+ Lockfile path: node_modules/weapon-regex
+ Package: https://www.npmjs.com/package/weapon-regex/v/2.0.6
+ Archive: https://registry.npmjs.org/weapon-regex/-/weapon-regex-2.0.6.tgz
+ Integrity: sha512-iJC2vqYp+bh3r4X6zjuP0h65XqXjgUX3nJgmhj0PQMrdhAbUwCfN76CSTEEQqV3G7468kjOJ69PmytTG8l/8Gg==
which@2.0.2 | runtime | ISC
Lockfile path: node_modules/which
Package: https://www.npmjs.com/package/which/v/2.0.2
@@ -2930,6 +3585,11 @@ yaml@2.9.0 | runtime | ISC
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==
+yoctocolors@2.2.0 | development | MIT
+ Lockfile path: node_modules/yoctocolors
+ Package: https://www.npmjs.com/package/yoctocolors/v/2.2.0
+ Archive: https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz
+ Integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==
zod@4.4.3 | runtime | MIT
Lockfile path: node_modules/zod
Package: https://www.npmjs.com/package/zod/v/4.4.3
diff --git a/licenses/npm-runtime-notices.txt b/licenses/npm-runtime-notices.txt
index 2a0c45d24..a2032f7a7 100644
--- a/licenses/npm-runtime-notices.txt
+++ b/licenses/npm-runtime-notices.txt
@@ -1,12 +1,12 @@
REA third-party npm runtime copyright, license and NOTICE texts
Collected from integrity-verified locked npm archives. Release-specific supplements and upstream notice gaps are identified in their entries.
External engines are separately obtained; see THIRD_PARTY_NOTICES.md.
-Runtime dependency fingerprint (SHA-256): 63d5b55e027a0cfb3e77989ca337aca905f1652d10cfbfc4d4dd876d3ecd4f3a
+Runtime dependency fingerprint (SHA-256): b0d0151958456c6e72c5720af80d783893be88a7b4c9a409db8505296fe10bd9
-===== @babel/helper-string-parser@8.0.0 =====
+===== @babel/helper-string-parser@8.0.6 =====
Declared license: MIT
-Archive: https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz
-Integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==
+Archive: https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.6.tgz
+Integrity: sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==
Scope: npm runtime dependency, including optional platform packages.
--- package/LICENSE ---
@@ -34,11 +34,10 @@ 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.
-
-===== @babel/helper-validator-identifier@8.0.4 =====
+===== @babel/helper-validator-identifier@8.0.6 =====
Declared license: MIT
-Archive: https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz
-Integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==
+Archive: https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz
+Integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==
Scope: npm runtime dependency, including optional platform packages.
--- package/LICENSE ---
@@ -66,7 +65,6 @@ 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.
-
===== @babel/parser@8.0.4 =====
Declared license: MIT
Archive: https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz
@@ -8443,5 +8441,3 @@ 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/native/windows/README.md b/native/windows/README.md
index 86103ce9a..ec2fb1bde 100644
--- a/native/windows/README.md
+++ b/native/windows/README.md
@@ -154,7 +154,9 @@ node scripts/verify-windows-native-inspection.mjs
This artifact injects one access-denied disposition result while using a real
NTFS directory. The verifier checks retained ownership, recovery on a later
query, cached successful admission, and cleanup on environment teardown. It
-does not reproduce or identify the host-dependent cause reported in #1701.
+renames the retained probe and recreates its old path, checking that cleanup
+removes the owned inode while preserving the replacement directory.
+It does not reproduce or identify the host-dependent cause reported in #1701.
The fixture is stored under `.cache/` and is excluded from the shipped addon.
The native lane checks DACL readback independently, source write/replacement
diff --git a/native/windows/src/authority.hpp b/native/windows/src/authority.hpp
index 13d7e4d60..c8bed30dc 100644
--- a/native/windows/src/authority.hpp
+++ b/native/windows/src/authority.hpp
@@ -123,7 +123,8 @@ napi_value wrap(napi_env env, std::unique_ptr resource);
Resource& resource(napi_env env, napi_value value, Kind kind);
std::unique_ptr openFile(const std::wstring& path, DWORD access = GENERIC_READ,
- bool directory = false);
+ bool directory = false,
+ DWORD finalSharing = FILE_SHARE_READ);
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path);
std::unique_ptr createRuntime(const std::wstring& parent, const std::wstring& prefix);
void closeRuntime(Runtime& runtime);
diff --git a/native/windows/src/filesystem.cc b/native/windows/src/filesystem.cc
index 71c8bbd44..e5129dbb7 100644
--- a/native/windows/src/filesystem.cc
+++ b/native/windows/src/filesystem.cc
@@ -61,7 +61,8 @@ static Handle openComponent(const std::wstring& path, DWORD access, bool directo
return handle;
}
-std::unique_ptr openFile(const std::wstring& requested, DWORD access, bool directory) {
+std::unique_ptr openFile(const std::wstring& requested, DWORD access, bool directory,
+ DWORD finalSharing) {
auto result = std::make_unique(localPath(requested));
result->requestedPath = requested;
const auto& path = result->path;
@@ -77,7 +78,8 @@ std::unique_ptr openFile(const std::wstring& requested, DWORD access, bool
while (offset < path.size()) {
auto end = path.find(L'\\', offset);
const bool final = end == std::wstring::npos;
- auto child = openComponent(path.substr(0, end), final ? access : 0, !final || directory);
+ auto child = openComponent(path.substr(0, end), final ? access : 0, !final || directory,
+ final ? finalSharing : FILE_SHARE_READ);
// Once the next component is pinned against deletion, its parent cannot
// become empty and NTFS cannot convert that parent into a reparse point.
// Relax only ancestor write sharing so unrelated child renames can proceed.
@@ -238,6 +240,100 @@ static void verifyPrivate(HANDLE handle, const std::wstring& path, bool root) {
require(user && system, "Private DACL must allow only current user and SYSTEM", path, ERROR_ACCESS_DENIED);
}
+static std::unique_ptr openPrivateBridgeDirectory(const std::wstring& path) {
+ // The retained runtime root handle carries DELETE authority for owned
+ // cleanup. Windows checks sharing in both directions, so this read-only
+ // inspection handle must share DELETE even though it requests no such right.
+ auto directory = openFile(path, FILE_LIST_DIRECTORY, true,
+ FILE_SHARE_READ | FILE_SHARE_DELETE);
+ verifyPrivate(directory->get(), path, true);
+ return directory;
+}
+
+static Handle openPrivateBridgeDescriptor(HANDLE parent, const std::wstring& name,
+ const std::wstring& path) {
+ require(!name.empty() && name.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos &&
+ name != L"." && name != L".." && name.size() <= 255,
+ "Private descriptor requires one filename component", path, ERROR_INVALID_PARAMETER);
+ UNICODE_STRING coordinate{};
+ coordinate.Buffer = const_cast(name.data());
+ coordinate.Length = static_cast(name.size() * sizeof(wchar_t));
+ coordinate.MaximumLength = coordinate.Length;
+ OBJECT_ATTRIBUTES attributes;
+ InitializeObjectAttributes(&attributes, &coordinate, OBJ_CASE_INSENSITIVE, parent, nullptr);
+ IO_STATUS_BLOCK observation{};
+ HANDLE raw = INVALID_HANDLE_VALUE;
+ // Immutable runtime files retain their original writer handle so later
+ // opens cannot acquire write or delete access. This read-only handle must
+ // share those granted rights for Windows' symmetric share check to pass.
+ const auto status = NtCreateFile(&raw, GENERIC_READ | READ_CONTROL | FILE_READ_ATTRIBUTES | SYNCHRONIZE,
+ &attributes, &observation, nullptr, FILE_ATTRIBUTE_NORMAL,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_OPEN,
+ FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT |
+ FILE_NON_DIRECTORY_FILE,
+ nullptr, 0);
+ if (status < 0)
+ throw Failure("Open Cutter bridge descriptor relative to its verified directory failed", path,
+ RtlNtStatusToDosError(status));
+ Handle descriptor(raw);
+ require(descriptor.valid() && observation.Information == FILE_OPENED,
+ "Cutter bridge descriptor was not opened as an existing file", path, ERROR_ACCESS_DENIED);
+ FILE_STANDARD_INFO standard{};
+ require(GetFileInformationByHandleEx(descriptor.get(), FileStandardInfo, &standard, sizeof(standard)),
+ "Read Cutter bridge descriptor metadata failed", path);
+ require(!standard.Directory && standard.EndOfFile.QuadPart >= 0 &&
+ standard.EndOfFile.QuadPart <= 64 * 1024,
+ "Cutter bridge descriptor exceeds its byte limit or is not a file", path, ERROR_FILE_TOO_LARGE);
+ FILE_ATTRIBUTE_TAG_INFO fileAttributes{};
+ require(GetFileInformationByHandleEx(descriptor.get(), FileAttributeTagInfo, &fileAttributes, sizeof(fileAttributes)),
+ "Read Cutter bridge descriptor attributes failed", path);
+ require((fileAttributes.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0,
+ "Cutter bridge descriptor cannot be a reparse point", path, ERROR_REPARSE_TAG_INVALID);
+ verifyPrivate(descriptor.get(), path, false);
+ return descriptor;
+}
+
+static napi_value verifyCutterBridgeDirectory(napi_env env, const std::vector& args) {
+ require(args.size() == 1, "Cutter bridge directory verification requires one path", L"",
+ ERROR_INVALID_PARAMETER);
+ const auto path = wide(env, args[0]);
+ const auto directory = openPrivateBridgeDirectory(path);
+ auto result = object(env);
+ set(env, result, "privateDacl", boolean(env, true));
+ set(env, result, "identity", identity(env, directory->get(), path));
+ return result;
+}
+
+static napi_value readCutterBridgeDescriptor(napi_env env, const std::vector& args) {
+ require(args.size() == 3, "Cutter bridge descriptor read requires directory, name, and byte limit", L"",
+ ERROR_INVALID_PARAMETER);
+ const auto path = wide(env, args[0]);
+ const auto name = wide(env, args[1]);
+ const double maximum = numeric(env, args[2]);
+ require(maximum >= 1 && maximum <= 64 * 1024 && maximum == static_cast(maximum),
+ "Cutter bridge descriptor limit is outside the supported range", path, ERROR_INVALID_PARAMETER);
+ auto directory = openPrivateBridgeDirectory(path);
+ auto descriptor = openPrivateBridgeDescriptor(directory->get(), name, path + L"\\" + name);
+ FILE_STANDARD_INFO standard{};
+ require(GetFileInformationByHandleEx(descriptor.get(), FileStandardInfo, &standard, sizeof(standard)),
+ "Read Cutter bridge descriptor size failed", path);
+ require(standard.EndOfFile.QuadPart <= maximum,
+ "Cutter bridge descriptor exceeds the requested byte limit", path, ERROR_FILE_TOO_LARGE);
+ std::vector bytes(static_cast(standard.EndOfFile.QuadPart));
+ size_t offset = 0;
+ while (offset < bytes.size()) {
+ DWORD count = 0;
+ const DWORD requested = static_cast(std::min(bytes.size() - offset, MAXDWORD));
+ require(ReadFile(descriptor.get(), bytes.data() + offset, requested, &count, nullptr) && count > 0,
+ "Read Cutter bridge descriptor through its verified handle failed", path);
+ offset += count;
+ }
+ napi_value result;
+ const void* source = bytes.empty() ? static_cast("") : bytes.data();
+ check(napi_create_buffer_copy(env, bytes.size(), source, nullptr, &result));
+ return result;
+}
+
std::unique_ptr createRuntime(const std::wstring& parentPath, const std::wstring& prefix) {
std::unique_ptr parent;
try { parent = openFile(parentPath, 0, true); }
@@ -559,6 +655,8 @@ static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root,
}
napi_value filesystemCall(napi_env env, const std::wstring& operation, const std::vector& args) {
+ if (operation == L"cutter_bridge_verify_directory") return verifyCutterBridgeDirectory(env, args);
+ if (operation == L"cutter_bridge_read_descriptor") return readCutterBridgeDescriptor(env, args);
const auto expected = operation == L"open" || operation == L"close" || operation == L"runtime_close" || operation == L"runtime_snapshot_cancel" ? 1
: operation == L"runtime_create" || operation == L"runtime_mkdir" || operation == L"runtime_open" ? 2 : 3;
require(args.size() == static_cast(expected), "Wrong native filesystem argument count", L"", ERROR_INVALID_PARAMETER);
diff --git a/native/windows/src/process.cc b/native/windows/src/process.cc
index 80a4d0ac5..42ee9582b 100644
--- a/native/windows/src/process.cc
+++ b/native/windows/src/process.cc
@@ -464,6 +464,7 @@ napi_value inspect(napi_env env, std::unique_ptr& probe) {
set(env, result, "architecture", string(env, std::string("x64")));
set(env, result, "filesystem", filesystem);
set(env, result, "privateDacl", boolean(env, true));
+ set(env, result, "privateBridgeDescriptorAccess", boolean(env, true));
set(env, result, "atomicJobAssignment", boolean(env, true));
set(env, result, "killOnOwnerClose", boolean(env, true));
set(env, result, "protocolStdin", boolean(env, true));
diff --git a/package-lock.json b/package-lock.json
index 7eae3799e..48764a796 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54,6 +54,9 @@
},
"devDependencies": {
"@fast-check/vitest": "0.5.0",
+ "@stryker-mutator/core": "10.0.0",
+ "@stryker-mutator/typescript-checker": "10.0.0",
+ "@stryker-mutator/vitest-runner": "10.0.0",
"@types/cross-spawn": "6.0.6",
"@types/node": "26.6.4",
"@types/semver": "7.7.1",
@@ -70,6 +73,7 @@
"turbo": "2.11.7",
"typescript": "7.0.2",
"typescript-termux": "npm:typescript@5.9.3",
+ "typescript6": "npm:typescript@6.0.3",
"vitepress": "1.6.4",
"vitest": "5.0.3"
},
@@ -338,24 +342,390 @@
"node": ">= 14.0.0"
}
},
- "node_modules/@babel/helper-string-parser": {
+ "node_modules/@babel/code-frame": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.6.tgz",
+ "integrity": "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^8.0.6",
+ "js-tokens": "^10.0.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.5.tgz",
+ "integrity": "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.7.tgz",
+ "integrity": "sha512-ZIsZWBG6Y1oa+8fkQVp0xeCdjCHBkXFCCIWrmg9SvejVvfcVQgKKRmGrBDWfM7rilhvnRoEuaqa5dkK7KB3pHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^8.0.6",
+ "@babel/generator": "^8.0.6",
+ "@babel/helper-compilation-targets": "^8.0.7",
+ "@babel/helpers": "^8.0.7",
+ "@babel/parser": "^8.0.7",
+ "@babel/template": "^8.0.0",
+ "@babel/traverse": "^8.0.7",
+ "@babel/types": "^8.0.6",
+ "@types/gensync": "^1.0.5",
+ "convert-source-map": "^2.0.0",
+ "empathic": "^2.0.1",
+ "gensync": "^1.0.0-beta.2",
+ "import-meta-resolve": "^4.2.0",
+ "json5": "^2.2.3",
+ "obug": "^3.0.0",
+ "verkit": "^0.5.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/@babel/parser": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.7.tgz",
+ "integrity": "sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^8.0.6"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/core/node_modules/@babel/types": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz",
+ "integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^8.0.6",
+ "@babel/helper-validator-identifier": "^8.0.6"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/core/node_modules/obug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
+ "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.6.tgz",
+ "integrity": "sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^8.0.6",
+ "@babel/types": "^8.0.6",
+ "@jridgewell/gen-mapping": "0.4.0-beta.0",
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "@types/jsesc": "^2.5.0",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/generator/node_modules/@babel/parser": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.7.tgz",
+ "integrity": "sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^8.0.6"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/generator/node_modules/@babel/types": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz",
+ "integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^8.0.6",
+ "@babel/helper-validator-identifier": "^8.0.6"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
"version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-8.0.0.tgz",
+ "integrity": "sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^8.0.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.7.tgz",
+ "integrity": "sha512-ilAZCdOOlOVA7dQbaFNw2f/59p6GW33NxTopc3DvEE3efp3ZXTrGX/yZScaKw8/vd9bq31Ag0EcXiLw32bAgJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^8.0.5",
+ "@babel/helper-validator-option": "^8.0.0",
+ "browserslist": "^4.24.0",
+ "flru": "1.0.2",
+ "verkit": "^0.5.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-8.0.7.tgz",
+ "integrity": "sha512-7ug3KrOhNVPPxFFLTel0/pMzimKzU/vTlNJKVL/dKguCpmNJZjjibyFbWg+Qs8QHmiIZPNuZ2UNnDGdHWGEpOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^8.0.0",
+ "@babel/helper-member-expression-to-functions": "^8.0.5",
+ "@babel/helper-optimise-call-expression": "^8.0.0",
+ "@babel/helper-replace-supers": "^8.0.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0",
+ "@babel/traverse": "^8.0.7",
+ "verkit": "^0.5.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.6.tgz",
+ "integrity": "sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-8.0.5.tgz",
+ "integrity": "sha512-GLe05QD98BkNFTkjaqeqF9QSRoHLKrrB4tpoplyQuuPrQJ72rtmkM4wvqJLX/sjZPqkbK5MUYSsuUTqdJoOVjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^8.0.5",
+ "@babel/types": "^8.0.5"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz",
+ "integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^8.0.6",
+ "@babel/helper-validator-identifier": "^8.0.6"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-8.0.0.tgz",
+ "integrity": "sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^8.0.0",
+ "@babel/types": "^8.0.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-8.0.6.tgz",
+ "integrity": "sha512-YUi+3X0cb0v97xKhuqkLdrwQtP6XXv1eVbhLRCFyMwG481UskbMQk4mjH7K/Jg3oJ1KLskvFwwq4kF/QnABkWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^8.0.0",
+ "@babel/helper-validator-identifier": "^8.0.6",
+ "@babel/traverse": "^8.0.6"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-8.0.0.tgz",
+ "integrity": "sha512-3W6satvtPuCUkUx63S2jMoW9EQNYkADgs1HTfufmL7gCmAulHMKupA/12WNz4A0GMMFn/YnWWwqOT9IZrJHQjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^8.0.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-8.0.1.tgz",
+ "integrity": "sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-8.0.1.tgz",
+ "integrity": "sha512-B1SZADIcy3tmH8CmWvj4SHi/oAPom4UL3uknTc2QRNsPVLFk/sPnZvQL/8kj7Y5omvjMqie0vklvs6XM4OLW5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^8.0.0",
+ "@babel/helper-optimise-call-expression": "^8.0.0",
+ "@babel/traverse": "^8.0.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-8.0.0.tgz",
+ "integrity": "sha512-xmCA9kP3IhySsqhzwIdWGlDN/1A4cCKNBO/uwZx/3YzmDoMePwno2Q5/Bq0q+tYaKbeF940YiKV/kaW8Mzvpjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^8.0.0",
+ "@babel/types": "^8.0.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.6.tgz",
+ "integrity": "sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==",
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz",
+ "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==",
"license": "MIT",
"engines": {
"node": "^22.18.0 || >=24.11.0"
}
},
+ "node_modules/@babel/helper-validator-option": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz",
+ "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.7.tgz",
+ "integrity": "sha512-5dMj5kFwS2AY7Dt4PZp4px3c5ThMbbEa7tZfuHTncCGSFYq8uoA1ufDE1IS/lYpQedE+3+GQ3cPZUiplIu195w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^8.0.0",
+ "@babel/types": "^8.0.6"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/helpers/node_modules/@babel/types": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz",
+ "integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^8.0.6",
+ "@babel/helper-validator-identifier": "^8.0.6"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
"node_modules/@babel/parser": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
@@ -371,6 +741,329 @@
"node": "^22.18.0 || >=24.11.0"
}
},
+ "node_modules/@babel/plugin-proposal-decorators": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-8.0.2.tgz",
+ "integrity": "sha512-+C6O6KKXU7BBq1GNaIkFJxrALUVGRcr+WeWm4OcuRl3h+l/CmNfcTLMrT2Lm3uvGBimBH/8pEBRrXJFLoO67Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^8.0.1",
+ "@babel/helper-plugin-utils": "^8.0.1",
+ "@babel/plugin-syntax-decorators": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-decorators": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-8.0.1.tgz",
+ "integrity": "sha512-NI+0S/6MvR6GlcQFwjDZ+WIc2qvG6TXN534lYs9llNldwW4b7Dh6KTtk030FA0xWdYGs4t1lWo+OEWN8wGB+Nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-8.0.1.tgz",
+ "integrity": "sha512-n0jtCOxEovhU7METqSQjcZO9pX53nu9uNIjMS+hEt+Nt9jA7oOZoBIgbCxhhASmF6T6rPDGge5UAvh6Z4eFz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-8.0.3.tgz",
+ "integrity": "sha512-jmTPwps7oSQSZaV1SxkQ3C12UWyufGysGc5OzDpZzvPAIX4mO7dJT3hoqkWVrSImvkcMiknir1iLN1SNV/CZzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-8.0.5.tgz",
+ "integrity": "sha512-evjxc5fGvpXG2WMGSalpX8IJZVumLIDgk5r5eRhvXRDqFX/GPWq1BeRfJalPCcwJfrR5A/wvegUhx9W5Nr7/mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-explicit-resource-management": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-8.0.5.tgz",
+ "integrity": "sha512-Vr20jk/ZxRGu30o0jYvcKl2Pf0tiyr8H9q2ZGxYD6yjOpv9DdP6//I85q0cSOdADZhsu/ZZoApjGVCfl4xnVJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^8.0.1",
+ "@babel/plugin-transform-destructuring": "^8.0.5"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-8.0.1.tgz",
+ "integrity": "sha512-PMuzulWrrzFNmY3lXSk/tV9NRb7y0eZZLJY4UEo2TKszroxvUZHAPPi+T9FDyrQhod+TQA+t+8/QYaaMpiEuhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^8.0.1",
+ "@babel/helper-plugin-utils": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-display-name": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-8.0.1.tgz",
+ "integrity": "sha512-soLishXlkyu6jcICPyO3HEP7A3GCzKEnn7XfvYrImuWEOwFAz93qShmWSYPf5ww0ZkO4By0zsN2bVIDF54fSdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-8.0.1.tgz",
+ "integrity": "sha512-NgkoF7Uq+30TmOPDdNUimT0Nta02uVjqJRFNlVWKrbOCu/CkzfHa4aMnIs0lMpkMmZmWA1e42Va+F04i/pY1zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^8.0.0",
+ "@babel/helper-module-imports": "^8.0.0",
+ "@babel/helper-plugin-utils": "^8.0.1",
+ "@babel/plugin-syntax-jsx": "^8.0.1",
+ "@babel/types": "^8.0.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-development": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-8.0.1.tgz",
+ "integrity": "sha512-Hb+HUZpV9KFHjm+F+P3aLDMi8QXU9l3ROCQv20z18Me2sGyW5nNNR5YTevNlgHvCpFek3BnAwhDGq/BRndXViw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/plugin-transform-react-jsx": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-pure-annotations": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-8.0.1.tgz",
+ "integrity": "sha512-7/8UwU8hoPBurXa9tUiTTC8aACTRy5tCqLUtqikHp2eGiWoEB57AduOdbQ71OOMTEvawKrGhv3WfzkDpI+/oSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^8.0.0",
+ "@babel/helper-plugin-utils": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-8.0.6.tgz",
+ "integrity": "sha512-WF3yRlQtClzScvQbWct00ix63+w3DPiyU0WmCBU4YqK539n3VvJFElElpevoD8wfu6953CbiNoUZBi2B7/5jXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^8.0.0",
+ "@babel/helper-create-class-features-plugin": "^8.0.6",
+ "@babel/helper-plugin-utils": "^8.0.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0",
+ "@babel/plugin-syntax-typescript": "^8.0.3"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/preset-react": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-8.0.1.tgz",
+ "integrity": "sha512-jrFuPp/pTddFZbtmWhdLNAYc6UMcpboeUPnw0BBrm4nOmcAko/1TRcFi1PzWCeOFRU+VaSiKmat87W1HvR7mIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^8.0.1",
+ "@babel/helper-validator-option": "^8.0.0",
+ "@babel/plugin-transform-react-display-name": "^8.0.1",
+ "@babel/plugin-transform-react-jsx": "^8.0.1",
+ "@babel/plugin-transform-react-jsx-development": "^8.0.1",
+ "@babel/plugin-transform-react-pure-annotations": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-8.0.7.tgz",
+ "integrity": "sha512-sUFLFphAG6YNjCnJpSKpyoNxkfyeM2KNuUjJJN0R6dZusMcK2V3Fg11MVHPFvb37AZeDNocc+xM748vdh9z82Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^8.0.1",
+ "@babel/helper-validator-option": "^8.0.0",
+ "@babel/plugin-transform-modules-commonjs": "^8.0.1",
+ "@babel/plugin-transform-typescript": "^8.0.6"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^8.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz",
+ "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^8.0.0",
+ "@babel/parser": "^8.0.0",
+ "@babel/types": "^8.0.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.7.tgz",
+ "integrity": "sha512-bVV0brinsTWgc6ptSIoQo3GbITtl0VZAqEUJbHHcyfDPEu+q/fEvlYEa+6H85HoK8IdksKBFtKJbJFZ6Kff+zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^8.0.6",
+ "@babel/generator": "^8.0.6",
+ "@babel/helper-globals": "^8.0.6",
+ "@babel/parser": "^8.0.7",
+ "@babel/template": "^8.0.0",
+ "@babel/types": "^8.0.6",
+ "obug": "^3.0.0"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/parser": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.7.tgz",
+ "integrity": "sha512-UkDwEbPO1px2JQSrdh4qIgaH1JgG7GO/pFeJkpxhDxIETkT/QvGpnigXIw75ViHYPtIgKITmVUJqLjiO+JuWjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^8.0.6"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/types": {
+ "version": "8.0.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz",
+ "integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^8.0.6",
+ "@babel/helper-validator-identifier": "^8.0.6"
+ },
+ "engines": {
+ "node": "^22.18.0 || >=24.11.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/obug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
+ "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
"node_modules/@babel/types": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
@@ -579,6 +1272,361 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@inquirer/ansi": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.9.tgz",
+ "integrity": "sha512-0Ywt3S8tqM6TWUcsRlBFQ1x+X3/o8kktX0ZiKAnByYb9GFDknnOhse1PVtfiD2BXPI8ar+BWx9PV9R0lQIcygw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ }
+ },
+ "node_modules/@inquirer/checkbox": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.6.tgz",
+ "integrity": "sha512-XkOardciF3tow2NUlY6/Qjk1QqYa+6WpmVLTpxXl97Uv4erczDEZ+xg1j6LqBf0ekcRSUiICQA4nQXHHkVKQ9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^2.0.9",
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/figures": "^2.0.9",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/confirm": {
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.3.tgz",
+ "integrity": "sha512-ImOBgpl/sJTFp8naeSkNu4S0Sf8F8pxv3/CUx+LoBAdQvCVqFz6rSB2A/oZYsJge3+yphbEDxEkfaRNYq1OU0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.4.tgz",
+ "integrity": "sha512-M5fB67y7jPObI45vhD82RskWwCSm0scOvenV/I6K8tIi2gfr9DXqQqJ+iw+Wi6SgapikdOiJibYzz1gW47fsDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^2.0.9",
+ "@inquirer/figures": "^2.0.9",
+ "@inquirer/type": "4.2.0",
+ "cli-width": "^4.1.0",
+ "fast-wrap-ansi": "^0.2.0",
+ "mute-stream": "^3.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/editor": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.4.tgz",
+ "integrity": "sha512-imzxQDfw26NpcJAubMj/dE5RjU9Mp80IcT+FzsD6YoxXmL8L1QYnAFdCNv7ccGWh//73+0YP+1kfhwnExdl2VQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/external-editor": "^3.0.6",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/expand": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.6.tgz",
+ "integrity": "sha512-JfiLKi7qGuHmcQO/DqR10UFEp054Y1FXZaszP5ZEfHhgHeuFoT5nNFQIXFhed/5/p07SbsV2tIJFwG/dg5+NmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/external-editor": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.6.tgz",
+ "integrity": "sha512-rWmw6WG8JZ7LMn/srXEylYGhL6EXXJeROE7/4x4GREh6NgzqJBDbIgpNGsYoLPfobeb4VZtEYzPGnT5EFNYhiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^2.1.1",
+ "iconv-lite": "^0.7.2"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz",
+ "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ }
+ },
+ "node_modules/@inquirer/input": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.7.tgz",
+ "integrity": "sha512-Icrs9tuMBCxGI+lFYNO71VFOnqGO8IIrLIGqnTGh07X8jZ2la9QLAYxXcikHZQjjVhIZUtU2xyH89DVN/pPhVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/number": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.4.tgz",
+ "integrity": "sha512-C6g80U4U2+w2Vxa1CB0MN3a2WnQIfzLSUG0lduO3Au9GsirJsKATZkOwZPK+vZ1BlAlz+/Z5ParfQCS9E0svmg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/password": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.3.tgz",
+ "integrity": "sha512-1Uo2qfI2K3wXbfunMpvwAzoZPIUf5iEETwDGqRVbswoorXGqYQJ/bfIJFeLj+pxjq+e2UFPNOmx+fsIl4Urigw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^2.0.9",
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/prompts": {
+ "version": "8.7.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.3.tgz",
+ "integrity": "sha512-DHK20kO+H5k/XcJQpHQUpVQbMYHHhZchlGWhZVMUV4WIuRuBW5744hipOtriE1vMqPLgnIyXFMHWKkIbTepa4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/checkbox": "^5.2.6",
+ "@inquirer/confirm": "^6.3.3",
+ "@inquirer/editor": "^5.3.4",
+ "@inquirer/expand": "^5.1.6",
+ "@inquirer/input": "^5.1.7",
+ "@inquirer/number": "^4.2.4",
+ "@inquirer/password": "^5.2.3",
+ "@inquirer/rawlist": "^5.3.6",
+ "@inquirer/search": "^4.3.4",
+ "@inquirer/select": "^5.2.6"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/rawlist": {
+ "version": "5.3.6",
+ "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.6.tgz",
+ "integrity": "sha512-2exExhQ5xTBqfN/rRlZkvM1Gm3b0eiLcWe2NqxWxe7qYmnU4M3j/n9TwzfPUb+Q7dAAz9TmBtDWWdF3jbp4ZLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/search": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.4.tgz",
+ "integrity": "sha512-xsk0UeFP04jsbEeFMzghjUaH2aHXyt+WykSajQzq/8qyBNHsgGYHea/sFwv5T08kW7Sp+KDX8aF124zPUIs1Ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/figures": "^2.0.9",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/select": {
+ "version": "5.2.6",
+ "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.6.tgz",
+ "integrity": "sha512-jFu7CFghlr0vpi/326a4wz6BGkJH9X3H/iIu+ahyV2Ja52ktqQE3fC9BYP47JKeSPQYGUv5xquE2Vp9PbepZ3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^2.0.9",
+ "@inquirer/core": "^12.0.4",
+ "@inquirer/figures": "^2.0.9",
+ "@inquirer/type": "4.2.0"
+ },
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/type": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.2.0.tgz",
+ "integrity": "sha512-2rnHX8RPo4glJYqnFc/v3nQeIhDcjyRRyaT7MGcfL/8xv/8K03SXYoA2tX06WVSKywWOE/ncbG3dtuzLjR5TCQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=23.5.0 || ^22.13.0 || ^20.17.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.4.0-beta.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.4.0-beta.0.tgz",
+ "integrity": "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.6.0-beta.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -2655,6 +3703,13 @@
"node": ">=22"
}
},
+ "node_modules/@sec-ant/readable-stream": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
+ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@shikijs/core": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz",
@@ -2742,6 +3797,148 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+ "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@stryker-mutator/api": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-10.0.0.tgz",
+ "integrity": "sha512-ZtAJ0ZT3MVRCWJTBE2h90XB/6E+4lifHYtcTyNG6nU2nLekPgTo4gD5esjX6Okxo1b/JB4jJzyxYB54fwKAoJw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mutation-testing-metrics": "3.8.4",
+ "mutation-testing-report-schema": "3.8.4",
+ "tslib": "~2.8.0",
+ "typed-inject": "~5.0.0"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@stryker-mutator/core": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-10.0.0.tgz",
+ "integrity": "sha512-ZvMsRyaXQQ5e6Thcid9pkuODv6Fn9E3nrBQJUap+hcJuGJ4unm26afo3m6YKSjn8kinyxJ/3TXf0cTWRDaTxVw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@inquirer/prompts": "^8.0.0",
+ "@stryker-mutator/api": "10.0.0",
+ "@stryker-mutator/instrumenter": "10.0.0",
+ "@stryker-mutator/util": "10.0.0",
+ "ajv": "~8.20.0",
+ "chalk": "~5.6.0",
+ "commander": "~14.0.0",
+ "diff-match-patch": "1.0.5",
+ "emoji-regex": "~10.6.0",
+ "execa": "~9.6.0",
+ "json-rpc-2.0": "^1.7.0",
+ "lodash.groupby": "~4.6.0",
+ "minimatch": "~10.2.4",
+ "mutation-server-protocol": "~0.4.0",
+ "mutation-testing-elements": "3.8.4",
+ "mutation-testing-metrics": "3.8.4",
+ "mutation-testing-report-schema": "3.8.4",
+ "npm-run-path": "~6.0.0",
+ "progress": "~2.0.3",
+ "rxjs": "~7.8.1",
+ "semver": "^7.6.3",
+ "source-map": "~0.7.4",
+ "tree-kill": "~1.2.2",
+ "tslib": "2.8.1",
+ "typed-inject": "~5.0.0",
+ "typed-rest-client": "~2.3.0"
+ },
+ "bin": {
+ "stryker": "bin/stryker.js"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@stryker-mutator/instrumenter": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-10.0.0.tgz",
+ "integrity": "sha512-B7Wmn1KlEWyFeOz6D6oGvQGRfi5Xw3VemG6dEKvFQp4qLvxD9Mf4kcZghfxffgnYwXd3bFgqXsJ+ZGlhdfIOrQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/core": "~8.0.0",
+ "@babel/generator": "~8.0.0",
+ "@babel/parser": "~8.0.0",
+ "@babel/plugin-proposal-decorators": "~8.0.0",
+ "@babel/plugin-transform-explicit-resource-management": "^8.0.0",
+ "@babel/preset-react": "~8.0.0",
+ "@babel/preset-typescript": "~8.0.0",
+ "@babel/traverse": "~8.0.4",
+ "@stryker-mutator/api": "10.0.0",
+ "@stryker-mutator/util": "10.0.0",
+ "angular-html-parser": "~10.11.0",
+ "semver": "~7.8.0",
+ "tslib": "2.8.1",
+ "weapon-regex": "~2.0.0"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@stryker-mutator/typescript-checker": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@stryker-mutator/typescript-checker/-/typescript-checker-10.0.0.tgz",
+ "integrity": "sha512-GLvC0LVd8RJ5SxR3eFcgb7DFKmw07DYtPXs942z6Qh7l6p7YBnFyd7lPw7nvA9UKEYDd22embrFmLlyC1XJjXw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@stryker-mutator/api": "10.0.0",
+ "@stryker-mutator/util": "10.0.0",
+ "semver": "~7.8.0"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ },
+ "peerDependencies": {
+ "@stryker-mutator/core": "10.0.0",
+ "typescript": ">=3.6"
+ }
+ },
+ "node_modules/@stryker-mutator/util": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-10.0.0.tgz",
+ "integrity": "sha512-LzOpHiJaCp2ABQgnPMlrQQcsK43bd5Vo/2FGL78aN62yDoeRQ+4j3tzeuXxK5OAHdC3fUz6TDoy4IsoAuLAd3w==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@stryker-mutator/vitest-runner": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@stryker-mutator/vitest-runner/-/vitest-runner-10.0.0.tgz",
+ "integrity": "sha512-SHK2/vfvRUpiz7jXPnQMBnr6zLdm69DK03Mo5mPhaZWcRSygrKUqYsPqWsXsK+5ySHzlMTfCyFK5NQ/X9sJFFw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@stryker-mutator/api": "10.0.0",
+ "@stryker-mutator/util": "10.0.0",
+ "semver": "^7.7.4",
+ "tslib": "~2.8.0"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ },
+ "peerDependencies": {
+ "@stryker-mutator/core": "10.0.0",
+ "vitest": ">=2.0.0"
+ }
+ },
"node_modules/@toon-format/toon": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.3.1.tgz",
@@ -2880,6 +4077,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/gensync": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz",
+ "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/hast": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
@@ -2890,6 +4094,13 @@
"@types/unist": "*"
}
},
+ "node_modules/@types/jsesc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
+ "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
@@ -3892,6 +5103,16 @@
"node": ">= 14.0.0"
}
},
+ "node_modules/angular-html-parser": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.11.0.tgz",
+ "integrity": "sha512-3vERzJ65UFDr3C7uozLJwsNcQS3FS784dSh583oDgDTTZMgXe3/pdyXgKndxiP5R2lvYRfW6gSl145Hnyf2OFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ansi-escapes": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
@@ -4015,6 +5236,19 @@
],
"license": "MIT"
},
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.28",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.28.tgz",
+ "integrity": "sha512-br0LOmjJqr6Rt4AEkadbcNf67Zpo7EPwKsWbjnRrJIa1QYFHQBqM4C7F2WTbV8ahTEcg1Eo+Y0aWjjORInYFyA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
@@ -4099,6 +5333,40 @@
"node": "20 || >=22"
}
},
+ "node_modules/browserslist": {
+ "version": "4.29.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz",
+ "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.11.26",
+ "caniuse-lite": "^1.0.30001813",
+ "electron-to-chromium": "^1.5.439",
+ "node-releases": "^2.0.57",
+ "update-browserslist-db": "^1.3.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -4170,6 +5438,27 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001815",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001815.tgz",
+ "integrity": "sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
"node_modules/canonicalize": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-3.0.0.tgz",
@@ -4212,6 +5501,19 @@
"node": ">=18"
}
},
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/character-entities-html4": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
@@ -4234,6 +5536,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chardet": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz",
+ "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
@@ -4280,6 +5589,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
@@ -4308,6 +5627,13 @@
"node": ">=20"
}
},
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/copy-anything": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.1.5.tgz",
@@ -4406,6 +5732,17 @@
"node": ">=6"
}
},
+ "node_modules/des.js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
+ "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -4439,6 +5776,13 @@
"node": ">=0.3.1"
}
},
+ "node_modules/diff-match-patch": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
+ "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/diff3": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz",
@@ -4459,6 +5803,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.452",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.452.tgz",
+ "integrity": "sha512-cVvk1YBSj6571VmV6hdJTqj94Y79xSu8cj6Y9t6ae4Y4431z8/7EJFvu3rVp90xhfuv9BpPKUmaLfTo4+FOGBg==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
@@ -4473,6 +5824,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/empathic": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.1.0.tgz",
+ "integrity": "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -4546,6 +5907,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -4614,6 +5985,33 @@
"node": ">=22"
}
},
+ "node_modules/execa": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
+ "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/merge-streams": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "figures": "^6.1.0",
+ "get-stream": "^9.0.0",
+ "human-signals": "^8.0.1",
+ "is-plain-obj": "^4.1.0",
+ "is-stream": "^4.0.1",
+ "npm-run-path": "^6.0.0",
+ "pretty-ms": "^9.2.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^4.0.0",
+ "yoctocolors": "^2.1.1"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
@@ -4731,6 +6129,22 @@
}
}
},
+ "node_modules/figures": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
+ "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-unicode-supported": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -4738,6 +6152,16 @@
"license": "MIT",
"optional": true
},
+ "node_modules/flru": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz",
+ "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/focus-trap": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz",
@@ -4827,6 +6251,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"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",
@@ -4877,6 +6311,23 @@
"node": ">= 0.4"
}
},
+ "node_modules/get-stream": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
+ "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sec-ant/readable-stream": "^0.4.1",
+ "is-stream": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/get-tsconfig": {
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz",
@@ -5042,6 +6493,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/human-signals": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
+ "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
@@ -5058,6 +6519,23 @@
"url": "https://github.com/sponsors/typicode"
}
},
+ "node_modules/iconv-lite": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
+ "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -5087,6 +6565,17 @@
"node": ">= 4"
}
},
+ "node_modules/import-meta-resolve": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
+ "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -5171,6 +6660,32 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
+ "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
@@ -5186,6 +6701,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-unicode-supported": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+ "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -5251,6 +6779,13 @@
"url": "https://github.com/sponsors/panva"
}
},
+ "node_modules/js-md4": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
+ "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/js-tokens": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
@@ -5363,12 +6898,45 @@
"win32"
]
},
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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",
+ "integrity": "sha512-BmN42JKn6FI36pcMMzHCXQhpKNEfbeqgSIcUYHKnifq0/4Ey6PDinovwUOgajT7NsgUtpWdDXaCWlpI4LHXN3g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/jsonc-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
@@ -5745,6 +7313,13 @@
"node": ">=20.0.0"
}
},
+ "node_modules/lodash.groupby": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
+ "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==",
+ "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",
@@ -6026,6 +7601,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -6089,6 +7671,53 @@
"license": "MIT",
"optional": true
},
+ "node_modules/mutation-server-protocol": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz",
+ "integrity": "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "zod": "^4.1.12"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/mutation-testing-elements": {
+ "version": "3.8.4",
+ "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.8.4.tgz",
+ "integrity": "sha512-5CF1SNa7at5ZH33vEr+21wNebTSrtNIVvnzaUlxortHajOrIPaSLczIWvg6sI/fsExekQ7jookwf2cHftkckqQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/mutation-testing-metrics": {
+ "version": "3.8.4",
+ "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.8.4.tgz",
+ "integrity": "sha512-DZcmndJBH6nrNs3tpiB3OcMVq9KkG2cHCpJSnDxSxPwi9qrafRmoec40xjhkbzOoX1n7/4UkDqg5tIj4A6nvCw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mutation-testing-report-schema": "3.8.4"
+ }
+ },
+ "node_modules/mutation-testing-report-schema": {
+ "version": "3.8.4",
+ "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.8.4.tgz",
+ "integrity": "sha512-s4G71R6Lt/PpZ0cqeglIcgyBdzLM8E+SeCHZAPg1wkSsPtRBa4XfPzAozYKdiJk/TLbNEEb7En9t0/bveuPuxA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/mute-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz",
+ "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
@@ -6128,6 +7757,59 @@
"node": ">=10"
}
},
+ "node_modules/node-releases": {
+ "version": "2.0.58",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.58.tgz",
+ "integrity": "sha512-BdiKAgbfRlvfMq/XzO/AF9NPmaA8a+YpLAyAFTVFBj3gARrAIfWvNq7Q7NE4se3KgFu0KkrvQMxTS0XVnsGzHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
+ "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/obug": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
@@ -6380,6 +8062,19 @@
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
+ "node_modules/parse-ms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
+ "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parse5": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
@@ -6621,6 +8316,22 @@
"node": ">=10"
}
},
+ "node_modules/pretty-ms": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz",
+ "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parse-ms": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -6646,6 +8357,16 @@
],
"license": "MIT"
},
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/property-information": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
@@ -6685,6 +8406,22 @@
],
"license": "MIT"
},
+ "node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
@@ -6889,6 +8626,16 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -6918,6 +8665,13 @@
"node": ">=10"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/search-insights": {
"version": "2.17.3",
"resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz",
@@ -7013,6 +8767,82 @@
"@types/hast": "^3.0.4"
}
},
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -7114,6 +8944,16 @@
"atomic-sleep": "^1.0.0"
}
},
+ "node_modules/source-map": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
@@ -7249,6 +9089,19 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
+ "node_modules/strip-final-newline": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
+ "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
@@ -7416,6 +9269,16 @@
"integrity": "sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ==",
"license": "MIT"
},
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -7432,8 +9295,17 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
- "license": "0BSD",
- "optional": true
+ "license": "0BSD"
+ },
+ "node_modules/tunnel": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+ "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+ }
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
@@ -7480,6 +9352,33 @@
"node": ">= 0.4"
}
},
+ "node_modules/typed-inject": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz",
+ "integrity": "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/typed-rest-client": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz",
+ "integrity": "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "des.js": "^1.1.0",
+ "js-md4": "^0.3.2",
+ "qs": "6.15.1",
+ "tunnel": "0.0.6",
+ "underscore": "^1.13.8"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ }
+ },
"node_modules/typescript": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
@@ -7530,6 +9429,21 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript6": {
+ "name": "typescript",
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/unbash": {
"version": "4.0.12",
"resolved": "https://registry.npmjs.org/unbash/-/unbash-4.0.12.tgz",
@@ -7540,6 +9454,13 @@
"node": ">=14"
}
},
+ "node_modules/underscore": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
+ "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/undici-types": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
@@ -7547,6 +9468,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/unist-util-is": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
@@ -7620,6 +9554,37 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/update-browserslist-db": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz",
+ "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -7627,6 +9592,19 @@
"license": "MIT",
"optional": true
},
+ "node_modules/verkit": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz",
+ "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ },
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
@@ -8466,6 +10444,13 @@
"node": "20 || >=22"
}
},
+ "node_modules/weapon-regex": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-2.0.6.tgz",
+ "integrity": "sha512-iJC2vqYp+bh3r4X6zjuP0h65XqXjgUX3nJgmhj0PQMrdhAbUwCfN76CSTEEQqV3G7468kjOJ69PmytTG8l/8Gg==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -8615,6 +10600,19 @@
"url": "https://github.com/sponsors/eemeli"
}
},
+ "node_modules/yoctocolors": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz",
+ "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
diff --git a/package.json b/package.json
index 15e7a9a2e..45e73afc0 100644
--- a/package.json
+++ b/package.json
@@ -21,10 +21,14 @@
"mcp-server",
"model-context-protocol",
"static-analysis",
- "disassembler",
+ "rea",
"decompiler",
"mcp",
- "hopper"
+ "hopper",
+ "electron",
+ "javascript-analysis",
+ "ghidra",
+ "ida"
],
"publishConfig": {
"access": "public"
@@ -45,6 +49,7 @@
"dist",
"install.sh",
"bridge/hopper_bridge.py",
+ "bridge/cutter/rea_bridge.py",
"bridge/native/NativeUIChildren.swift",
"bridge/native/ReaNativeUI.swift",
"bridge/native/rea_lldb_tracer.py",
@@ -113,7 +118,9 @@
"precheck:pr": "npm run deps:check",
"check:pr": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos",
"precheck:ci": "npm run deps:check",
- "check:ci": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint:built format:check knip metadata:check jscpd scan:todos && npm run verify:architecture-guards",
+ "check:ci": "node scripts/run-exclusive.mjs artifacts npm run check:ci:tasks && npm run verify:architecture-guards",
+ "check:ci:tasks": "turbo run typecheck lint:built format:check knip metadata:check jscpd scan:todos",
+ "ci:static-context": "node scripts/ci/static-task-context.mjs",
"build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills",
"build:termux": "node scripts/run-exclusive.mjs artifacts npm run build:termux:run",
"build:termux:run": "npm run compile:termux && npm run artifacts:skills",
@@ -133,7 +140,8 @@
"test:run": "vitest run",
"test:python": "python3 -m unittest bridge/native/test_rea_lldb_tracer.py bridge/mitmproxy/test_capture.py -v && python3 -m unittest discover -s experiments/analysis-efficiency -p 'test_*.py' -v",
"test:local": "node scripts/run-development-tests.mjs local",
- "test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation",
+ "test:fast": "node scripts/ci/test-group.mjs source",
+ "test:built": "node scripts/ci/test-group.mjs built",
"pretest:boundary": "npm run test:prepare",
"test:boundary": "vitest run --project boundary --project cli-boundary --project process-boundary --project mcp-boundary --project process-global",
"pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills artifacts:mcp-catalog",
@@ -145,6 +153,10 @@
"test:integration": "vitest run --project boundary --project cli-boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
"pretest:coverage": "npm run test:prepare",
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
+ "test:coverage:reverse-tools": "vitest run --config vitest.reverse-tools.config.ts --coverage.enabled",
+ "pretest:coverage:reverse-tools": "npm run test:prepare",
+ "test:mutation:reverse-tools": "node scripts/prepare-reverse-tools-mutation.mjs && stryker run stryker.config.mjs && stryker run stryker.semantics.config.mjs",
+ "pretest:mutation:reverse-tools": "npm run test:prepare",
"test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
"pretest:ci:shard": "npm run test:prepare",
"test:ci:shard": "npm run test:ci:shard:run",
@@ -174,6 +186,7 @@
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ghidra:seeds": "npm run build:cached && node scripts/verify/ghidra/seeds.mjs",
"verify:ghidra:mips": "npm run build:cached && node scripts/verify-real-ghidra-mips.mjs",
+ "verify:ghidra:ppc": "npm run build:cached && node scripts/verify-real-ghidra-ppc.mjs",
"verify:ghidra:riscv": "npm run build:cached && node scripts/verify-real-ghidra-riscv.mjs",
"verify:ghidra:6502": "npm run build:cached && node scripts/verify-real-ghidra-6502.mjs",
"verify:ghidra:mips-shared": "npm run build:cached && node scripts/verify-real-ghidra-mips-shared.mjs",
@@ -253,6 +266,7 @@
"build:windows-native": "node scripts/build-windows-native.mjs",
"verify:windows-native:artifact": "node scripts/verify-windows-native-artifact.mjs",
"prepublishOnly": "npm run verify:windows-native:artifact",
+ "verify:cutter": "npm run build:cached && node scripts/verify-real-cutter.mjs",
"verify:windows-native": "node scripts/verify-windows-native.mjs",
"verify:ghidra:windows:package": "node scripts/verify-packaged-ghidra-windows.mjs",
"build:ghidra:nativeaot": "node scripts/build-ghidra-nativeaot.mjs",
@@ -344,6 +358,9 @@
},
"devDependencies": {
"@fast-check/vitest": "0.5.0",
+ "@stryker-mutator/core": "10.0.0",
+ "@stryker-mutator/typescript-checker": "10.0.0",
+ "@stryker-mutator/vitest-runner": "10.0.0",
"@types/cross-spawn": "6.0.6",
"@types/node": "26.6.4",
"@types/semver": "7.7.1",
@@ -360,6 +377,7 @@
"turbo": "2.11.7",
"typescript": "7.0.2",
"typescript-termux": "npm:typescript@5.9.3",
+ "typescript6": "npm:typescript@6.0.3",
"vitepress": "1.6.4",
"vitest": "5.0.3"
},
diff --git a/scripts/ci/artifacts.d.mts b/scripts/ci/artifacts.d.mts
new file mode 100644
index 000000000..93cf2f701
--- /dev/null
+++ b/scripts/ci/artifacts.d.mts
@@ -0,0 +1,67 @@
+import type { NormalizedWorkflowRun } from "./workflow-run.mjs";
+import type { StaticTaskContext } from "./static-task-context.mjs";
+export type { StaticTaskContext } from "./static-task-context.mjs";
+
+export interface StaticDiagnostic {
+ readonly kind: "lint" | "jscpd";
+ readonly path?: string;
+ readonly paths?: readonly string[];
+ readonly code?: string;
+ readonly message?: string;
+ readonly severity?: string;
+}
+
+export function classifyFailure(input: {
+ repository: string;
+ defaultBranch: string;
+ run: Record & NormalizedWorkflowRun;
+ job: Record;
+ diagnostics: readonly StaticDiagnostic[];
+ context: StaticTaskContext;
+ mainRun: Record & NormalizedWorkflowRun;
+ mainJob: Record;
+ mainDiagnostics: readonly StaticDiagnostic[];
+ mainContext: StaticTaskContext;
+ executionSourcesVerified: boolean;
+ ancestry: { status: string; mergeBaseSha: string | null };
+ blobs: Readonly>;
+}): {
+ classification: "matching-main-diagnostics" | "unknown";
+ reason: string;
+};
+
+export function diagnosticPaths(item: StaticDiagnostic): readonly string[];
+export function isFailedConclusion(
+ value: unknown,
+): value is "failure" | "timed_out";
+export function isEligibleMainRun(
+ run: Record,
+ defaultBranch: string,
+ repository: string,
+): boolean;
+export function pullRequestBaseSha(
+ run: Record,
+ repository: string,
+): string | null;
+export function safeSha(value: unknown): string | null;
+export function validRepoPath(value: unknown): value is string;
+export function selectStaticCheckArtifact(
+ page: unknown,
+ run: { id?: string | number; head_sha?: string },
+ runAttempt: number,
+): unknown | null;
+
+export function readStaticCheckArtifact(options: {
+ repository: string;
+ artifactId: number | string;
+ token: string;
+ fetchImpl?: typeof fetch;
+}): Promise<{
+ context: StaticTaskContext;
+ diagnostics: readonly StaticDiagnostic[];
+}>;
+
+export function parseStaticArchive(archivePath: string): Promise<{
+ context: StaticTaskContext;
+ diagnostics: readonly StaticDiagnostic[];
+}>;
diff --git a/scripts/ci/artifacts.mjs b/scripts/ci/artifacts.mjs
new file mode 100644
index 000000000..250a15872
--- /dev/null
+++ b/scripts/ci/artifacts.mjs
@@ -0,0 +1,442 @@
+import { execFile } from "node:child_process";
+import { mkdtemp, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { isAbsolute, join } from "node:path";
+import { promisify } from "node:util";
+
+import { parseStaticTaskContext } from "./static-task-context.mjs";
+
+const execute = promisify(execFile);
+const MAX_ARCHIVE_BYTES = 32 * 1024 * 1024;
+const MAX_MEMBER_BYTES = 4 * 1024 * 1024;
+const MAX_DIAGNOSTICS = 10_000;
+const API_ROOT = "https://api.github.com/repos";
+const SHA = /^[0-9a-f]{40}$/iu;
+const TASK_HASH = /^[a-f0-9]{16}$/u;
+
+/** Compare parsed static diagnostics only after source, task, host, and ancestry proof. */
+export function classifyFailure({
+ repository,
+ defaultBranch,
+ run,
+ job,
+ diagnostics,
+ context,
+ mainRun,
+ mainJob,
+ mainDiagnostics,
+ mainContext,
+ executionSourcesVerified,
+ ancestry,
+ blobs,
+}) {
+ const failedGate = firstFailure([
+ () => [
+ !isCompletedFailure(run) ||
+ job?.status !== "completed" ||
+ !isFailedConclusion(job?.conclusion),
+ "run_or_job_not_a_completed_failure",
+ ],
+ () => [
+ !isEligiblePullRun(run, repository) ||
+ !isEligibleMainRun(mainRun, defaultBranch, repository),
+ "run_event_or_repository_identity_unverified",
+ ],
+ () => [
+ job?.name !== mainJob?.name || job?.name !== "Static checks",
+ "failed_job_not_the_same_static_gate",
+ ],
+ () => [
+ !Array.isArray(diagnostics) ||
+ diagnostics.length === 0 ||
+ !Array.isArray(mainDiagnostics),
+ "structured_diagnostics_missing_or_invalid",
+ ],
+ () => [
+ !validContext(context) || !validContext(mainContext),
+ "static_task_context_missing_or_invalid",
+ ],
+ () => [
+ executionSourcesVerified !== true,
+ "checkout_sources_not_verified_against_api_commits",
+ ],
+ () => [
+ !sameExecutionContext(context, mainContext),
+ "task_or_toolchain_identity_differs",
+ ],
+ () => [
+ ancestry?.status !== "ahead" && ancestry?.status !== "identical",
+ "main_head_not_proven_ancestor_of_tested_base",
+ ],
+ () => [
+ ancestry?.mergeBaseSha !== mainRun?.head_sha,
+ "main_head_is_not_the_verified_merge_base",
+ ],
+ () => [
+ !sameTaskHashes(diagnostics, context, mainContext),
+ "owning_task_hash_differs",
+ ],
+ ]);
+ if (failedGate) return outcome("unknown", failedGate.reason);
+ const keys = diagnostics.map(diagnosticKey);
+ const mainKeys = new Set(mainDiagnostics.map(diagnosticKey).filter(Boolean));
+ if (keys.some((key) => key === null))
+ return outcome("unknown", "diagnostic_shape_not_supported");
+ if (!keys.every((key) => mainKeys.has(key)))
+ return outcome(
+ "unknown",
+ "main_run_does_not_contain_the_same_structured_diagnostic",
+ );
+ const blobStatus = blobIdentityStatus(diagnostics, blobs);
+ if (blobStatus !== "identical")
+ return outcome(
+ "unknown",
+ blobStatus === "different"
+ ? "diagnostic_source_content_not_identical_at_main_and_tested_base"
+ : "diagnostic_source_blob_identity_unavailable",
+ );
+ return outcome(
+ "matching-main-diagnostics",
+ "supported_diagnostics_match_failed_task_and_source_on_verified_main_ancestor",
+ );
+}
+
+export function diagnosticPaths(item) {
+ return item.kind === "jscpd" ? item.paths : [item.path];
+}
+export function isFailedConclusion(value) {
+ return value === "failure" || value === "timed_out";
+}
+export function isEligibleMainRun(run, defaultBranch, repository) {
+ return (
+ isEligibleStaticRun(run) &&
+ run.repository.full_name === repository &&
+ run.head_branch === defaultBranch &&
+ ["push", "schedule", "workflow_dispatch"].includes(run.event)
+ );
+}
+export function pullRequestBaseSha(run, repository) {
+ if (
+ run?.event !== "pull_request" ||
+ run.repository?.full_name !== repository ||
+ !Array.isArray(run.pull_requests) ||
+ run.pull_requests.length !== 1
+ )
+ return null;
+ const base = run.pull_requests[0].base;
+ return base?.repo?.full_name === repository && SHA.test(base?.sha ?? "")
+ ? base.sha
+ : null;
+}
+
+export function safeSha(value) {
+ return typeof value === "string" && SHA.test(value)
+ ? value.toLowerCase()
+ : null;
+}
+export function selectStaticCheckArtifact(page, run, runAttempt) {
+ if (runAttempt !== 1) return null;
+ if (
+ !Array.isArray(page?.artifacts) ||
+ !Number.isSafeInteger(page.total_count) ||
+ page.total_count !== page.artifacts.length ||
+ page.artifacts.length >= 100
+ )
+ return null;
+ const matches = page.artifacts.filter(
+ (artifact) => artifact.name === "static-check-reports",
+ );
+ if (matches.length !== 1) return null;
+ const [artifact] = matches;
+ if (
+ artifact.expired !== false ||
+ String(artifact.workflow_run?.id) !== String(run.id) ||
+ artifact.workflow_run?.head_sha !== run.head_sha
+ )
+ return null;
+ return artifact;
+}
+export function validRepoPath(value) {
+ return (
+ typeof value === "string" &&
+ value.length > 0 &&
+ !isAbsolute(value) &&
+ !value.includes("\\") &&
+ value
+ .split("/")
+ .every((part) => part !== "" && part !== "." && part !== "..") &&
+ !value.startsWith("/")
+ );
+}
+
+/** Download one named static report archive and parse only whitelisted JSON members. */
+export async function readStaticCheckArtifact({
+ repository,
+ artifactId,
+ token,
+ fetchImpl = fetch,
+}) {
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repository ?? ""))
+ throw new Error("artifact repository identity is invalid");
+ if (!token) throw new Error("artifact download requires a GitHub token");
+ const url = `${API_ROOT}/${repository}/actions/artifacts/${positiveId(artifactId)}/zip`;
+ let response = await fetchImpl(url, {
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `Bearer ${token}`,
+ "X-GitHub-Api-Version": "2022-11-28",
+ },
+ signal: AbortSignal.timeout(10_000),
+ redirect: "manual",
+ });
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
+ const location = response.headers.get("location");
+ if (!location || new URL(location).protocol !== "https:")
+ throw new Error("static_artifact_redirect_invalid");
+ response = await fetchImpl(location, {
+ signal: AbortSignal.timeout(10_000),
+ redirect: "error",
+ });
+ }
+ if (!response.ok) throw new Error(`github_http_${response.status}`);
+ const bytes = await readLimitedBytes(response, MAX_ARCHIVE_BYTES);
+ const directory = await mkdtemp(join(tmpdir(), "rea-ci-static-artifact-"));
+ const archive = join(directory, "artifact.zip");
+ try {
+ await writeFile(archive, bytes, { flag: "wx" });
+ return await parseStaticArchive(archive);
+ } finally {
+ await rm(directory, { recursive: true, force: true });
+ }
+}
+
+/** Accept only the three named producer outputs; no archive paths are written to disk. */
+export async function parseStaticArchive(archivePath) {
+ const { stdout: listing } = await execute("unzip", ["-Z1", archivePath], {
+ encoding: "utf8",
+ maxBuffer: 128 * 1024,
+ timeout: 5_000,
+ });
+ const allowed = new Set([
+ "oxlint.json",
+ "jscpd/jscpd-report.json",
+ "static-task-context.json",
+ ]);
+ const members = new Map();
+ for (const name of listing.split(/\r?\n/u).filter(Boolean)) {
+ const prefix = ".cache/static-checks/";
+ const member = name.startsWith(prefix) ? name.slice(prefix.length) : name;
+ if (!allowed.has(member)) continue;
+ if (members.has(member))
+ throw new Error("static artifact contains duplicate report members");
+ const { stdout } = await execute("unzip", ["-p", archivePath, name], {
+ encoding: "buffer",
+ maxBuffer: MAX_MEMBER_BYTES,
+ timeout: 5_000,
+ });
+ members.set(member, JSON.parse(stdout.toString("utf8")));
+ }
+ const context = parseStaticTaskContext(
+ members.get("static-task-context.json"),
+ );
+ const diagnostics = normalizeDiagnostics(
+ members.get("oxlint.json"),
+ members.get("jscpd/jscpd-report.json"),
+ );
+ if (diagnostics.length > MAX_DIAGNOSTICS)
+ throw new Error("static diagnostic count exceeds the reporting limit");
+ return { context, diagnostics };
+}
+
+function normalizeDiagnostics(lint, duplicate) {
+ return [...normalizeLint(lint), ...normalizeDuplicates(duplicate)];
+}
+
+function normalizeLint(report) {
+ if (report === undefined) return [];
+ if (!Array.isArray(report?.diagnostics))
+ throw new Error("oxlint diagnostics are malformed");
+ return report.diagnostics
+ .filter((item) => item.severity === "error")
+ .map((item) => {
+ if (
+ typeof item.filename !== "string" ||
+ !repoPath(item.filename) ||
+ typeof item.code !== "string" ||
+ typeof item.message !== "string"
+ )
+ throw new Error("oxlint error diagnostic is malformed");
+ return {
+ kind: "lint",
+ path: item.filename,
+ code: item.code,
+ message: item.message,
+ severity: "error",
+ };
+ });
+}
+
+function normalizeDuplicates(report) {
+ if (report === undefined) return [];
+ if (
+ !Array.isArray(report?.duplicates) ||
+ !Number.isFinite(report.statistics?.total?.percentage) ||
+ report.statistics.total.percentage < 0
+ )
+ throw new Error("jscpd report is malformed");
+ // The producer owns the checker outcome; do not copy or infer its threshold.
+ const exitCode = report.reaCheck?.exitCode;
+ if (exitCode === undefined || exitCode === 0) return [];
+ if (!Number.isSafeInteger(exitCode) || exitCode < 0)
+ throw new Error("jscpd checker outcome is malformed");
+ if (exitCode !== 1) return [];
+ return report.duplicates.map((item) => {
+ const paths = [item.firstFile?.name, item.secondFile?.name].sort();
+ if (paths.some((path) => !repoPath(path)))
+ throw new Error("jscpd duplicate paths are malformed");
+ return { kind: "jscpd", paths };
+ });
+}
+
+async function readLimitedBytes(response, limit) {
+ const reader = response.body?.getReader();
+ if (!reader) throw new Error("static artifact response has no body");
+ const chunks = [];
+ let size = 0;
+ try {
+ for (;;) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ size += value.byteLength;
+ if (size > limit) {
+ await reader.cancel();
+ throw new Error("static artifact exceeds the download limit");
+ }
+ chunks.push(value);
+ }
+ } finally {
+ reader.releaseLock();
+ }
+ const result = new Uint8Array(size);
+ let offset = 0;
+ for (const chunk of chunks) {
+ result.set(chunk, offset);
+ offset += chunk.byteLength;
+ }
+ return result;
+}
+
+function repoPath(value) {
+ return (
+ typeof value === "string" &&
+ value.length > 0 &&
+ !value.startsWith("/") &&
+ !value.includes("\\") &&
+ value
+ .split("/")
+ .every((part) => part !== "" && part !== "." && part !== "..")
+ );
+}
+
+function positiveId(value) {
+ const id = Number(value);
+ if (!Number.isSafeInteger(id) || id < 1)
+ throw new Error("artifact ID must be a positive integer");
+ return String(id);
+}
+
+function diagnosticKey(item) {
+ if (
+ item?.kind === "lint" &&
+ validRepoPath(item.path) &&
+ typeof item.code === "string" &&
+ typeof item.message === "string" &&
+ item.severity === "error"
+ )
+ return JSON.stringify([
+ item.kind,
+ item.path,
+ item.code,
+ item.message,
+ item.severity,
+ ]);
+ if (
+ item?.kind === "jscpd" &&
+ Array.isArray(item.paths) &&
+ item.paths.length === 2 &&
+ item.paths.every(validRepoPath)
+ )
+ return JSON.stringify([item.kind, ...[...item.paths].sort()]);
+ return null;
+}
+function blobIdentityStatus(diagnostics, blobs) {
+ const paths = [...new Set(diagnostics.flatMap(diagnosticPaths))];
+ if (paths.some((path) => blobs?.[path] === false)) return "different";
+ return paths.length > 0 && paths.every((path) => blobs?.[path] === true)
+ ? "identical"
+ : "unavailable";
+}
+function sameTaskHashes(diagnostics, left, right) {
+ return diagnostics.every((diagnostic) => {
+ const task = diagnostic.kind === "jscpd" ? "jscpd" : "lint:built";
+ return (
+ TASK_HASH.test(left.taskHashes[task] ?? "") &&
+ left.taskHashes[task] === right.taskHashes[task]
+ );
+ });
+}
+function validContext(value) {
+ return (
+ value?.version === 1 &&
+ SHA.test(value.sourceSha ?? "") &&
+ value.sourceClean === true &&
+ typeof value.nodeVersion === "string" &&
+ typeof value.npmVersion === "string" &&
+ typeof value.turboVersion === "string" &&
+ typeof value.platform === "string" &&
+ typeof value.architecture === "string" &&
+ value.taskHashes &&
+ typeof value.taskHashes === "object" &&
+ !Array.isArray(value.taskHashes)
+ );
+}
+function sameExecutionContext(left, right) {
+ return [
+ "nodeVersion",
+ "npmVersion",
+ "turboVersion",
+ "platform",
+ "architecture",
+ ].every((key) => left[key] === right[key]);
+}
+function isEligiblePullRun(run, repository) {
+ return (
+ isEligibleStaticRun(run) && pullRequestBaseSha(run, repository) !== null
+ );
+}
+function isEligibleStaticRun(run) {
+ return (
+ isRecord(run) &&
+ run.status === "completed" &&
+ isFailedConclusion(run.conclusion) &&
+ SHA.test(run.head_sha ?? "") &&
+ typeof run.repository?.full_name === "string" &&
+ run.workflowPath?.file === ".github/workflows/ci.yml"
+ );
+}
+function isCompletedFailure(run) {
+ return run?.status === "completed" && isFailedConclusion(run.conclusion);
+}
+function isRecord(value) {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+function firstFailure(checks) {
+ for (const check of checks) {
+ const [failed, reason] = check();
+ if (failed) return { reason };
+ }
+ return null;
+}
+function outcome(classification, reason) {
+ return { classification, reason };
+}
diff --git a/scripts/ci/failure-report.d.mts b/scripts/ci/failure-report.d.mts
new file mode 100644
index 000000000..47ac65745
--- /dev/null
+++ b/scripts/ci/failure-report.d.mts
@@ -0,0 +1,159 @@
+import type { NormalizedWorkflowRun } from "./workflow-run.mjs";
+import type { StaticDiagnostic } from "./artifacts.mjs";
+export type { StaticDiagnostic } from "./artifacts.mjs";
+import type { StaticTaskContext } from "./static-task-context.mjs";
+export type { StaticTaskContext } from "./static-task-context.mjs";
+export interface CiRun {
+ readonly id?: string | number;
+ readonly run_attempt?: number;
+ readonly status?: string;
+ readonly conclusion?: string | null;
+ readonly event?: string;
+ readonly path?: string;
+ readonly head_sha?: string;
+ readonly head_branch?: string;
+ readonly repository?: { readonly full_name?: string };
+ readonly pull_requests?: readonly {
+ readonly base?: {
+ readonly sha?: string;
+ readonly repo?: { readonly full_name?: string };
+ };
+ }[];
+}
+
+export interface CiJob {
+ readonly name?: string;
+ readonly status?: string;
+ readonly conclusion?: string | null;
+ readonly run_id?: number;
+ readonly run_attempt?: number;
+}
+
+export interface StaticEvidence {
+ readonly context: StaticTaskContext;
+ readonly diagnostics: readonly StaticDiagnostic[];
+}
+
+export interface CiFailureReport {
+ readonly version: 1;
+ readonly observedAt: string;
+ readonly repository: string;
+ readonly defaultBranch: string;
+ readonly informationalOnly: true;
+ readonly gateEffect: string;
+ readonly boundedInput: {
+ readonly candidateRuns: number;
+ readonly limit: number;
+ readonly mainCandidateLimit: number;
+ readonly mainCandidateRuns: number;
+ readonly mainRunLookupLimit: number;
+ readonly manifestTruncated: boolean;
+ };
+ readonly counts: Readonly<
+ Record<"matching-main-diagnostics" | "unknown", number>
+ >;
+ readonly failures: readonly {
+ readonly runId: number | string;
+ readonly runAttempt: number;
+ readonly classification: "matching-main-diagnostics" | "unknown";
+ readonly reason: string;
+ readonly candidateFailures?: readonly {
+ readonly runId: number | string;
+ readonly runAttempt: number;
+ readonly reason: string;
+ }[];
+ readonly runHead?: string | null;
+ readonly event?: string | null;
+ readonly job?: string;
+ readonly conclusion?: string | null;
+ }[];
+}
+
+export interface FailureReportApi {
+ getRun(repository: string, id: string | number): Promise;
+ getJobs(
+ repository: string,
+ id: string | number,
+ attempt: number,
+ ): Promise;
+ getStaticEvidence(
+ repository: string,
+ run: CiRun,
+ job: CiJob,
+ ): Promise;
+ getAncestry(
+ repository: string,
+ ancestor: string,
+ descendant: string,
+ ): Promise<{ status: string; mergeBaseSha: string | null }>;
+ verifyExecutionSource(
+ repository: string,
+ run: CiRun,
+ context: StaticTaskContext,
+ ): Promise;
+ getBlobIdentity(
+ repository: string,
+ ancestor: string,
+ descendant: string,
+ paths: readonly string[],
+ ): Promise>>;
+}
+
+export function classifyFailure(input: {
+ repository: string;
+ defaultBranch: string;
+ run: CiRun & NormalizedWorkflowRun;
+ job: CiJob;
+ diagnostics: readonly StaticDiagnostic[];
+ context: StaticTaskContext;
+ mainRun: CiRun & NormalizedWorkflowRun;
+ mainJob: CiJob;
+ mainDiagnostics: readonly StaticDiagnostic[];
+ mainContext: StaticTaskContext;
+ executionSourcesVerified: boolean;
+ ancestry: { status: string; mergeBaseSha: string | null };
+ blobs: Readonly>;
+}): {
+ classification: "matching-main-diagnostics" | "unknown";
+ reason: string;
+};
+
+export function reportCiFailures(options: {
+ repository: string;
+ defaultBranch: string;
+ batchManifest: {
+ readonly repository: string;
+ readonly runs: readonly {
+ readonly id: number | string;
+ readonly runAttempt: number;
+ readonly workflow: string;
+ readonly event?: string | null;
+ readonly headBranch?: string | null;
+ readonly conclusion: string | null;
+ readonly status: string;
+ }[];
+ readonly truncation?: readonly unknown[];
+ readonly coverage?: { readonly exhaustive?: boolean };
+ readonly selection?: { readonly exhaustive?: boolean };
+ };
+ api: FailureReportApi;
+ outputDirectory?: string;
+ summaryPath?: string;
+ observedAt?: string;
+}): Promise;
+
+export function createGitHubApi(options?: {
+ token?: string;
+ fetchImpl?: typeof fetch;
+ apiRequest?: (
+ method: string,
+ endpoint: string,
+ budget: { requests: number; startedAt: number; limitMs: number },
+ ) => Promise;
+}): FailureReportApi;
+
+export function writeFailureReport(
+ report: CiFailureReport,
+ outputDirectory?: string,
+ summaryPath?: string,
+): Promise<{ jsonPath: string; markdownPath: string }>;
diff --git a/scripts/ci/failure-report.mjs b/scripts/ci/failure-report.mjs
new file mode 100644
index 000000000..12364c59f
--- /dev/null
+++ b/scripts/ci/failure-report.mjs
@@ -0,0 +1,533 @@
+import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+import {
+ classifyFailure,
+ diagnosticPaths,
+ isEligibleMainRun,
+ isFailedConclusion,
+ pullRequestBaseSha,
+ readStaticCheckArtifact,
+ safeSha,
+ selectStaticCheckArtifact,
+ validRepoPath,
+} from "./artifacts.mjs";
+import { createGhApiRequest } from "./github-api.mjs";
+import { normalizeWorkflowRun } from "./workflow-run.mjs";
+
+const OUTPUT_DIRECTORY = join(".cache", "ci-failures");
+const BATCH_DIRECTORY = join(".cache", "ci-job-timings");
+const MAX_JSON_BYTES = 8 * 1024 * 1024;
+const MAX_FAILED_RUNS = 20;
+const MAX_MAIN_CANDIDATES = 5;
+const MAX_MAIN_RUN_LOOKUPS = 100;
+const MAX_PATHS_PER_COMPARISON = 50;
+const MAX_API_REQUESTS = 250;
+
+export { classifyFailure } from "./artifacts.mjs";
+
+/** Uses only failed CI candidates present in the supplied bounded timing manifest. */
+export async function reportCiFailures({
+ repository,
+ defaultBranch,
+ batchManifest,
+ api,
+ outputDirectory = OUTPUT_DIRECTORY,
+ summaryPath,
+ observedAt = new Date().toISOString(),
+}) {
+ validateRepository(repository);
+ if (typeof defaultBranch !== "string" || defaultBranch.length === 0)
+ throw new Error("defaultBranch is required");
+ if (
+ !batchManifest ||
+ batchManifest.repository !== repository ||
+ !Array.isArray(batchManifest.runs)
+ )
+ throw new Error("timing manifest repository or runs are invalid");
+
+ const failedRuns = batchManifest.runs.filter(
+ (entry) =>
+ entry.workflow === "CI" &&
+ entry.status === "collected" &&
+ isFailedConclusion(entry.conclusion),
+ );
+ const prRuns = failedRuns.filter((entry) => entry.event === "pull_request");
+ const candidates = prRuns.slice(0, MAX_FAILED_RUNS);
+ const mainEntries = failedRuns.filter(
+ (entry) =>
+ entry.headBranch === defaultBranch &&
+ ["push", "schedule", "workflow_dispatch"].includes(entry.event),
+ );
+ const lookupRun = verifiedRunLookup(repository, api);
+ const mainCandidates = [];
+ for (const entry of mainEntries.slice(0, MAX_MAIN_RUN_LOOKUPS)) {
+ try {
+ const run = await lookupRun(entry);
+ if (isEligibleMainRun(run, defaultBranch, repository))
+ mainCandidates.push({
+ run,
+ getEvidence: mainEvidenceLookup(repository, api, entry, run),
+ });
+ if (mainCandidates.length === MAX_MAIN_CANDIDATES) break;
+ } catch {
+ // Unverified metadata cannot consume the eligible-main candidate budget.
+ }
+ }
+ const entries = [];
+ for (const entry of candidates) {
+ try {
+ entries.push(
+ ...(await analyzeFailedRun({
+ repository,
+ defaultBranch,
+ entry,
+ run: await lookupRun(entry),
+ mainCandidates,
+ api,
+ })),
+ );
+ } catch (error) {
+ entries.push({
+ runId: entry.id,
+ runAttempt: entry.runAttempt,
+ classification: "unknown",
+ reason: errorCode(error),
+ });
+ }
+ }
+ const report = {
+ version: 1,
+ observedAt,
+ repository,
+ defaultBranch,
+ informationalOnly: true,
+ gateEffect: "none; this report never changes CI conclusions",
+ boundedInput: {
+ candidateRuns: candidates.length,
+ limit: MAX_FAILED_RUNS,
+ mainCandidateLimit: MAX_MAIN_CANDIDATES,
+ mainCandidateRuns: mainCandidates.length,
+ mainRunLookupLimit: MAX_MAIN_RUN_LOOKUPS,
+ manifestTruncated:
+ prRuns.length > MAX_FAILED_RUNS ||
+ batchManifest.truncation?.length > 0 ||
+ batchManifest.selection?.exhaustive === false ||
+ batchManifest.coverage?.exhaustive === false,
+ },
+ counts: Object.fromEntries(
+ ["matching-main-diagnostics", "unknown"].map((category) => [
+ category,
+ entries.filter((item) => item.classification === category).length,
+ ]),
+ ),
+ failures: entries,
+ };
+ await writeFailureReport(report, outputDirectory, summaryPath);
+ return report;
+}
+
+async function analyzeFailedRun({
+ repository,
+ defaultBranch,
+ entry,
+ run,
+ mainCandidates,
+ api,
+}) {
+ const jobs = await api.getJobs(repository, entry.id, entry.runAttempt);
+ if (jobs.some((job) => !hasJobIdentity(job, entry)))
+ throw new Error("workflow_job_identity_mismatch");
+ const failedJobs = jobs.filter(
+ (job) => job.status === "completed" && isFailedConclusion(job.conclusion),
+ );
+ if (failedJobs.length === 0)
+ return [
+ {
+ runId: entry.id,
+ runAttempt: entry.runAttempt,
+ classification: "unknown",
+ reason: "failed_jobs_not_present_in_api_snapshot",
+ },
+ ];
+ const failures = [];
+ for (const job of failedJobs) {
+ let classification;
+ try {
+ const evidence = await api.getStaticEvidence(repository, run, job);
+ classification = await attributeToMain({
+ repository,
+ defaultBranch,
+ run,
+ job,
+ evidence,
+ mainCandidates,
+ api,
+ });
+ } catch (error) {
+ classification = outcome("unknown", errorCode(error));
+ }
+ failures.push({
+ runId: entry.id,
+ runAttempt: entry.runAttempt,
+ runHead: safeSha(run.head_sha),
+ event: run.event ?? null,
+ job: job.name,
+ conclusion: job.conclusion,
+ ...classification,
+ });
+ }
+ return failures;
+}
+
+async function attributeToMain({
+ repository,
+ defaultBranch,
+ run,
+ job,
+ evidence,
+ mainCandidates,
+ api,
+}) {
+ const baseSha = pullRequestBaseSha(run, repository);
+ if (baseSha === null || evidence === null)
+ return outcome("unknown", "no_matching_verified_main_failure");
+ let result = outcome("unknown", "no_matching_verified_main_failure");
+ const candidateFailures = [];
+ for (const candidate of mainCandidates) {
+ try {
+ const main = await compareCandidate({
+ repository,
+ defaultBranch,
+ run,
+ job,
+ evidence,
+ baseSha,
+ candidate,
+ api,
+ });
+ if (main !== null) result = main;
+ if (main?.classification === "matching-main-diagnostics") break;
+ } catch (error) {
+ candidateFailures.push({
+ runId: candidate.run.id,
+ runAttempt: candidate.run.run_attempt,
+ reason: errorCode(error),
+ });
+ }
+ }
+ return {
+ ...result,
+ ...(candidateFailures.length > 0 ? { candidateFailures } : {}),
+ };
+}
+
+async function compareCandidate({
+ repository,
+ defaultBranch,
+ run,
+ job,
+ evidence,
+ baseSha,
+ candidate,
+ api,
+}) {
+ const mainRun = candidate.run;
+ const main = await candidate.getEvidence(job.name);
+ if (!main?.evidence) return null;
+ const { job: mainJob, evidence: mainEvidence } = main;
+ const ancestry = await api.getAncestry(repository, mainRun.head_sha, baseSha);
+ const paths = [
+ ...new Set(evidence.diagnostics.flatMap(diagnosticPaths)),
+ ].slice(0, MAX_PATHS_PER_COMPARISON);
+ const blobs = await api.getBlobIdentity(
+ repository,
+ mainRun.head_sha,
+ baseSha,
+ paths,
+ );
+ const executionSourcesVerified =
+ (await api.verifyExecutionSource(repository, run, evidence.context)) &&
+ (await api.verifyExecutionSource(
+ repository,
+ mainRun,
+ mainEvidence.context,
+ ));
+ return classifyFailure({
+ repository,
+ defaultBranch,
+ run,
+ job,
+ diagnostics: evidence.diagnostics,
+ context: evidence.context,
+ mainRun,
+ mainJob,
+ mainDiagnostics: mainEvidence.diagnostics,
+ mainContext: mainEvidence.context,
+ executionSourcesVerified,
+ ancestry,
+ blobs,
+ });
+}
+
+// Each verified main run/attempt owns its lazy jobs and parsed evidence for this
+// report only. Cache nulls and rejected reads too; retries cannot spend the budget
+// repeatedly on an immutable observation that was unavailable or invalid.
+function mainEvidenceLookup(repository, api, entry, run) {
+ let jobs;
+ const evidence = new Map();
+ return (name) => {
+ jobs ??= Promise.resolve().then(async () => {
+ const items = await api.getJobs(repository, entry.id, entry.runAttempt);
+ return items.some((item) => !hasJobIdentity(item, entry)) ? [] : items;
+ });
+ if (!evidence.has(name))
+ evidence.set(
+ name,
+ jobs.then(async (items) => {
+ const job = items.find(
+ (item) =>
+ item.name === name &&
+ item.status === "completed" &&
+ isFailedConclusion(item.conclusion),
+ );
+ if (!job) return null;
+ return {
+ job,
+ evidence: await api.getStaticEvidence(repository, run, job),
+ };
+ }),
+ );
+ return evidence.get(name);
+ };
+}
+
+// Selection and attribution share one immutable API observation per run/attempt.
+function verifiedRunLookup(repository, api) {
+ const runs = new Map();
+ return (entry) => {
+ const key = `${positiveId(entry.id)}:${positiveId(entry.runAttempt)}`;
+ if (!runs.has(key))
+ runs.set(
+ key,
+ Promise.resolve().then(async () => {
+ const run = normalizeWorkflowRun(
+ await api.getRun(repository, entry.id),
+ );
+ if (!hasRunIdentity(run, entry))
+ throw new Error("workflow_run_identity_mismatch");
+ return run;
+ }),
+ );
+ return runs.get(key);
+ };
+}
+
+function hasRunIdentity(run, entry) {
+ return (
+ String(run?.id) === String(entry.id) &&
+ Number(run.run_attempt) === Number(entry.runAttempt) &&
+ run.workflowPath?.file === ".github/workflows/ci.yml"
+ );
+}
+function hasJobIdentity(job, entry) {
+ return (
+ String(job.run_id) === String(entry.id) &&
+ Number(job.run_attempt) === Number(entry.runAttempt)
+ );
+}
+
+export function createGitHubApi({
+ token = process.env.GH_TOKEN,
+ fetchImpl = fetch,
+ apiRequest = createGhApiRequest(),
+} = {}) {
+ if (!token) throw new Error("GH_TOKEN is required");
+ const budget = { requests: 0, startedAt: Date.now(), limitMs: 60_000 };
+ const request = async (endpoint) => {
+ if (budget.requests >= MAX_API_REQUESTS)
+ throw new Error("github_api_request_budget_exhausted");
+ budget.requests += 1;
+ return await apiRequest("GET", endpoint, budget);
+ };
+ return {
+ async getRun(repository, id) {
+ return await request(
+ `repos/${repository}/actions/runs/${positiveId(id)}`,
+ );
+ },
+ async getJobs(repository, id, attempt) {
+ const page = await request(
+ `repos/${repository}/actions/runs/${positiveId(id)}/attempts/${positiveId(attempt)}/jobs?per_page=100&page=1`,
+ );
+ if (!Array.isArray(page.jobs) || page.jobs.length >= 100)
+ throw new Error("job_list_incomplete_or_malformed");
+ return page.jobs;
+ },
+ async getStaticEvidence(repository, run, job) {
+ if (job.name !== "Static checks") return null;
+ if (run.run_attempt !== 1) return null;
+ const page = await request(
+ `repos/${repository}/actions/runs/${positiveId(run.id)}/artifacts?per_page=100&page=1`,
+ );
+ const artifact = selectStaticCheckArtifact(page, run, run.run_attempt);
+ if (!artifact) return null;
+ return await readStaticCheckArtifact({
+ repository,
+ artifactId: artifact.id,
+ token,
+ fetchImpl,
+ });
+ },
+ async getAncestry(repository, ancestor, descendant) {
+ const comparison = await request(
+ `repos/${repository}/compare/${safeSha(ancestor)}...${safeSha(descendant)}`,
+ );
+ return {
+ status: comparison.status,
+ mergeBaseSha: safeSha(comparison.merge_base_commit?.sha),
+ };
+ },
+ async verifyExecutionSource(repository, run, context) {
+ const sourceSha = safeSha(context.sourceSha);
+ if (!sourceSha) return false;
+ const pr =
+ run.event === "pull_request"
+ ? pullRequestBaseSha(run, repository)
+ : null;
+ if (
+ run.event === "pull_request" &&
+ (pr === null || context.prHeadSha !== run.head_sha)
+ )
+ return false;
+ const commit = await request(`repos/${repository}/commits/${sourceSha}`);
+ if (safeSha(commit.sha) !== sourceSha) return false;
+ if (sourceSha === run.head_sha) return true;
+ if (
+ pr === null ||
+ !Array.isArray(commit.parents) ||
+ commit.parents.length !== 2
+ )
+ return false;
+ return (
+ safeSha(commit.parents[0]?.sha) === pr &&
+ safeSha(commit.parents[1]?.sha) === run.head_sha
+ );
+ },
+ async getBlobIdentity(repository, ancestor, descendant, paths) {
+ const result = {};
+ for (const path of paths) {
+ if (!validRepoPath(path)) continue;
+ const [left, right] = await Promise.all([
+ getContentBlobSha(repository, ancestor, path, request),
+ getContentBlobSha(repository, descendant, path, request),
+ ]);
+ result[path] = left === right;
+ }
+ return result;
+ },
+ };
+}
+
+async function getContentBlobSha(repository, ref, path, request) {
+ const encodedPath = path.split("/").map(encodeURIComponent).join("/");
+ const value = await request(
+ `repos/${repository}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`,
+ );
+ const sha = safeSha(value?.sha);
+ if (value?.type !== "file" || sha === null)
+ throw new Error("github_content_blob_identity_unavailable");
+ return sha;
+}
+
+function outcome(classification, reason) {
+ return { classification, reason };
+}
+function validateRepository(value) {
+ if (
+ typeof value !== "string" ||
+ !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(value)
+ )
+ throw new Error("repository must be owner/name");
+}
+function positiveId(value) {
+ if (!Number.isSafeInteger(Number(value)) || Number(value) < 1)
+ throw new Error(
+ "GitHub run and attempt IDs must be positive safe integers",
+ );
+ return String(Number(value));
+}
+function errorCode(error) {
+ return error instanceof Error ? error.message.slice(0, 160) : "unknown_error";
+}
+
+export async function writeFailureReport(
+ report,
+ outputDirectory = OUTPUT_DIRECTORY,
+ summaryPath,
+) {
+ await mkdir(outputDirectory, { recursive: true });
+ const jsonPath = join(outputDirectory, "report.json");
+ const markdownPath = join(outputDirectory, "report.md");
+ const matchedDiagnostics = report.failures.filter(
+ (item) => item.classification === "matching-main-diagnostics",
+ );
+ const lines = [
+ "# CI failure attribution",
+ "",
+ "Informational only. This report never changes a CI conclusion or required gate.",
+ "",
+ `Observed ${report.observedAt}; ${String(matchedDiagnostics.length)} of ${String(report.failures.length)} failure reports contain supported diagnostics matching a failed static task on an ancestor main revision. This does not attribute other failures in the same job. Unknown means evidence was incomplete or did not match.`,
+ "",
+ ];
+ for (const item of report.failures)
+ lines.push(
+ `- Run ${item.runId} attempt ${item.runAttempt}${item.job ? `, ${item.job}` : ""}: **${item.classification}** — ${item.reason}.`,
+ );
+ if (report.failures.length === 0)
+ lines.push(
+ "- No completed failed CI attempts were present in the supplied timing snapshot.",
+ );
+ await writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
+ await writeFile(markdownPath, `${lines.join("\n")}\n`, "utf8");
+ if (summaryPath)
+ await appendFile(summaryPath, `${lines.join("\n")}\n`, "utf8");
+ return { jsonPath, markdownPath };
+}
+
+async function main() {
+ const repository = process.env.CI_REPOSITORY;
+ const defaultBranch = process.env.CI_DEFAULT_BRANCH;
+ const batchPath =
+ process.env.CI_TIMING_MANIFEST ??
+ join(BATCH_DIRECTORY, "batch-manifest.json");
+ const batchManifest = JSON.parse(await readBoundedFile(batchPath));
+ const api = createGitHubApi();
+ await reportCiFailures({
+ repository,
+ defaultBranch,
+ batchManifest,
+ api,
+ outputDirectory: process.env.CI_FAILURES_DIRECTORY ?? OUTPUT_DIRECTORY,
+ summaryPath: process.env.GITHUB_STEP_SUMMARY,
+ });
+}
+async function readBoundedFile(path) {
+ const bytes = await readFile(path);
+ if (bytes.byteLength > MAX_JSON_BYTES)
+ throw new Error("timing manifest exceeds size limit");
+ return bytes.toString("utf8");
+}
+
+if (
+ process.argv[1] &&
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url)
+) {
+ try {
+ await main();
+ } catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ process.exitCode = 1;
+ }
+}
diff --git a/scripts/ci/github-api.d.mts b/scripts/ci/github-api.d.mts
new file mode 100644
index 000000000..67e157e6d
--- /dev/null
+++ b/scripts/ci/github-api.d.mts
@@ -0,0 +1,16 @@
+export interface GitHubApiBudget {
+ requests: number;
+ startedAt: number;
+ limitMs: number;
+}
+
+export type GitHubApiRequest = (
+ method: string,
+ endpoint: string,
+ budget: GitHubApiBudget,
+) => Promise;
+
+export function createGhApiRequest(options?: {
+ env?: NodeJS.ProcessEnv;
+ now?: () => number;
+}): GitHubApiRequest;
diff --git a/scripts/ci/github-api.mjs b/scripts/ci/github-api.mjs
new file mode 100644
index 000000000..9567bf93b
--- /dev/null
+++ b/scripts/ci/github-api.mjs
@@ -0,0 +1,33 @@
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+
+const execFileAsync = promisify(execFile);
+const MAX_BUFFER = 8 * 1024 * 1024;
+const REQUEST_TIMEOUT_MS = 10_000;
+
+/** Create a bounded GitHub CLI JSON transport with GET-only authority. */
+export function createGhApiRequest({ env = process.env, now = Date.now } = {}) {
+ return async (method, endpoint, budget) => {
+ if (method !== "GET")
+ throw new Error(
+ "GitHub API transport supports read-only GET requests only",
+ );
+ const remaining = budget.limitMs - (now() - budget.startedAt);
+ if (remaining <= 0) throw new Error("lookup_time_budget_exhausted");
+ const { stdout } = await execFileAsync(
+ "gh",
+ ["api", "--method", "GET", endpoint],
+ {
+ env,
+ encoding: "utf8",
+ maxBuffer: MAX_BUFFER,
+ timeout: Math.min(REQUEST_TIMEOUT_MS, remaining),
+ },
+ );
+ try {
+ return JSON.parse(stdout);
+ } catch (cause) {
+ throw new Error("github_api_malformed_json", { cause });
+ }
+ };
+}
diff --git a/scripts/ci/job-timings-batch.mjs b/scripts/ci/job-timings-batch.mjs
index 842e884f0..f2dca7143 100644
--- a/scripts/ci/job-timings-batch.mjs
+++ b/scripts/ci/job-timings-batch.mjs
@@ -103,6 +103,9 @@ export async function collectJobTimingsBatch({
id: run.id,
runAttempt: run.run_attempt,
workflow: workflow.name,
+ event: typeof run.event === "string" ? run.event : null,
+ headBranch:
+ typeof run.head_branch === "string" ? run.head_branch : null,
conclusion: run.conclusion ?? null,
createdAt: run.created_at ?? null,
});
diff --git a/scripts/ci/plan.mjs b/scripts/ci/plan.mjs
index 86879216d..6e3f950b8 100644
--- a/scripts/ci/plan.mjs
+++ b/scripts/ci/plan.mjs
@@ -62,6 +62,10 @@ function classifyPackage(base, head) {
]),
].filter((key) => beforeScripts?.[key] !== afterScripts?.[key]);
for (const script of changed) {
+ if (/^test:(coverage|mutation):reverse-tools$/.test(script)) {
+ selected.reverse_tools = true;
+ continue;
+ }
if (script === "test:sqlite" || script === "test:sqlite:run") {
selected.sqlite = true;
continue;
@@ -162,6 +166,7 @@ if (tier === "reused") {
.split("\0")
.filter(Boolean);
for (const file of files) {
+ if (matchesAny(file, SCOPES.reverse_tools)) selected.reverse_tools = true;
if (
/^(README(?:_[^/]+)?\.md|AGENTS\.md|CONTRIBUTING\.md|CHANGELOG\.md|SECURITY\.md|native\/windows\/README\.md|\.github\/pull_request_template\.md)$/.test(
file,
diff --git a/scripts/ci/pr-ci-health.d.mts b/scripts/ci/pr-ci-health.d.mts
new file mode 100644
index 000000000..0db2454bd
--- /dev/null
+++ b/scripts/ci/pr-ci-health.d.mts
@@ -0,0 +1,95 @@
+export interface PrCiHead {
+ repositoryId: number | null;
+ repository?: string | null;
+ ref: string | null;
+ sha: string | null;
+}
+
+export interface PrCiPullRequest {
+ number: number | null;
+ draft?: boolean;
+ identityValid: boolean;
+ mergeable?: boolean;
+ head: PrCiHead;
+ [key: string]: unknown;
+}
+
+export interface PrCiWorkflowRun {
+ id?: number;
+ run_attempt?: number;
+ event?: string;
+ head_branch?: string;
+ head_repository?: { id?: number | null } | null;
+ head_sha?: string;
+ status?: string;
+ conclusion?: string | null;
+ created_at?: string;
+ html_url?: string;
+ pull_requests?: Array<{
+ number?: number;
+ head?: { sha?: string; repo?: { id?: number | null } } | null;
+ }>;
+ [key: string]: unknown;
+}
+
+export interface PrCiClassification {
+ category: string;
+ reason: string;
+ runs: Array>;
+}
+
+export interface PrCiHealthReport {
+ schemaVersion: 1;
+ observedAt: string;
+ repository: string;
+ repositoryId: number | null;
+ defaultBranch: string;
+ lookbackDays: number;
+ scan: {
+ status: "complete" | "partial";
+ cutoff: string;
+ pullRequestPages: number;
+ workflowRunPages: number;
+ pullRequestInventoryComplete: boolean;
+ workflowRunInventoryComplete: boolean;
+ pullRequestDetailsFetched: number;
+ pullRequestDetailsLimit: number;
+ apiRequests: number;
+ apiRequestLimit: number;
+ errors: string[];
+ };
+ counts: Record;
+ pullRequests: Array;
+}
+
+export function collectPrCiHealth(options: {
+ repository: string;
+ repositoryId?: number | string | null;
+ defaultBranch: string;
+ lookbackDays?: number;
+ now?: number;
+ apiRequest?: GitHubApiRequest;
+}): Promise;
+
+export function classifyPrCi(input: {
+ pull: PrCiPullRequest;
+ runs: Array<{ run: PrCiWorkflowRun; basis: string }>;
+ runsComplete: boolean;
+ pullRequestInventoryComplete: boolean;
+ ambiguousRuns: Array<{ run: PrCiWorkflowRun; basis: string }>;
+}): PrCiClassification;
+
+export function matchRunToPullRequest(input: {
+ run: PrCiWorkflowRun;
+ pull: PrCiPullRequest;
+ identityCounts: Map;
+ inventoryComplete?: boolean;
+}): { kind: "none" } | { kind: "ambiguous" } | { kind: "match"; basis: string };
+
+export function writePrCiHealthReport(
+ report: PrCiHealthReport,
+ outputDirectory?: string,
+): Promise<{ jsonPath: string; markdownPath: string }>;
+
+export function renderMarkdownSummary(report: PrCiHealthReport): string;
+import type { GitHubApiRequest } from "./github-api.mjs";
diff --git a/scripts/ci/pr-ci-health.mjs b/scripts/ci/pr-ci-health.mjs
new file mode 100644
index 000000000..d312dcecd
--- /dev/null
+++ b/scripts/ci/pr-ci-health.mjs
@@ -0,0 +1,718 @@
+import { appendFile, mkdir, writeFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+import { createGhApiRequest } from "./github-api.mjs";
+
+const OUTPUT_DIRECTORY = join(".cache", "ci-pr-health");
+const PAGE_SIZE = 100;
+const MAX_PULL_REQUEST_PAGES = 3;
+const MAX_WORKFLOW_RUN_PAGES = 5;
+const MAX_PULL_REQUEST_DETAILS = 40;
+const MAX_API_REQUESTS =
+ MAX_PULL_REQUEST_PAGES + MAX_WORKFLOW_RUN_PAGES + MAX_PULL_REQUEST_DETAILS;
+const LOOKBACK_DAYS = 14;
+const MAX_LOOKBACK_DAYS = 30;
+const MAX_TOTAL_TIME_MS = 60_000;
+const SHA_PATTERN = /^[0-9a-f]{40}$/iu;
+const COMPLETED_CATEGORIES = new Map([
+ ["failure", "completed_failure"],
+ ["timed_out", "completed_failure"],
+ ["cancelled", "completed_canceled"],
+ ["success", "completed_success"],
+]);
+
+/** Read workflow/PR state and conservatively distinguish absent CI from unknown. */
+export async function collectPrCiHealth({
+ repository,
+ repositoryId,
+ defaultBranch,
+ lookbackDays = LOOKBACK_DAYS,
+ now = Date.now(),
+ apiRequest = createGhApiRequest(),
+}) {
+ const identity = validateInputs({
+ repository,
+ repositoryId,
+ defaultBranch,
+ lookbackDays,
+ now,
+ });
+ const observedAt = new Date(now).toISOString();
+ const cutoff = now - identity.lookbackDays * 24 * 60 * 60 * 1_000;
+ const snapshot = await collectApiSnapshot({ apiRequest, identity, cutoff });
+ const classified = classifyPullRequests(snapshot);
+ const { pulls, runs, budget } = snapshot;
+ const errors = [pulls.error, runs.error, ...snapshot.detailsErrors].filter(
+ Boolean,
+ );
+ return {
+ schemaVersion: 1,
+ observedAt,
+ repository: identity.repository,
+ repositoryId: identity.repositoryId,
+ defaultBranch: identity.defaultBranch,
+ lookbackDays: identity.lookbackDays,
+ scan: {
+ status:
+ errors.length === 0 && pulls.complete && runs.complete
+ ? "complete"
+ : "partial",
+ cutoff: new Date(cutoff).toISOString(),
+ pullRequestPages: pulls.pages,
+ workflowRunPages: runs.pages,
+ pullRequestInventoryComplete: pulls.complete,
+ workflowRunInventoryComplete: runs.complete,
+ pullRequestDetailsFetched: snapshot.detailsFetched,
+ pullRequestDetailsLimit: MAX_PULL_REQUEST_DETAILS,
+ apiRequests: budget.requests,
+ apiRequestLimit: MAX_API_REQUESTS,
+ errors,
+ },
+ counts: countCategories(classified),
+ pullRequests: classified,
+ };
+}
+
+async function collectApiSnapshot({ apiRequest, identity, cutoff }) {
+ const budget = {
+ requests: 0,
+ startedAt: Date.now(),
+ limitMs: MAX_TOTAL_TIME_MS,
+ };
+ const requestGet = async (endpoint) => {
+ if (budget.requests >= MAX_API_REQUESTS)
+ throw new Error("lookup_request_budget_exhausted");
+ if (Date.now() - budget.startedAt >= budget.limitMs)
+ throw new Error("lookup_time_budget_exhausted");
+ budget.requests += 1;
+ return await apiRequest("GET", endpoint, budget);
+ };
+ const pulls = await listOpenPullRequests({
+ requestGet,
+ identity,
+ });
+ const candidates = pulls.items.filter(
+ (pull) =>
+ !pull.draft &&
+ Number.isFinite(Date.parse(pull.createdAt ?? "")) &&
+ Date.parse(pull.createdAt ?? "") >= cutoff &&
+ pull.base.ref === identity.defaultBranch &&
+ (identity.repositoryId === null ||
+ pull.base.repositoryId === identity.repositoryId),
+ );
+ const runs = await listRecentWorkflowRuns({
+ requestGet,
+ identity,
+ cutoff,
+ });
+
+ const detailResult = await resolveMergeability({
+ candidates,
+ requestGet,
+ identity,
+ });
+ return {
+ budget,
+ pulls,
+ runs,
+ candidates,
+ detailsFetched: detailResult.fetched,
+ detailsErrors: detailResult.errors,
+ };
+}
+
+function classifyPullRequests(snapshot) {
+ const { candidates, pulls, runs } = snapshot;
+ const identityCounts = countHeadIdentities(pulls.items);
+ return candidates.map((pull) => {
+ const relevant = [];
+ const ambiguousRuns = [];
+ for (const run of runs.items) {
+ const match = matchRunToPullRequest({
+ run,
+ pull,
+ identityCounts,
+ inventoryComplete: pulls.complete,
+ });
+ if (match.kind === "match") relevant.push({ run, basis: match.basis });
+ if (match.kind === "ambiguous")
+ ambiguousRuns.push({ run, basis: "ambiguous_head_identity" });
+ }
+ return {
+ ...pull,
+ ci: classifyPrCi({
+ pull,
+ runs: relevant,
+ runsComplete: runs.complete,
+ pullRequestInventoryComplete: pulls.complete,
+ ambiguousRuns,
+ }),
+ };
+ });
+}
+
+async function resolveMergeability({ candidates, requestGet, identity }) {
+ let detailsFetched = 0;
+ const errors = new Set();
+ for (const pull of candidates) {
+ if (pull.mergeable !== undefined) continue;
+ if (detailsFetched >= MAX_PULL_REQUEST_DETAILS) {
+ errors.add("pull_request_mergeability_lookup_budget_exhausted");
+ continue;
+ }
+ const result = await getPullRequestDetails({ requestGet, identity, pull });
+ detailsFetched += 1;
+ if (result.error !== null) {
+ errors.add(result.error);
+ continue;
+ }
+ const { detail } = result;
+ const identityChanged =
+ detail.headSha !== pull.head.sha ||
+ detail.headRef !== pull.head.ref ||
+ detail.headRepositoryId !== pull.head.repositoryId ||
+ detail.baseRef !== pull.base.ref ||
+ detail.baseRepositoryId !== pull.base.repositoryId;
+ if (identityChanged) {
+ pull.identityValid = false;
+ pull.identityChangedDuringScan = true;
+ errors.add("pull_request_identity_changed_during_scan");
+ } else {
+ pull.mergeable = detail.mergeable;
+ pull.detailFetched = true;
+ if (detail.mergeable === undefined)
+ errors.add("pull_request_mergeability_unknown");
+ }
+ }
+ return { fetched: detailsFetched, errors: [...errors] };
+}
+
+function countCategories(pulls) {
+ const categories = [
+ "missing",
+ "startup_failure",
+ "queued_pending_executing",
+ "approval_required",
+ "completed_success",
+ "completed_failure",
+ "completed_canceled",
+ "completed_other",
+ "merge_conflict",
+ "unknown",
+ "deferred",
+ ];
+ return Object.fromEntries(
+ categories.map((category) => [
+ category,
+ pulls.filter(({ ci }) => ci.category === category).length,
+ ]),
+ );
+}
+
+/** Pure status classifier; missing requires complete, identity-safe inventories. */
+export function classifyPrCi({
+ pull,
+ runs,
+ runsComplete,
+ pullRequestInventoryComplete,
+ ambiguousRuns,
+}) {
+ if (!pull.identityValid)
+ return result("unknown", "pull_request_head_identity_invalid", []);
+ const evidence = [...runs, ...ambiguousRuns];
+ if (pull.mergeable === false)
+ return result("merge_conflict", "github_reports_merge_conflict", evidence);
+ // The newest collected run is not proven current until pagination completes:
+ // an older-created run outside this snapshot may have been retried recently.
+ if (!runsComplete)
+ return result(
+ evidence.length > 0 ? "unknown" : "deferred",
+ evidence.length > 0
+ ? "workflow_run_inventory_incomplete"
+ : "lookup_inventory_incomplete",
+ evidence,
+ );
+ const selection = newestRun(runs);
+ if (!selection.known)
+ return result("unknown", "workflow_run_order_unknown", evidence);
+ const latest = selection.latest;
+ if (
+ ambiguousRuns.some((candidate) => {
+ const order = latest ? compareRunEvidence(candidate, latest) : null;
+ return order === null || order >= 0;
+ })
+ )
+ return result("unknown", "workflow_run_identity_ambiguous", evidence);
+ if (latest !== undefined) {
+ const run = latest.run;
+ if (run.conclusion === "startup_failure")
+ return result("startup_failure", "workflow_startup_failure", evidence);
+ if (run.status === "waiting" || run.conclusion === "action_required")
+ return result(
+ "approval_required",
+ "workflow_waiting_for_approval_or_gate",
+ evidence,
+ );
+ if (["queued", "requested", "pending", "in_progress"].includes(run.status))
+ return result(
+ "queued_pending_executing",
+ `workflow_${run.status}`,
+ evidence,
+ );
+ if (run.status === "completed")
+ return result(
+ COMPLETED_CATEGORIES.get(run.conclusion) ?? "completed_other",
+ run.conclusion === "success"
+ ? "workflow_succeeded"
+ : `workflow_${run.conclusion ?? "unknown_conclusion"}`,
+ evidence,
+ );
+ return result("unknown", "workflow_status_unrecognized", evidence);
+ }
+ if (!pullRequestInventoryComplete)
+ return result("deferred", "lookup_inventory_incomplete", []);
+ if (pull.mergeable !== true)
+ return result("unknown", "pull_request_mergeability_unknown", []);
+ return result("missing", "no_pull_request_event_run_for_current_head", []);
+}
+
+/** Match only a pull_request event proven to belong to this immutable PR head. */
+export function matchRunToPullRequest({
+ run,
+ pull,
+ identityCounts,
+ inventoryComplete = true,
+}) {
+ if (run?.event !== "pull_request") return { kind: "none" };
+ if (!pull.identityValid || !isRecord(run)) return { kind: "none" };
+ const association = matchExplicitPullAssociation(run, pull);
+ if (association === "match")
+ return { kind: "match", basis: "event_pr_number_and_head_sha" };
+ if (association === "ambiguous") return { kind: "ambiguous" };
+ if (association === "other" || !sameHeadRepositoryAndRef(run, pull))
+ return { kind: "none" };
+ if (!uniqueHeadRepositoryAndRef(pull, identityCounts, inventoryComplete))
+ return { kind: "ambiguous" };
+ if (isSha(run.head_sha) && run.head_sha.toLowerCase() === pull.head.sha)
+ return { kind: "match", basis: "event_unique_head_ref_and_repository_id" };
+ return { kind: "none" };
+}
+
+function matchExplicitPullAssociation(run, pull) {
+ const associations = Array.isArray(run.pull_requests)
+ ? run.pull_requests
+ : [];
+ if (associations.length === 0) return "absent";
+ const matchingNumber = associations.filter(
+ (item) => item?.number === pull.number,
+ );
+ if (matchingNumber.length === 0) return "other";
+ const exactHead = matchingNumber.find(
+ (item) => item?.head?.sha === pull.head.sha,
+ );
+ if (exactHead === undefined) {
+ return matchingNumber.some(
+ (item) =>
+ typeof item?.head?.sha === "string" && item.head.sha !== pull.head.sha,
+ )
+ ? "other"
+ : "absent";
+ }
+ const associatedHeadRepositoryId = positiveInteger(exactHead.head?.repo?.id);
+ const runHeadRepositoryId = positiveInteger(run.head_repository?.id);
+ if (associatedHeadRepositoryId === null || runHeadRepositoryId === null)
+ return "ambiguous";
+ if (
+ associatedHeadRepositoryId !== pull.head.repositoryId ||
+ runHeadRepositoryId !== pull.head.repositoryId
+ )
+ return "other";
+ const baseRefMatches =
+ typeof exactHead.base?.ref !== "string" ||
+ exactHead.base.ref === pull.base.ref;
+ const baseRepositoryId = positiveInteger(exactHead.base?.repo?.id);
+ const baseRepositoryMatches =
+ baseRepositoryId === null || baseRepositoryId === pull.base.repositoryId;
+ return baseRefMatches && baseRepositoryMatches ? "match" : "other";
+}
+
+function sameHeadRepositoryAndRef(run, pull) {
+ return (
+ run.head_branch === pull.head.ref &&
+ run.head_repository?.id === pull.head.repositoryId
+ );
+}
+
+function uniqueHeadRepositoryAndRef(pull, identityCounts, inventoryComplete) {
+ if (!inventoryComplete) return false;
+ const key = headIdentityKey(pull.head.repositoryId, pull.head.ref);
+ const unknownRepositoryKey = headIdentityKey(null, pull.head.ref);
+ return (
+ identityCounts.get(key) === 1 &&
+ (identityCounts.get(unknownRepositoryKey) ?? 0) === 0
+ );
+}
+
+function normalizePullRequest(raw, identity) {
+ const number = positiveInteger(raw?.number);
+ const base = normalizeBase(raw?.base);
+ const head = normalizeHead(raw?.head);
+ const draft = raw?.draft === true;
+ return {
+ number,
+ draft,
+ createdAt: typeof raw?.created_at === "string" ? raw.created_at : null,
+ url: typeof raw?.html_url === "string" ? raw.html_url : null,
+ base,
+ head,
+ mergeable: typeof raw?.mergeable === "boolean" ? raw.mergeable : undefined,
+ identityValid: hasValidPullIdentity({ raw, identity, number, base, head }),
+ };
+}
+
+function normalizeBase(raw) {
+ return {
+ ref: typeof raw?.ref === "string" ? raw.ref : null,
+ repositoryId: positiveInteger(raw?.repo?.id),
+ };
+}
+
+function normalizeHead(raw) {
+ return {
+ repositoryId: positiveInteger(raw?.repo?.id),
+ repository:
+ typeof raw?.repo?.full_name === "string" ? raw.repo.full_name : null,
+ ref: typeof raw?.ref === "string" ? raw.ref : null,
+ sha:
+ typeof raw?.sha === "string" && isSha(raw.sha)
+ ? raw.sha.toLowerCase()
+ : null,
+ };
+}
+
+function hasValidPullIdentity({ raw, identity, number, base, head }) {
+ return (
+ number !== null &&
+ typeof raw?.draft === "boolean" &&
+ base.ref === identity.defaultBranch &&
+ (identity.repositoryId === null ||
+ base.repositoryId === identity.repositoryId) &&
+ head.repositoryId !== null &&
+ head.sha !== null &&
+ head.ref !== null
+ );
+}
+
+function countHeadIdentities(pulls) {
+ const counts = new Map();
+ for (const pull of pulls) {
+ if (typeof pull.head.ref !== "string") continue;
+ const key = headIdentityKey(pull.head.repositoryId, pull.head.ref);
+ counts.set(key, (counts.get(key) ?? 0) + 1);
+ }
+ return counts;
+}
+
+function headIdentityKey(repositoryId, ref) {
+ return `${String(repositoryId)}\u0000${ref}`;
+}
+
+function newestRun(runs) {
+ let latest = runs[0];
+ for (const candidate of runs) {
+ if (latest && (compareRunEvidence(candidate, latest) ?? 0) > 0)
+ latest = candidate;
+ }
+ const known = runs.every((candidate) => {
+ if (candidate === latest) return true;
+ const order = compareRunEvidence(candidate, latest);
+ return order !== null && order <= 0;
+ });
+ return { known, latest };
+}
+
+// Attempt ordering is meaningful within one run; different run IDs are not clocks.
+function compareRunEvidence(left, right) {
+ const leftRun = left.run;
+ const rightRun = right.run;
+ const id = positiveInteger(leftRun.id);
+ if (id !== null && id === positiveInteger(rightRun.id)) {
+ const leftAttempt = positiveInteger(leftRun.run_attempt);
+ const rightAttempt = positiveInteger(rightRun.run_attempt);
+ if (leftAttempt === null || rightAttempt === null) return null;
+ if (leftAttempt !== rightAttempt) return leftAttempt - rightAttempt;
+ return leftRun.status === rightRun.status &&
+ leftRun.conclusion === rightRun.conclusion
+ ? 0
+ : null;
+ }
+ // A retry may start long after its original creation; without an attempt
+ // clock, creation dates only order first attempts from different runs.
+ if (
+ positiveInteger(leftRun.run_attempt) !== 1 ||
+ positiveInteger(rightRun.run_attempt) !== 1
+ )
+ return null;
+ const time =
+ Date.parse(leftRun.created_at ?? "") -
+ Date.parse(rightRun.created_at ?? "");
+ return Number.isFinite(time) && time !== 0 ? time : null;
+}
+
+function result(category, reason, runs) {
+ return {
+ category,
+ reason,
+ runs: runs.map(({ run, basis }) => ({
+ id: positiveInteger(run.id),
+ attempt: positiveInteger(run.run_attempt),
+ status: typeof run.status === "string" ? run.status : null,
+ conclusion: typeof run.conclusion === "string" ? run.conclusion : null,
+ headSha: isSha(run.head_sha) ? run.head_sha.toLowerCase() : null,
+ createdAt: typeof run.created_at === "string" ? run.created_at : null,
+ url: typeof run.html_url === "string" ? run.html_url : null,
+ identityBasis: basis,
+ })),
+ };
+}
+
+async function listOpenPullRequests({ requestGet, identity }) {
+ const items = [];
+ let complete = false;
+ let pages = 0;
+ let error = null;
+ for (let page = 1; page <= MAX_PULL_REQUEST_PAGES; page += 1) {
+ try {
+ const endpoint = `repos/${identity.repository}/pulls?state=open&sort=created&direction=desc&per_page=${PAGE_SIZE}&page=${page}`;
+ const response = await requestGet(endpoint);
+ if (!Array.isArray(response))
+ throw new Error("pull_request_page_malformed");
+ pages += 1;
+ const normalized = response.map((raw) =>
+ normalizePullRequest(raw, identity),
+ );
+ if (
+ normalized.some(
+ (pull) => !Number.isFinite(Date.parse(pull.createdAt ?? "")),
+ )
+ )
+ throw new Error("pull_request_page_malformed");
+ items.push(...normalized);
+ if (response.length < PAGE_SIZE) {
+ complete = true;
+ break;
+ }
+ } catch (cause) {
+ error = safeErrorCode(cause);
+ break;
+ }
+ }
+ return { items, pages, complete, error };
+}
+
+async function listRecentWorkflowRuns({ requestGet, identity, cutoff }) {
+ const items = [];
+ let complete = false;
+ let pages = 0;
+ let error = null;
+ for (let page = 1; page <= MAX_WORKFLOW_RUN_PAGES; page += 1) {
+ try {
+ const cutoffDate = new Date(cutoff).toISOString().slice(0, 10);
+ const endpoint = `repos/${identity.repository}/actions/workflows/ci.yml/runs?event=pull_request&created=%3E%3D${cutoffDate}&per_page=${PAGE_SIZE}&page=${page}`;
+ const response = await requestGet(endpoint);
+ if (!isRecord(response) || !Array.isArray(response.workflow_runs))
+ throw new Error("workflow_run_page_malformed");
+ pages += 1;
+ if (!response.workflow_runs.every(isRecord))
+ throw new Error("workflow_run_page_malformed");
+ items.push(...response.workflow_runs);
+ if (response.workflow_runs.length < PAGE_SIZE) {
+ complete = true;
+ break;
+ }
+ } catch (cause) {
+ error = safeErrorCode(cause);
+ break;
+ }
+ }
+ return { items, pages, complete, error };
+}
+
+async function getPullRequestDetails({ requestGet, identity, pull }) {
+ if (pull.number === null)
+ return { detail: null, error: "pull_request_detail_identity_missing" };
+ try {
+ const response = await requestGet(
+ `repos/${identity.repository}/pulls/${pull.number}`,
+ );
+ if (!isRecord(response))
+ return { detail: null, error: "pull_request_detail_malformed" };
+ return {
+ detail: {
+ mergeable:
+ typeof response.mergeable === "boolean"
+ ? response.mergeable
+ : undefined,
+ headSha: isSha(response.head?.sha)
+ ? response.head.sha.toLowerCase()
+ : null,
+ headRef:
+ typeof response.head?.ref === "string" ? response.head.ref : null,
+ headRepositoryId: positiveInteger(response.head?.repo?.id),
+ baseRef:
+ typeof response.base?.ref === "string" ? response.base.ref : null,
+ baseRepositoryId: positiveInteger(response.base?.repo?.id),
+ },
+ error: null,
+ };
+ } catch (cause) {
+ return { detail: null, error: safeErrorCode(cause) };
+ }
+}
+
+function validateInputs({
+ repository,
+ repositoryId,
+ defaultBranch,
+ lookbackDays,
+ now,
+}) {
+ if (
+ typeof repository !== "string" ||
+ !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repository)
+ )
+ throw new Error(
+ "CI_REPOSITORY must be an owner/name repository identifier",
+ );
+ if (
+ typeof defaultBranch !== "string" ||
+ defaultBranch.length === 0 ||
+ /[\r\n]/u.test(defaultBranch)
+ )
+ throw new Error("CI_DEFAULT_BRANCH must be a nonempty branch name");
+ const parsedRepositoryId =
+ repositoryId === undefined || repositoryId === null || repositoryId === ""
+ ? null
+ : positiveInteger(repositoryId);
+ if (
+ repositoryId !== undefined &&
+ repositoryId !== null &&
+ repositoryId !== "" &&
+ parsedRepositoryId === null
+ )
+ throw new Error("CI_REPOSITORY_ID must be a positive safe integer");
+ if (
+ !Number.isInteger(lookbackDays) ||
+ lookbackDays < 1 ||
+ lookbackDays > MAX_LOOKBACK_DAYS
+ )
+ throw new Error(
+ `lookbackDays must be an integer from 1 to ${String(MAX_LOOKBACK_DAYS)}`,
+ );
+ if (!Number.isFinite(now)) throw new Error("now must be a finite timestamp");
+ return {
+ repository,
+ repositoryId: parsedRepositoryId,
+ defaultBranch,
+ lookbackDays,
+ };
+}
+
+function positiveInteger(value) {
+ const number = typeof value === "number" ? value : Number(value);
+ return Number.isSafeInteger(number) && number > 0 ? number : null;
+}
+
+function isSha(value) {
+ return typeof value === "string" && SHA_PATTERN.test(value);
+}
+
+function isRecord(value) {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+
+function safeErrorCode(cause) {
+ if (cause?.message === "lookup_time_budget_exhausted")
+ return "lookup_time_budget_exhausted";
+ if (cause?.message === "lookup_request_budget_exhausted")
+ return "lookup_request_budget_exhausted";
+ if (cause?.message === "github_api_malformed_json")
+ return "github_api_malformed_json";
+ if (cause?.message?.endsWith("_malformed")) return cause.message;
+ if (cause?.code === "ETIMEDOUT" || cause?.killed) return "github_api_timeout";
+ return "github_api_request_failed";
+}
+
+export async function writePrCiHealthReport(
+ report,
+ outputDirectory = OUTPUT_DIRECTORY,
+) {
+ const jsonPath = join(outputDirectory, "report.json");
+ const markdownPath = join(outputDirectory, "summary.md");
+ await mkdir(outputDirectory, { recursive: true });
+ await writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
+ await writeFile(markdownPath, renderMarkdownSummary(report), "utf8");
+ return { jsonPath, markdownPath };
+}
+
+export function renderMarkdownSummary(report) {
+ const rows = report.pullRequests.map(
+ (pull) =>
+ `| #${String(pull.number ?? "?")} | ${escapeCell(pull.head.repository ?? "unknown")} | ${escapeCell(pull.head.ref ?? "unknown")} | \`${pull.head.sha ?? "unknown"}\` | ${pull.ci.category} | ${escapeCell(pull.ci.reason)} |`,
+ );
+ return [
+ "# Pull request CI health snapshot",
+ "",
+ `Observed ${report.observedAt} for \`${report.repository}\`, target branch \`${escapeCell(report.defaultBranch)}\`; recent window ${String(report.lookbackDays)} days. Scan: **${report.scan.status}**.`,
+ "",
+ "This is read-only point-in-time telemetry. Missing is reported only when PR and workflow-run inventories are complete and the current immutable PR head identity is unambiguous. Incomplete lookups remain deferred or unknown.",
+ "",
+ `API pages: ${String(report.scan.pullRequestPages)} PR, ${String(report.scan.workflowRunPages)} workflow runs; ${String(report.scan.apiRequests)} bounded requests. Errors: ${report.scan.errors.length === 0 ? "none" : report.scan.errors.join(", ")}.`,
+ "",
+ "| PR | Head repository | Head ref | Head SHA | CI classification | Evidence |",
+ "| ---: | --- | --- | --- | --- | --- |",
+ ...rows,
+ "",
+ "Categories: missing, startup failure, queued/pending/executing, approval required, completed outcomes, merge conflict, unknown, and deferred.",
+ "",
+ ].join("\n");
+}
+
+function escapeCell(value) {
+ return String(value)
+ .replaceAll("|", "\\|")
+ .replaceAll("\n", " ")
+ .replaceAll("\r", " ");
+}
+
+if (
+ process.argv[1] !== undefined &&
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url)
+) {
+ try {
+ const report = await collectPrCiHealth({
+ repository: process.env.CI_REPOSITORY,
+ repositoryId: process.env.CI_REPOSITORY_ID,
+ defaultBranch: process.env.CI_DEFAULT_BRANCH,
+ });
+ const paths = await writePrCiHealthReport(
+ report,
+ process.env.CI_PR_HEALTH_DIRECTORY ?? OUTPUT_DIRECTORY,
+ );
+ if (process.env.GITHUB_STEP_SUMMARY)
+ await appendFile(
+ process.env.GITHUB_STEP_SUMMARY,
+ renderMarkdownSummary(report),
+ "utf8",
+ );
+ process.stdout.write(
+ `Wrote ${report.pullRequests.length} recent PR CI records to ${paths.jsonPath}\n`,
+ );
+ } catch (cause) {
+ process.stderr.write(
+ `PR CI health telemetry failed: ${cause instanceof Error ? cause.message : "unknown_error"}\n`,
+ );
+ process.exitCode = 1;
+ }
+}
diff --git a/scripts/ci/qualification.mjs b/scripts/ci/qualification.mjs
index ab47c168f..8dfb3f20c 100644
--- a/scripts/ci/qualification.mjs
+++ b/scripts/ci/qualification.mjs
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { normalizeWorkflowRun } from "./workflow-run.mjs";
const ARTIFACT_NAME = "ci-full-qualification-v1";
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
@@ -119,8 +120,8 @@ function admit() {
// Artifact metadata narrows the search before run lookups. Never scan an
// unbounded history or treat a missing receipt as a successful qualification.
for (const artifact of candidates.slice(0, 3)) {
- const run = api(
- `repos/${repository}/actions/runs/${artifact.workflow_run.id}`,
+ const run = normalizeWorkflowRun(
+ api(`repos/${repository}/actions/runs/${artifact.workflow_run.id}`),
);
// An old run can upload a new artifact when only its gate is rerun.
// Bound the original run age as well so retries cannot renew stale proof.
@@ -133,7 +134,7 @@ function admit() {
run.id === artifact.workflow_run.id &&
run.status === "completed" &&
run.conclusion === "success" &&
- run.path === ".github/workflows/ci.yml" &&
+ run.workflowPath?.file === ".github/workflows/ci.yml" &&
["schedule", "workflow_dispatch"].includes(run.event) &&
run.head_sha === head &&
run.head_branch === branch &&
diff --git a/scripts/ci/required.mjs b/scripts/ci/required.mjs
index 01907de37..1839a1149 100644
--- a/scripts/ci/required.mjs
+++ b/scripts/ci/required.mjs
@@ -30,7 +30,12 @@ if (outputs.tier === "full" && selectedScopes.length !== CI_FLAGS.length)
throw new Error("Full qualification requires every canonical CI scope");
const required = reused ? ["changes"] : ["changes", "static", "test"];
const lanes = {
- code: ["build", "test-shard"],
+ code: [
+ "build",
+ "test-shard",
+ ...(outputs.tier === "fast" ? ["test-source"] : []),
+ ],
+ reverse_tools: ["reverse-tools-quality"],
runtime: ["windows-native-build", "package-e2e"],
package: ["windows-curated"],
sqlite: ["sqlite-runtime"],
diff --git a/scripts/ci/scopes.mjs b/scripts/ci/scopes.mjs
index a4f0f0fac..b13057b33 100644
--- a/scripts/ci/scopes.mjs
+++ b/scripts/ci/scopes.mjs
@@ -2,6 +2,62 @@
// Shared build inputs are classified once in plan.mjs rather than repeated in
// every workflow.
export const SCOPES = {
+ reverse_tools: [
+ "src/application/reverse/**",
+ "src/application/binary/BinarySession.ts",
+ "src/cutter/**",
+ "src/gdb/**",
+ "src/frida/frida-bindings.d.ts",
+ "src/objdump/**",
+ "src/cli/reverseEngineeringCommands.ts",
+ "src/process/ownedDebuggerSession.ts",
+ "src/rizin/**",
+ "src/contracts/*ToolContracts.ts",
+ "src/server/registerCutterTools.ts",
+ "src/server/externalMutationAdmission.ts",
+ "src/server/registerGdbTools.ts",
+ "src/server/registerReverseEngineeringTools.ts",
+ "src/server/providerEnvironment.ts",
+ "src/process/ExecFileOutput.ts",
+ "src/process/ExecFileOutput.test.ts",
+ "tests/composition/analysis-sessions/binarySession.test.ts",
+ "tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts",
+ "tests/boundary/filesystem/reverseToolsMutationConfig.test.ts",
+ "src/server/registerRizinDebugTools.ts",
+ "tests/boundary/providers/CutterBridge*.test.ts",
+ "tests/boundary/providers/cutterBridgeProducer.test.ts",
+ "tests/boundary/providers/GdbSession*.test.ts",
+ "tests/boundary/providers/GdbCommandQueue.test.ts",
+ "tests/boundary/providers/gdbTestHelpers.ts",
+ "tests/boundary/providers/RizinDebugSessionCoverage.part*.test.ts",
+ "tests/boundary/providers/RizinCommandTimeout.test.ts",
+ "tests/boundary/providers/RizinTimeoutEvidence.test.ts",
+ "tests/boundary/providers/RizinDebugSession*.test.ts",
+ "tests/boundary/providers/RizinInferiorTty.test.ts",
+ "tests/boundary/providers/RizinTestInferiorTty.ts",
+ "tests/boundary/process/ownedDebuggerSession.test.ts",
+ "tests/boundary/cli/gdbStartupCleanupCli.test.ts",
+ "tests/boundary/cli/reverseEngineeringCommands.test.ts",
+ "tests/boundary/mcp/cutter*.test.ts",
+ "tests/boundary/mcp/gdbCancellationMcp.test.ts",
+ "tests/boundary/mcp/debuggerCancellationMcp.test.ts",
+ "tests/boundary/mcp/rizinDebuggerCancellationMcp.test.ts",
+ "tests/boundary/mcp/reverseEngineeringPartialMcp.test.ts",
+ "tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts",
+ "tests/boundary/mcp/reverseEngineeringEnvironmentMcp.test.ts",
+ "tests/support/reverseEngineering*.ts",
+ "tests/support/ownedRizinCommandEvidenceTests.ts",
+ "tests/support/reverseDebuggerMcpTestHelpers.ts",
+ "tests/processOwnershipRunner.ts",
+ "tests/composition/reverseEngineering*.test.ts",
+ "vitest.reverse-tools.config.ts",
+ "vitest.reverse-tools-semantics.config.ts",
+ "stryker.config.mjs",
+ "stryker.semantics.config.mjs",
+ "tsconfig.reverse-tools-mutation.json",
+ "scripts/prepare-reverse-tools-mutation.mjs",
+ "scripts/lib/reverse-tools-mutation-targets.mjs",
+ ],
// Apple artifact CLI/MCP lane on macOS, including its focused regressions.
apple: [
"src/native/Apple*",
diff --git a/scripts/ci/static-check-report.mjs b/scripts/ci/static-check-report.mjs
index 04460b2c2..eecaf750a 100644
--- a/scripts/ci/static-check-report.mjs
+++ b/scripts/ci/static-check-report.mjs
@@ -133,6 +133,8 @@ try {
for (const clone of report.duplicates)
for (const file of [clone.firstFile, clone.secondFile])
file.name = relative(process.cwd(), file.name).split(sep).join("/");
+ // Record the actual checker result, including caller-selected thresholds.
+ report.reaCheck = { exitCode: result.status };
await writeFile(reportPath, JSON.stringify(report, null, 2));
const { sources, clones, percentage } = report.statistics.total;
console.log(
diff --git a/scripts/ci/static-task-context.d.mts b/scripts/ci/static-task-context.d.mts
new file mode 100644
index 000000000..164756b15
--- /dev/null
+++ b/scripts/ci/static-task-context.d.mts
@@ -0,0 +1,18 @@
+export interface StaticTaskContext {
+ readonly version: 1;
+ readonly sourceSha: string;
+ readonly sourceClean: boolean;
+ readonly prHeadSha?: string;
+ readonly nodeVersion: string;
+ readonly npmVersion: string;
+ readonly turboVersion: string;
+ readonly platform: string;
+ readonly architecture: string;
+ readonly taskHashes: Readonly>;
+}
+export function parseStaticTaskContext(value: unknown): StaticTaskContext;
+export function recordStaticTaskContext(options?: {
+ root?: string;
+ npmCli?: string;
+ outputDirectory?: string;
+}): Promise;
diff --git a/scripts/ci/static-task-context.mjs b/scripts/ci/static-task-context.mjs
new file mode 100644
index 000000000..879c568d1
--- /dev/null
+++ b/scripts/ci/static-task-context.mjs
@@ -0,0 +1,104 @@
+import { execFile } from "node:child_process";
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { promisify } from "node:util";
+import { z } from "zod";
+
+const execute = promisify(execFile);
+const contextSchema = z.object({
+ version: z.literal(1),
+ sourceSha: z.string().regex(/^[a-f0-9]{40}$/u),
+ sourceClean: z.boolean(),
+ prHeadSha: z
+ .string()
+ .regex(/^[a-f0-9]{40}$/u)
+ .optional(),
+ nodeVersion: z.string().min(1),
+ npmVersion: z.string().min(1),
+ turboVersion: z.string().min(1),
+ platform: z.string().min(1),
+ architecture: z.string().min(1),
+ taskHashes: z.record(z.string(), z.string().regex(/^[a-f0-9]{16}$/u)),
+});
+
+/** Parse the producing check graph's receipt; incomplete provenance is unknown. */
+export const parseStaticTaskContext = (value) => contextSchema.parse(value);
+
+/** Retain only named task hashes and tool/source identity, never the environment. */
+export async function recordStaticTaskContext({
+ root = process.cwd(),
+ npmCli = process.env.npm_execpath,
+ outputDirectory = join(root, ".cache/static-checks"),
+} = {}) {
+ if (typeof npmCli !== "string" || !npmCli)
+ throw new Error(
+ "Run static context recording through npm run ci:static-context",
+ );
+ const options = {
+ cwd: root,
+ encoding: "utf8",
+ timeout: 30_000,
+ maxBuffer: 32 * 1024 * 1024,
+ };
+ const [graph, source, status, npmVersion, turbo] = await Promise.all([
+ execute(
+ process.execPath,
+ [npmCli, "run", "--silent", "check:ci:tasks", "--", "--dry=json"],
+ options,
+ ),
+ execute("git", ["rev-parse", "HEAD"], options),
+ execute(
+ "git",
+ ["status", "--porcelain", "--untracked-files=normal"],
+ options,
+ ),
+ execute(process.execPath, [npmCli, "--version"], options),
+ readFile(join(root, "node_modules/turbo/package.json"), "utf8"),
+ ]);
+ const tasks = z
+ .object({
+ tasks: z
+ .array(
+ z.object({
+ task: z.string().min(1),
+ hash: z.string().regex(/^[a-f0-9]{16}$/u),
+ }),
+ )
+ .min(1),
+ })
+ .parse(JSON.parse(graph.stdout)).tasks;
+ if (new Set(tasks.map(({ task }) => task)).size !== tasks.length)
+ throw new Error("Static graph contains duplicate task identities");
+ const context = parseStaticTaskContext({
+ version: 1,
+ sourceSha: source.stdout.trim(),
+ sourceClean: status.stdout.length === 0,
+ ...(process.env.CI_PR_HEAD_SHA
+ ? { prHeadSha: process.env.CI_PR_HEAD_SHA }
+ : {}),
+ nodeVersion: process.version,
+ npmVersion: npmVersion.stdout.trim(),
+ turboVersion: z.object({ version: z.string() }).parse(JSON.parse(turbo))
+ .version,
+ platform: process.platform,
+ architecture: process.arch,
+ taskHashes: Object.fromEntries(tasks.map(({ task, hash }) => [task, hash])),
+ });
+ if (process.env.GITHUB_SHA && process.env.GITHUB_SHA !== context.sourceSha)
+ throw new Error(
+ "Static context source does not match the workflow revision",
+ );
+ await mkdir(outputDirectory, { recursive: true });
+ await writeFile(
+ join(outputDirectory, "static-task-context.json"),
+ JSON.stringify(context, null, 2) + "\n",
+ );
+ return context;
+}
+
+if (
+ process.argv[1] &&
+ resolve(process.argv[1]) === fileURLToPath(import.meta.url)
+)
+ await recordStaticTaskContext();
diff --git a/scripts/ci/test-group.mjs b/scripts/ci/test-group.mjs
new file mode 100644
index 000000000..7d99175b4
--- /dev/null
+++ b/scripts/ci/test-group.mjs
@@ -0,0 +1,46 @@
+import { spawn } from "node:child_process";
+import { resolve } from "node:path";
+
+import {
+ BUILT_TEST_PROJECTS,
+ SOURCE_TEST_PROJECTS,
+} from "../lib/test-projects.mjs";
+
+const [group, ...arguments_] = process.argv.slice(2);
+const projects = { source: SOURCE_TEST_PROJECTS, built: BUILT_TEST_PROJECTS }[
+ group
+];
+if (!projects) throw new Error("Expected source or built test group");
+// The group owns project selection; callers can request reporters or shards.
+if (
+ arguments_.some((argument) => /^--(?:project|exclude)(?:=|$)/u.test(argument))
+)
+ throw new Error("Test groups cannot override their project inventory");
+const child = spawn(
+ process.execPath,
+ [
+ resolve("node_modules/vitest/vitest.mjs"),
+ "run",
+ ...projects.flatMap(({ name }) => ["--project", name]),
+ ...arguments_,
+ ],
+ { stdio: "inherit" },
+);
+let shutdownSignal;
+for (const signal of ["SIGINT", "SIGTERM"])
+ process.on(signal, () => {
+ shutdownSignal ??= signal;
+ child.kill(signal);
+ });
+child.once("error", (error) => {
+ console.error(error.message);
+ process.exitCode = 1;
+});
+child.once("close", (code) => {
+ process.exitCode =
+ shutdownSignal === "SIGINT"
+ ? 130
+ : shutdownSignal === "SIGTERM"
+ ? 143
+ : (code ?? 1);
+});
diff --git a/scripts/ci/test-history.mjs b/scripts/ci/test-history.mjs
index 4386f48af..14dbb582b 100644
--- a/scripts/ci/test-history.mjs
+++ b/scripts/ci/test-history.mjs
@@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
+import { normalizeWorkflowRun } from "./workflow-run.mjs";
const ARTIFACT_NAME = "test-timings";
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
@@ -68,8 +69,8 @@ function findTimingRun({ repository, branch, repositoryId, now = Date.now() }) {
.slice(0, MAX_CANDIDATES);
for (const artifact of candidates) {
- const run = api(
- `repos/${repository}/actions/runs/${artifact.workflow_run.id}`,
+ const run = normalizeWorkflowRun(
+ api(`repos/${repository}/actions/runs/${artifact.workflow_run.id}`),
);
const runAge = now - Date.parse(run?.created_at);
if (
@@ -80,7 +81,7 @@ function findTimingRun({ repository, branch, repositoryId, now = Date.now() }) {
run.id === artifact.workflow_run.id &&
run.status === "completed" &&
run.conclusion === "success" &&
- run.path === ".github/workflows/ci.yml" &&
+ run.workflowPath?.file === ".github/workflows/ci.yml" &&
["schedule", "workflow_dispatch"].includes(run.event) &&
run.head_branch === branch &&
run.head_repository?.full_name === repository
diff --git a/scripts/ci/test-report.mjs b/scripts/ci/test-report.mjs
index 035a01990..7f85c6ec5 100644
--- a/scripts/ci/test-report.mjs
+++ b/scripts/ci/test-report.mjs
@@ -41,9 +41,20 @@ const files = report.files.map(({ name, durationMs, state }) => {
throw new Error(`Test report path is outside the checkout: ${name}`);
return { name: path.split(sep).join("/"), durationMs, state };
});
-const complete = process.env.SHARD_RESULT === "success";
+// Keep group outcomes intact: an unavailable group is not a completed failure.
+const groupResults = [
+ process.env.SHARD_RESULT,
+ ...(process.env.SOURCE_REQUIRED === "true"
+ ? [process.env.SOURCE_RESULT]
+ : []),
+];
+const groupsCompleted = groupResults.every((state) =>
+ ["success", "failure"].includes(state),
+);
const passed =
- complete && process.env.MERGE_RESULT === "success" && report.success;
+ groupResults.every((state) => state === "success") &&
+ process.env.MERGE_RESULT === "success" &&
+ report.success;
// Successful shards and merged reports must account for the complete portable
// inventory. Coverage thresholds alone cannot prove that every file ran.
const verifyInventory = process.env.CI_VERIFY_TEST_INVENTORY === "true";
@@ -69,7 +80,7 @@ if (process.env.CI_TEST_SELECTION) {
comparison.complete &&
report.unhandledErrors === 0 &&
["passed", "failed"].includes(report.reason) &&
- ["success", "failure"].includes(process.env.SHARD_RESULT) &&
+ groupsCompleted &&
["success", "failure"].includes(process.env.MERGE_RESULT),
executionSha: process.env.GITHUB_SHA ?? null,
prHeadSha: process.env.CI_PR_HEAD_SHA ?? null,
@@ -91,7 +102,9 @@ if (passed && verifyInventory) {
);
}
const summary = [
- complete ? "## Merged test diagnostics" : "## Partial test diagnostics",
+ groupsCompleted
+ ? "## Merged test diagnostics"
+ : "## Partial test diagnostics",
"",
...(selection
? [
@@ -99,11 +112,14 @@ const summary = [
"",
`Plan: ${selection.plan.mode}. Proposed files: ${selection.selected.length}; omitted: ${selection.omitted.length}.`,
`Full-run comparison: ${selection.complete ? "complete" : "incomplete"}; failures outside proposed selection: ${selection.missedFailures.length}.`,
- "The complete suite still ran. This report cannot enable omissions or qualify a revision. Skipped, missing, and interrupted observations do not establish parity.",
+ "This observational report cannot enable omissions or qualify a revision. Skipped, missing, and interrupted observations do not establish parity.",
"",
]
: []),
`Shard result: ${process.env.SHARD_RESULT ?? "unknown"}; merge result: ${process.env.MERGE_RESULT ?? "unknown"}.`,
+ ...(process.env.SOURCE_REQUIRED === "true"
+ ? [`Source result: ${process.env.SOURCE_RESULT ?? "unknown"}.`]
+ : []),
passed
? "Successful timing history is available."
: "This report is diagnostic only and is not a successful timing baseline.",
diff --git a/scripts/ci/workflow-run.d.mts b/scripts/ci/workflow-run.d.mts
new file mode 100644
index 000000000..b0e626378
--- /dev/null
+++ b/scripts/ci/workflow-run.d.mts
@@ -0,0 +1,10 @@
+export interface WorkflowPath {
+ readonly file: string;
+ readonly ref: string | null;
+}
+export interface NormalizedWorkflowRun {
+ readonly workflowPath: WorkflowPath | null;
+}
+export function normalizeWorkflowRun>(
+ value: T,
+): Omit & NormalizedWorkflowRun;
diff --git a/scripts/ci/workflow-run.mjs b/scripts/ci/workflow-run.mjs
new file mode 100644
index 000000000..8c33214bf
--- /dev/null
+++ b/scripts/ci/workflow-run.mjs
@@ -0,0 +1,15 @@
+/** Keep the API path as evidence and parse its optional ref at the boundary. */
+export function normalizeWorkflowRun(value) {
+ if (typeof value !== "object" || value === null || Array.isArray(value))
+ throw new Error("workflow_run_malformed");
+ const match =
+ typeof value.path === "string" && !/[\u0000-\u001f\u007f]/u.test(value.path)
+ ? /^(\.github\/workflows\/[^/\\@\s]+\.ya?ml)(?:@([^\s]+))?$/u.exec(
+ value.path,
+ )
+ : null;
+ return {
+ ...value,
+ workflowPath: match ? { file: match[1], ref: match[2] ?? null } : null,
+ };
+}
diff --git a/scripts/lib/browser-nested-source-map-e2e.mjs b/scripts/lib/browser-nested-source-map-e2e.mjs
index ff2af8863..d7bac5ee2 100644
--- a/scripts/lib/browser-nested-source-map-e2e.mjs
+++ b/scripts/lib/browser-nested-source-map-e2e.mjs
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { TraceMap, eachMapping } from "@jridgewell/trace-mapping";
import { CdpBrowserProvider } from "../../dist/browser/CdpBrowserProvider.js";
+import { listBrowserTargetsInputSchema } from "../../dist/domain/browserObservation.js";
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { traceSourceMap } from "../../dist/javascript/sourceMaps/TraceSourceMap.js";
@@ -89,10 +90,12 @@ async function verifyCase(executable, entrypoint, scenario) {
fixture = await startRuntimeFixtureBrowser(executable, site.origin);
const endpoint = `http://127.0.0.1:${fixture.port}`;
const provider = new CdpBrowserProvider();
- const targets = await provider.listTargets({
- cdp_endpoint: endpoint,
- allowed_origins: [site.origin],
- });
+ const targets = await provider.listTargets(
+ listBrowserTargetsInputSchema.parse({
+ cdp_endpoint: endpoint,
+ allowed_origins: [site.origin],
+ }),
+ );
if (!targets.ok) throw targets.error;
const target = targets.value.targets[0];
assert.ok(target, "Owned fixture target missing");
diff --git a/scripts/lib/browser-runtime-e2e.mjs b/scripts/lib/browser-runtime-e2e.mjs
index c4cb4f09f..b35cde09a 100644
--- a/scripts/lib/browser-runtime-e2e.mjs
+++ b/scripts/lib/browser-runtime-e2e.mjs
@@ -8,6 +8,7 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { CdpBrowserProvider } from "../../dist/browser/CdpBrowserProvider.js";
+import { listBrowserTargetsInputSchema } from "../../dist/domain/browserObservation.js";
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { webExecutionSchema } from "../../dist/domain/webExecution.js";
@@ -45,10 +46,12 @@ export async function verifyBrowserRuntime(
try {
fixture = await startRuntimeFixtureBrowser(executable, site.origin);
const endpoint = `http://127.0.0.1:${fixture.port}`;
- const targets = await new CdpBrowserProvider().listTargets({
- cdp_endpoint: endpoint,
- allowed_origins: [site.origin],
- });
+ const targets = await new CdpBrowserProvider().listTargets(
+ listBrowserTargetsInputSchema.parse({
+ cdp_endpoint: endpoint,
+ allowed_origins: [site.origin],
+ }),
+ );
if (!targets.ok) throw targets.error;
const target = targets.value.targets[0];
assert.ok(target, "owned fixture page target missing");
diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs
index d924ef49c..48f90c6e7 100644
--- a/scripts/lib/catalog-builders.mjs
+++ b/scripts/lib/catalog-builders.mjs
@@ -19,6 +19,11 @@ export const toolFamilyCatalog = (sources) => {
contracts: [
...sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
...sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
+ ...sources.cutterContracts.CUTTER_TOOL_CONTRACTS,
+ ...sources.reverseEngineeringContracts
+ .REVERSE_ENGINEERING_TOOL_CONTRACTS,
+ ...sources.gdbContracts.GDB_TOOL_CONTRACTS,
+ ...sources.rizinDebugContracts.RIZIN_DEBUG_TOOL_CONTRACTS,
],
},
{
@@ -28,6 +33,7 @@ export const toolFamilyCatalog = (sources) => {
...sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
...sources.evmContracts.EVM_TOOL_CONTRACTS,
...sources.goContracts.GO_TOOL_CONTRACTS,
+ ...sources.rustContracts.RUST_TOOL_CONTRACTS,
...sources.wasmContracts.WASM_TOOL_CONTRACTS,
...sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
...sources.captureContracts.CAPTURE_TOOL_CONTRACTS,
@@ -163,6 +169,32 @@ export const providerCatalog = (sources) => {
activeContracts,
} = electronContractSlices(sources);
return [
+ {
+ identity: sources.cutterProvider.CUTTER_PROVIDER_IDENTITY,
+ contracts: sources.cutterContracts.CUTTER_TOOL_CONTRACTS,
+ },
+ {
+ identity: sources.objdumpProvider.OBJDUMP_PROVIDER_IDENTITY,
+ contracts:
+ sources.reverseEngineeringContracts.REVERSE_ENGINEERING_TOOL_CONTRACTS.filter(
+ ({ name }) => name === "inspect_with_objdump",
+ ),
+ },
+ {
+ identity: sources.rizinProvider.RIZIN_PROVIDER_IDENTITY,
+ contracts:
+ sources.reverseEngineeringContracts.REVERSE_ENGINEERING_TOOL_CONTRACTS.filter(
+ ({ name }) => name === "execute_rizin_command",
+ ),
+ },
+ {
+ identity: sources.gdbProvider.GDB_PROVIDER_IDENTITY,
+ contracts: sources.gdbContracts.GDB_TOOL_CONTRACTS,
+ },
+ {
+ identity: sources.rizinDebugProvider.RIZIN_DEBUGGER_PROVIDER_IDENTITY,
+ contracts: sources.rizinDebugContracts.RIZIN_DEBUG_TOOL_CONTRACTS,
+ },
{
identity: sources.fridaProvider.FRIDA_PROVIDER_IDENTITY,
contracts: sources.fridaContracts.FRIDA_TOOL_CONTRACTS,
@@ -225,6 +257,10 @@ export const providerCatalog = (sources) => {
identity: sources.goProvider.GO_BINARY_PROVIDER_IDENTITY,
contracts: sources.goContracts.GO_TOOL_CONTRACTS,
},
+ {
+ identity: sources.rustProvider.RUST_BINARY_PROVIDER_IDENTITY,
+ contracts: sources.rustContracts.RUST_TOOL_CONTRACTS,
+ },
{
identity: sources.sqliteDomain.SQLITE_PROVIDER_IDENTITY,
contracts: sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
diff --git a/scripts/lib/catalog-core.mjs b/scripts/lib/catalog-core.mjs
index f5488a9e6..262493b73 100644
--- a/scripts/lib/catalog-core.mjs
+++ b/scripts/lib/catalog-core.mjs
@@ -43,6 +43,16 @@ const SOURCE_PATHS = {
sessionContracts: "dist/contracts/sessionToolContracts.js",
binaryDiagnosticsContracts:
"dist/contracts/native/binaryDiagnosticsToolContracts.js",
+ cutterContracts: "dist/contracts/cutterToolContracts.js",
+ cutterProvider: "dist/cutter/CutterBridgeClient.js",
+ reverseEngineeringContracts:
+ "dist/contracts/reverseEngineeringToolContracts.js",
+ gdbContracts: "dist/contracts/gdbToolContracts.js",
+ gdbProvider: "dist/gdb/GdbSessionManager.js",
+ rizinDebugContracts: "dist/contracts/rizinDebugToolContracts.js",
+ objdumpProvider: "dist/objdump/ObjdumpCommand.js",
+ rizinProvider: "dist/rizin/RizinCommand.js",
+ rizinDebugProvider: "dist/rizin/RizinDebugSessionManager.js",
fridaContracts: "dist/contracts/fridaToolContracts.js",
fridaProvider: "dist/frida/FridaInstrumentationManager.js",
pwntoolsProvider: "dist/native/pwntools/PwntoolsRelease.js",
@@ -59,6 +69,8 @@ const SOURCE_PATHS = {
evmProvider: "dist/evm/EvmoleRelease.js",
goContracts: "dist/contracts/go/goToolContracts.js",
goProvider: "dist/go/GoBinaryProvider.js",
+ rustContracts: "dist/contracts/rust/rustToolContracts.js",
+ rustProvider: "dist/rust/RustBinaryProvider.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managed/managedToolContracts.js",
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
diff --git a/scripts/lib/development-tests.mjs b/scripts/lib/development-tests.mjs
index 8ddebe115..6aaf10768 100644
--- a/scripts/lib/development-tests.mjs
+++ b/scripts/lib/development-tests.mjs
@@ -1,17 +1,4 @@
-const sourceProjects = [
- "domain",
- "services",
- "adapters",
- "composition",
- "conformance",
- "evaluation",
-];
-
-const sourceTest = (path) =>
- path.startsWith("src/") ||
- ["composition", "conformance", "evaluation"].some((project) =>
- path.startsWith(`tests/${project}/`),
- );
+import { SOURCE_TEST_PROJECTS, projectForTest } from "./test-projects.mjs";
/** Parse explicit test paths and an optional branch-comparison base. */
export const parseDevelopmentTestRequest = (mode, arguments_) => {
@@ -35,10 +22,9 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => {
path.split("/").some((part) => part === ".." || part === ".")
)
throw new Error(`Expected an exact repository test path: ${argument}`);
- if (mode !== "focused" && !sourceTest(path))
- throw new Error(
- `Use test:focused for compiled boundary tests: ${path}`,
- );
+ const project = projectForTest(path);
+ if (mode !== "focused" && project.group !== "source")
+ throw new Error(`Use test:focused for built test projects: ${path}`);
paths.push(path);
}
}
@@ -72,9 +58,14 @@ export const developmentTestPlan = (request, baseCommit) => {
!/^[a-f0-9]{40,64}$/u.test(baseCommit ?? "")
)
throw new Error("Changed tests require a resolved Git merge base");
- const projects = request.mode === "focused" ? [] : sourceProjects;
+ const projects =
+ request.mode === "focused"
+ ? []
+ : SOURCE_TEST_PROJECTS.map(({ name }) => name);
return {
- needsBuild: request.paths.some((path) => !sourceTest(path)),
+ needsBuild: request.paths.some(
+ (path) => projectForTest(path).group === "built",
+ ),
artifactTasks: Object.entries(artifactConsumers).flatMap(
([task, consumers]) =>
request.paths.some((path) => consumers.includes(path)) ? [task] : [],
diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs
index 160dad7c0..a6c74491c 100644
--- a/scripts/lib/module-boundaries.mjs
+++ b/scripts/lib/module-boundaries.mjs
@@ -8,6 +8,7 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
"sqlite",
"evm",
"go",
+ "rust",
"wasm",
"captures",
"signals",
diff --git a/scripts/lib/native-value-e2e.mjs b/scripts/lib/native-value-e2e.mjs
index 513259b30..916533a87 100644
--- a/scripts/lib/native-value-e2e.mjs
+++ b/scripts/lib/native-value-e2e.mjs
@@ -215,6 +215,27 @@ async function verifyNativeValueAdapters(
const functionEvidence = parseEvidence(analyzed.structuredContent);
assert.equal(functionEvidence.operation, "analyze_function");
assert.equal(functionEvidence.provider.id, "ghidra");
+ assert.deepEqual(
+ functionEvidence.normalized_result.facet_availability,
+ Object.fromEntries(
+ [
+ "pseudocode",
+ "assembly",
+ "comments",
+ "callers",
+ "callees",
+ "references",
+ "strings_names",
+ "cfg",
+ ].map((facet) => [facet, { available: true }]),
+ ),
+ );
+ assert.equal(functionEvidence.relationship_coverage?.status, "complete");
+ assert.ok(
+ functionEvidence.relationship_coverage.diagnostics.some((diagnostic) =>
+ diagnostic.includes("indirect calls remain unknown"),
+ ),
+ );
const functionProfile = analysisProfileSchema.parse(
functionEvidence.analysis_profile,
);
@@ -235,7 +256,11 @@ async function verifyNativeValueAdapters(
);
assert.notEqual(comparison.isError, true, JSON.stringify(comparison));
const comparisonEvidence = parseEvidence(comparison.structuredContent);
- assert.equal(comparisonEvidence.normalized_result.status, "unchanged");
+ assert.equal(
+ comparisonEvidence.normalized_result.status,
+ "unchanged",
+ JSON.stringify(comparisonEvidence.normalized_result.dimensions),
+ );
assert.deepEqual(
comparisonEvidence.normalized_result,
comparison.structuredContent?.normalized_result,
@@ -243,6 +268,7 @@ async function verifyNativeValueAdapters(
assert.ok(
comparisonEvidence.evidence_links.includes(functionEvidence.evidence_id),
);
+ await verifyUnresolvedCallEvidence(client);
const bundle = await client.callTool({
name: "get_evidence_bundle",
arguments: {},
@@ -303,3 +329,63 @@ async function verifyNativeValueAdapters(
unknowns: cliGraph.unknowns.length,
};
}
+
+async function verifyUnresolvedCallEvidence(client) {
+ const inventory = await client.callTool({
+ name: "list_procedures",
+ arguments: {},
+ });
+ assert.notEqual(inventory.isError, true, JSON.stringify(inventory));
+ const procedures = parseEvidence(
+ inventory.structuredContent,
+ ).normalized_result;
+ const indirect = procedures.find((item) =>
+ item.value.endsWith("rea_ghidra_inventory_indirect"),
+ );
+ const leaf = procedures.find((item) =>
+ item.value.endsWith("rea_ghidra_inventory_leaf"),
+ );
+ assert.ok(
+ indirect && leaf,
+ "Source fixture lacks the unresolved callback and its possible target",
+ );
+ const analyzed = await client.callTool(
+ { name: "analyze_function", arguments: { procedure: indirect.address } },
+ { timeout: 180000 },
+ );
+ assert.notEqual(analyzed.isError, true, JSON.stringify(analyzed));
+ const evidence = parseEvidence(analyzed.structuredContent);
+ assert.equal(evidence.relationship_coverage?.status, "partial");
+ assert.ok(evidence.normalized_result.unresolved_calls.length > 0);
+ const traced = await client.callTool(
+ {
+ name: "trace_call_path",
+ arguments: {
+ start: indirect.address,
+ goal: leaf.address,
+ direction: "forward",
+ },
+ },
+ { timeout: 180000 },
+ );
+ assert.notEqual(traced.isError, true, JSON.stringify(traced));
+ const trace = parseEvidence(traced.structuredContent).normalized_result;
+ assert.equal(trace.goal_status, "unknown");
+ assert.equal(trace.truncated, true);
+ assert.ok(trace.residual_unknowns.length > 0);
+ const compared = await client.callTool(
+ {
+ name: "compare_functions",
+ arguments: {
+ left: analyzed.structuredContent,
+ right: analyzed.structuredContent,
+ },
+ },
+ { timeout: 180000 },
+ );
+ assert.notEqual(compared.isError, true, JSON.stringify(compared));
+ assert.equal(
+ parseEvidence(compared.structuredContent).normalized_result.status,
+ "unknown",
+ );
+}
diff --git a/scripts/lib/real-ghidra-boundaries.mjs b/scripts/lib/real-ghidra-boundaries.mjs
index f6ee1527a..54376c389 100644
--- a/scripts/lib/real-ghidra-boundaries.mjs
+++ b/scripts/lib/real-ghidra-boundaries.mjs
@@ -131,6 +131,25 @@ export async function verifyGhidraBoundaries(
const externalDossier = await call("analyze_function", {
procedure: external.address,
});
+ assert.equal(externalDossier.pseudocode, "");
+ assert.deepEqual(externalDossier.assembly, []);
+ assert.deepEqual(externalDossier.callees, []);
+ assert.deepEqual(externalDossier.facet_availability.pseudocode, {
+ available: false,
+ reason: "Ghidra does not decompile external functions.",
+ });
+ for (const facet of [
+ "assembly",
+ "comments",
+ "callers",
+ "callees",
+ "references",
+ "strings_names",
+ "cfg",
+ ])
+ assert.deepEqual(externalDossier.facet_availability[facet], {
+ available: true,
+ });
const externalEntry = await call("resolve_containing_procedure", {
address: external.address,
});
diff --git a/scripts/lib/real-hopper-search.d.mts b/scripts/lib/real-hopper-search.d.mts
index e64031652..0a7260227 100644
--- a/scripts/lib/real-hopper-search.d.mts
+++ b/scripts/lib/real-hopper-search.d.mts
@@ -10,6 +10,25 @@ interface StringExpectation {
readonly recognitionOptional?: boolean;
}
+/** Verify native fragments against a complete, independently read source extent. */
+export function verifyHopperStringFragments(
+ call: VerifierCall,
+ inventory: readonly z.output[],
+ expectation: {
+ readonly value: string;
+ readonly symbol: string;
+ readonly recognitionOptional?: boolean;
+ },
+): Promise<{
+ readonly sourceBytes: number;
+ readonly recognition: {
+ readonly address: string;
+ readonly state: "observed" | "not_observed";
+ readonly decodedObject: "verified" | "unverified";
+ };
+ readonly fragments: readonly z.output[];
+}>;
+
/** Verify source bytes, observed decoding, address filters and literal searches. */
export function verifyHopperStringObjects(
call: VerifierCall,
diff --git a/scripts/lib/real-hopper-search.mjs b/scripts/lib/real-hopper-search.mjs
index 3456bc6b2..0efd2bfb7 100644
--- a/scripts/lib/real-hopper-search.mjs
+++ b/scripts/lib/real-hopper-search.mjs
@@ -106,27 +106,18 @@ async function verifySearchModes(call, address) {
[{ address, value: "REA_Search_Upper" }],
);
const strings = await call("list_strings");
- const linux = process.platform === "linux";
- const fullLiteral = "REA_LONG_LITERAL_" + "x".repeat(4096) + "needle";
- // Recognition is a provider/loader capability, not an adapter guarantee.
- // Always retain the original size in raw-byte coverage; Linux additionally
- // requires an extent observed to produce native fragmented string objects.
- const fullBytes = Buffer.from(fullLiteral + "\0");
- const raw = await call("read_bytes", {
- address: `${fixtureSymbolPrefix}rea_long_literal`,
- length: fullBytes.length,
+ const fullLiteral = await verifyHopperStringFragments(call, strings, {
+ symbol: `${fixtureSymbolPrefix}rea_long_literal`,
+ value: "REA_LONG_LITERAL_" + "x".repeat(4096) + "needle",
+ recognitionOptional: true,
});
- assert.equal(raw.complete, true);
- assert.equal(raw.bytes_hex, fullBytes.toString("hex"));
- const long = linux
- ? "REA_FRAGMENTED_LITERAL_" + "x".repeat(512) + "fragment_tail"
- : fullLiteral;
- const longItem = strings.find(
- (item) => long.startsWith(item.value) && item.value.startsWith("REA_"),
- );
- assert.ok(longItem, "fixture omitted its real long literal");
- if (!linux) assert.equal(longItem.address, raw.address);
- assert.ok(long.startsWith(longItem.value));
+ const fragmentedLiteral = await verifyHopperStringFragments(call, strings, {
+ symbol: `${fixtureSymbolPrefix}rea_fragmented_literal`,
+ value: "REA_FRAGMENTED_LITERAL_" + "x".repeat(512) + "fragment_tail",
+ });
+ const [longItem] = fragmentedLiteral.fragments;
+ assert.ok(longItem, "fixture omitted its typed fragmented literal");
+ assert.ok(fragmentedLiteral.fragments.length > 1);
assert.ok(longItem.provider_value.endsWith("…"));
assert.equal(longItem.string.termination, "missing");
const literalProcedure = (await call("list_procedures")).find((item) =>
@@ -154,33 +145,13 @@ async function verifySearchModes(call, address) {
"function dossier discarded typed string provenance",
);
- const fragments = strings.filter((item) => {
- const offset = BigInt(item.address) - BigInt(longItem.address);
- return offset >= 0n && offset < BigInt(Buffer.byteLength(long));
- });
- assert.equal(fragments.map((item) => item.value).join(""), long);
- for (const fragment of fragments) {
- assert.equal(
- Buffer.byteLength(fragment.value),
- fragment.string.byte_length -
- (fragment.string.termination === "missing" ? 0 : 1),
- );
- const bytes = await call("read_bytes", {
- address: fragment.address,
- length: fragment.string.byte_length,
- });
- const expected = Buffer.from(
- fragment.value + (fragment.string.termination === "missing" ? "" : "\0"),
- );
- assert.equal(bytes.bytes_hex, expected.toString("hex"));
- }
assert.deepEqual(
await call("search_strings", {
- pattern: linux ? "fragment_tail" : "needle",
+ pattern: "fragment_tail",
mode: "literal",
case_sensitive: true,
}),
- [fragments.at(-1)],
+ [fragmentedLiteral.fragments.at(-1)],
);
const expected = strings.filter((item) =>
/^REA_C_(?:ENTRY|LEAF)$/u.test(item.value),
@@ -201,7 +172,7 @@ async function verifySearchModes(call, address) {
value: "REA_UTF8_é_😀",
encoding: "utf-8",
symbol: `${fixtureSymbolPrefix}rea_utf8_literal`,
- recognitionOptional: linux,
+ recognitionOptional: true,
},
{ value: 'REA_ESCAPED_"\\line\nend\t\r', encoding: "utf-8" },
{ value: "REA_LITERAL_BACKSLASH_\\n", encoding: "utf-8" },
@@ -209,33 +180,90 @@ async function verifySearchModes(call, address) {
value: "REA_LITERAL_…",
encoding: "utf-8",
symbol: `${fixtureSymbolPrefix}rea_ellipsis_literal`,
- recognitionOptional: linux,
+ recognitionOptional: true,
},
{
value: "REA_LATIN1_ÿ",
encoding: "latin-1",
symbol: `${fixtureSymbolPrefix}rea_latin1_literal`,
- recognitionOptional: linux,
+ recognitionOptional: true,
},
],
strings,
);
return {
- fullLiteralSourceBytes: fullBytes.length,
- fragmentedLiteralSourceBytes: Buffer.byteLength(long),
- fullLiteralRecognition: {
- address: raw.address,
- state: strings.some((item) => item.address === raw.address)
- ? "observed"
- : "not_observed",
- // Linux validates the original through raw bytes; its required decoded
- // fragment/dossier checks exercise the separate supported-size literal.
- decodedObject: linux ? "unverified" : "verified",
- },
+ fullLiteralSourceBytes: fullLiteral.sourceBytes,
+ fragmentedLiteralSourceBytes: fragmentedLiteral.sourceBytes,
+ fullLiteralRecognition: fullLiteral.recognition,
...objects,
};
}
+/** Bind native fragments to exact source bytes without assuming loader recognition. */
+export async function verifyHopperStringFragments(
+ call,
+ strings,
+ { symbol, value, recognitionOptional = false },
+) {
+ const expectedBytes = Buffer.from(value + "\0");
+ const raw = await call("read_bytes", {
+ address: symbol,
+ length: expectedBytes.length,
+ });
+ assert.equal(raw.complete, true);
+ assert.equal(raw.bytes_hex, expectedBytes.toString("hex"));
+ const start = BigInt(raw.address);
+ const end = start + BigInt(expectedBytes.length);
+ const fragments = strings.filter((item) => {
+ const address = BigInt(item.address);
+ return address >= start && address < end;
+ });
+ if (fragments.length === 0 && recognitionOptional)
+ return {
+ sourceBytes: expectedBytes.length,
+ recognition: {
+ address: raw.address,
+ state: "not_observed",
+ decodedObject: "unverified",
+ },
+ fragments,
+ };
+ assert.ok(fragments.length > 0, "fixture omitted its typed string fragments");
+ let next = start;
+ for (const [index, fragment] of fragments.entries()) {
+ assert.equal(
+ BigInt(fragment.address),
+ next,
+ "string fragments have a gap, overlap or wrong source identity",
+ );
+ const terminal = index === fragments.length - 1;
+ assert.equal(
+ fragment.string.termination,
+ terminal ? "present_or_not_required" : "missing",
+ );
+ const expected = Buffer.from(fragment.value + (terminal ? "\0" : ""));
+ assert.equal(fragment.string.byte_length, expected.length);
+ const bytes = await call("read_bytes", {
+ address: fragment.address,
+ length: expected.length,
+ });
+ assert.equal(bytes.complete, true);
+ assert.equal(bytes.bytes_hex, expected.toString("hex"));
+ next += BigInt(expected.length);
+ }
+ assert.equal(next, end, "string fragments do not cover the source extent");
+ assert.equal(fragments.map((item) => item.value).join(""), value);
+ return {
+ sourceBytes: expectedBytes.length,
+ recognition: {
+ address: raw.address,
+ state: "observed",
+ decodedObject: "verified",
+ },
+ fragments,
+ };
+}
+
/** Compare native string identities and values with the source fixture's bytes. */
export async function verifyHopperStringObjects(
call,
diff --git a/scripts/lib/real-hopper-semantic.mjs b/scripts/lib/real-hopper-semantic.mjs
index cb11b433c..7739db6be 100644
--- a/scripts/lib/real-hopper-semantic.mjs
+++ b/scripts/lib/real-hopper-semantic.mjs
@@ -327,14 +327,57 @@ export const analyzeFixtureProcedure = async (
options,
procedure,
normalizedResult,
-) =>
- normalizedResult(
+) => {
+ const reply = await client.callTool(
+ {
+ name: "analyze_function",
+ arguments: { procedure: procedure.address },
+ },
+ options,
+ );
+ const dossier = normalizedResult(reply, "analyze_function");
+ assert.deepEqual(reply.structuredContent.relationship_coverage, {
+ status: "complete",
+ });
+ for (const facet of [
+ "pseudocode",
+ "assembly",
+ "comments",
+ "callers",
+ "callees",
+ "references",
+ "strings_names",
+ "cfg",
+ ])
+ assert.deepEqual(dossier.facet_availability[facet], { available: true });
+ const comparison = normalizedResult(
await client.callTool(
{
- name: "analyze_function",
- arguments: { procedure: procedure.address },
+ name: "compare_functions",
+ arguments: {
+ left: reply.structuredContent,
+ right: reply.structuredContent,
+ },
},
options,
),
- "analyze_function",
+ "compare_functions",
);
+ assert.deepEqual(
+ Object.fromEntries(
+ comparison.dimensions.map(({ dimension, status }) => [dimension, status]),
+ ),
+ {
+ identity: "unchanged",
+ pseudocode: "unchanged",
+ assembly: "unchanged",
+ comments: "unchanged",
+ calls: "unchanged",
+ references: "unknown",
+ strings_names: "unchanged",
+ cfg: "unchanged",
+ },
+ JSON.stringify(comparison.dimensions),
+ );
+ return dossier;
+};
diff --git a/scripts/lib/reverse-tools-mutation-targets.mjs b/scripts/lib/reverse-tools-mutation-targets.mjs
new file mode 100644
index 000000000..cccf986ac
--- /dev/null
+++ b/scripts/lib/reverse-tools-mutation-targets.mjs
@@ -0,0 +1,182 @@
+import { readFileSync } from "node:fs";
+import ts from "typescript6";
+
+const compact = (text) => text.replace(/\s+/gu, "");
+const nameOf = (node, source) => node.name?.getText(source);
+
+const methodOwner = (node, source) => {
+ for (let parent = node; parent !== undefined; parent = parent.parent) {
+ if (ts.isMethodDeclaration(parent) || ts.isConstructorDeclaration(parent))
+ return {
+ owner: nameOf(parent.parent, source),
+ body: parent.body,
+ method: ts.isConstructorDeclaration(parent)
+ ? "constructor"
+ : nameOf(parent, source),
+ };
+ }
+ return undefined;
+};
+
+const withinOwner = (node, selector, source) => {
+ if (selector.owner === undefined) return true;
+ const owner = methodOwner(node, source);
+ return (
+ owner?.owner === selector.owner &&
+ (selector.method === undefined || owner.method === selector.method)
+ );
+};
+
+const contains = (node, predicate) =>
+ predicate(node) ||
+ ts.forEachChild(node, (child) => contains(child, predicate)) === true;
+
+const inBlockWithBinding = (node, binding) => {
+ if (binding === undefined) return true;
+ let block = node.parent;
+ while (block !== undefined && !ts.isBlock(block)) block = block.parent;
+ return block?.statements.some(
+ (statement) =>
+ ts.isVariableStatement(statement) &&
+ statement.declarationList.declarations.some(
+ (declaration) =>
+ ts.isIdentifier(declaration.name) &&
+ declaration.name.text === binding,
+ ),
+ );
+};
+
+const isCommandHandler = (node, command) => {
+ if (!ts.isArrowFunction(node)) return false;
+ const property = node.parent;
+ if (
+ !ts.isPropertyAssignment(property) ||
+ !ts.isIdentifier(property.name) ||
+ property.name.text !== "run"
+ )
+ return false;
+ const call = property.parent.parent;
+ if (
+ !ts.isCallExpression(call) ||
+ !ts.isPropertyAccessExpression(call.expression) ||
+ !ts.isIdentifier(call.expression.expression) ||
+ call.expression.expression.text !== "cli" ||
+ call.expression.name.text !== "command"
+ )
+ return false;
+ const selected = call.arguments[0];
+ return (
+ selected !== undefined &&
+ ts.isPropertyAccessExpression(selected) &&
+ ts.isIdentifier(selected.expression) &&
+ selected.expression.text === "CLI_COMMANDS" &&
+ selected.name.text === command
+ );
+};
+
+const matches = (node, selector, source) => {
+ if (!withinOwner(node, selector, source)) return false;
+ if (
+ selector.direct === true &&
+ node.parent !== methodOwner(node, source)?.body
+ )
+ return false;
+ switch (selector.kind) {
+ case "expression":
+ return (
+ ts.isBinaryExpression(node) &&
+ compact(node.getText(source)) === compact(selector.text)
+ );
+ case "declaration":
+ return (
+ ts.isVariableDeclaration(node) &&
+ nameOf(node, source) === selector.name &&
+ inBlockWithBinding(node, selector.withinBlockWithBinding)
+ );
+ case "method":
+ return (
+ ts.isMethodDeclaration(node) && nameOf(node, source) === selector.name
+ );
+ case "constructor":
+ return ts.isConstructorDeclaration(node);
+ case "guard":
+ return (
+ ts.isIfStatement(node) &&
+ compact(node.expression.getText(source)) === compact(selector.condition)
+ );
+ case "call":
+ return (
+ ts.isCallExpression(node) &&
+ compact(node.expression.getText(source)) === compact(selector.callee) &&
+ (selector.event === undefined ||
+ (node.arguments[0] !== undefined &&
+ ts.isStringLiteral(node.arguments[0]) &&
+ node.arguments[0].text === selector.event))
+ );
+ case "try":
+ return (
+ ts.isTryStatement(node) &&
+ contains(node.tryBlock, (child) =>
+ selector.assignment !== undefined
+ ? ts.isBinaryExpression(child) &&
+ child.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
+ ts.isIdentifier(child.left) &&
+ child.left.text === selector.assignment
+ : ts.isCallExpression(child) &&
+ compact(child.expression.getText(source)) ===
+ compact(selector.callee),
+ )
+ );
+ case "command-handler":
+ return isCommandHandler(node, selector.command);
+ default:
+ throw new Error(
+ `Unknown mutation selector kind: ${String(selector.kind)}`,
+ );
+ }
+};
+
+/** Parse a source once and resolve one owned region for every requested selector. */
+export const mutationRanges = (path, selectors) => {
+ if (!Array.isArray(selectors) || selectors.length === 0)
+ throw new Error(`Mutation selectors must be nonempty: ${path}`);
+ for (const selector of selectors)
+ if (
+ ["method", "constructor", "guard", "call", "try"].includes(
+ selector.kind,
+ ) &&
+ typeof selector.owner !== "string"
+ )
+ throw new Error(
+ `Mutation selector requires a named class owner: ${path}`,
+ );
+ const source = ts.createSourceFile(
+ path,
+ readFileSync(path, "utf8"),
+ ts.ScriptTarget.Latest,
+ true,
+ );
+ if (source.parseDiagnostics.length !== 0)
+ throw new Error(
+ `Cannot select mutations from malformed TypeScript: ${path}`,
+ );
+ const found = selectors.map(() => []);
+ const visit = (node) => {
+ for (const [index, selector] of selectors.entries())
+ if (matches(node, selector, source)) found[index].push(node);
+ ts.forEachChild(node, visit);
+ };
+ visit(source);
+ return selectors.map((selector, index) => {
+ const candidates = found[index];
+ if (candidates.length !== 1)
+ throw new Error(
+ `Expected one mutation target in ${path}: ${JSON.stringify(selector)}; found ${candidates.length}`,
+ );
+ const node = candidates[0];
+ const first =
+ source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
+ const last = source.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
+ return `${path}:${first}-${last}`;
+ });
+};
diff --git a/scripts/lib/test-projects.d.mts b/scripts/lib/test-projects.d.mts
new file mode 100644
index 000000000..a9d6ae356
--- /dev/null
+++ b/scripts/lib/test-projects.d.mts
@@ -0,0 +1,10 @@
+export interface TestProjectDefinition {
+ readonly name: string;
+ readonly group: "source" | "built";
+ readonly include: readonly string[];
+ readonly exclude?: readonly string[];
+}
+
+export const SOURCE_TEST_PROJECTS: readonly TestProjectDefinition[];
+export const BUILT_TEST_PROJECTS: readonly TestProjectDefinition[];
+export function projectForTest(testPath: string): TestProjectDefinition;
diff --git a/scripts/lib/test-projects.mjs b/scripts/lib/test-projects.mjs
new file mode 100644
index 000000000..68fb90dbd
--- /dev/null
+++ b/scripts/lib/test-projects.mjs
@@ -0,0 +1,107 @@
+import { matchesGlob } from "node:path";
+
+/** Canonical file ownership shared by Vitest and development-test planning. */
+export const SOURCE_TEST_PROJECTS = [
+ {
+ name: "domain",
+ group: "source",
+ include: ["src/{contracts,domain}/**/*.test.ts"],
+ },
+ {
+ name: "services",
+ group: "source",
+ include: ["src/application/**/*.test.ts"],
+ },
+ {
+ name: "adapters",
+ group: "source",
+ include: [
+ "src/*.test.ts",
+ "src/{apktool,jeb,adb,artifacts,browser,dotnet,evm,firmware,flutter,pcap,ghidra,go,hopper,ida,inspector,javascript,native,process,reference,rust,server,wasm,windows}/**/*.test.ts",
+ ],
+ },
+ {
+ name: "composition",
+ group: "source",
+ include: ["tests/composition/**/*.test.ts"],
+ },
+ {
+ name: "conformance",
+ group: "source",
+ include: ["tests/conformance/**/*.test.ts"],
+ exclude: ["tests/conformance/providers/hopper/realHopperSearch.test.ts"],
+ },
+ {
+ name: "evaluation",
+ group: "source",
+ include: ["tests/evaluation/**/*.test.ts"],
+ },
+];
+
+export const BUILT_TEST_PROJECTS = [
+ {
+ name: "boundary",
+ group: "built",
+ include: [
+ "tests/boundary/**/*.test.ts",
+ "tests/conformance/providers/hopper/realHopperSearch.test.ts",
+ ],
+ exclude: [
+ "tests/boundary/cli/**/*.test.ts",
+ "tests/boundary/mcp/**/*.test.ts",
+ "tests/boundary/process/**/*.test.ts",
+ ],
+ },
+ {
+ name: "cli-boundary",
+ group: "built",
+ include: ["tests/boundary/cli/**/*.test.ts"],
+ },
+ {
+ name: "process-boundary",
+ group: "built",
+ include: ["tests/boundary/process/**/*.test.ts"],
+ },
+ {
+ name: "mcp-boundary",
+ group: "built",
+ include: ["tests/boundary/mcp/**/*.test.ts"],
+ },
+ {
+ name: "acceptance",
+ group: "built",
+ include: ["tests/acceptance/**/*.test.ts"],
+ },
+ {
+ name: "process-global",
+ group: "built",
+ include: ["tests/process-global/**/*.test.ts"],
+ },
+];
+
+const ALL_TEST_PROJECTS = [...SOURCE_TEST_PROJECTS, ...BUILT_TEST_PROJECTS];
+
+/** Return the unique Vitest owner for an exact repository-relative test path. */
+export const projectForTest = (testPath) => {
+ if (
+ typeof testPath !== "string" ||
+ testPath.startsWith("/") ||
+ testPath.includes("\\") ||
+ testPath.split("/").some((part) => part === ".." || part === ".") ||
+ !testPath.endsWith(".test.ts")
+ )
+ throw new Error(`Expected a normalized repository test path: ${testPath}`);
+
+ const owners = ALL_TEST_PROJECTS.filter(
+ ({ include, exclude = [] }) =>
+ include.some((pattern) => matchesGlob(testPath, pattern)) &&
+ !exclude.some((pattern) => matchesGlob(testPath, pattern)),
+ );
+ if (owners.length !== 1)
+ throw new Error(
+ owners.length === 0
+ ? `No test project owns ${testPath}`
+ : `Multiple test projects own ${testPath}: ${owners.map(({ name }) => name).join(", ")}`,
+ );
+ return owners[0];
+};
diff --git a/scripts/prepare-reverse-tools-mutation.mjs b/scripts/prepare-reverse-tools-mutation.mjs
new file mode 100644
index 000000000..a591138a5
--- /dev/null
+++ b/scripts/prepare-reverse-tools-mutation.mjs
@@ -0,0 +1,19 @@
+import { mkdir, rm, symlink } from "node:fs/promises";
+import { dirname, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const compiler = resolve(root, "node_modules/typescript6");
+
+for (const packageName of ["core", "typescript-checker"]) {
+ const packagePath = resolve(
+ root,
+ "node_modules/@stryker-mutator",
+ packageName,
+ );
+ const nodeModulesPath = resolve(packagePath, "node_modules");
+ const typescriptPath = resolve(nodeModulesPath, "typescript");
+ await mkdir(nodeModulesPath, { recursive: true });
+ await rm(typescriptPath, { recursive: true, force: true });
+ await symlink(compiler, typescriptPath, "dir");
+}
diff --git a/scripts/verify-package-discovery.d.mts b/scripts/verify-package-discovery.d.mts
index 7c5d27821..1531e9dba 100644
--- a/scripts/verify-package-discovery.d.mts
+++ b/scripts/verify-package-discovery.d.mts
@@ -1,12 +1,31 @@
-/** Derive agent and Hopper setup support from the corresponding doctor checks. */
+/**
+ * Host facts the packaged setup support is derived from.
+ *
+ * `linuxDistribution` is the distribution record the Hopper installer owner
+ * parses; it is absent when no distribution could be read, which leaves
+ * installer applicability unknown rather than unsupported.
+ */
+export interface SetupHostFacts {
+ readonly platform: NodeJS.Platform;
+ readonly linuxDistribution?: {
+ readonly hopperSetupSupported: boolean;
+ };
+}
+
+/**
+ * Derive agent setup support from the doctor node check and automated Hopper
+ * installer applicability from the facts their owners parse. Applicability is
+ * undefined when its owning fact is unavailable.
+ */
export function packageSetupSupport(
checks:
| readonly { readonly name: string; readonly ok: boolean }[]
| undefined,
- platform: NodeJS.Platform,
+ host: SetupHostFacts,
): {
supportedSetupHost: boolean;
- hopperSetupSupported: boolean;
+ hopperReadinessRequired: boolean;
+ hopperSetupSupported: boolean | undefined;
};
/** Validate --help, --llms, and doctor output and determine host setup support. */
@@ -15,5 +34,5 @@ export function verifyPackageDiscovery(input: {
readonly environment: Readonly>;
}): Promise<{
supportedSetupHost: boolean;
- hopperSetupSupported: boolean;
+ hopperSetupSupported: boolean | undefined;
}>;
diff --git a/scripts/verify-package-discovery.mjs b/scripts/verify-package-discovery.mjs
index 299b9d07c..9f2a5d7a8 100644
--- a/scripts/verify-package-discovery.mjs
+++ b/scripts/verify-package-discovery.mjs
@@ -27,15 +27,56 @@ const REQUIRED_LLM_TOPICS = [
"analyze-javascript-application",
];
-/** Derive agent and Hopper setup support from the corresponding doctor checks. */
-export function packageSetupSupport(checks, platform) {
+/**
+ * Derive agent setup support from the doctor node check and automated Hopper
+ * installer applicability from the facts their owners parse.
+ *
+ * Host readiness and installer applicability are different facts: a
+ * distribution REA supports can still be absent from the Hopper package's
+ * tested list, so the doctor host check cannot answer the second question.
+ * Applicability stays undefined when its owning fact is unavailable, which is
+ * distinct from a host known to lack an automated installer.
+ */
+export function packageSetupSupport(checks, host) {
const supportedSetupHost =
checks?.find(({ name }) => name === "node")?.ok === true;
- const hopperSetupSupported =
- supportedSetupHost &&
- checks?.find(({ name }) => name === "host")?.ok === true &&
- platform !== "win32";
- return { supportedSetupHost, hopperSetupSupported };
+ const hopperReadinessRequired =
+ host.platform !== "win32" &&
+ checks?.find(({ name }) => name === "host")?.ok === true;
+ return {
+ supportedSetupHost,
+ hopperReadinessRequired,
+ hopperSetupSupported: hopperInstallerSupported(
+ host,
+ checks?.find(({ name }) => name === "host")?.ok,
+ ),
+ };
+}
+
+/**
+ * Whether `rea setup --install-hopper` can install Hopper automatically here.
+ * On macOS the doctor host check already is the owner's installer predicate
+ * (macOS 12 or later); on Linux it reports REA host support instead, so the
+ * distribution's own parsed applicability governs.
+ */
+function hopperInstallerSupported(host, hostCheckOk) {
+ if (host.platform === "win32") return false;
+ if (host.platform === "linux")
+ return host.linuxDistribution?.hopperSetupSupported;
+ return hostCheckOk;
+}
+
+/**
+ * The distribution facts the Hopper installer owner parses, taken from the
+ * built application so the verifier follows the predicate the package ships.
+ * Loaded only where a distribution can exist, which also keeps the derivation
+ * above testable without a build.
+ */
+async function readHostLinuxDistribution() {
+ if (process.platform !== "linux") return undefined;
+ const { readLinuxDistribution } =
+ await import("../dist/application/LinuxHopper.js");
+ return readLinuxDistribution();
}
/** Validate --help, --llms, and doctor output and determine host setup support. */
@@ -48,10 +89,11 @@ export async function verifyPackageDiscovery({ cli, environment }) {
environment,
);
const doctor = json(doctorExecution.stdout);
- const { supportedSetupHost, hopperSetupSupported } = packageSetupSupport(
- doctor.checks,
- process.platform,
- );
+ const { supportedSetupHost, hopperReadinessRequired, hopperSetupSupported } =
+ packageSetupSupport(doctor.checks, {
+ platform: process.platform,
+ linuxDistribution: await readHostLinuxDistribution(),
+ });
const hopperReady = doctor.checks?.find(({ name }) => name === "hopper")?.ok;
const expectedDoctorHealth = doctor.checks?.every(({ ok }) => ok) === true;
const missingHelp = REQUIRED_HELP_COMMANDS.filter(
@@ -65,7 +107,7 @@ export async function verifyPackageDiscovery({ cli, environment }) {
missingLlms.length !== 0 ||
doctor.healthy !== expectedDoctorHealth ||
doctorExecution.status !== (expectedDoctorHealth ? 0 : 1) ||
- (hopperSetupSupported && hopperReady !== true)
+ (hopperReadinessRequired && hopperReady !== true)
)
throw new Error(
`packaged CLI discovery or doctor failed: ${JSON.stringify({ helpSetup: help.includes("setup"), llmsDecompile: llms.includes("decompile"), doctor })}`,
diff --git a/scripts/verify-package-platform.d.mts b/scripts/verify-package-platform.d.mts
index 4342027c2..631ddc93b 100644
--- a/scripts/verify-package-platform.d.mts
+++ b/scripts/verify-package-platform.d.mts
@@ -8,4 +8,10 @@ export function assertLinuxPackageProviderFailure(execution: {
export function verifyPackagePlatform(input: {
readonly cli: string;
readonly environment: Readonly>;
+ readonly platform?: string;
+ readonly runCommand?: (
+ command: string,
+ arguments_: readonly string[],
+ environment: Readonly>,
+ ) => Promise;
}): Promise;
diff --git a/scripts/verify-package-platform.mjs b/scripts/verify-package-platform.mjs
index b4db6bf16..253937688 100644
--- a/scripts/verify-package-platform.mjs
+++ b/scripts/verify-package-platform.mjs
@@ -1,8 +1,13 @@
import { json, run, runWithStatus } from "./lib/verify-package-core.mjs";
/** Run platform-specific deep-analysis smoke tests. */
-export async function verifyPackagePlatform({ cli, environment }) {
- if (process.platform === "linux") {
+export async function verifyPackagePlatform({
+ cli,
+ environment,
+ platform = process.platform,
+ runCommand = run,
+}) {
+ if (platform === "linux") {
const unsupportedExecution = await runWithStatus(
cli,
["analyze", process.execPath, "--json"],
@@ -11,25 +16,27 @@ export async function verifyPackagePlatform({ cli, environment }) {
assertLinuxPackageProviderFailure(unsupportedExecution);
} else {
const overview = json(
- await run(cli, ["analyze", process.execPath, "--json"], environment),
+ await runCommand(
+ cli,
+ ["analyze", process.execPath, "--json"],
+ environment,
+ ),
);
- if (
- overview.operation !== "binary_overview" ||
- overview.normalized_result?.procedure_count < 1
- )
+ if (!hasPositiveProcedureCount(overview))
throw new Error(
`packaged Hopper-backed analyze CLI failed: ${JSON.stringify(overview)}`,
);
const inspected = json(
- await run(cli, ["inspect", process.execPath, "--json"], environment),
+ await runCommand(
+ cli,
+ ["inspect", process.execPath, "--json"],
+ environment,
+ ),
);
- if (
- inspected.operation !== "binary_overview" ||
- inspected.normalized_result?.procedure_count < 1
- )
+ if (!hasPositiveProcedureCount(inspected))
throw new Error("packaged inspect CLI failed");
const functionResult = json(
- await run(
+ await runCommand(
cli,
["function", process.execPath, "0x1000", "--json"],
environment,
@@ -44,7 +51,7 @@ export async function verifyPackagePlatform({ cli, environment }) {
`packaged function CLI failed: ${JSON.stringify(functionResult)}`,
);
const xrefs = json(
- await run(
+ await runCommand(
cli,
["xrefs", process.execPath, "0x1000", "--json"],
environment,
@@ -56,7 +63,7 @@ export async function verifyPackagePlatform({ cli, environment }) {
)
throw new Error(`packaged xrefs CLI failed: ${JSON.stringify(xrefs)}`);
const trace = json(
- await run(
+ await runCommand(
cli,
["trace", process.execPath, "fixture", "--json"],
environment,
@@ -93,3 +100,10 @@ export const assertLinuxPackageProviderFailure = (execution) => {
`packaged Linux Hopper verification did not fail closed: ${JSON.stringify(failure)}`,
);
};
+
+const hasPositiveProcedureCount = (result) =>
+ typeof result === "object" &&
+ result !== null &&
+ result.operation === "binary_overview" &&
+ Number.isInteger(result.normalized_result?.procedure_count) &&
+ result.normalized_result.procedure_count > 0;
diff --git a/scripts/verify-package-setup.mjs b/scripts/verify-package-setup.mjs
index b415965e0..9a87a1695 100644
--- a/scripts/verify-package-setup.mjs
+++ b/scripts/verify-package-setup.mjs
@@ -356,9 +356,8 @@ const verifySetupApply = async ({
hopperSetupSupported,
}) => {
const status = "ready";
- const hopperStatus = hopperSetupSupported
- ? "needs_confirmation"
- : "needs_human";
+ const hopperStatus =
+ hopperSetupSupported === true ? "needs_confirmation" : "needs_human";
const firstExecution = await runWithStatus(
cli,
["setup", "--yes", "--all-detected", "--json"],
diff --git a/scripts/verify-packed-bridge.mjs b/scripts/verify-packed-bridge.mjs
index 0ed18eaa3..b83dc7715 100644
--- a/scripts/verify-packed-bridge.mjs
+++ b/scripts/verify-packed-bridge.mjs
@@ -4,6 +4,7 @@ import { promisify } from "node:util";
const exec = promisify(execFile);
const packedHopperBridge = "package/bridge/hopper_bridge.py";
+const packedCutterBridge = "package/bridge/cutter/rea_bridge.py";
const packedGhidraBridge = "package/bridge/ghidra/ReaGhidraBridge.java";
const packedGhidraActivator =
"package/bridge/ghidra/ReaGhidraBundleActivator.java";
@@ -12,6 +13,7 @@ const packedGhidraActivator =
export async function verifyPackedBridge({ workspace, tarball, packedFiles }) {
for (const path of [
packedHopperBridge,
+ packedCutterBridge,
packedGhidraBridge,
packedGhidraActivator,
]) {
diff --git a/scripts/verify-real-cutter.mjs b/scripts/verify-real-cutter.mjs
new file mode 100644
index 000000000..5ffde438d
--- /dev/null
+++ b/scripts/verify-real-cutter.mjs
@@ -0,0 +1,214 @@
+#!/usr/bin/env node
+
+import assert from "node:assert/strict";
+import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
+import { execFile } from "node:child_process";
+import { createHash, randomUUID } from "node:crypto";
+import {
+ access,
+ constants,
+ mkdtemp,
+ readFile,
+ rm,
+ writeFile,
+} from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join, resolve } from "node:path";
+import { promisify } from "node:util";
+import { createServer } from "../dist/server/createServer.js";
+
+const execFileAsync = promisify(execFile);
+const cli = resolve("scripts/rea.mjs");
+const runCli = async (...arguments_) => {
+ const { stdout } = await execFileAsync(
+ process.execPath,
+ [cli, ...arguments_, "--format", "json"],
+ {
+ encoding: "utf8",
+ maxBuffer: 4 * 1024 * 1024,
+ env: process.env,
+ },
+ );
+ return JSON.parse(stdout);
+};
+
+const projectRoot = await mkdtemp(join(tmpdir(), "rea-cutter-smoke-"));
+const projectPath = join(projectRoot, "smoke.rzdb");
+const rizinQuote = (value) =>
+ `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
+const unavailableAnalysis = {
+ execute: async () => {
+ throw new Error(
+ "The Cutter verification does not call unrelated analysis tools.",
+ );
+ },
+};
+const expectedPlatform = {
+ Linux: "linux",
+ macOS: "darwin",
+ Windows: "win32",
+}[process.env.EXPECTED_PLATFORM];
+if (expectedPlatform !== undefined)
+ assert.equal(
+ process.platform,
+ expectedPlatform,
+ "Cutter runner did not match its declared platform.",
+ );
+try {
+ const discovery = await runCli("list-cutter-sessions");
+ assert.equal(
+ discovery.discovery_status,
+ "sessions_found",
+ "Open upstream Cutter with the REA plugin before running this smoke test.",
+ );
+ assert.ok(discovery.sessions.length > 0);
+ const session = discovery.sessions[0];
+ assert.ok(session);
+ assert.ok(
+ session.cutter_version,
+ "The upstream Cutter API did not report its version.",
+ );
+ if (process.env.REA_CUTTER_EXPECTED_VERSION !== undefined)
+ assert.ok(
+ session.cutter_version.includes(process.env.REA_CUTTER_EXPECTED_VERSION),
+ "The live Cutter version did not match the configured version.",
+ );
+ assert.ok(
+ session.current_file,
+ "Open a disposable local binary in Cutter before running this smoke test.",
+ );
+ const artifactPath = session.current_file;
+ const artifactDigest = createHash("sha256")
+ .update(await readFile(artifactPath))
+ .digest("hex");
+
+ const inspection = await runCli(
+ "cutter-command",
+ session.session_id,
+ String(session.document_generation),
+ "ij",
+ "--json",
+ );
+ assert.equal(inspection.predicate_type, "rea.cutter.command-observation");
+ assert.ok(
+ Object.hasOwn(inspection, "raw_result"),
+ "Cutter ij result was not retained as Evidence.",
+ );
+ assert.equal(typeof inspection.raw_result.bin?.arch, "string");
+
+ const mcpServer = createServer(
+ { kind: "fixed", analysis: unavailableAnalysis },
+ {
+ providerEnvironment: process.env,
+ },
+ );
+ const mcpClient = new Client({
+ name: "rea-cutter-upstream-smoke",
+ version: "1",
+ });
+ const [clientTransport, serverTransport] =
+ InMemoryTransport.createLinkedPair();
+ try {
+ await mcpServer.connect(serverTransport);
+ await mcpClient.connect(clientTransport);
+ const mcpDiscovery = await mcpClient.callTool({
+ name: "list_cutter_sessions",
+ arguments: {},
+ });
+ assert.notEqual(mcpDiscovery.isError, true);
+ assert.equal(
+ mcpDiscovery.structuredContent.discovery_status,
+ "sessions_found",
+ );
+ const mcpInspection = await mcpClient.callTool({
+ name: "cutter_command",
+ arguments: {
+ session_id: session.session_id,
+ expected_generation: session.document_generation,
+ command: "ij",
+ json: true,
+ },
+ });
+ assert.notEqual(mcpInspection.isError, true);
+ assert.deepEqual(
+ mcpInspection.structuredContent.normalized_result,
+ inspection.normalized_result,
+ "Cutter CLI and MCP returned different analysis results.",
+ );
+ } finally {
+ await mcpClient.close();
+ await mcpServer.close();
+ }
+
+ const annotation = `rea-smoke-${randomUUID()}`;
+ await runCli(
+ "cutter-command",
+ session.session_id,
+ String(session.document_generation),
+ `CC ${annotation} @ entry0`,
+ );
+ await runCli(
+ "cutter-command",
+ session.session_id,
+ String(session.document_generation),
+ `Ps ${rizinQuote(projectPath)}`,
+ );
+ await access(projectPath, constants.R_OK | constants.W_OK);
+ await runCli(
+ "cutter-command",
+ session.session_id,
+ String(session.document_generation),
+ "CC- @ entry0",
+ );
+ const removed = await runCli(
+ "cutter-command",
+ session.session_id,
+ String(session.document_generation),
+ "CC. @ entry0",
+ );
+ assert.ok(
+ !String(removed.raw_result).includes(annotation),
+ "The in-memory annotation was not removed before project reopening.",
+ );
+ await runCli(
+ "cutter-command",
+ session.session_id,
+ String(session.document_generation),
+ `Po ${rizinQuote(projectPath)}`,
+ );
+ const recovered = await runCli(
+ "cutter-command",
+ session.session_id,
+ String(session.document_generation),
+ "CC. @ entry0",
+ );
+ assert.equal(recovered.predicate_type, "rea.cutter.command-observation");
+ assert.ok(
+ String(recovered.raw_result).includes(annotation),
+ "The saved project did not preserve the analysis comment after reopening.",
+ );
+ const finalArtifactDigest = createHash("sha256")
+ .update(await readFile(artifactPath))
+ .digest("hex");
+ assert.equal(
+ finalArtifactDigest,
+ artifactDigest,
+ "Cutter project edits changed the executable bytes.",
+ );
+
+ const report = {
+ ok: true,
+ platform: process.platform,
+ cutter_version: session.cutter_version,
+ document_generation: session.document_generation,
+ cli_mcp_evidence_parity: true,
+ annotation_saved_and_reopened: true,
+ executable_digest_unchanged: true,
+ };
+ await writeFile("cutter-smoke.json", `${JSON.stringify(report, null, 2)}\n`, {
+ mode: 0o600,
+ });
+ console.log(JSON.stringify(report));
+} finally {
+ await rm(projectRoot, { recursive: true, force: true });
+}
diff --git a/scripts/verify-real-ghidra-ppc.mjs b/scripts/verify-real-ghidra-ppc.mjs
new file mode 100644
index 000000000..591d32b2b
--- /dev/null
+++ b/scripts/verify-real-ghidra-ppc.mjs
@@ -0,0 +1,282 @@
+#!/usr/bin/env node
+import assert from "node:assert/strict";
+import { execFile } from "node:child_process";
+import { createHash } from "node:crypto";
+import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { promisify } from "node:util";
+import { Client } from "@modelcontextprotocol/client";
+import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
+import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
+import {
+ inspectPowerPcReadelf,
+ assertPowerPcLoadedImage,
+ assertPowerPcProbe,
+ assertPowerPcGlobal,
+} from "../tests/conformance/ghidra/ppc-checks.mjs";
+import { parseEvidence } from "../dist/domain/evidence.js";
+import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js";
+import { functionDossierSchema } from "../dist/domain/hopperValues.js";
+import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
+import { buildPowerPcFixture } from "../tests/conformance/ghidra/ppc-fixture.mjs";
+import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
+import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
+
+// Optional cross-target lane. No tool acquisition and no target execution.
+if (process.argv.length !== 2)
+ throw new Error("Usage: node scripts/verify-real-ghidra-ppc.mjs");
+if (!["linux", "darwin"].includes(process.platform))
+ throw new Error(
+ "PowerPC verification requires a supported Linux/macOS Ghidra host",
+ );
+if (process.env.GHIDRA_INSTALL_DIR === undefined)
+ throw new Error("Missing prerequisite: caller-supplied GHIDRA_INSTALL_DIR");
+const installation = inspectGhidraInstallation({
+ environment: process.env,
+ installDir: process.env.GHIDRA_INSTALL_DIR,
+ ...(process.env.JAVA_HOME === undefined
+ ? {}
+ : { javaHome: process.env.JAVA_HOME }),
+});
+if (installation.status !== "available")
+ throw new Error(
+ `Ghidra prerequisite unavailable: ${JSON.stringify(installation)}`,
+ );
+const execute = promisify(execFile);
+const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
+const run = createVerifierRun();
+const workspace = await mkdtemp(join(tmpdir(), "rea-ppc-proof-"));
+const runtime = join(workspace, "runtime");
+await mkdir(runtime);
+const env = {
+ PATH: process.env.PATH ?? "/usr/bin:/bin",
+ TMPDIR: runtime,
+ REA_LOG_LEVEL: "silent",
+ REA_ANALYSIS_PROVIDER: "ghidra",
+ REA_PROCESS_RUN_ID: run.run_id,
+ GHIDRA_INSTALL_DIR: installation.installDir,
+ JAVA_HOME: installation.javaHome,
+ HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
+};
+for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
+ if (process.env[setting] !== undefined) env[setting] = process.env[setting];
+}
+const reports = [];
+let primaryFailure;
+try {
+ {
+ const fixture = await buildPowerPcFixture(workspace);
+ const independent = await inspectPowerPcReadelf(fixture.path);
+ const resolved = await parseBinaryTarget(fixture.path);
+ if (!resolved.ok) throw resolved.error;
+ assert.deepEqual(
+ resolved.value.powerpc,
+ independent.powerpc,
+ "REA ELF interpretation disagrees with GNU readelf",
+ );
+ assert.equal(resolved.value.sha256, fixture.sha256);
+ const snapshotPath = join(workspace, "ppc.snapshot.json");
+ const transport = new StdioClientTransport({
+ command: process.execPath,
+ args: [entrypoint, "mcp"],
+ env,
+ stderr: "pipe",
+ });
+ const client = new Client({ name: "rea-real-ppc-proof", version: "1" });
+ let stderr = "";
+ transport.stderr?.on("data", (chunk) => {
+ stderr = (stderr + chunk.toString()).slice(-65536);
+ });
+ const call = async (name, args = {}) => {
+ const reply = await client.callTool(
+ { name, arguments: args },
+ undefined,
+ { timeout: 180000 },
+ );
+ return requireMcpOperationResult(reply, name);
+ };
+ try {
+ await client.connect(transport);
+ const target = await call("open_binary", {
+ path: fixture.path,
+ provider_id: "ghidra",
+ });
+ assert.equal(target.architecture, "powerpc");
+ assert.equal(target.sha256, fixture.sha256);
+ // ELF load-image verification is not the DOS comparison lane. Its retained
+ // observations still expose the actual Ghidra language and loaded bytes;
+ // the plain loader would have chosen e500 for this header.
+ const image = await call("inspect_native_load_image");
+ assertPowerPcLoadedImage(image.observations, fixture.sha256);
+ const procedures = await call("list_procedures");
+ const entry = procedures.find((p) => p.value === "rea_ppc_entry");
+ const leaf = procedures.find((p) => p.value === "rea_ppc_leaf");
+ assert.ok(entry, "Entry function missing from real Ghidra inventory");
+ assert.ok(leaf, "Leaf function missing from real Ghidra inventory");
+ assert.equal(
+ BigInt(entry.address),
+ BigInt(independent.symbols.rea_ppc_entry),
+ );
+ assert.equal(
+ BigInt(leaf.address),
+ BigInt(independent.symbols.rea_ppc_leaf),
+ );
+ const probe = independent.symbols.rea_ppc_probe;
+ assert.ok(
+ procedures.some(
+ (p) =>
+ p.value === "rea_ppc_probe" && BigInt(p.address) === BigInt(probe),
+ ),
+ );
+ const move = await call("inspect_native_instruction", { address: probe });
+ const branch = await call("inspect_native_instruction", {
+ address: `0x${(BigInt(probe) + 8n).toString(16)}`,
+ });
+ assertPowerPcProbe(move, branch, probe);
+ const global = await call("read_bytes", {
+ address: independent.symbols.rea_ppc_global,
+ length: 4,
+ });
+ assert.equal(global.returned_bytes, 4);
+ assertPowerPcGlobal(global.bytes_hex);
+ const marker = await call("read_bytes", {
+ address: independent.symbols.rea_ppc_marker,
+ length: 29,
+ });
+ assert.equal(
+ marker.bytes_hex,
+ Buffer.from("rea-ppc-source-owned-fixture\0").toString("hex"),
+ );
+ const callees = await call("procedure_callees", {
+ procedure: entry.address,
+ });
+ assert.ok(
+ callees.includes(leaf.address),
+ "Expected direct call not recovered",
+ );
+ const assembly = await call("procedure_assembly", {
+ procedure: entry.address,
+ });
+ assert.ok(assembly.trim().length > 0);
+ const dossier = functionDossierSchema.parse(
+ await call("analyze_function", { procedure: entry.address }),
+ );
+ assert.ok(dossier.pseudocode.trim().length > 0);
+ const mapping = await call("address_to_file_offset", {
+ address: entry.address,
+ });
+ const memory = await call("read_bytes", {
+ address: entry.address,
+ length: 16,
+ });
+ const sourceBytes = await readFile(fixture.path);
+ assert.equal(memory.returned_bytes, 16);
+ assert.equal(
+ memory.bytes_hex,
+ sourceBytes
+ .subarray(mapping.file_offset, mapping.file_offset + 16)
+ .toString("hex"),
+ );
+ const saved = await call("close_binary", {
+ snapshot_path: snapshotPath,
+ });
+ assert.ok(saved.evidence_records > 0);
+ const snapshot = parseAnalysisSnapshot(
+ JSON.parse(await readFile(snapshotPath, "utf8")),
+ );
+ assert.equal(snapshot.target.architecture, "powerpc");
+ assert.equal(
+ snapshot.binding.analysis_profile.parameters.language_id,
+ "PowerPC:BE:32:default",
+ );
+ assert.ok(snapshot.evidence_bundle.records.length > 0);
+ for (const evidence of snapshot.evidence_bundle.records)
+ assert.equal(parseEvidence(evidence).subject?.architecture, "powerpc");
+ await call("open_binary", {
+ path: fixture.path,
+ provider_id: "ghidra",
+ snapshot_path: snapshotPath,
+ });
+ assert.deepEqual(await call("list_procedures"), procedures);
+ await call("close_binary");
+
+ // Same public operation through CLI, not a private injected provider.
+ const cli = await execute(
+ process.execPath,
+ [
+ entrypoint,
+ "function",
+ fixture.path,
+ "rea_ppc_entry",
+ "--provider",
+ "ghidra",
+ "--json",
+ ],
+ { env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
+ );
+ const evidence = parseEvidence(JSON.parse(cli.stdout));
+ assert.equal(evidence.subject?.architecture, "powerpc");
+ assert.equal(evidence.subject?.digest.sha256, fixture.sha256);
+ const cliDossier = functionDossierSchema.parse(
+ evidence.normalized_result,
+ );
+ assert.equal(cliDossier.procedure.address, dossier.procedure.address);
+ assert.ok(cliDossier.pseudocode.trim().length > 0);
+ assert.equal(
+ createHash("sha256")
+ .update(await readFile(fixture.path))
+ .digest("hex"),
+ fixture.sha256,
+ "Analysis modified the source executable",
+ );
+ reports.push({
+ ...fixture,
+ independent_reader: independent,
+ effective_language: image.observations.language_id,
+ effective_compiler_spec: image.observations.compiler_spec_id,
+ checks: [
+ "independent ELF header and symbols",
+ "loaded Ghidra identity and bound language",
+ "fixed instruction bytes and immediate",
+ "conditional branch destination",
+ "global word and marker bytes",
+ "CLI/MCP and snapshot identity",
+ ],
+ procedures: procedures.length,
+ status: "passed",
+ });
+ } catch (cause) {
+ throw new Error(`PowerPC verification failed; MCP stderr: ${stderr}`, {
+ cause,
+ });
+ } finally {
+ try {
+ await client.close();
+ } finally {
+ await transport.close();
+ }
+ }
+ }
+} catch (cause) {
+ primaryFailure = cause;
+}
+const completed = await completeVerifierRun(run);
+if (
+ completed.process_lineage.status !== "verified" ||
+ completed.process_lineage.descendants.length !== 0
+)
+ throw new Error(
+ `Cleanup unverified; retained diagnostic workspace: ${workspace}`,
+ { cause: primaryFailure },
+ );
+if (primaryFailure !== undefined)
+ throw new Error(
+ `PowerPC verification failed; retained workspace: ${workspace}`,
+ {
+ cause: primaryFailure,
+ },
+ );
+await rm(workspace, { recursive: true, force: true });
+console.log(JSON.stringify({ ...completed, fixtures: reports }, null, 2));
diff --git a/scripts/verify-real-hopper.mjs b/scripts/verify-real-hopper.mjs
index 8cad9a256..a82c1429a 100644
--- a/scripts/verify-real-hopper.mjs
+++ b/scripts/verify-real-hopper.mjs
@@ -213,7 +213,7 @@ const verifyCurrentTarget = async (client, options) => {
);
return {
procedure: dossier.procedure,
- procedureCount: procedures.total,
+ procedureCount: procedures.length,
boundedPseudocodeChars: boundedPseudocode.length,
analyzedPseudocodeChars: [...dossier.pseudocode].length,
callerCount: relationships.procedure_callers.length,
diff --git a/scripts/verify-signal-recording.mjs b/scripts/verify-signal-recording.mjs
index f5baaf0b9..2d3910e62 100644
--- a/scripts/verify-signal-recording.mjs
+++ b/scripts/verify-signal-recording.mjs
@@ -276,12 +276,41 @@ try {
)
).stdout.split(/\r?\n/);
if (nativeHelp.at(-1) === "") nativeHelp.pop();
+ const nativeCatalogText = (
+ await execute(
+ process.env.REA_SIGROK_COMMAND,
+ ["--dont-scan", "--list-supported"],
+ { env: environment, timeout: 30000, maxBuffer: 4 * 1024 * 1024 },
+ )
+ ).stdout;
+ const catalogHeading = "Supported protocol decoders:\n";
+ assert.ok(nativeCatalogText.includes(catalogHeading));
+ // Independent native-output oracle, without importing the production parser.
+ const nativeCatalog = nativeCatalogText
+ .split(catalogHeading)[1]
+ .trim()
+ .split("\n")
+ .map((line) => {
+ const record = line.trimStart();
+ const separator = record.indexOf(" ");
+ assert.ok(separator > 0);
+ return {
+ id: record.slice(0, separator),
+ name: record.slice(separator).trimStart(),
+ };
+ });
+ assert.ok(nativeCatalog.length > 0);
for (const input of [
- { query: "uart" },
+ {},
+ { query: "UaRt" },
{ limit: 1 },
{ offset: 1, limit: 2 },
+ { limit: null },
+ { offset: nativeCatalog.length, limit: 2 },
+ { decoder: "uart" },
{ decoder: "uart", query: "BaUd" },
{ decoder: "uart", offset: 50, limit: null },
+ { decoder: "uart", query: "[" },
{ query: "rea_no_such_decoder" },
]) {
const started = performance.now();
@@ -306,23 +335,28 @@ try {
JSON.stringify(response),
),
});
- if (input.decoder) {
- const matched = nativeHelp
- .map((text, i) => ({ line_number: i + 1, text }))
- .filter(({ text }) =>
- text.toLowerCase().includes((input.query ?? "").toLowerCase()),
- );
- assert.deepEqual(
- report.help_lines,
- matched.slice(
- input.offset ?? 0,
- input.limit === null
- ? undefined
- : (input.offset ?? 0) + (input.limit ?? 50),
- ),
- );
- } else if (input.query === "uart")
- assert.ok(report.decoders.some(({ id }) => id === "uart"));
+ const nativeRecords = input.decoder
+ ? nativeHelp.map((text, i) => ({ line_number: i + 1, text }))
+ : nativeCatalog;
+ const matched = nativeRecords.filter((record) =>
+ ("text" in record ? record.text : `${record.id} ${record.name}`)
+ .toLowerCase()
+ .includes((input.query ?? "").toLowerCase()),
+ );
+ const offset = input.offset ?? 0;
+ const page = matched.slice(
+ offset,
+ input.limit === null ? undefined : offset + (input.limit ?? 50),
+ );
+ assert.deepEqual(input.decoder ? report.help_lines : report.decoders, page);
+ assert.deepEqual(input.decoder ? report.decoders : report.help_lines, []);
+ assert.deepEqual(report.output_window, {
+ unit: input.decoder ? "help-lines" : "decoders",
+ examined: nativeRecords.length,
+ matching: matched.length,
+ returned: page.length,
+ has_more: offset + page.length < matched.length,
+ });
const flags = Object.entries(input).flatMap(([key, value]) =>
value === null ? ["--all"] : [`--${key}`, String(value)],
);
diff --git a/scripts/verify-wasm-artifact.mjs b/scripts/verify-wasm-artifact.mjs
index 305063ccc..5123d3400 100644
--- a/scripts/verify-wasm-artifact.mjs
+++ b/scripts/verify-wasm-artifact.mjs
@@ -190,12 +190,21 @@ try {
const candidate = join(candidateRoot, "module.wasm");
await writeFile(join(candidateRoot, "source.wat"), "(module (memory 2))");
await tool("wat2wasm", ["source.wat", "-o", "module.wasm"], candidateRoot);
- const glue = join(fixtureRoot, "glue.js");
- await writeFile(
- glue,
- 'fetch("./module.wasm"); new URL("https://example.invalid/module.wasm?version=1", import.meta.url);',
+ const glueSource =
+ 'fetch("./module.wasm"); new URL("https://example.invalid/module.wasm?version=1", import.meta.url);';
+ const glueSources = [
+ glueSource,
+ `\uFEFF${glueSource}`,
+ `\uFEFF\n${glueSource}`,
+ `#!/usr/bin/env node\n${glueSource}`,
+ `\uFEFF#!/usr/bin/env node\n${glueSource}`,
+ ];
+ const gluePaths = glueSources.map((_, index) =>
+ join(fixtureRoot, `glue-${index}.js`),
);
- const input = { path, glue_paths: [glue], candidate_paths: [candidate] };
+ for (const [index, gluePath] of gluePaths.entries())
+ await writeFile(gluePath, glueSources[index]);
+ const input = { path, glue_paths: gluePaths, candidate_paths: [candidate] };
assert.ok(ajv.validate(advertised.inputSchema, input));
const cliValue = await inspectCli(input);
assert.equal(cliValue.category, undefined, JSON.stringify(cliValue));
@@ -219,11 +228,31 @@ try {
assert.equal(report.wat.sha256, sha256(wat));
assert.equal(report.headers, `${headers}\n`);
assert.equal(report.details, details.slice(1));
- assert.equal(report.glue[0].artifact.sha256, sha256(await readFile(glue)));
- assert.deepEqual(
- report.glue[0].references.map(({ candidate_paths }) => candidate_paths),
- [[path], [path, candidate]],
- );
+ assert.equal(report.glue.length, glueSources.length);
+ for (const [index, source] of glueSources.entries()) {
+ const observed = report.glue[index];
+ assert.equal(observed.artifact.path, gluePaths[index]);
+ assert.equal(observed.artifact.sha256, sha256(source));
+ assert.equal(observed.artifact.bytes, Buffer.byteLength(source));
+ assert.equal(await readFile(gluePaths[index], "utf8"), source);
+ assert.equal(observed.parse_status, "complete");
+ assert.deepEqual(
+ observed.references.map(({ candidate_paths }) => candidate_paths),
+ [[path], [path, candidate]],
+ );
+ assert.deepEqual(
+ observed.references.map(({ line, column }) => ({ line, column })),
+ ["./module.wasm", "https://example.invalid/module.wasm?version=1"].map(
+ (value) => {
+ const offset = source.indexOf(`"${value}"`);
+ return {
+ line: source.slice(0, offset).split("\n").length,
+ column: offset - source.lastIndexOf("\n", offset) - 1,
+ };
+ },
+ ),
+ );
+ }
assert.notEqual(report.candidates[0].sha256, report.candidates[1].sha256);
for (const command of report.tool_profile.commands)
assert.equal(command.sha256, sha256(await readFile(command.path)));
diff --git a/scripts/verify-windows-native-inspection.mjs b/scripts/verify-windows-native-inspection.mjs
index e5f807da3..94adaf5a6 100644
--- a/scripts/verify-windows-native-inspection.mjs
+++ b/scripts/verify-windows-native-inspection.mjs
@@ -5,7 +5,9 @@ import {
mkdir,
mkdtemp,
readdir,
+ rmdir,
rm,
+ stat,
symlink,
writeFile,
} from "node:fs/promises";
@@ -52,7 +54,7 @@ try {
join(workspace, "dist", "windows", "WindowsNativeLoader.js"),
).href;
const source = `import assert from 'node:assert/strict';
-import {readdirSync,renameSync,statSync} from 'node:fs';
+import {existsSync,mkdirSync,readdirSync,renameSync,statSync} from 'node:fs';
import {join} from 'node:path';
import {systemWindowsNativeAuthority} from ${JSON.stringify(loader)};
const temp=process.env.TEMP;
@@ -62,14 +64,20 @@ assert.equal(first.retryable,true);
assert.match(first.reason,/Delete owned object by handle failed.*Win32 5/s);
const [name,...extra]=readdirSync(temp);assert.ok(name);assert.equal(extra.length,0);
const path=join(temp,name);const identity=statSync(path,{bigint:true});
-assert.throws(()=>renameSync(path,path+'-replacement'));
-assert.equal(statSync(path,{bigint:true}).ino,identity.ino);
+// Ownership follows the retained NTFS handle even when the directory moves.
+// A replacement at its old pathname must remain caller-owned.
+const moved=path+'-renamed';renameSync(path,moved);
+assert.equal(statSync(moved,{bigint:true}).ino,identity.ino);
+mkdirSync(path);const replacement=statSync(path,{bigint:true});
+assert.notEqual(replacement.ino,identity.ino);
if(process.argv[2]!=='teardown'){
const second=systemWindowsNativeAuthority();assert.equal(second.available,true);
- assert.equal(readdirSync(temp).length,0);
+ assert.equal(existsSync(moved),false);
+ assert.deepEqual(readdirSync(temp),[name]);
+ assert.equal(statSync(path,{bigint:true}).ino,replacement.ino);
assert.equal(systemWindowsNativeAuthority(),second);
}
-console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:true,inode:identity.ino.toString()}));
+console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:true,inode:identity.ino.toString(),replacementName:name,replacementInode:replacement.ino.toString()}));
`;
const fixture = join(workspace, "probe.mjs");
await writeFile(fixture, source);
@@ -85,10 +93,17 @@ console.log(JSON.stringify({mode:process.argv[2],firstFailed:true,retainedOwner:
}),
);
assert.equal(observed.retainedOwner, true);
+ assert.deepEqual(await readdir(temp), [observed.replacementName]);
+ const replacement = join(temp, observed.replacementName);
+ assert.equal(
+ (await stat(replacement, { bigint: true })).ino.toString(),
+ observed.replacementInode,
+ );
+ await rmdir(replacement);
assert.deepEqual(await readdir(temp), []);
}
process.stdout.write(
- `${JSON.stringify({ ok: true, controlledDispositionFailure: true, retainedOwner: true, retryRecovers: true, successfulAdmissionCached: true, environmentTeardownCleansProbe: true, issue1701HostCauseReproduced: false })}\n`,
+ `${JSON.stringify({ ok: true, controlledDispositionFailure: true, retainedOwner: true, renamedOwnerCleanup: true, replacementPreserved: true, retryRecovers: true, successfulAdmissionCached: true, environmentTeardownCleansProbe: true, issue1701HostCauseReproduced: false })}\n`,
);
} finally {
await rm(workspace, { recursive: true, force: true });
diff --git a/scripts/verify-windows-native.mjs b/scripts/verify-windows-native.mjs
index 0a42cb604..fb4226bf4 100644
--- a/scripts/verify-windows-native.mjs
+++ b/scripts/verify-windows-native.mjs
@@ -148,6 +148,10 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
"//localhost/share/target.bin",
])
assert.throws(() => native.call("open", [rejected]));
+ assert.throws(
+ () => native.call("cutter_bridge_verify_directory", [sourceDirectory]),
+ /owner|DACL/u,
+ );
report.controls.ordinaryDriveSeparatorsAndRequestedIdentity = true;
// An owned runtime's DELETE lease conflicts with a fresh parent admission.
// This produces a genuine sharing failure after successful authority caching,
@@ -282,6 +286,10 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
() => native.call("open", [join(junction, "target.bin")]),
/Reparse/u,
);
+ assert.throws(
+ () => native.call("cutter_bridge_verify_directory", [junction]),
+ /Reparse/u,
+ );
assert.throws(() => native.call("open", [source + ":stream"]));
const runtime = native.call("runtime_create", [
workspace.replaceAll("\\", "/"),
@@ -326,6 +334,47 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
"descriptor.json",
Buffer.from("fixture"),
]);
+ assert.equal(
+ native.call("cutter_bridge_verify_directory", [runtime.path]).privateDacl,
+ true,
+ );
+ assert.equal(
+ native
+ .call("cutter_bridge_read_descriptor", [
+ runtime.path,
+ "descriptor.json",
+ 64 * 1024,
+ ])
+ .toString("utf8"),
+ "fixture",
+ );
+ const descriptorJunction = join(runtime.path, "descriptor-link.json");
+ await symlink(sourceDirectory, descriptorJunction, "junction");
+ assert.throws(() =>
+ native.call("cutter_bridge_read_descriptor", [
+ runtime.path,
+ "descriptor-link.json",
+ 64 * 1024,
+ ]),
+ );
+ assert.throws(
+ () =>
+ native.call("cutter_bridge_read_descriptor", [
+ runtime.path,
+ "descriptor.json",
+ 3,
+ ]),
+ /byte limit/u,
+ );
+ assert.throws(
+ () =>
+ native.call("cutter_bridge_read_descriptor", [
+ runtime.path,
+ "..\\escape.json",
+ 64 * 1024,
+ ]),
+ /filename component/u,
+ );
assert.throws(() =>
native.call("runtime_write", [
runtime.handle,
diff --git a/src/adb/AdbProvider.ts b/src/adb/AdbProvider.ts
index bce989aca..efe3eb3fa 100644
--- a/src/adb/AdbProvider.ts
+++ b/src/adb/AdbProvider.ts
@@ -1707,9 +1707,7 @@ export class AdbProvider {
},
provider,
{
- limitations: [
- "Push writes the caller's file to the device; this is the only device-mutating ADB tool",
- ],
+ limitations: ["Push writes the caller's file to the device"],
},
),
);
@@ -1731,7 +1729,7 @@ export class AdbProvider {
{
limitations: [
"The device did not answer sha256sum, so the transfer is not device-side verified",
- "Push writes the caller's file to the device; this is the only device-mutating ADB tool",
+ "Push writes the caller's file to the device",
],
},
),
diff --git a/src/application/BinaryTargetResolver.ts b/src/application/BinaryTargetResolver.ts
index b07364adf..348454e72 100644
--- a/src/application/BinaryTargetResolver.ts
+++ b/src/application/BinaryTargetResolver.ts
@@ -40,10 +40,14 @@ import {
} from "../domain/zipPackageFormat.js";
import {
+ parseCodeViewData,
parseExecutableHeader,
parsePeRecord,
+ peDebugDirectoryEntry,
+ peRvaToFileOffset,
type ExecutableMetadata,
} from "../domain/binaryTarget.js";
+import type { PeCodeViewRecord } from "../domain/binaryTargetTypes.js";
/** Options for resolving a local binary target and its stable identity. */
export interface BinaryTargetResolverOptions {
@@ -549,10 +553,98 @@ const readPeMetadata = async (
return err(`unsupported ${windowsHeader.signature} executable in MZ image`);
const optionalHeaderSize = fileHeader.readUInt16LE(20);
if (optionalHeaderSize > 4096) return err("invalid PE optional header size");
+ const numberOfSections = fileHeader.readUInt16LE(6);
const record = Buffer.alloc(24 + optionalHeaderSize);
const recordRead = await handle.read(record, 0, record.length, offset);
throwIfTargetResolutionCancelled(signal);
- return recordRead.bytesRead === record.length
- ? parsePeRecord(record)
- : err("invalid or truncated PE header");
+ if (recordRead.bytesRead !== record.length)
+ return err("invalid or truncated PE header");
+ const codeView = await readPeCodeView(
+ handle,
+ offset,
+ numberOfSections,
+ record.subarray(24),
+ optionalHeaderSize,
+ signal,
+ );
+ return parsePeRecord(record, codeView);
+};
+
+const readPeCodeView = async (
+ handle: FileHandle,
+ peOffset: number,
+ numberOfSections: number,
+ optionalHeader: Buffer,
+ optionalHeaderSize: number,
+ signal?: AbortSignal,
+): Promise => {
+ throwIfTargetResolutionCancelled(signal);
+ const debugDir = peDebugDirectoryEntry(optionalHeader);
+ if (debugDir === undefined || numberOfSections > 96) return undefined;
+ const sectionTableOffset = peOffset + 24 + optionalHeaderSize;
+ const sectionTableSize = numberOfSections * 40;
+ const sectionTable = Buffer.alloc(sectionTableSize);
+ const sectionRead = await handle.read(
+ sectionTable,
+ 0,
+ sectionTableSize,
+ sectionTableOffset,
+ );
+ throwIfTargetResolutionCancelled(signal);
+ if (sectionRead.bytesRead !== sectionTableSize) return undefined;
+ const debugFileOffset = peRvaToFileOffset(
+ debugDir.debugRva,
+ sectionTable,
+ numberOfSections,
+ debugDir.sizeOfHeaders,
+ Math.min(debugDir.debugSize, 4096),
+ );
+ if (debugFileOffset === undefined) return undefined;
+ const maxDebugSize = Math.min(debugDir.debugSize, 4096);
+ const debugDirectoryBuffer = Buffer.alloc(maxDebugSize);
+ const debugDirRead = await handle.read(
+ debugDirectoryBuffer,
+ 0,
+ maxDebugSize,
+ debugFileOffset,
+ );
+ throwIfTargetResolutionCancelled(signal);
+ const numEntries = Math.floor(debugDirRead.bytesRead / 28);
+ for (let j = 0; j < numEntries; j++) {
+ const entryOffset = j * 28;
+ const type = debugDirectoryBuffer.readUInt32LE(entryOffset + 12);
+ if (type !== 2) continue; // IMAGE_DEBUG_TYPE_CODEVIEW
+ const sizeOfData = debugDirectoryBuffer.readUInt32LE(entryOffset + 16);
+ let pointerToRawData = debugDirectoryBuffer.readUInt32LE(entryOffset + 24);
+ if (pointerToRawData === 0) {
+ const addressOfRawData = debugDirectoryBuffer.readUInt32LE(
+ entryOffset + 20,
+ );
+ const mapped = peRvaToFileOffset(
+ addressOfRawData,
+ sectionTable,
+ numberOfSections,
+ debugDir.sizeOfHeaders,
+ Math.min(sizeOfData, 4096),
+ );
+ if (mapped === undefined) continue;
+ pointerToRawData = mapped;
+ }
+ if (sizeOfData < 16) continue;
+ const maxCodeViewRead = Math.min(sizeOfData, 4096);
+ const codeViewBuffer = Buffer.alloc(maxCodeViewRead);
+ const codeViewRead = await handle.read(
+ codeViewBuffer,
+ 0,
+ maxCodeViewRead,
+ pointerToRawData,
+ );
+ throwIfTargetResolutionCancelled(signal);
+ const parsed = parseCodeViewData(
+ codeViewBuffer.subarray(0, codeViewRead.bytesRead),
+ sizeOfData,
+ );
+ if (parsed !== undefined) return parsed;
+ }
+ return undefined;
};
diff --git a/src/application/CallPathTracing.ts b/src/application/CallPathTracing.ts
index cc4ce41f4..640f7c510 100644
--- a/src/application/CallPathTracing.ts
+++ b/src/application/CallPathTracing.ts
@@ -158,7 +158,7 @@ const expandAddress = async (
state.residual.add(`Call relationships were unavailable for ${address}.`);
return ok(false);
}
- const related = parseRelatedAddresses(result.value.result, relation);
+ const related = parseRelatedAddresses(result.value.result, relation, tool);
if (!related.ok) {
state.failures.push({
address,
diff --git a/src/application/CapabilityInventory.test.ts b/src/application/CapabilityInventory.test.ts
index 4286ff1d0..94b9ac659 100644
--- a/src/application/CapabilityInventory.test.ts
+++ b/src/application/CapabilityInventory.test.ts
@@ -498,6 +498,106 @@ it("reports host denial before target mismatch but keeps composed workflow prere
).toBe("target_required");
});
+describe("binary_overview capability requirements", () => {
+ it("admits a provider with current_document and other mandatory capabilities", () => {
+ const capabilities = [
+ "list_segments",
+ "current_document",
+ "list_procedures",
+ "list_strings",
+ ].map((operation) => ({ operation, available: true, reason: null }));
+ expect(
+ entry(
+ "binary_overview",
+ status({ open: true, kind: "executable", capabilities }),
+ ),
+ ).toMatchObject({
+ available: true,
+ reason: "available",
+ remediation: null,
+ });
+ });
+
+ it("admits a provider with list_documents and other mandatory capabilities", () => {
+ const capabilities = [
+ "list_segments",
+ "list_documents",
+ "list_procedures",
+ "list_strings",
+ ].map((operation) => ({ operation, available: true, reason: null }));
+ expect(
+ entry(
+ "binary_overview",
+ status({ open: true, kind: "executable", capabilities }),
+ ),
+ ).toMatchObject({
+ available: true,
+ reason: "available",
+ remediation: null,
+ });
+ });
+
+ it("reports document selection remediation when neither current_document nor list_documents is declared", () => {
+ const capabilities = [
+ "list_segments",
+ "list_procedures",
+ "list_strings",
+ ].map((operation) => ({ operation, available: true, reason: null }));
+ expect(
+ entry(
+ "binary_overview",
+ status({ open: true, kind: "executable", capabilities }),
+ ),
+ ).toMatchObject({
+ available: false,
+ reason: "provider_missing",
+ remediation:
+ "Configure a provider for one of: current_document, list_documents.",
+ });
+ });
+
+ it("reports missing segment enumeration independently when segments are missing", () => {
+ const capabilities = [
+ "current_document",
+ "list_procedures",
+ "list_strings",
+ ].map((operation) => ({ operation, available: true, reason: null }));
+ expect(
+ entry(
+ "binary_overview",
+ status({ open: true, kind: "executable", capabilities }),
+ ),
+ ).toMatchObject({
+ available: false,
+ reason: "provider_missing",
+ remediation: "Configure a provider for required operation list_segments.",
+ });
+ });
+
+ it("reports provider_unavailable when declared document selection capability is unavailable", () => {
+ const capabilities = [
+ { operation: "list_segments", available: true, reason: null },
+ {
+ operation: "current_document",
+ available: false,
+ reason: "Document view is closed",
+ },
+ { operation: "list_procedures", available: true, reason: null },
+ { operation: "list_strings", available: true, reason: null },
+ ];
+ expect(
+ entry(
+ "binary_overview",
+ status({ open: true, kind: "executable", capabilities }),
+ ),
+ ).toMatchObject({
+ available: false,
+ reason: "provider_unavailable",
+ remediation: "Document view is closed",
+ });
+ });
+});
+
describe("trace_feature requirements", () => {
const capabilities = (operations: readonly string[]) =>
operations.map((operation) => ({
diff --git a/src/application/CapabilityInventory.ts b/src/application/CapabilityInventory.ts
index f4584eb29..87734fbd1 100644
--- a/src/application/CapabilityInventory.ts
+++ b/src/application/CapabilityInventory.ts
@@ -98,7 +98,9 @@ type AvailabilityContext = {
readonly policy: AvailabilityPolicy;
};
-const ENHANCED_REQUIREMENTS: Readonly> = {
+const ENHANCED_REQUIREMENTS: Readonly<
+ Record
+> = {
trace_native_values: ["analyze_function", "resolve_native_call_targets"],
trace_native_ui_action: [
"decode_interface_builder",
@@ -114,7 +116,7 @@ const ENHANCED_REQUIREMENTS: Readonly> = {
find_xrefs_to_name: ["list_names", "xrefs"],
binary_overview: [
"list_segments",
- "list_documents",
+ ["current_document", "list_documents"],
"list_procedures",
"list_strings",
],
@@ -343,7 +345,7 @@ const workflowAvailabilityFor = ({
kind,
policy,
}: AvailabilityContext): Availability | null => {
- if (name === "inspect_go_binary")
+ if (name === "inspect_go_binary" || name === "inspect_rust_binary")
return { reason: "available", remediation: null };
if (name === "inspect_sqlite_database") {
const capability = sqliteDatabaseCapability(process.versions.node);
@@ -642,27 +644,62 @@ const composedAvailability = (
};
const composedAvailabilityFor = (
- requirements: readonly string[],
+ requirements: readonly (string | readonly string[])[],
descriptors: ReadonlyMap,
): Availability => {
- const missing = requirements.find((operation) => !descriptors.has(operation));
- if (missing !== undefined)
- return {
- reason: "provider_missing",
- remediation: `Configure a provider for required operation ${missing}.`,
- };
- const unavailable = requirements
- .map((operation) => descriptors.get(operation))
- .find((descriptor) => descriptor?.available === false);
- if (unavailable !== undefined)
- return {
- reason:
- unavailable.availability_code === "unsupported_host"
- ? "unsupported_host"
- : "provider_unavailable",
- remediation:
- unavailable.reason ?? "Choose another target or configured provider.",
- };
+ for (const requirement of requirements) {
+ if (typeof requirement === "string") {
+ if (!descriptors.has(requirement))
+ return {
+ reason: "provider_missing",
+ remediation: `Configure a provider for required operation ${requirement}.`,
+ };
+ } else {
+ const declared = requirement.some((operation) =>
+ descriptors.has(operation),
+ );
+ if (!declared)
+ return {
+ reason: "provider_missing",
+ remediation: `Configure a provider for one of: ${requirement.join(", ")}.`,
+ };
+ }
+ }
+
+ for (const requirement of requirements) {
+ if (typeof requirement === "string") {
+ const descriptor = descriptors.get(requirement);
+ if (descriptor?.available === false)
+ return {
+ reason:
+ descriptor.availability_code === "unsupported_host"
+ ? "unsupported_host"
+ : "provider_unavailable",
+ remediation:
+ descriptor.reason ??
+ "Choose another target or configured provider.",
+ };
+ } else {
+ const anyAvailable = requirement.some(
+ (operation) => descriptors.get(operation)?.available === true,
+ );
+ if (!anyAvailable) {
+ const unavailable = requirement
+ .map((operation) => descriptors.get(operation))
+ .find((descriptor) => descriptor?.available === false);
+ if (unavailable !== undefined)
+ return {
+ reason:
+ unavailable.availability_code === "unsupported_host"
+ ? "unsupported_host"
+ : "provider_unavailable",
+ remediation:
+ unavailable.reason ??
+ "Choose another target or configured provider.",
+ };
+ }
+ }
+ }
return { reason: "available", remediation: null };
};
diff --git a/src/application/ClientConfigurationDocument.ts b/src/application/ClientConfigurationDocument.ts
index a226f8cb6..f7ab8f0e2 100644
--- a/src/application/ClientConfigurationDocument.ts
+++ b/src/application/ClientConfigurationDocument.ts
@@ -1520,12 +1520,25 @@ export const parseGeminiMcpPolicy = (parsed: ClientConfigurationDocument) =>
.passthrough()
.parse(parsed.document.mcp ?? {});
+/** One server-name comparison for the exclusion, intersection and membership rules below. */
+const normalizeGeminiServerName = (name: string): string =>
+ name.toLowerCase().trim();
+
/** Gemini consolidates excludes by union and allowlists by normalized intersection. */
export const geminiServerPolicyBlock = (
policies: readonly ReturnType[],
serverKey: string,
): string | undefined => {
- if (policies.some(({ excluded }) => excluded?.includes(serverKey)))
+ // Hand-authored settings cannot be assumed to spell the product key exactly,
+ // so every rule compares normalized names. Testing the consolidated list
+ // against the raw key instead would make the outcome depend on which policy
+ // supplied the spelling that survived the intersection.
+ const key = normalizeGeminiServerName(serverKey);
+ if (
+ policies.some(({ excluded }) =>
+ excluded?.some((name) => normalizeGeminiServerName(name) === key),
+ )
+ )
return `Gemini mcp.excluded blocks ${serverKey}. Review the selected settings policies before rerunning setup.`;
const allowlists = policies.flatMap(({ allowed }) =>
allowed === undefined ? [] : [allowed],
@@ -1533,11 +1546,9 @@ export const geminiServerPolicyBlock = (
const allowed = allowlists.reduce(
(previous, current) => {
if (previous === undefined) return current;
- const normalized = new Set(
- current.map((name) => name.toLowerCase().trim()),
- );
+ const normalized = new Set(current.map(normalizeGeminiServerName));
return previous.filter((name) =>
- normalized.has(name.toLowerCase().trim()),
+ normalized.has(normalizeGeminiServerName(name)),
);
},
undefined,
@@ -1546,7 +1557,7 @@ export const geminiServerPolicyBlock = (
if (
allowed !== undefined &&
allowed.length > 0 &&
- !allowed.includes(serverKey)
+ !allowed.some((name) => normalizeGeminiServerName(name) === key)
)
return `Gemini mcp.allowed does not include ${serverKey}. Review the selected settings policies before rerunning setup.`;
return undefined;
diff --git a/src/application/HermesConfigurationYaml.ts b/src/application/HermesConfigurationYaml.ts
index 8d245da37..61eb0dfde 100644
--- a/src/application/HermesConfigurationYaml.ts
+++ b/src/application/HermesConfigurationYaml.ts
@@ -1,7 +1,7 @@
import {
isScalar,
- parse,
parseDocument,
+ type Document,
type ParsedNode,
type ScalarTag,
type Tags,
@@ -56,9 +56,28 @@ const HERMES_YAML_OPTIONS = {
a.source === b.source),
};
-/** Parse a Hermes `config.yaml`. */
-export const parseHermesConfigurationYaml = (text: string): unknown =>
- parse(text, HERMES_YAML_OPTIONS);
+// A comment-only document has no root; a bare marker has an unwritten null
+// scalar. Explicit null values, tags and anchors remain actual values.
+const isEmptyYamlRoot = (root: Document["contents"]): boolean =>
+ root === null ||
+ (isScalar(root) &&
+ root.value === null &&
+ root.source === "" &&
+ root.tag === undefined &&
+ root.anchor === undefined);
+
+const parseHermesDocument = (text: string) => {
+ const document = parseDocument(text, HERMES_YAML_OPTIONS);
+ const error = document.errors[0];
+ if (error !== undefined) throw error;
+ return document;
+};
+
+/** Parse a Hermes `config.yaml`, preserving the distinction between no root and explicit null. */
+export const parseHermesConfigurationYaml = (text: string): unknown => {
+ const document = parseHermesDocument(text);
+ return isEmptyYamlRoot(document.contents) ? {} : document.toJS();
+};
/** Set or delete one value in a Hermes `config.yaml`, keeping the rest as written. */
export const editHermesConfigurationYaml = (
@@ -66,7 +85,16 @@ export const editHermesConfigurationYaml = (
path: readonly string[],
value: unknown,
): string => {
- const document = parseDocument(text, HERMES_YAML_OPTIONS);
+ // A plain Document, so a bare `---` root can be replaced by a new map.
+ const document: Document = parseHermesDocument(text);
+ const root = document.contents;
+ if (isScalar(root) && isEmptyYamlRoot(root)) {
+ const map = document.createNode({});
+ map.commentBefore = [root.commentBefore, root.comment]
+ .filter((comment) => comment !== undefined)
+ .join("\n");
+ document.contents = map;
+ }
if (value === undefined) document.deleteIn(path);
else document.setIn(path, value);
// Editing REA must not reflow unrelated user prompts or other YAML scalars.
diff --git a/src/application/MacHopper.ts b/src/application/MacHopper.ts
index a211e9f2d..858e7fe48 100644
--- a/src/application/MacHopper.ts
+++ b/src/application/MacHopper.ts
@@ -15,6 +15,8 @@ import { promisify } from "node:util";
import { z } from "zod";
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
+import { HOPPER_PUBLISHER } from "../domain/hopperPublisher.js";
+import { macDeveloperIdRequirement } from "../process/MacCodeSignature.js";
import {
downloadHopperPackage,
HOPPER_DOWNLOAD_BYTES,
@@ -24,29 +26,10 @@ const execFileAsync = promisify(execFile);
const RELEASES_URL =
"https://www.hopperapp.com/include/files-api.php?request=releases&public=true";
const DOWNLOAD_PREFIX = "https://www.hopperapp.com:443/downloader/public/";
-
-/**
- * Apple publisher identity pinned for the official Hopper macOS bundle. The
- * team identifier was captured from `Hopper-6.6.0-demo.dmg` at
- * `https://www.hopperapp.com:443/downloader/public/Hopper-6.6.0-demo.dmg`
- * (34846271 bytes, SHA-1 0be530f038249742590992199c7b042ea20ca955) with
- * `codesign -dvvv`, which reported
- * `Authority=Developer ID Application: Cryptic Apps (2AMA2753NF)`.
- */
-export const HOPPER_PUBLISHER = Object.freeze({
- bundleIdentifier: "com.cryptic-apps.hopper-web-4",
- teamIdentifier: "2AMA2753NF",
-});
-
-/**
- * Code requirement enforcing the bundle identifier and signing team together.
- * The identifier is read from the sealed CodeDirectory, so it cannot be changed
- * by editing `Info.plist` the way the previous string comparison could.
- */
-const HOPPER_PUBLISHER_REQUIREMENT =
- `= identifier "${HOPPER_PUBLISHER.bundleIdentifier}"` +
- ` and anchor apple generic` +
- ` and certificate leaf[subject.OU] = "${HOPPER_PUBLISHER.teamIdentifier}"`;
+const HOPPER_PUBLISHER_REQUIREMENT = macDeveloperIdRequirement(
+ HOPPER_PUBLISHER.bundleIdentifier,
+ HOPPER_PUBLISHER.teamIdentifier,
+);
/** Outcome of reading a mounted bundle's signature and publisher identity. */
export type MacHopperBundleVerification =
diff --git a/src/application/SetupInstallFailure.test.ts b/src/application/SetupInstallFailure.test.ts
index 980e22151..d8ba27bdb 100644
--- a/src/application/SetupInstallFailure.test.ts
+++ b/src/application/SetupInstallFailure.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { HOPPER_PUBLISHER } from "./MacHopper.js";
+import { HOPPER_PUBLISHER } from "../domain/hopperPublisher.js";
import { setupInstallFailure } from "./SetupInstallFailure.js";
describe("macOS Hopper setup failure projection", () => {
diff --git a/src/application/SetupInstallFailure.ts b/src/application/SetupInstallFailure.ts
index 0b332fa25..944a60343 100644
--- a/src/application/SetupInstallFailure.ts
+++ b/src/application/SetupInstallFailure.ts
@@ -1,4 +1,4 @@
-import { HOPPER_PUBLISHER } from "./MacHopper.js";
+import { HOPPER_PUBLISHER } from "../domain/hopperPublisher.js";
export type SetupFailureCode =
| "unsupported_host"
diff --git a/src/application/SupportedClients.ts b/src/application/SupportedClients.ts
index 55a651a52..20ab3ed29 100644
--- a/src/application/SupportedClients.ts
+++ b/src/application/SupportedClients.ts
@@ -606,6 +606,14 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
markerPath: traeUserDirectory,
format: "json",
},
+ {
+ name: "workbuddy",
+ displayName: "WorkBuddy",
+ skillPath: [".workbuddy", "skills"],
+ configPath: [".workbuddy", "mcp.json"],
+ markerPath: [".workbuddy"],
+ format: "json",
+ },
{
name: "grok_build",
displayName: "Grok Build",
diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts
index baaa6b7ca..f12033ee0 100644
--- a/src/application/analysisView/AnalysisViewService.ts
+++ b/src/application/analysisView/AnalysisViewService.ts
@@ -49,7 +49,10 @@ export const inspectAnalysisViewValidated = (
): Result => {
const accumulator =
input.view.kind === "api-surface"
- ? new WebNetworkApiSurfaceAccumulator()
+ ? new WebNetworkApiSurfaceAccumulator(
+ undefined,
+ input.view.include_query_parameter_names === true,
+ )
: undefined;
if (input.source.kind === "inline" && accumulator !== undefined) {
const admitted = accumulator.admitInlineEvidence(input.source.evidence);
diff --git a/src/application/binary/AnalysisProviderRegistry.test.ts b/src/application/binary/AnalysisProviderRegistry.test.ts
index 8eae6c9c9..fde2f6718 100644
--- a/src/application/binary/AnalysisProviderRegistry.test.ts
+++ b/src/application/binary/AnalysisProviderRegistry.test.ts
@@ -186,6 +186,28 @@ describe("analysis provider registry: binding failures and cancellation", () =>
expect(explicit.error.reason).toBe("target_unsupported");
});
+ it("rejects an unknown environment preference even when every candidate rejects the target", async () => {
+ const alpha = candidate("alpha");
+ const misconfigured = new AnalysisProviderRegistry(
+ [alpha.provider],
+ "missing",
+ );
+
+ for (const selected of [
+ await misconfigured.select(ARTIFACT_TARGET),
+ await misconfigured.select(ARTIFACT_TARGET, "missing"),
+ ]) {
+ if (selected.ok) throw new Error("expected unknown provider rejection");
+ expect(projectAnalysisError(selected.error)).toMatchObject({
+ details: {
+ selection_reason: "unknown_provider",
+ requested_provider_id: "missing",
+ candidate_ids: ["alpha"],
+ },
+ });
+ }
+ });
+
it("does not bind a profile with unresolved version or adapter failure", async () => {
const unresolved = candidate("unresolved", { profile: "missing" });
const failed = candidate("failed", { profile: "error" });
diff --git a/src/application/binary/AnalysisProviderRegistry.ts b/src/application/binary/AnalysisProviderRegistry.ts
index 5b7e78813..033677881 100644
--- a/src/application/binary/AnalysisProviderRegistry.ts
+++ b/src/application/binary/AnalysisProviderRegistry.ts
@@ -176,7 +176,7 @@ export class AnalysisProviderRegistry {
return err(new AnalysisCancelledError("open_binary"));
if (
request === undefined &&
- selector !== "auto" &&
+ this.#byId.has(selector) &&
baseline.length > 0 &&
baseline.every(
({ targetSupport }) => targetSupport.status === "unsupported",
diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts
index b151958e7..350642016 100644
--- a/src/application/binary/BinarySession.ts
+++ b/src/application/binary/BinarySession.ts
@@ -5,7 +5,10 @@ import {
analysisProfilesEqual,
type AnalysisProfileCommitment,
} from "../../domain/analysisProfile.js";
-import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
+import {
+ AnalysisCancelledError,
+ AnalysisOutputError,
+} from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import type { JsonValue } from "../../domain/jsonValue.js";
@@ -73,6 +76,7 @@ export class BinarySession
implements BinarySessionPort
{
#active: SessionBinding | undefined;
+ #externalMutationCleanupFailure: AnalysisError | undefined;
#pendingCleanup:
| {
readonly client: AnalysisClient;
@@ -158,6 +162,10 @@ export class BinarySession
);
}
+ invalidateSnapshotForExternalMutation(): void {
+ this.invalidateSnapshot();
+ }
+
/** Return the selected immutable profile, optionally scoped to an operation. */
analysisProfile(
operation?: AnalysisOperation,
@@ -245,110 +253,180 @@ export class BinarySession
resolve: () => Promise>,
options: Pick,
): Promise> {
- return this.#serialize(async (admittedThrough) => {
- if (isAborted(options.signal))
- return err(new AnalysisCancelledError("open_binary"));
- const resolved = await resolve();
- if (!resolved.ok) return resolved;
- const { target, route, sameTarget } = resolved.value;
- const { profile } = route;
- if (isAborted(options.signal))
- return err(new AnalysisCancelledError("open_binary"));
- const activeProfile = this.#active?.profile;
- const sameProfile =
- activeProfile === null || activeProfile === undefined
- ? profile === null
- : profile !== null && analysisProfilesEqual(activeProfile, profile);
- await this.#drainCalls(admittedThrough);
- if (isAborted(options.signal))
- return err(new AnalysisCancelledError("open_binary"));
- const pendingClosed = await this.#retryPendingCleanup();
- if (!pendingClosed.ok) return pendingClosed;
- if (isAborted(options.signal))
- return err(new AnalysisCancelledError("open_binary"));
- const retained = sameTarget && sameProfile ? this.#active : undefined;
- if (retained !== undefined) {
- const fastPath = await this.#tryRetainedFastPath(
- retained,
- target,
- options,
- );
- if (fastPath !== undefined) return fastPath;
- }
- const previous = this.#active;
- this.#active = undefined;
- if (previous !== undefined) {
- const closed = await this.#retireClient(
- previous.client,
- previous.route.identity.id,
- );
- if (!closed.ok) {
- // A failed open retires the provider cache, not the investigation.
- // Preserve Evidence and Unknown history for export and recovery.
- this.clearSnapshot();
- this.#clearRuntimeAvailability();
- return closed;
- }
- }
- if (isAborted(options.signal))
- return err(
- await this.#restoreAfterFailedOpen(
- previous,
- new AnalysisCancelledError("open_binary"),
- ),
- );
- const runId = randomUUID();
- const client = route.createClient(target, { runId });
- const started = await client.execute("health", {}, options);
- if (!started.ok) {
- return err(
- await this.#failedOpen(
- client,
- route.identity.id,
- previous,
- started.error,
- ),
- );
- }
- if (isAborted(options.signal)) {
- return err(
- await this.#failedOpen(
- client,
- route.identity.id,
- previous,
- new AnalysisCancelledError("open_binary"),
- ),
- );
- }
- this.#active = {
+ return this.#serialize((admittedThrough) =>
+ this.#openWithinTransition(admittedThrough, resolve, options),
+ );
+ }
+
+ async #openWithinTransition(
+ admittedThrough: number,
+ resolve: () => Promise>,
+ options: Pick,
+ ): Promise> {
+ if (this.#externalMutationCleanupFailure?.cleanupIncomplete === true)
+ return err(this.#externalMutationCleanupFailure);
+ if (isAborted(options.signal))
+ return err(new AnalysisCancelledError("open_binary"));
+ const resolved = await resolve();
+ if (!resolved.ok) return resolved;
+ const { target, route, sameTarget } = resolved.value;
+ const { profile } = route;
+ if (isAborted(options.signal))
+ return err(new AnalysisCancelledError("open_binary"));
+ const activeProfile = this.#active?.profile;
+ const sameProfile =
+ activeProfile === null || activeProfile === undefined
+ ? profile === null
+ : profile !== null && analysisProfilesEqual(activeProfile, profile);
+ await this.#drainCalls(admittedThrough);
+ if (isAborted(options.signal))
+ return err(new AnalysisCancelledError("open_binary"));
+ const pendingClosed = await this.#retryPendingCleanup();
+ if (!pendingClosed.ok) return pendingClosed;
+ if (isAborted(options.signal))
+ return err(new AnalysisCancelledError("open_binary"));
+ const retained = sameTarget && sameProfile ? this.#active : undefined;
+ if (retained !== undefined) {
+ const fastPath = await this.#tryRetainedFastPath(
+ retained,
target,
- client,
- profile,
- route,
- runId,
- nativeSourceAdmitted: false,
- };
- this.#clearRuntimeAvailability();
- this.resetSnapshotInvalidation();
- if (options.snapshot === undefined) {
- if (profile === null) this.clearSnapshot();
- else this.selectSnapshot(target, profile);
+ options,
+ );
+ if (fastPath !== undefined) return fastPath;
+ }
+ const previous = this.#active;
+ this.#active = undefined;
+ if (previous !== undefined) {
+ const closed = await this.#retireClient(
+ previous.client,
+ previous.route.identity.id,
+ );
+ if (!closed.ok) {
+ // A failed open retires the provider cache, not the investigation.
+ // Preserve Evidence and Unknown history for export and recovery.
+ this.clearSnapshot();
+ this.#clearRuntimeAvailability();
+ return closed;
}
- if (options.snapshot !== undefined) {
- const imported = this.importAnalysisSnapshot(options.snapshot);
- if (!imported.ok) {
- this.#active = undefined;
- return err(
- await this.#failedOpen(
- client,
- route.identity.id,
- previous,
- imported.error,
- ),
- );
- }
+ }
+ if (isAborted(options.signal))
+ return err(
+ await this.#restoreAfterFailedOpen(
+ previous,
+ new AnalysisCancelledError("open_binary"),
+ ),
+ );
+ const runId = randomUUID();
+ const client = route.createClient(target, { runId });
+ const started = await client.execute("health", {}, options);
+ if (!started.ok) {
+ return err(
+ await this.#failedOpen(
+ client,
+ route.identity.id,
+ previous,
+ started.error,
+ ),
+ );
+ }
+ if (isAborted(options.signal)) {
+ return err(
+ await this.#failedOpen(
+ client,
+ route.identity.id,
+ previous,
+ new AnalysisCancelledError("open_binary"),
+ ),
+ );
+ }
+ this.#active = {
+ target,
+ client,
+ profile,
+ route,
+ runId,
+ nativeSourceAdmitted: false,
+ };
+ this.#clearRuntimeAvailability();
+ this.resetSnapshotInvalidation();
+ if (options.snapshot === undefined) {
+ if (profile === null) this.clearSnapshot();
+ else this.selectSnapshot(target, profile);
+ }
+ if (options.snapshot !== undefined) {
+ const imported = this.importAnalysisSnapshot(options.snapshot);
+ if (!imported.ok) {
+ this.#active = undefined;
+ return err(
+ await this.#failedOpen(
+ client,
+ route.identity.id,
+ previous,
+ imported.error,
+ ),
+ );
}
- return ok(target);
+ }
+ return ok(target);
+ }
+
+ /** Serialize an external mutation and retire the potentially stale analysis. */
+ withAdmittedExternalMutation(
+ _path: string | undefined,
+ signal: AbortSignal | undefined,
+ operation: () => Promise>,
+ retirementFailure: (
+ value: Result,
+ error: AnalysisError,
+ ) => AnalysisError,
+ operationName = "execute_rizin_command",
+ cleanupRecovery?: (
+ failure: AnalysisError | undefined,
+ ) => Promise>,
+ ): Promise, AnalysisError>> {
+ return this.#serialize(async (admittedThrough) => {
+ if (isAborted(signal))
+ return err(new AnalysisCancelledError(operationName));
+ await this.#drainCalls(admittedThrough);
+ if (isAborted(signal))
+ return err(new AnalysisCancelledError(operationName));
+
+ if (this.#externalMutationCleanupFailure?.cleanupIncomplete === true) {
+ if (cleanupRecovery === undefined)
+ return err(this.#externalMutationCleanupFailure);
+ const recovered = await cleanupRecovery(
+ this.#externalMutationCleanupFailure,
+ );
+ if (!recovered.ok) return recovered;
+ if (this.#externalMutationCleanupFailure.cleanupIncomplete)
+ return err(this.#externalMutationCleanupFailure);
+ this.#externalMutationCleanupFailure = undefined;
+ }
+
+ const pendingCleanup = await this.#retryPendingCleanup();
+ if (!pendingCleanup.ok) return pendingCleanup;
+
+ const activeAtDispatch = this.#active;
+ if (activeAtDispatch?.route.binding?.identity.id === "ghidra")
+ return err(
+ new AnalysisOutputError(
+ operationName,
+ "Close the active Ghidra session explicitly before an external mutation; rebinding would discard its ephemeral analysis edits.",
+ ),
+ );
+ // A protocol reply does not prove that background writers have stopped.
+ // Retire the old analysis and require an explicit fresh open rather than
+ // silently binding a snapshot while an unrestricted command can still write.
+ const retireTarget = activeAtDispatch !== undefined;
+ this.invalidateSnapshot();
+ const value = await operation();
+ this.#externalMutationCleanupFailure =
+ !value.ok && value.error.cleanupIncomplete ? value.error : undefined;
+ if (!retireTarget) return ok(value);
+
+ const retired = await this.#closeActive({}, false);
+ if (!retired.ok) return err(retirementFailure(value, retired.error));
+ return ok(value);
});
}
@@ -396,6 +474,7 @@ export class BinarySession
options: Pick & {
readonly retainProviderDocuments?: boolean;
},
+ clearRecords = true,
): Promise> {
const previous = this.#active;
this.#active = undefined;
@@ -417,7 +496,7 @@ export class BinarySession
: {}),
},
);
- this.clearSessionRecords();
+ if (clearRecords) this.clearSessionRecords();
this.#clearRuntimeAvailability();
const fileResources = await this.closeWriteResources();
if (!closed.ok && !fileResources.ok)
diff --git a/src/application/binary/BinarySessionPort.ts b/src/application/binary/BinarySessionPort.ts
index 749e98b2f..49f513c69 100644
--- a/src/application/binary/BinarySessionPort.ts
+++ b/src/application/binary/BinarySessionPort.ts
@@ -48,6 +48,20 @@ export interface BinarySessionPort
signal: AbortSignal | undefined,
operation: (analysis: AnalysisOperationPort) => Promise,
): Promise>;
+ /** Serialize an external mutation and retire any active target afterward. */
+ withAdmittedExternalMutation(
+ _path: string | undefined,
+ signal: AbortSignal | undefined,
+ operation: () => Promise>,
+ retirementFailure: (
+ value: Result,
+ error: AnalysisError,
+ ) => AnalysisError,
+ operationName?: string,
+ cleanupRecovery?: (
+ failure: AnalysisError | undefined,
+ ) => Promise>,
+ ): Promise, AnalysisError>>;
open(
path: string,
options?: {
@@ -82,6 +96,8 @@ export interface BinarySessionPort
operation?: AnalysisOperation,
): AnalysisProfileCommitment | undefined;
allowsSnapshotReplay(operation: AnalysisOperation): boolean;
+ /** Invalidate cached Evidence before an external command may mutate the target. */
+ invalidateSnapshotForExternalMutation(): void;
recordWorkflowSnapshot(
input: WorkflowSnapshotRecordInput,
): Result;
diff --git a/src/application/frida/FridaInstrumentationManager.test.ts b/src/application/frida/FridaInstrumentationManager.test.ts
index 7a527a807..d66280f66 100644
--- a/src/application/frida/FridaInstrumentationManager.test.ts
+++ b/src/application/frida/FridaInstrumentationManager.test.ts
@@ -1023,6 +1023,28 @@ it("serializes script loading against concurrent close and unloads the loaded sc
expect(bindings.device.session.isDetached()).toBe(true);
});
+it("drains startup when the provider loader initiates shutdown", async () => {
+ const bindings = new FakeDeviceManager();
+ let closing: Promise | undefined;
+ const manager = new FridaInstrumentationManager({
+ deviceManagerLoader: async () => {
+ closing = manager.closeAll();
+ return bindings;
+ },
+ });
+ const started = await manager.startSession({
+ mode: "spawn",
+ deviceId: "local",
+ program: "/tmp/fixture",
+ });
+ if (!started.ok) throw started.error;
+ expect(closing).toBeDefined();
+ await closing;
+ expect(bindings.device.session.isDetached()).toBe(true);
+ expect(bindings.device.resumed).toEqual([512]);
+ expect(manager.status(started.value.sessionId)).toBeUndefined();
+});
+
it("names the unknown session or script ID it rejects", async () => {
const { manager, started } = await startLocalAttachedSession();
if (!started.ok) throw started.error;
diff --git a/src/application/frida/FridaInstrumentationPort.ts b/src/application/frida/FridaInstrumentationPort.ts
index 39c51eb26..39fa30936 100644
--- a/src/application/frida/FridaInstrumentationPort.ts
+++ b/src/application/frida/FridaInstrumentationPort.ts
@@ -1,4 +1,8 @@
-import type { AnalysisError } from "../../domain/analysisErrorBase.js";
+import type {
+ AnalysisError,
+ AnalysisErrorOptions,
+} from "../../domain/analysisErrorBase.js";
+import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import type { OptionalProviderLoadFailure } from "../OptionalObservationProviders.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { Result } from "../../domain/result.js";
@@ -60,6 +64,15 @@ export interface FridaScriptObservation {
readonly messagesTruncated: boolean;
}
+export class FridaScriptLoadCancelledError extends AnalysisCancelledError {
+ constructor(
+ readonly script: FridaScriptObservation,
+ options?: AnalysisErrorOptions,
+ ) {
+ super("load_frida_script", options);
+ }
+}
+
export interface FridaSessionStatus extends FridaSessionObservation {
/** True only after REA has successfully resumed this spawned target. */
readonly targetResumeObserved: boolean;
@@ -100,14 +113,20 @@ export interface FridaInstrumentationPort {
startSession(
input: StartFridaSessionInput,
): Promise>;
+ /** Own cancellation and new-script cleanup before returning a final load outcome. */
loadScript(
sessionId: string,
source: FridaScriptSource,
+ signal?: AbortSignal,
): Promise>;
- resumeSession(sessionId: string): Promise>;
+ resumeSession(
+ sessionId: string,
+ signal?: AbortSignal,
+ ): Promise>;
unloadScript(
sessionId: string,
scriptId: string,
+ signal?: AbortSignal,
): Promise>;
status(sessionId: string): FridaSessionStatus | undefined;
closeSession(
diff --git a/src/application/frida/FridaInstrumentationService.test.ts b/src/application/frida/FridaInstrumentationService.test.ts
index c1251f9a5..dc25bc62b 100644
--- a/src/application/frida/FridaInstrumentationService.test.ts
+++ b/src/application/frida/FridaInstrumentationService.test.ts
@@ -3,6 +3,7 @@ import { join } from "node:path";
import { expect, it } from "vitest";
+import { FridaScriptLoadCancelledError } from "./FridaInstrumentationPort.js";
import type { FridaInstrumentationPort } from "./FridaInstrumentationPort.js";
import { FridaInstrumentationService } from "./FridaInstrumentationService.js";
import {
@@ -569,3 +570,74 @@ it("cleans up a session and returns typed cancellation after a pending start", a
if (!result.ok) expect(result.error._tag).toBe("AnalysisCancelledError");
expect(closed).toEqual(["00000000-0000-4000-8000-000000000001"]);
});
+
+it("retains a cancelled provider load's source and messages in one-shot Evidence", async () => {
+ const controller = new AbortController();
+ const script = {
+ ...createScriptObservation(),
+ messages: [{ type: "send", payload: "cancelled-load" }],
+ };
+ const service = new FridaInstrumentationService(
+ createProvider({
+ async loadScript() {
+ controller.abort(new Error("cancel during native load"));
+ return {
+ ok: false,
+ error: new FridaScriptLoadCancelledError(script, {
+ cause: controller.signal.reason,
+ }),
+ };
+ },
+ }),
+ );
+ const result = await service.instrument(
+ {
+ mode: "spawn",
+ program: "/fixture",
+ source: { sourceKind: "inline", source: "send('cancelled-load')" },
+ durationMs: 0,
+ },
+ controller.signal,
+ );
+ if (result.ok) throw new Error("Cancelled instrumentation succeeded");
+ expect(result.error.partialObservation).toMatchObject({
+ normalized_result: {
+ source_kind: "inline",
+ source_sha256: script.sourceSha256,
+ messages: script.messages,
+ instrumentation_cleanup: "released",
+ },
+ });
+});
+
+it("preserves a provider-committed load when cancellation arrives during result delivery", async () => {
+ const controller = new AbortController();
+ let unloads = 0;
+ const script = createScriptObservation();
+ const service = new FridaInstrumentationService(
+ createProvider({
+ loadScript() {
+ const completed = Promise.resolve({ ok: true as const, value: script });
+ void completed.then(() =>
+ controller.abort(new Error("late cancellation")),
+ );
+ return completed;
+ },
+ async unloadScript() {
+ unloads += 1;
+ return { ok: true, value: null };
+ },
+ }),
+ );
+ const loaded = await service.loadScript(
+ createSession().sessionId,
+ { sourceKind: "inline", source: "send('ready')" },
+ controller.signal,
+ );
+ expect(controller.signal.aborted).toBe(true);
+ if (!loaded.ok) throw loaded.error;
+ expect(loaded.value.normalized_result).toMatchObject({
+ script_id: script.scriptId,
+ });
+ expect(unloads).toBe(0);
+});
diff --git a/src/application/frida/FridaInstrumentationService.ts b/src/application/frida/FridaInstrumentationService.ts
index c359850d6..3cba20d41 100644
--- a/src/application/frida/FridaInstrumentationService.ts
+++ b/src/application/frida/FridaInstrumentationService.ts
@@ -12,6 +12,7 @@ import {
AnalysisInputError,
} from "../../domain/analysisErrorCore.js";
import { err, ok, type Result } from "../../domain/result.js";
+import { FridaScriptLoadCancelledError } from "./FridaInstrumentationPort.js";
import type {
FridaDeviceSelector,
FridaInstrumentationPort,
@@ -181,8 +182,17 @@ export class FridaInstrumentationService {
async loadScript(
sessionId: string,
source: FridaScriptSource,
+ signal?: AbortSignal,
): Promise> {
- const loaded = await this.#provider.loadScript(sessionId, source);
+ if (signal?.aborted)
+ return err(
+ new AnalysisCancelledError("load_frida_script", {
+ cause: signal.reason,
+ }),
+ );
+ const loaded = await this.#provider.loadScript(sessionId, source, signal);
+ if (!loaded.ok && loaded.error instanceof FridaScriptLoadCancelledError)
+ return err(cancelledScriptEvidence(sessionId, loaded.error));
return loaded.ok
? ok(
fridaEvidence("load_frida_script", scriptOutput(loaded.value), {
@@ -192,12 +202,12 @@ export class FridaInstrumentationService {
: loaded;
}
- resumeSession(sessionId: string) {
- return this.#provider.resumeSession(sessionId);
+ resumeSession(sessionId: string, signal?: AbortSignal) {
+ return this.#provider.resumeSession(sessionId, signal);
}
- unloadScript(sessionId: string, scriptId: string) {
- return this.#provider.unloadScript(sessionId, scriptId);
+ unloadScript(sessionId: string, scriptId: string, signal?: AbortSignal) {
+ return this.#provider.unloadScript(sessionId, scriptId, signal);
}
status(sessionId: string): FridaSessionStatus | undefined {
@@ -243,14 +253,19 @@ export class FridaInstrumentationService {
const loaded = await this.#provider.loadScript(
started.value.sessionId,
input.source,
+ signal,
);
+ let scriptObservation: FridaScriptObservation | undefined;
+ if (loaded.ok) scriptObservation = loaded.value;
+ else if (loaded.error instanceof FridaScriptLoadCancelledError)
+ scriptObservation = loaded.error.script;
if (signal?.aborted)
return this.#cancelSession(
"instrument_with_frida",
started.value,
input,
signal.reason,
- loaded.ok ? loaded.value : undefined,
+ scriptObservation,
);
if (!loaded.ok) {
const closed = await this.#provider.closeSession(started.value.sessionId);
@@ -267,6 +282,7 @@ export class FridaInstrumentationService {
if (started.value.mode === "spawn") {
const resumed = await this.#provider.resumeSession(
started.value.sessionId,
+ signal,
);
if (signal?.aborted)
return this.#cancelSession(
@@ -462,3 +478,23 @@ const sessionStatusOutput = (status: FridaSessionStatus) => ({
messages: status.messages,
messages_truncated: status.messagesTruncated,
});
+
+const cancelledScriptEvidence = (
+ sessionId: string,
+ error: FridaScriptLoadCancelledError,
+): AnalysisCancelledError =>
+ new AnalysisCancelledError(error.operation, {
+ cause: error.cause,
+ cleanup: error.cleanup,
+ partialObservation: fridaEvidence(
+ error.operation,
+ {
+ ...scriptOutput(error.script),
+ cleanup_error: error.cleanup?.reason ?? null,
+ script_cleanup: error.cleanupIncomplete
+ ? "incomplete_or_unknown"
+ : "released",
+ },
+ { session_id: sessionId },
+ ),
+ });
diff --git a/src/application/javascript/ElectronActiveObservationService.ts b/src/application/javascript/ElectronActiveObservationService.ts
index 2d3036228..faf68db80 100644
--- a/src/application/javascript/ElectronActiveObservationService.ts
+++ b/src/application/javascript/ElectronActiveObservationService.ts
@@ -1,5 +1,6 @@
import { dirname } from "node:path";
-import { realpath } from "node:fs/promises";
+import { constants } from "node:fs";
+import { access, realpath, stat } from "node:fs/promises";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type {
@@ -43,11 +44,35 @@ const resolveInputPath = async (
}
};
+/** Resolve the executable and require a launchable regular file before any spawn. */
+const resolveExecutablePath = async (
+ selected: string,
+): ReturnType => {
+ const resolved = await resolveInputPath("executable_path", selected);
+ if (!resolved.ok) return resolved;
+ try {
+ if (!(await stat(resolved.value)).isFile())
+ throw new Error("not a regular file");
+ if (process.platform !== "win32")
+ await access(resolved.value, constants.X_OK);
+ return resolved;
+ } catch (cause: unknown) {
+ return err({
+ issue: {
+ path: ["executable_path"],
+ reason: "invalid_value",
+ message: `executable_path ${selected} is not an executable file: ${cause instanceof Error ? cause.message : String(cause)}`,
+ },
+ cause,
+ });
+ }
+};
+
const canonicalizeInput = async (
input: ElectronActiveObservationInput,
): Promise> => {
const [executable, application, root] = await Promise.all([
- resolveInputPath("executable_path", input.executable_path),
+ resolveExecutablePath(input.executable_path),
resolveInputPath("application_path", input.application_path),
input.application_root === undefined
? Promise.resolve(ok(undefined))
diff --git a/src/application/javascript/ElectronBoundaryAnalysis.ts b/src/application/javascript/ElectronBoundaryAnalysis.ts
index bd090f709..f2721a083 100644
--- a/src/application/javascript/ElectronBoundaryAnalysis.ts
+++ b/src/application/javascript/ElectronBoundaryAnalysis.ts
@@ -3,7 +3,10 @@ import type { ElectronIpcFinding } from "../../domain/javascript/electronStaticA
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
-import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
+import {
+ resolveArtifactPathByContext,
+ type ResolveArtifactPathInput,
+} from "./JavaScriptArtifactPathResolution.js";
/** One IPC fact retaining its exact owning artifact for graph projection. */
export interface ElectronIpcRecord {
@@ -160,6 +163,7 @@ export const summarizeElectronBoundaries = (
specifier: finding.module_path,
context: finding.module_resolution_context,
files,
+ runtimePathKind: "node-entrypoint",
}),
).length,
native_addon_bindings: nativeBindings.length,
@@ -204,6 +208,7 @@ interface ResolveToFileInput {
readonly files: ReadonlyMap;
readonly kind?: JavaScriptArtifactFile["kind"];
readonly moduleKind?: "import" | "require";
+ readonly runtimePathKind?: ResolveArtifactPathInput["runtimePathKind"];
}
const resolvesToFile = (input: ResolveToFileInput): boolean => {
@@ -212,6 +217,9 @@ const resolvesToFile = (input: ResolveToFileInput): boolean => {
sourcePath: input.file.path,
context: input.context,
files: input.files,
+ ...(input.runtimePathKind === undefined
+ ? {}
+ : { runtimePathKind: input.runtimePathKind }),
...(input.moduleKind === undefined ? {} : { moduleKind: input.moduleKind }),
}).resolved_path;
return (
diff --git a/src/application/javascript/ElectronBoundaryGraphCallPaths.ts b/src/application/javascript/ElectronBoundaryGraphCallPaths.ts
index b815e82bf..d1f1886db 100644
--- a/src/application/javascript/ElectronBoundaryGraphCallPaths.ts
+++ b/src/application/javascript/ElectronBoundaryGraphCallPaths.ts
@@ -165,7 +165,8 @@ export const addElectronCallPaths = (
pairing_basis: "unique-exact-literal-channel",
resolution: "candidate",
handler_path: to.file.path,
- conditions: to.path.conditions,
+ bridge_conditions: from.path.conditions,
+ handler_conditions: to.path.conditions,
},
limitations: [
"The unique static channel match does not establish runtime registration, sender acceptance, or dispatch.",
diff --git a/src/application/javascript/ElectronBoundaryGraphWindows.ts b/src/application/javascript/ElectronBoundaryGraphWindows.ts
index 63f62bd12..2ffe550c6 100644
--- a/src/application/javascript/ElectronBoundaryGraphWindows.ts
+++ b/src/application/javascript/ElectronBoundaryGraphWindows.ts
@@ -23,7 +23,10 @@ import {
electronFindingSourceNode,
electronRangeKey,
} from "./ElectronBoundaryGraphContext.js";
-import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
+import {
+ artifactPathResolutionProperties,
+ resolveArtifactPathByContext,
+} from "./JavaScriptArtifactPathResolution.js";
interface FindingInput {
readonly context: JavaScriptArtifactGraphContext;
@@ -137,6 +140,7 @@ const addWindowPreload = (
sourcePath: input.file.path,
context: resolutionContext,
files: input.context.filesByPath,
+ runtimePathKind: "exact-file",
});
const role = createElectronRoleNode(input.context, {
kind: "electron-preload",
@@ -160,6 +164,7 @@ const addWindowPreload = (
resolved_path: resolution.resolved_path,
resolution_status: resolution.resolution_status,
limitations: resolution.limitations,
+ ...artifactPathResolutionProperties(resolution),
},
});
linkElectronRoleToAsset(input.context, {
@@ -253,6 +258,16 @@ const addUtilityProcess = (
},
],
});
+ const resolution =
+ value.module_path === null
+ ? undefined
+ : resolveArtifactPathByContext({
+ declaredPath: value.module_path,
+ sourcePath: file.path,
+ context: value.module_resolution_context,
+ files: context.filesByPath,
+ runtimePathKind: "node-entrypoint",
+ });
addElectronInferenceEdge(context, {
source: input.source,
target: utility,
@@ -264,15 +279,19 @@ const addUtilityProcess = (
properties: {
declared_path: value.module_path,
expression: value.module_expression,
+ ...(resolution === undefined
+ ? {}
+ : {
+ resolution_context: resolution.resolution_context,
+ resolved_path: resolution.resolved_path,
+ resolution_status: resolution.resolution_status,
+ limitations: resolution.limitations,
+ ...artifactPathResolutionProperties(resolution),
+ }),
},
});
if (value.module_path === null) return;
- const resolution = resolveArtifactPathByContext({
- declaredPath: value.module_path,
- sourcePath: file.path,
- context: value.module_resolution_context,
- files: context.filesByPath,
- });
+ if (resolution === undefined) return;
const resolved = resolution.resolved_path;
const target =
resolved === null
diff --git a/src/application/javascript/JavaScriptArtifactGraphContext.ts b/src/application/javascript/JavaScriptArtifactGraphContext.ts
index 1779bdaae..1070c8ae0 100644
--- a/src/application/javascript/JavaScriptArtifactGraphContext.ts
+++ b/src/application/javascript/JavaScriptArtifactGraphContext.ts
@@ -22,6 +22,7 @@ import {
unavailableAstEvidence,
} from "./JavaScriptArtifactGraphEvidence.js";
import {
+ artifactPathResolutionProperties,
resolveArtifactPathByContext,
type ArtifactPathResolution,
} from "./JavaScriptArtifactPathResolution.js";
@@ -273,6 +274,7 @@ export const createElectronRoleNode = (
resolved_path: input.resolution.resolved_path,
resolution_status: input.resolution.resolution_status,
limitations: input.resolution.limitations,
+ ...artifactPathResolutionProperties(input.resolution),
mechanism: input.mechanism,
},
evidence: staticInferenceEvidence({
diff --git a/src/application/javascript/JavaScriptArtifactGraphFindings.ts b/src/application/javascript/JavaScriptArtifactGraphFindings.ts
index 0020ef577..5e939d0f9 100644
--- a/src/application/javascript/JavaScriptArtifactGraphFindings.ts
+++ b/src/application/javascript/JavaScriptArtifactGraphFindings.ts
@@ -283,6 +283,14 @@ const addDiscoveredRole = (
sourcePath: input.file.path,
context: input.value.resolution_context,
files: context.filesByPath,
+ ...(input.value.role === "preload" ||
+ (input.value.role === "renderer" &&
+ input.value.resolution_context === "filesystem-expression")
+ ? { runtimePathKind: "exact-file" as const }
+ : input.value.role === "renderer" &&
+ input.value.resolution_context === "module-specifier"
+ ? { runtimePathKind: "url-resource" as const }
+ : {}),
});
const role = createElectronRoleNode(context, {
kind:
diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts
index b746d7e97..6f40767ef 100644
--- a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts
+++ b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts
@@ -1318,6 +1318,29 @@ describe("package import alias resolution", () => {
});
});
+ it("keeps package import aliases available for runtime entrypoints", () => {
+ const files = aliasFiles({
+ "package.json": JSON.stringify({
+ imports: { "#worker": "./worker.js" },
+ }),
+ "main.cjs": "",
+ "worker.js": "",
+ });
+ expect(
+ resolve({
+ declaredPath: "#worker",
+ sourcePath: "main.cjs",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "exact-file",
+ }),
+ ).toMatchObject({
+ resolution_status: "resolved",
+ resolved_path: "worker.js",
+ declared_path: "#worker",
+ });
+ });
+
it("uses a nested package imports map rather than the application root", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
@@ -2093,6 +2116,26 @@ describe("url-reference directory and empty-segment semantics", () => {
});
});
+describe("suffix-only non-runtime URL references", () => {
+ it.each(["", "?cache=1", "#worker"])(
+ "rejects URL reference %s without a path or known base document",
+ (declaredPath) => {
+ expect(
+ resolve({
+ declaredPath,
+ sourcePath: "dist/app.mjs",
+ context: "url-reference",
+ files: fileMap([file("dist/app.mjs", "root")]),
+ }),
+ ).toMatchObject({
+ declared_path: declaredPath,
+ resolution_status: "rejected",
+ resolved_path: null,
+ });
+ },
+ );
+});
+
describe("url-reference JavaScript artifact path resolution", () => {
it.each([
["C|?cache=1#entry", "dist/C|"],
@@ -2327,3 +2370,178 @@ describe("exact HTML resource resolution", () => {
});
});
});
+
+describe("exact Electron runtime entrypoint resolution", () => {
+ it("requires a protocol for Electron URL resources", () => {
+ const files = fileMap([
+ file("app/main.cjs", "root"),
+ file("app/renderer", "root"),
+ file("app/renderer.js", "root"),
+ ]);
+ expect(
+ resolve({
+ declaredPath: "./renderer",
+ sourcePath: "app/main.cjs",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "url-resource",
+ }),
+ ).toMatchObject({
+ resolution_status: "unavailable",
+ resolved_path: null,
+ limitations: [expect.stringContaining("requires a URL protocol")],
+ });
+ expect(
+ resolve({
+ declaredPath: "file:///app/renderer",
+ sourcePath: "app/main.cjs",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "url-resource",
+ }),
+ ).toMatchObject({
+ resolution_status: "resolved",
+ resolved_path: "app/renderer",
+ });
+ });
+
+ it("checks local paths exactly while preserving bare package lookup", () => {
+ const files = fileMap([
+ file("app/main.cjs", "root"),
+ file("app/worker.js", "root"),
+ file("app/worker", "root"),
+ file("app/worker#v1.js", "root"),
+ file(
+ "node_modules/exported/package.json",
+ "root",
+ '{"exports":{"./parser":"./parser.cjs"}}',
+ ),
+ file("node_modules/exported/parser.cjs", "root"),
+ file("app/fallback/index.js", "root"),
+ ]);
+ expect(
+ resolve({
+ declaredPath: "./worker.js",
+ sourcePath: "app/main.cjs",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "exact-file",
+ }),
+ ).toMatchObject({
+ resolution_status: "resolved",
+ resolved_path: "app/worker.js",
+ });
+ expect(
+ resolve({
+ declaredPath: "exported/parser",
+ sourcePath: "app/main.cjs",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "exact-file",
+ }),
+ ).toMatchObject({
+ resolution_status: "resolved",
+ resolved_path: "node_modules/exported/parser.cjs",
+ });
+ expect(
+ resolve({
+ declaredPath: "./worker#v1.js",
+ sourcePath: "app/main.cjs",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "exact-file",
+ }),
+ ).toMatchObject({
+ resolution_status: "resolved",
+ resolved_path: "app/worker#v1.js",
+ });
+ expect(
+ resolve({
+ declaredPath: "./fallback",
+ sourcePath: "app/main.cjs",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "exact-file",
+ }),
+ ).toMatchObject({
+ resolution_status: "not-found",
+ resolved_path: null,
+ });
+ });
+
+ it("accepts an exact extensionless file but not a directory index", () => {
+ const files = fileMap([
+ file("app/main.cjs", "root"),
+ file("app/preload", "root"),
+ file("app/preload.js", "root"),
+ file("app/fallback/index.js", "root"),
+ ]);
+ expect(
+ resolve({
+ declaredPath: "./preload",
+ sourcePath: "app/main.cjs",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "exact-file",
+ }),
+ ).toMatchObject({
+ resolution_status: "resolved",
+ resolved_path: "app/preload",
+ });
+ expect(
+ resolve({
+ declaredPath: "./fallback",
+ sourcePath: "app/main.cjs",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "exact-file",
+ }),
+ ).toMatchObject({
+ resolution_status: "not-found",
+ resolved_path: null,
+ });
+ });
+});
+
+it.each(["#home", "?theme=dark", ""])(
+ "does not invent a document base for runtime URL %s",
+ (declaredPath) => {
+ expect(
+ resolve({
+ declaredPath,
+ sourcePath: "main.cjs",
+ context: "url-reference",
+ runtimePathKind: "url-resource",
+ files: fileMap([file("main.cjs", "root")]),
+ }),
+ ).toMatchObject({
+ declared_path: declaredPath,
+ resolution_status: "unavailable",
+ resolved_path: null,
+ });
+ },
+);
+
+it("resolves Node runtime entrypoints without substituting emitted TypeScript sources", () => {
+ const files = fileMap([
+ file("main.ts", "root"),
+ file("worker.ts", "root"),
+ file("worker.js", "other-container"),
+ file("runtime.js", "root"),
+ file("directory/index.js", "root"),
+ ]);
+ for (const [declaredPath, status, path] of [
+ ["./worker.js", "not-found", null],
+ ["./runtime", "resolved", "runtime.js"],
+ ["./directory", "resolved", "directory/index.js"],
+ ] as const)
+ expect(
+ resolve({
+ declaredPath,
+ sourcePath: "main.ts",
+ context: "module-specifier",
+ files,
+ runtimePathKind: "node-entrypoint",
+ }),
+ ).toMatchObject({ resolution_status: status, resolved_path: path });
+});
diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.ts b/src/application/javascript/JavaScriptArtifactPathResolution.ts
index 88aa1508f..b51eeebda 100644
--- a/src/application/javascript/JavaScriptArtifactPathResolution.ts
+++ b/src/application/javascript/JavaScriptArtifactPathResolution.ts
@@ -78,6 +78,8 @@ export interface ResolveArtifactPathInput {
readonly htmlBaseHref?: string | null;
/** Import/require semantics; runtime resource paths omit this provenance. */
readonly moduleKind?: JavaScriptModuleResolutionMode;
+ /** Runtime paths retain their loader owner independently of source imports. */
+ readonly runtimePathKind?: "exact-file" | "node-entrypoint" | "url-resource";
}
/** Preserve an explicitly retained loader mode on graph relationships. */
@@ -189,6 +191,8 @@ const conditionalResolution = (
});
const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
+ if (input.runtimePathKind === "node-entrypoint")
+ return resolveNodeRuntimeEntrypoint(input);
if (input.moduleKind === "compiler-dependent") {
const imported = resolvePath({ ...input, moduleKind: "import" });
const required = resolvePath({ ...input, moduleKind: "require" });
@@ -222,6 +226,10 @@ const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
],
};
}
+ if (input.runtimePathKind === "url-resource")
+ return resolveExactUrlResource(input);
+ if (input.runtimePathKind === "exact-file" && isRuntimePathDeclaration(input))
+ return resolveRuntimeEntrypoint(input);
// Package import keys are literal lookup identities, not artifact paths.
// The selected target is admitted separately by resolveExportTarget.
if (
@@ -260,6 +268,82 @@ const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
return resolveCandidate(input, confined, new Set(), emittedSourceCandidates);
};
+const resolveExactUrlResource = (
+ input: ResolveArtifactPathInput,
+): CandidateResolution => {
+ // Electron loadURL requires a protocol. A scheme-less string is not relative
+ // to the calling script; filesystem paths belong to loadFile.
+ if (!hasScheme(input.declaredPath))
+ return unresolvedCandidate("unavailable", [
+ "Electron loadURL requires a URL protocol; relative filesystem paths belong to loadFile.",
+ ]);
+ if (!/^file:/iu.test(input.declaredPath))
+ return unresolvedCandidate("external", [
+ "Only file URLs can be mapped to selected local application artifacts.",
+ ]);
+ const urlContextCandidate = contextualCandidate(input);
+ if (typeof urlContextCandidate !== "string") return urlContextCandidate;
+ const confined = confineCandidate(urlContextCandidate);
+ if (typeof confined !== "string") return confined;
+ return (
+ resolveFileCandidates(input, [confined]) ?? {
+ resolvedPath: null,
+ status: "not-found",
+ candidatePath: confined,
+ limitations: [
+ `The exact URL resource ${confined} was not found among the selected application files.`,
+ ],
+ }
+ );
+};
+
+const isRuntimePathDeclaration = (input: ResolveArtifactPathInput): boolean =>
+ input.context === "filesystem-expression" ||
+ (input.context === "module-specifier" &&
+ (isRelativeJavaScriptModuleSpecifier(input.declaredPath) ||
+ input.declaredPath.startsWith("/") ||
+ fileUrlPath(input.declaredPath) !== undefined));
+
+const resolveNodeRuntimeEntrypoint = (
+ input: ResolveArtifactPathInput,
+): CandidateResolution => {
+ const { runtimePathKind: _runtimePathKind, ...sourceInput } = input;
+ const runtimeInput = { ...sourceInput, moduleKind: "require" as const };
+ if (!isRuntimePathDeclaration(runtimeInput)) return resolvePath(runtimeInput);
+ const candidate = contextualCandidate(runtimeInput);
+ if (typeof candidate !== "string") return candidate;
+ const confined = confineCandidate(candidate);
+ if (typeof confined !== "string") return confined;
+ // Node entrypoints use runtime file/directory probes. They do not originate
+ // in emitted TypeScript import syntax and cannot select a source substitute.
+ return resolveCandidate(runtimeInput, confined, new Set());
+};
+
+const resolveRuntimeEntrypoint = (
+ input: ResolveArtifactPathInput,
+): CandidateResolution => {
+ const rejected = rejectDeclaration(input.declaredPath);
+ if (rejected !== null) return rejected;
+ if (hasScheme(input.declaredPath) || looksExternal(input.declaredPath))
+ return unresolvedCandidate("external", [
+ "Electron runtime entrypoints must identify local paths, not external URLs.",
+ ]);
+ const candidate = confineCandidate(
+ relativeCandidate(input.sourcePath, input.declaredPath),
+ );
+ if (typeof candidate !== "string") return candidate;
+ return (
+ resolveFileCandidates(input, [candidate]) ?? {
+ resolvedPath: null,
+ status: "not-found",
+ candidatePath: candidate,
+ limitations: [
+ `The declared Electron runtime entrypoint ${candidate} was not found as an exact file among the selected application files; extension and directory fallback were not attempted.`,
+ ],
+ }
+ );
+};
+
const resolveRuntimeSourceCandidates = (
input: ResolveArtifactPathInput,
exactPath: string,
@@ -292,30 +376,53 @@ const resolveRuntimeSourceCandidates = (
return null;
};
-const rejectDeclaration = (declared: string): CandidateResolution | null => {
- if (declared.length === 0)
+const rejectDeclaration = (
+ declared: string,
+ literalPercent = false,
+ allowEmptyPath = false,
+): CandidateResolution | null => {
+ if (declared.length === 0 && !allowEmptyPath)
return unresolvedCandidate("rejected", ["The declared path is empty."]);
if (declared.includes("\0") || declared.includes("\\"))
return unresolvedCandidate("rejected", [
"NUL and backslash path syntax are not admitted for canonical artifact paths.",
]);
- if (!admitsCanonicalPathSyntax(declared))
+ if (!literalPercent && !admitsCanonicalPathSyntax(declared))
return unresolvedCandidate("rejected", [
"Encoded dot or separator bytes are rejected before artifact path resolution.",
]);
return null;
};
+/**
+ * Node's CommonJS loader never percent-decodes a request or a legacy package
+ * main, so `%2e` and `%2f` there are literal filename bytes, not encoded dots
+ * or separators. Root confinement still applies to the joined candidate.
+ */
+const declaresLiteralPercent = (input: ResolveArtifactPathInput): boolean =>
+ input.moduleKind === "require" &&
+ (input.context === "module-specifier" ||
+ input.context === "package-entrypoint");
+
const contextualCandidate = (
input: ResolveArtifactPathInput,
): string | CandidateResolution => {
const { context } = input;
+ if (context === "html-reference") return htmlCandidate(input);
// URL suffixes are evidence, not pathname bytes. Admit only the path while
// retaining the complete declaration on the resolution result.
let declared =
context === "url-reference"
? stripQueryAndFragment(input.declaredPath)
: input.declaredPath;
+ if (context === "url-reference" && declared.length === 0)
+ return input.runtimePathKind === "url-resource"
+ ? unresolvedCandidate("unavailable", [
+ "A suffix-only runtime URL requires the window's current document URL; the calling script does not establish that base.",
+ ])
+ : unresolvedCandidate("rejected", [
+ "The URL reference contains no pathname, and its base document is not established by this analysis.",
+ ]);
if (context === "module-specifier" && /^file:/iu.test(declared)) {
const fileUrl = fileUrlPath(declared);
if (fileUrl === null)
@@ -332,9 +439,8 @@ const contextualCandidate = (
"File URL authorities or invalid file URL forms cannot be mapped to artifact-local paths.",
]);
}
- const rejected = rejectDeclaration(declared);
+ const rejected = rejectDeclaration(declared, declaresLiteralPercent(input));
if (rejected !== null) return rejected;
- if (context === "html-reference") return htmlCandidate(input);
if (context === "module-specifier") {
if (hasScheme(declared))
return unresolvedCandidate("external", [
@@ -736,7 +842,8 @@ const resolveExportTarget = (
const encodedPath = target.url.pathname.slice(
PACKAGE_ARTIFACT_ROOT_URL.pathname.length,
);
- const rejected = rejectDeclaration(encodedPath);
+ // Package targets are URLs for both loaders, so their percent bytes encode.
+ const rejected = rejectDeclaration(encodedPath, false);
if (rejected !== null) return withTargetSource(rejected);
const decoded = decodeModuleUrlPath(encodedPath);
if (typeof decoded !== "string") return withTargetSource(decoded);
@@ -815,6 +922,15 @@ const htmlCandidate = (
input: ResolveArtifactPathInput,
): string | CandidateResolution => {
const declared = stripQueryAndFragment(htmlUrlText(input.declaredPath));
+ // HTML owns URL-text normalization and pathname admission. A nonempty URL
+ // reference may have no pathname; suffix bytes remain declaration evidence.
+ // HTML references are URL text, so percent bytes still encode.
+ const rejected = rejectDeclaration(
+ declared,
+ false,
+ input.declaredPath !== "",
+ );
+ if (rejected !== null) return rejected;
if (looksExternal(declared))
return unresolvedCandidate("external", [
"External HTML references are not mapped to local artifact assets.",
@@ -828,12 +944,6 @@ const htmlCandidate = (
return unresolvedCandidate("external", [
"The document base href is external, so its script reference is not a local artifact path.",
]);
- // Removing URL tabs and newlines can join an encoded dot or separator that
- // the raw declaration split, so admit the parsed URL text again.
- if (!admitsCanonicalPathSyntax(declared))
- return unresolvedCandidate("rejected", [
- "Encoded dot or separator bytes are rejected before artifact path resolution.",
- ]);
const declaredPath = percentDecodeUrlPath(declared);
if (declaredPath === null)
return unresolvedCandidate("rejected", [
diff --git a/src/application/managed/ManagedMemberComparisonService.ts b/src/application/managed/ManagedMemberComparisonService.ts
index 252feb8ca..04fae187f 100644
--- a/src/application/managed/ManagedMemberComparisonService.ts
+++ b/src/application/managed/ManagedMemberComparisonService.ts
@@ -92,7 +92,11 @@ export const compareManagedMemberPaths = async (
throw new EvidenceIntegrityError(
`Managed artifact digest changed after open: expected ${target.sha256}, observed ${snapshot.sha256} at ${target.path}`,
);
- const inspection = inspectManagedMembersBytes(snapshot.bytes, target);
+ // Member comparison never reads literals, so it does not spend the
+ // decode budget on them.
+ const inspection = inspectManagedMembersBytes(snapshot.bytes, target, {
+ userStrings: false,
+ });
const evidence = createEvidence(target, MANAGED_STATIC_PROVIDER, {
operation: "inspect_managed_members",
parameters: {},
diff --git a/src/application/reverse/ReverseEngineeringCommandPort.ts b/src/application/reverse/ReverseEngineeringCommandPort.ts
new file mode 100644
index 000000000..947af38dd
--- /dev/null
+++ b/src/application/reverse/ReverseEngineeringCommandPort.ts
@@ -0,0 +1,19 @@
+import type { EvidenceProvider } from "../../domain/evidence.js";
+import type {
+ ObjdumpInput,
+ RizinInput,
+} from "../../domain/reverseEngineering.js";
+
+/** Provider-neutral command profile consumed by the shared application workflow. */
+export interface ReverseEngineeringProviderCommand {
+ readonly executable: string;
+ readonly arguments: readonly string[];
+ readonly identity: EvidenceProvider;
+ readonly environmentOverrides?: Readonly;
+}
+
+/** Provider-specific command construction stays outside the application layer. */
+export interface ReverseEngineeringCommandPort {
+ objdump(input: ObjdumpInput): ReverseEngineeringProviderCommand;
+ rizin(input: RizinInput): ReverseEngineeringProviderCommand;
+}
diff --git a/src/application/reverse/ReverseEngineeringService.ts b/src/application/reverse/ReverseEngineeringService.ts
new file mode 100644
index 000000000..41589747b
--- /dev/null
+++ b/src/application/reverse/ReverseEngineeringService.ts
@@ -0,0 +1,948 @@
+import { createHash, randomUUID } from "node:crypto";
+import { createReadStream } from "node:fs";
+import { stat as statPath } from "node:fs/promises";
+import type { Stats } from "node:fs";
+import { resolve } from "node:path";
+
+import {
+ AnalysisAccessDeniedError,
+ AnalysisCancelledError,
+ AnalysisCapabilityUnavailableError,
+ AnalysisInputError,
+ AnalysisResourceConstraintError,
+ AnalysisTimeoutError,
+} from "../../domain/analysisErrorCore.js";
+import type {
+ AnalysisCleanupObservation,
+ AnalysisError,
+} from "../../domain/analysisErrorBase.js";
+import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
+import { createEvidence, type Evidence } from "../../domain/evidence.js";
+import type { JsonValue } from "../../domain/jsonValue.js";
+import { err, ok, type Result } from "../../domain/result.js";
+import {
+ objdumpInputSchema,
+ rizinInputSchema,
+} from "../../domain/reverseEngineering.js";
+import type {
+ ReverseEngineeringCommandPort,
+ ReverseEngineeringProviderCommand,
+} from "./ReverseEngineeringCommandPort.js";
+import {
+ execFileOutput,
+ execFileOutputFailure,
+} from "../../process/ExecFileOutput.js";
+import {
+ OwnedCommandFailure,
+ runOwnedCommand,
+} from "../../process/OwnedCommand.js";
+import type { ProviderProcessSupervisor } from "../../process/ProviderProcess.js";
+
+export type ReverseEngineeringOperation =
+ | "inspect_with_objdump"
+ | "execute_rizin_command";
+
+const DEFAULT_TIMEOUT_MS = 120_000;
+const MAX_COMBINED_OUTPUT_BYTES = 16 * 1024 * 1024;
+// Eight concurrent captures bound retained provider output to 128 MiB.
+const MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS = 8;
+
+export interface ReverseEngineeringServiceOptions {
+ readonly environment?: Readonly;
+ readonly commands: ReverseEngineeringCommandPort;
+ readonly run?: ReverseEngineeringCommandRunner;
+ readonly runOwned?: typeof runOwnedCommand;
+ readonly digest?: typeof digestFile;
+ readonly stat?: (path: string) => Promise;
+}
+
+interface ReverseEngineeringCommandRunner {
+ (
+ command: string,
+ arguments_: readonly string[],
+ options?: Parameters[2],
+ ): Promise<{
+ readonly stdout: string;
+ readonly stderr: string;
+ readonly stdoutInvalidUtf8?: boolean;
+ readonly stderrInvalidUtf8?: boolean;
+ readonly stdoutBytes?: number;
+ readonly stderrBytes?: number;
+ }>;
+}
+
+export type ReverseEngineeringServiceRuntimeOptions = Omit<
+ ReverseEngineeringServiceOptions,
+ "commands" | "environment"
+>;
+
+/** Shared application workflow for caller-selected local analysis commands. */
+export class ReverseEngineeringService {
+ readonly #environment: Readonly;
+ readonly #commands: ReverseEngineeringCommandPort;
+ readonly #run: ReverseEngineeringCommandRunner;
+ readonly #digest: typeof digestFile;
+ readonly #stat: (path: string) => Promise;
+ readonly #pendingCleanup = new Map<
+ ProviderProcessSupervisor,
+ {
+ readonly observation: AnalysisCleanupObservation;
+ readonly failures: Set;
+ readonly retryFailure?: AnalysisError;
+ }
+ >();
+ readonly #activeOperations = new Map<
+ AbortController,
+ Promise>
+ >();
+ #closing = false;
+ #closePromise: Promise | undefined;
+
+ constructor(options: ReverseEngineeringServiceOptions) {
+ this.#environment = { ...(options.environment ?? process.env) };
+ this.#commands = options.commands;
+ this.#run =
+ options.run ??
+ runOwnedCommandOutput(
+ options.runOwned ?? runOwnedCommand,
+ (owner, cleanup) =>
+ this.#pendingCleanup.set(owner, {
+ observation: cleanup,
+ failures: this.#pendingCleanup.get(owner)?.failures ?? new Set(),
+ }),
+ );
+ this.#digest = options.digest ?? digestFile;
+ this.#stat = options.stat ?? statPath;
+ }
+
+ /** Ask retained process owners to settle cleanup before admitting more work. */
+ async retryCleanup(
+ operation = "reverse_engineering_cleanup",
+ ): Promise> {
+ let failure: AnalysisError | undefined;
+ for (const [owner, retained] of this.#pendingCleanup) {
+ let stopped: Awaited>;
+ try {
+ stopped = await owner.stop();
+ } catch (cause) {
+ stopped = {
+ status: "incomplete",
+ reason: cause instanceof Error ? cause.message : String(cause),
+ };
+ }
+ if (stopped.status === "incomplete") {
+ const observation = {
+ reason: stopped.reason,
+ resources: retained.observation.resources,
+ };
+ let retryFailure = retained.retryFailure;
+ if (retryFailure === undefined) {
+ retryFailure = new AnalysisOutputError(
+ operation,
+ "A previous provider process still requires cleanup.",
+ { cleanup: observation },
+ );
+ retained.failures.add(retryFailure);
+ }
+ this.#pendingCleanup.set(owner, {
+ observation,
+ failures: retained.failures,
+ retryFailure,
+ });
+ if (failure === undefined) failure = retryFailure;
+ } else {
+ for (const previous of retained.failures) previous.resolveCleanup();
+ this.#pendingCleanup.delete(owner);
+ }
+ }
+ return failure === undefined ? ok(null) : err(failure);
+ }
+
+ /** Retry cleanup for provider process groups retained after an incomplete stop. */
+ close(): Promise {
+ this.#closing = true;
+ this.#closePromise ??= this.#closeActiveOperationsAndCleanup().catch(
+ (cause: unknown) => {
+ this.#closePromise = undefined;
+ throw cause;
+ },
+ );
+ return this.#closePromise;
+ }
+
+ async #closeActiveOperationsAndCleanup(): Promise {
+ const active = [...this.#activeOperations.entries()];
+ for (const [controller] of active) controller.abort();
+ await Promise.allSettled(active.map(([, operation]) => operation));
+
+ const cleanup = await this.retryCleanup();
+ if (!cleanup.ok) throw cleanup.error;
+ }
+
+ inspectWithObjdump(
+ rawInput: unknown,
+ options: { readonly signal?: AbortSignal } = {},
+ ): Promise> {
+ const parsed = objdumpInputSchema.safeParse(rawInput);
+ if (!parsed.success)
+ return Promise.resolve(
+ err(new AnalysisInputError("inspect_with_objdump")),
+ );
+ const input = parsed.data;
+ const command = this.#commands.objdump(input);
+ return this.#observe(
+ "inspect_with_objdump",
+ command,
+ input.operation === "architectures" ? undefined : input.path,
+ input,
+ options.signal,
+ );
+ }
+
+ executeRizinCommand(
+ rawInput: unknown,
+ options: { readonly signal?: AbortSignal } = {},
+ ): Promise> {
+ const parsed = rizinInputSchema.safeParse(rawInput);
+ if (!parsed.success)
+ return Promise.resolve(
+ err(new AnalysisInputError("execute_rizin_command")),
+ );
+ const input = parsed.data;
+ const command = this.#commands.rizin(input);
+ return this.#observe(
+ "execute_rizin_command",
+ command,
+ input.path,
+ input,
+ options.signal,
+ );
+ }
+
+ #observe(
+ operation: ReverseEngineeringOperation,
+ profile: ReverseEngineeringProviderCommand,
+ path: string | undefined,
+ parameters: Readonly>,
+ signal?: AbortSignal,
+ ): Promise> {
+ if (signal?.aborted)
+ return Promise.resolve(err(new AnalysisCancelledError(operation)));
+ if (this.#closing)
+ return Promise.resolve(
+ err(
+ new AnalysisOutputError(
+ operation,
+ "Reverse-engineering service is closed.",
+ ),
+ ),
+ );
+ if (
+ this.#activeOperations.size >=
+ MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS
+ )
+ return Promise.resolve(
+ err(
+ new AnalysisResourceConstraintError(
+ operation,
+ "transport",
+ "Concurrent reverse-engineering operation capacity reached.",
+ {
+ max_concurrent_operations:
+ MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS,
+ active_operations: this.#activeOperations.size,
+ },
+ ),
+ ),
+ );
+ const controller = new AbortController();
+ const relayAbort = (): void => controller.abort(signal?.reason);
+ if (signal instanceof AbortSignal) {
+ if (signal.aborted) relayAbort();
+ else signal.addEventListener("abort", relayAbort, { once: true });
+ }
+ const operationSignal =
+ signal === undefined || signal instanceof AbortSignal
+ ? controller.signal
+ : signal;
+ const running = this.#observeProvider(
+ operation,
+ profile,
+ path,
+ parameters,
+ operationSignal,
+ );
+ const tracked = running.finally(() => {
+ signal?.removeEventListener("abort", relayAbort);
+ this.#activeOperations.delete(controller);
+ });
+ this.#activeOperations.set(controller, tracked);
+ return tracked;
+ }
+
+ async #observeProvider(
+ operation: ReverseEngineeringOperation,
+ profile: ReverseEngineeringProviderCommand,
+ path: string | undefined,
+ parameters: Readonly>,
+ signal?: AbortSignal,
+ ): Promise> {
+ const { executable: command, arguments: args } = profile;
+ if (signal?.aborted) return err(new AnalysisCancelledError(operation));
+ const cleanup = await this.retryCleanup(operation);
+ if (!cleanup.ok) return cleanup;
+ const absolutePath = path === undefined ? undefined : resolve(path);
+ let digest: string | undefined;
+ try {
+ if (absolutePath !== undefined)
+ digest = await this.#digest(absolutePath, signal, this.#stat);
+ } catch (cause) {
+ const code = (cause as NodeJS.ErrnoException).code;
+ if (signal?.aborted)
+ return err(new AnalysisCancelledError(operation, { cause }));
+ if (code === "EACCES" || code === "EPERM") {
+ const deniedPath = absolutePath === undefined ? command : absolutePath;
+ return err(
+ new AnalysisAccessDeniedError(operation, deniedPath, code, {
+ cause,
+ }),
+ );
+ }
+ if (code === "ETIMEDOUT")
+ return err(
+ new AnalysisTimeoutError(operation, DEFAULT_TIMEOUT_MS, { cause }),
+ );
+ return err(new AnalysisInputError(operation, { cause }));
+ }
+ let stdout = "";
+ let stderr = "";
+ let stdoutBytes = 0;
+ let stderrBytes = 0;
+ let exitCode: number | string | null = 0;
+ let signalName: string | null = null;
+ let outputTruncated = false;
+ let stdoutInvalidUtf8 = false;
+ let stderrInvalidUtf8 = false;
+ let cleanupIncomplete = false;
+ let cleanupOnlyFailure = false;
+ let completionStatus: "complete" | "unknown" = "complete";
+ let failure: AnalysisError | undefined;
+ let cleanupOwner: ProviderProcessSupervisor | undefined;
+ let earlyFailure: AnalysisError | undefined;
+ try {
+ const output = await this.#run(command, args, {
+ env: {
+ ...this.#environment,
+ ...profile.environmentOverrides,
+ },
+ signal,
+ timeout: DEFAULT_TIMEOUT_MS,
+ stopSignal: "SIGTERM",
+ maxBuffer: MAX_COMBINED_OUTPUT_BYTES,
+ });
+ stdout = output.stdout;
+ stderr = output.stderr;
+ stdoutBytes = output.stdoutBytes ?? Buffer.byteLength(stdout);
+ stderrBytes = output.stderrBytes ?? Buffer.byteLength(stderr);
+ stdoutInvalidUtf8 = output.stdoutInvalidUtf8 === true;
+ stderrInvalidUtf8 = output.stderrInvalidUtf8 === true;
+ } catch (cause) {
+ cleanupOwner =
+ cause instanceof OwnedOutputFailure ? cause.cleanupOwner : undefined;
+ if (
+ cause instanceof OwnedOutputFailure &&
+ cause.capabilityError !== undefined
+ )
+ earlyFailure = new AnalysisCapabilityUnavailableError(
+ cause.capabilityError.providerId,
+ cause.capabilityError.operation,
+ cause.capabilityError.reason,
+ {
+ cause: cause.capabilityError,
+ ...(cause.capabilityError.diagnostics === undefined
+ ? {}
+ : { diagnostics: cause.capabilityError.diagnostics }),
+ ...(cause.capabilityError.capturedOutput === undefined
+ ? {}
+ : { capturedOutput: cause.capabilityError.capturedOutput }),
+ ...(cause.capabilityError.partialObservation === undefined
+ ? {}
+ : {
+ partialObservation: cause.capabilityError.partialObservation,
+ }),
+ ...(cause.capabilityError.userMessage === undefined
+ ? {}
+ : { userMessage: cause.capabilityError.userMessage }),
+ retryable: cause.capabilityError.retryable,
+ ...ownedCleanupOptions(cause),
+ },
+ );
+ const capture = execFileOutputFailure(cause);
+ const code =
+ capture?.code ?? (cause as NodeJS.ErrnoException).code ?? undefined;
+ if (earlyFailure === undefined && code === "E2BIG")
+ earlyFailure = new AnalysisResourceConstraintError(
+ operation,
+ "transport",
+ "The host rejected the provider command because its argument list exceeds the process launch limit.",
+ {
+ argument_bytes: args.reduce(
+ (bytes, argument, index) =>
+ bytes + Buffer.byteLength(argument) + (index === 0 ? 0 : 1),
+ 0,
+ ),
+ },
+ { cause, ...ownedCleanupOptions(cause) },
+ );
+ if (
+ earlyFailure === undefined &&
+ (capture === undefined ||
+ code === "ENOENT" ||
+ code === "ERR_PROVIDER_LAUNCH")
+ ) {
+ if (signal?.aborted || code === "ABORT_ERR")
+ earlyFailure = new AnalysisCancelledError(operation, {
+ cause,
+ ...ownedCleanupOptions(cause),
+ });
+ else if (code === "ETIMEDOUT")
+ earlyFailure = new AnalysisTimeoutError(
+ operation,
+ DEFAULT_TIMEOUT_MS,
+ {
+ cause,
+ ...ownedCleanupOptions(cause),
+ },
+ );
+ else if (code === "EACCES" || code === "EPERM")
+ earlyFailure = new AnalysisAccessDeniedError(
+ operation,
+ command,
+ code,
+ {
+ cause,
+ ...ownedCleanupOptions(cause),
+ },
+ );
+ else
+ earlyFailure = new AnalysisCapabilityUnavailableError(
+ operation === "inspect_with_objdump" ? "objdump" : "rizin",
+ operation,
+ `Unable to run configured command (${String(code ?? "unknown error")})`,
+ { cause, ...ownedCleanupOptions(cause) },
+ );
+ }
+ if (earlyFailure === undefined && capture !== undefined) {
+ stdout = capture.stdout;
+ stderr = capture.stderr;
+ stdoutBytes = capture.stdoutBytes ?? Buffer.byteLength(stdout);
+ stderrBytes = capture.stderrBytes ?? Buffer.byteLength(stderr);
+ exitCode = capture.code;
+ signalName = capture.signal;
+ outputTruncated = capture.outputTruncated;
+ if (capture.collectionFailure) {
+ exitCode = null;
+ completionStatus = "unknown";
+ }
+ const capturedOutput = {
+ stdout,
+ stderr,
+ truncated: outputTruncated,
+ stdout_bytes: stdoutBytes,
+ stderr_bytes: stderrBytes,
+ exit_code:
+ !capture.collectionFailure && typeof capture.code === "number"
+ ? capture.code
+ : null,
+ signal: capture.signal,
+ };
+ if (signal?.aborted || code === "ABORT_ERR")
+ failure = new AnalysisCancelledError(operation, {
+ cause,
+ capturedOutput,
+ ...ownedCleanupOptions(cause),
+ });
+ else if (code === "ETIMEDOUT")
+ failure = new AnalysisTimeoutError(operation, DEFAULT_TIMEOUT_MS, {
+ cause,
+ capturedOutput,
+ ...ownedCleanupOptions(cause),
+ });
+ else if (code === "EACCES" || code === "EPERM")
+ failure = new AnalysisAccessDeniedError(operation, command, code, {
+ cause,
+ capturedOutput,
+ ...ownedCleanupOptions(cause),
+ });
+ if (
+ cause instanceof OwnedOutputFailure &&
+ cause.cleanup !== undefined
+ ) {
+ cleanupIncomplete = true;
+ if (failure === undefined) {
+ cleanupOnlyFailure =
+ !capture.collectionFailure &&
+ !capture.outputTruncated &&
+ !capture.killed &&
+ capture.signal === null &&
+ typeof capture.code === "number" &&
+ !signal?.aborted;
+ failure = new AnalysisOutputError(
+ operation,
+ "Provider process cleanup remains incomplete.",
+ {
+ cause,
+ capturedOutput,
+ cleanup: cause.cleanup,
+ cleanupOnly: cleanupOnlyFailure,
+ },
+ );
+ }
+ }
+ if (
+ failure === undefined &&
+ cause instanceof OwnedOutputFailure &&
+ cause.collectionFailure
+ )
+ failure = new AnalysisOutputError(
+ operation,
+ "Provider output collection failed.",
+ { cause, capturedOutput },
+ );
+ if (
+ capture.killed ||
+ capture.signal !== null ||
+ typeof capture.code !== "number"
+ ) {
+ exitCode = null;
+ completionStatus = "unknown";
+ }
+ if (cause instanceof OwnedOutputFailure) {
+ stdoutInvalidUtf8 = cause.stdoutInvalidUtf8;
+ stderrInvalidUtf8 = cause.stderrInvalidUtf8;
+ }
+ }
+ }
+ if (earlyFailure !== undefined)
+ return err(this.#registerCleanupFailure(earlyFailure, cleanupOwner));
+ if (failure !== undefined && cleanupOwner !== undefined)
+ failure = this.#registerCleanupFailure(failure, cleanupOwner);
+ let digestAfter: string | null = null;
+ if (absolutePath === undefined) {
+ digestAfter = null;
+ } else if (cleanupIncomplete) {
+ if (!cleanupOnlyFailure) completionStatus = "unknown";
+ } else {
+ try {
+ digestAfter = await this.#digest(absolutePath, signal, this.#stat);
+ } catch {
+ // Preserve the provider's collected observations if the artifact vanished or became unreadable.
+ }
+ }
+ if (signal?.aborted && failure === undefined) {
+ failure = new AnalysisCancelledError(operation, {
+ capturedOutput: {
+ stdout,
+ stderr,
+ truncated: outputTruncated,
+ stdout_bytes: stdoutBytes,
+ stderr_bytes: stderrBytes,
+ exit_code: typeof exitCode === "number" ? exitCode : null,
+ signal: signalName,
+ },
+ });
+ exitCode = null;
+ completionStatus = "unknown";
+ }
+ const artifactChanged =
+ absolutePath === undefined
+ ? null
+ : digestAfter === null || digest === undefined
+ ? null
+ : digestAfter !== digest;
+ const result = {
+ stdout,
+ stderr,
+ exit_code: exitCode,
+ signal: signalName,
+ output_truncated: outputTruncated,
+ completion_status: completionStatus,
+ artifact_sha256_after: absolutePath === undefined ? null : digestAfter,
+ artifact_changed: absolutePath === undefined ? null : artifactChanged,
+ };
+ const limitations = [
+ ...(absolutePath === undefined
+ ? [
+ "The objdump version is not reported; the output lists BFD targets advertised by the installed tool and does not establish support for the selected artifact.",
+ ]
+ : [
+ "The executable version and supported formats are reported as unknown because no reliable version probe was performed.",
+ ]),
+ "A non-zero process exit is preserved as an observed command result.",
+ ...(operation === "inspect_with_objdump" &&
+ parameters.follow_debug_links === true
+ ? [
+ "DWARF output may include local debug-link files whose identities are not bound to Evidence.",
+ ]
+ : []),
+ ...(operation === "execute_rizin_command"
+ ? [
+ "Rizin commands and retained plugins may read files or access network resources outside the selected artifact; those external inputs are not bound to Evidence.",
+ ]
+ : []),
+ ...(stdoutInvalidUtf8 || stderrInvalidUtf8
+ ? [
+ `Captured ${[
+ ...(stdoutInvalidUtf8 ? ["stdout"] : []),
+ ...(stderrInvalidUtf8 ? ["stderr"] : []),
+ ].join(
+ " and ",
+ )} bytes contain invalid or incomplete UTF-8; replacement characters in the text capture do not preserve the original bytes.`,
+ ]
+ : []),
+ ...(outputTruncated
+ ? [
+ "The provider command ended before completion could be confirmed; captured output is partial.",
+ ]
+ : []),
+ ...(signalName === null
+ ? []
+ : [
+ `The provider process terminated by signal ${signalName}; captured output may be partial and command completion is unknown.`,
+ ]),
+ ...(failure === undefined ||
+ (failure instanceof AnalysisOutputError && failure.cleanupOnly)
+ ? []
+ : [
+ `Command completion is unknown because the operation failed with ${failure._tag}: ${failure.message}`,
+ ]),
+ ...(absolutePath === undefined
+ ? [
+ "This output lists BFD targets advertised by the installed objdump and is independent of the selected artifact.",
+ ]
+ : [
+ "When both are available, pre-run and post-run digests detect artifact changes across the provider call but cannot prove the exact bytes consumed during the call.",
+ ]),
+ ...(artifactChanged === true
+ ? [
+ "The provider command may have changed the artifact while analysis was running; pre-run and post-run digests are both reported.",
+ ]
+ : []),
+ ...(absolutePath !== undefined && digestAfter === null
+ ? [
+ cleanupIncomplete
+ ? "The post-run artifact digest was not captured because owned provider cleanup was incomplete at observation time; artifact changes during the command remain unquantified."
+ : signal?.aborted
+ ? "The post-run artifact digest is unknown because cancellation interrupted the digest."
+ : "The post-run artifact digest is unknown because the artifact could not be read after the provider call.",
+ ]
+ : []),
+ ];
+ const evidence = createEvidence(
+ absolutePath === undefined || digest === undefined
+ ? undefined
+ : { path: absolutePath, sha256: digest, format: "unknown" },
+ profile.identity,
+ {
+ predicateType: "rea.reverse-engineering.command-observation",
+ operation,
+ parameters: {
+ ...(absolutePath === undefined
+ ? { operation: "architectures" }
+ : parameters),
+ provider_executable: command,
+ command_arguments: [...args],
+ },
+ rawResult: result,
+ result,
+ limitations,
+ ...(absolutePath === undefined
+ ? {}
+ : {
+ locations: [
+ { kind: "artifact-path" as const, path: absolutePath },
+ ],
+ }),
+ },
+ );
+ return failure === undefined
+ ? ok(evidence)
+ : err(failure.retainPartialObservation(evidence));
+ }
+
+ #registerCleanupFailure(
+ failure: AnalysisError,
+ cleanupOwner: ProviderProcessSupervisor | undefined,
+ ): AnalysisError {
+ if (cleanupOwner !== undefined && failure.cleanupIncomplete)
+ this.#pendingCleanup.get(cleanupOwner)?.failures.add(failure);
+ return failure;
+ }
+}
+
+interface OwnedOutputFailureOptions {
+ readonly stdout: string;
+ readonly stderr: string;
+ readonly code: number | string | null;
+ readonly signal: string | null;
+ readonly outputTruncated: boolean;
+ readonly stdoutInvalidUtf8: boolean;
+ readonly stderrInvalidUtf8: boolean;
+ readonly stdoutBytes: number;
+ readonly stderrBytes: number;
+ readonly capabilityError?: AnalysisCapabilityUnavailableError;
+ readonly collectionFailure?: boolean;
+ readonly cleanup?: AnalysisCleanupObservation;
+ readonly cleanupOwner?: ProviderProcessSupervisor;
+ readonly cause?: unknown;
+}
+
+class OwnedOutputFailure extends Error {
+ readonly stdout: string;
+ readonly stderr: string;
+ readonly code: number | string | null;
+ readonly signal: string | null;
+ readonly outputTruncated: boolean;
+ readonly stdoutInvalidUtf8: boolean;
+ readonly stderrInvalidUtf8: boolean;
+ readonly stdoutBytes: number;
+ readonly stderrBytes: number;
+ readonly capabilityError: AnalysisCapabilityUnavailableError | undefined;
+ readonly collectionFailure: boolean;
+ readonly cleanup: AnalysisCleanupObservation | undefined;
+ readonly cleanupOwner: ProviderProcessSupervisor | undefined;
+
+ constructor(options: OwnedOutputFailureOptions) {
+ super("Owned provider command did not produce a complete result.", {
+ cause: options.cause,
+ });
+ this.stdout = options.stdout;
+ this.stderr = options.stderr;
+ this.code = options.code;
+ this.signal = options.signal;
+ this.outputTruncated = options.outputTruncated;
+ this.stdoutInvalidUtf8 = options.stdoutInvalidUtf8;
+ this.stderrInvalidUtf8 = options.stderrInvalidUtf8;
+ this.stdoutBytes = options.stdoutBytes;
+ this.stderrBytes = options.stderrBytes;
+ this.capabilityError = options.capabilityError;
+ this.collectionFailure = options.collectionFailure ?? false;
+ this.cleanup = options.cleanup;
+ this.cleanupOwner = options.cleanupOwner;
+ }
+}
+
+const runOwnedCommandOutput =
+ (
+ runOwned: typeof runOwnedCommand,
+ onCleanupOwner: (
+ owner: ProviderProcessSupervisor,
+ cleanup: AnalysisCleanupObservation,
+ ) => void,
+ ): ReverseEngineeringCommandRunner =>
+ async (command, args, options = {}) => {
+ const spawnEnv = { ...options.env };
+ try {
+ const snapshot = await runOwned(
+ {
+ command,
+ arguments: [...args],
+ runId: randomUUID(),
+ expectedCommand: null,
+ env: spawnEnv,
+ hostEnvironment: {},
+ sweepTokenOwnedProcesses: true,
+ },
+ {
+ timeoutMs: DEFAULT_TIMEOUT_MS,
+ diagnosticBytes: MAX_COMBINED_OUTPUT_BYTES,
+ acceptNonZeroExit: true,
+ },
+ { signal: options.signal },
+ );
+ if (snapshot.signal !== null && snapshot.signal !== undefined)
+ throw new OwnedOutputFailure({
+ stdout: snapshot.stdout.text,
+ stderr: snapshot.stderr.text,
+ code: snapshot.exitCode ?? null,
+ signal: snapshot.signal,
+ outputTruncated: snapshot.diagnosticTruncated === true,
+ stdoutInvalidUtf8: snapshot.stdout.invalidUtf8 === true,
+ stderrInvalidUtf8: snapshot.stderr.invalidUtf8 === true,
+ stdoutBytes: snapshot.stdout.bytes,
+ stderrBytes: snapshot.stderr.bytes,
+ });
+ if (snapshot.exitCode !== 0)
+ throw new OwnedOutputFailure({
+ stdout: snapshot.stdout.text,
+ stderr: snapshot.stderr.text,
+ code: snapshot.exitCode ?? null,
+ signal: null,
+ outputTruncated: snapshot.diagnosticTruncated === true,
+ stdoutInvalidUtf8: snapshot.stdout.invalidUtf8 === true,
+ stderrInvalidUtf8: snapshot.stderr.invalidUtf8 === true,
+ stdoutBytes: snapshot.stdout.bytes,
+ stderrBytes: snapshot.stderr.bytes,
+ });
+ return {
+ stdout: snapshot.stdout.text,
+ stderr: snapshot.stderr.text,
+ stdoutInvalidUtf8: snapshot.stdout.invalidUtf8 === true,
+ stderrInvalidUtf8: snapshot.stderr.invalidUtf8 === true,
+ stdoutBytes: snapshot.stdout.bytes,
+ stderrBytes: snapshot.stderr.bytes,
+ };
+ } catch (cause) {
+ if (!(cause instanceof OwnedCommandFailure)) throw cause;
+ if (cause.cleanupOwner !== undefined && cause.cleanupFailure !== null)
+ onCleanupOwner(cause.cleanupOwner, {
+ reason: cause.cleanupFailure,
+ resources: cause.resources,
+ });
+ const snapshot = cause.snapshot;
+ const launchErrorCode =
+ cause.cause instanceof Error
+ ? Reflect.get(cause.cause, "code")
+ : undefined;
+ const code =
+ cause.reason === "cancelled"
+ ? "ABORT_ERR"
+ : cause.reason === "timeout"
+ ? "ETIMEDOUT"
+ : cause.reason === "output-limit"
+ ? "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
+ : launchErrorCode === "E2BIG"
+ ? "E2BIG"
+ : snapshot === null
+ ? "ERR_PROVIDER_LAUNCH"
+ : (snapshot.exitCode ?? null);
+ throw new OwnedOutputFailure({
+ stdout: snapshot?.stdout.text ?? "",
+ stderr: snapshot?.stderr.text ?? "",
+ code,
+ signal: snapshot?.signal ?? null,
+ outputTruncated: snapshot?.diagnosticTruncated === true,
+ stdoutInvalidUtf8: snapshot?.stdout.invalidUtf8 === true,
+ stderrInvalidUtf8: snapshot?.stderr.invalidUtf8 === true,
+ stdoutBytes: snapshot?.stdout.bytes ?? 0,
+ stderrBytes: snapshot?.stderr.bytes ?? 0,
+ ...(cause.cause instanceof AnalysisCapabilityUnavailableError
+ ? { capabilityError: cause.cause }
+ : {}),
+ ...(cause.collectionFailure ? { collectionFailure: true } : {}),
+ ...(cause.cleanupFailure === null
+ ? {}
+ : {
+ cleanup: {
+ reason: cause.cleanupFailure,
+ resources: cause.resources,
+ },
+ }),
+ ...(cause.cleanupOwner === undefined
+ ? {}
+ : { cleanupOwner: cause.cleanupOwner }),
+ cause,
+ });
+ }
+ };
+
+const ownedCleanupOptions = (
+ cause: unknown,
+): { readonly cleanup?: AnalysisCleanupObservation } =>
+ cause instanceof OwnedOutputFailure && cause.cleanup !== undefined
+ ? { cleanup: cause.cleanup }
+ : {};
+
+const digestFile = async (
+ path: string,
+ signal?: AbortSignal,
+ stat: (path: string) => Promise = statPath,
+): Promise => {
+ const info = await statWithDeadline(path, signal, stat);
+ if (!info.isFile())
+ throw Object.assign(new Error("Artifact digest requires a regular file"), {
+ code: "EINVAL",
+ });
+ return new Promise((resolveDigest, rejectDigest) => {
+ const hash = createHash("sha256");
+ const stream = createReadStream(path);
+ const timer = setTimeout(
+ () =>
+ stream.destroy(
+ Object.assign(new Error("Artifact digest timed out"), {
+ code: "ETIMEDOUT",
+ }),
+ ),
+ DEFAULT_TIMEOUT_MS,
+ );
+ const abort = (): void => {
+ stream.destroy(new AnalysisCancelledError("artifact_digest"));
+ };
+ const cleanup = (): void => {
+ clearTimeout(timer);
+ signal?.removeEventListener("abort", abort);
+ };
+ if (signal?.aborted) {
+ cleanup();
+ stream.destroy();
+ rejectDigest(new AnalysisCancelledError("artifact_digest"));
+ return;
+ }
+ signal?.addEventListener("abort", abort, { once: true });
+ stream.on("data", (chunk: Buffer | string) => hash.update(chunk));
+ stream.once("error", (cause: Error) => {
+ cleanup();
+ rejectDigest(cause);
+ });
+ stream.once("end", () => {
+ cleanup();
+ resolveDigest(hash.digest("hex"));
+ });
+ });
+};
+
+const statWithDeadline = (
+ path: string,
+ signal: AbortSignal | undefined,
+ stat: (path: string) => Promise,
+): Promise => {
+ if (signal?.aborted)
+ return Promise.reject(new AnalysisCancelledError("artifact_digest"));
+ return new Promise((resolveStat, rejectStat) => {
+ let settled = false;
+ const finish = (cause?: unknown, value?: Stats): void => {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timer);
+ signal?.removeEventListener("abort", abort);
+ if (cause !== undefined) rejectStat(cause);
+ else resolveStat(value!);
+ };
+ const abort = (): void =>
+ finish(new AnalysisCancelledError("artifact_digest"));
+ const timer = setTimeout(
+ () =>
+ finish(
+ Object.assign(new Error("Artifact stat timed out"), {
+ code: "ETIMEDOUT",
+ }),
+ ),
+ DEFAULT_TIMEOUT_MS,
+ );
+ signal?.addEventListener("abort", abort, { once: true });
+ if (signal?.aborted) {
+ abort();
+ return;
+ }
+ // Promise handlers stay attached after timeout/cancellation so a delayed
+ // stat rejection is observed and cannot become an unhandled rejection.
+ try {
+ void stat(path).then(
+ (info) => finish(undefined, info),
+ (cause: unknown) => finish(cause),
+ );
+ } catch (cause) {
+ finish(cause);
+ }
+ });
+};
diff --git a/src/application/rust/RustBinaryPort.ts b/src/application/rust/RustBinaryPort.ts
new file mode 100644
index 000000000..b0956aea1
--- /dev/null
+++ b/src/application/rust/RustBinaryPort.ts
@@ -0,0 +1,19 @@
+import type {
+ ExecutionOptions,
+ ProviderIdentity,
+} from "../AnalysisProvider.js";
+import type { AnalysisError } from "../../domain/analysisErrorBase.js";
+import type {
+ InspectRustBinaryInput,
+ RustBinary,
+} from "../../domain/rust/rustBinary.js";
+import type { Result } from "../../domain/result.js";
+
+/** Replaceable offline dependency reader with no target-execution authority. */
+export interface RustBinaryPort {
+ readonly identity: ProviderIdentity;
+ inspect(
+ input: InspectRustBinaryInput,
+ options?: ExecutionOptions,
+ ): Promise>;
+}
diff --git a/src/application/rust/RustBinaryService.test.ts b/src/application/rust/RustBinaryService.test.ts
new file mode 100644
index 000000000..7fbee615d
--- /dev/null
+++ b/src/application/rust/RustBinaryService.test.ts
@@ -0,0 +1,132 @@
+import { expect, it } from "vitest";
+import {
+ AnalysisInputError,
+ AnalysisOutputError,
+} from "../../domain/analysisErrorCore.js";
+import type { RustBinary } from "../../domain/rust/rustBinary.js";
+import { ok } from "../../domain/result.js";
+import { RustBinaryService } from "./RustBinaryService.js";
+
+const identity = {
+ id: "fixture-rust",
+ name: "Source-owned Rust reader seam",
+ version: "1",
+};
+const report: RustBinary = {
+ artifact: { path: "/artifacts/program", sha256: "a".repeat(64), bytes: 128 },
+ format: "elf",
+ architecture: "amd64",
+ bits: 64,
+ byte_order: "little",
+ auditable_metadata: {
+ section_name: ".dep-v0",
+ file_offset: 16,
+ section_bytes: 8,
+ compressed_bytes: 8,
+ uncompressed_bytes: 20,
+ format_version: 1,
+ packages: [
+ {
+ name: "sample",
+ version: "0.1.0",
+ source: "local",
+ kind: null,
+ dependencies: [],
+ root: true,
+ },
+ ],
+ root_package_index: 0,
+ dependency_edges: [],
+ cyclic_dependency_indices: [],
+ },
+ rustc_commit: null,
+ limitations: [
+ "No /rustc/<40-hex>/ path was found in read-only data, so a rustc commit hash is unknown.",
+ ],
+};
+
+it("rejects a relative path and extra arguments before reading", async () => {
+ let calls = 0;
+ const service = new RustBinaryService({
+ identity,
+ inspect: () => {
+ calls += 1;
+ return Promise.resolve(ok(report));
+ },
+ });
+ const relative = await service.inspect({ path: "program" });
+ const extra = await service.inspect({
+ path: report.artifact.path,
+ detail: "complete",
+ });
+ 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 artifact and the dependency section range", async () => {
+ const result = await new RustBinaryService({
+ 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: "elf", architecture: "x86_64" },
+ normalized_result: report,
+ locations: [
+ { kind: "artifact-path", path: report.artifact.path },
+ { kind: "file-offset-range", start: 16, end: 24 },
+ ],
+ });
+ const mismatched: RustBinary = {
+ ...report,
+ artifact: { ...report.artifact, path: "/artifacts/other" },
+ };
+ const invalid = await new RustBinaryService({
+ identity,
+ inspect: () => Promise.resolve(ok(mismatched)),
+ }).inspect({ path: report.artifact.path });
+ expect(invalid.ok).toBe(false);
+ if (!invalid.ok) expect(invalid.error).toBeInstanceOf(AnalysisOutputError);
+});
+
+it("rejects a cycle claim the package dependencies do not contain", async () => {
+ if (report.auditable_metadata === null) throw new Error("expected metadata");
+ const invalidReport: RustBinary = {
+ ...report,
+ auditable_metadata: {
+ ...report.auditable_metadata,
+ cyclic_dependency_indices: [0],
+ },
+ };
+ const result = await new RustBinaryService({
+ identity,
+ inspect: () => Promise.resolve(ok(invalidReport)),
+ }).inspect({ path: report.artifact.path });
+ expect(result.ok).toBe(false);
+ if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
+});
+
+it.each([
+ { from_index: 1, to_index: 0 },
+ { from_index: 0, to_index: 1 },
+])("rejects provider edges referencing missing packages: %j", async (edge) => {
+ if (report.auditable_metadata === null) throw new Error("expected metadata");
+ const invalidReport: RustBinary = {
+ ...report,
+ auditable_metadata: {
+ ...report.auditable_metadata,
+ dependency_edges: [edge],
+ },
+ };
+ const result = await new RustBinaryService({
+ identity,
+ inspect: () => Promise.resolve(ok(invalidReport)),
+ }).inspect({ path: report.artifact.path });
+ expect(result.ok).toBe(false);
+ if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
+});
diff --git a/src/application/rust/RustBinaryService.ts b/src/application/rust/RustBinaryService.ts
new file mode 100644
index 000000000..b112b366d
--- /dev/null
+++ b/src/application/rust/RustBinaryService.ts
@@ -0,0 +1,113 @@
+import { isAbsolute } from "node:path";
+import type { ExecutionOptions } from "../AnalysisProvider.js";
+import type { RustBinaryPort } from "./RustBinaryPort.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 { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
+import {
+ inspectRustBinaryInputSchema,
+ rustBinarySchema,
+} from "../../domain/rust/rustBinary.js";
+import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
+import { err, ok, type Result } from "../../domain/result.js";
+
+const OPERATION = "inspect_rust_binary";
+
+const architectureForSubject = (architecture: string) => {
+ switch (architecture) {
+ case "386":
+ return "x86";
+ case "amd64":
+ return "x86_64";
+ default:
+ return BINARY_ARCHITECTURES.find(
+ (supported) => supported === architecture,
+ );
+ }
+};
+
+/** Shared exact input validation and artifact-bound Evidence for CLI and MCP. */
+export class RustBinaryService {
+ constructor(readonly provider: RustBinaryPort) {}
+
+ /** Inspect one explicitly selected file without acquiring a binary session. */
+ async inspect(
+ rawInput: unknown,
+ options?: ExecutionOptions,
+ ): Promise> {
+ if (options?.signal?.aborted)
+ return err(new AnalysisCancelledError(OPERATION));
+ const input = inspectRustBinaryInputSchema.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 inspected = await this.provider.inspect(input.data, options);
+ if (!inspected.ok) return inspected;
+ if (options?.signal?.aborted)
+ return err(new AnalysisCancelledError(OPERATION));
+ const report = rustBinarySchema.safeParse(inspected.value);
+ if (!report.success || report.data.artifact.path !== input.data.path)
+ return err(
+ new AnalysisOutputError(
+ OPERATION,
+ "Rust dependency reader returned malformed metadata or changed the selected artifact identity.",
+ ),
+ );
+ return ok(evidenceFor(this.provider.identity, input.data, report.data));
+ }
+}
+
+const evidenceFor = (
+ provider: RustBinaryPort["identity"],
+ parameters: { readonly path: string },
+ value: ReturnType,
+): Evidence => {
+ const architecture = architectureForSubject(value.architecture);
+ const section = value.auditable_metadata;
+ return createEvidence(
+ {
+ path: value.artifact.path,
+ sha256: value.artifact.sha256,
+ format: value.format === "macho" ? "mach-o" : value.format,
+ ...(architecture === undefined ? {} : { architecture }),
+ },
+ provider,
+ {
+ operation: OPERATION,
+ parameters,
+ result: value,
+ confidence: "observed",
+ limitations: value.limitations,
+ locations: [
+ { kind: "artifact-path", path: value.artifact.path },
+ ...(section === null
+ ? []
+ : [
+ {
+ kind: "file-offset-range" as const,
+ start: section.file_offset,
+ end: section.file_offset + section.section_bytes,
+ },
+ ]),
+ ],
+ },
+ );
+};
diff --git a/src/application/wasm/WasmArtifactService.ts b/src/application/wasm/WasmArtifactService.ts
index 25b844e54..fc68acc90 100644
--- a/src/application/wasm/WasmArtifactService.ts
+++ b/src/application/wasm/WasmArtifactService.ts
@@ -6,6 +6,7 @@ import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
+ type AnalysisInputIssue,
} from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import {
@@ -41,22 +42,29 @@ export class WasmArtifactService {
cause: input.error,
}),
);
- if (
- ![
- input.data.path,
- ...input.data.glue_paths,
- ...input.data.candidate_paths,
- ].every(isAbsolute)
- )
- return err(
- new AnalysisInputError(OPERATION, undefined, [
- {
- path: ["path"],
- reason: "invalid_format",
- message: "Expected an absolute filesystem path on this host.",
- },
- ]),
- );
+ const pathIssues = [
+ { value: input.data.path, path: ["path"] },
+ ...input.data.glue_paths.map((value, index) => ({
+ value,
+ path: ["glue_paths", index],
+ })),
+ ...input.data.candidate_paths.map((value, index) => ({
+ value,
+ path: ["candidate_paths", index],
+ })),
+ ].flatMap(({ value, path }) =>
+ isAbsolute(value)
+ ? []
+ : [
+ {
+ path,
+ reason: "invalid_format",
+ message: "Expected an absolute filesystem path on this host.",
+ },
+ ],
+ );
+ if (pathIssues.length > 0)
+ return err(new AnalysisInputError(OPERATION, undefined, pathIssues));
const inspected = await this.provider.inspect(input.data, options);
if (!inspected.ok) return inspected;
if (options?.signal?.aborted)
diff --git a/src/artifacts/ArtifactReader.ts b/src/artifacts/ArtifactReader.ts
index f072dd35c..ad40de57d 100644
--- a/src/artifacts/ArtifactReader.ts
+++ b/src/artifacts/ArtifactReader.ts
@@ -1,5 +1,8 @@
import type { Readable } from "node:stream";
-import type { ArtifactCommand } from "../domain/artifactGraph.js";
+import type {
+ ArtifactCommand,
+ ArtifactInventoryCoverage,
+} from "../domain/artifactGraph.js";
import type { ZipPackageFormat } from "../domain/zipPackageFormat.js";
import type {
AnalysisCleanupObservation,
@@ -90,10 +93,36 @@ export const sameArtifactEntry = (
entry.sourceIdentity?.mtimeNs === produced.sourceIdentity?.mtimeNs &&
entry.sourceIdentity?.ctimeNs === produced.sourceIdentity?.ctimeNs;
+/** One entry or subtree deliberately excluded from a traversal. */
+export interface ArtifactTraversalOmission {
+ readonly path: string;
+ readonly kind:
+ | "fifo"
+ | "socket"
+ | "block-device"
+ | "character-device"
+ | "unsupported"
+ | "directory-descendants";
+}
+
+/** Facts from an exhausted traversal, separate from entry content certainty. */
+export interface ArtifactReaderTraversal {
+ readonly status: ArtifactInventoryCoverage["status"];
+ readonly limitations: readonly string[];
+}
+
+/** Per-iterator observation sink; adapters report intentional omissions without shared state. */
+export interface ArtifactTraversalOptions {
+ readonly onOmission?: (omission: ArtifactTraversalOmission) => void;
+}
+
/** Read-only adapter over one directory, archive, or virtual container. */
export interface ArtifactReader {
readonly format: "directory" | ZipPackageFormat | "asar" | "file";
- entries(signal?: AbortSignal): AsyncIterable;
+ entries(
+ signal?: AbortSignal,
+ options?: ArtifactTraversalOptions,
+ ): AsyncIterable;
open(entry: ArtifactEntry, signal?: AbortSignal): Promise;
provenance(): readonly ArtifactCommand[];
close(): Promise;
diff --git a/src/artifacts/DirectoryArtifactReader.ts b/src/artifacts/DirectoryArtifactReader.ts
index 4a1d67135..399bf979b 100644
--- a/src/artifacts/DirectoryArtifactReader.ts
+++ b/src/artifacts/DirectoryArtifactReader.ts
@@ -14,12 +14,18 @@ import {
sameArtifactSourceIdentity,
type ArtifactEntry,
type ArtifactReader,
+ type ArtifactTraversalOmission,
+ type ArtifactTraversalOptions,
} from "./ArtifactReader.js";
import { hostExecutableMarking } from "./hostExecutableMarking.js";
import { OwnedDirectoryHandle } from "../filesystem/OwnedDirectoryHandle.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
+interface DirectoryTraversalOptions extends ArtifactTraversalOptions {
+ readonly descendInto?: (path: string) => boolean;
+}
+
/** Non-symlink-following directory reader. */
export class DirectoryArtifactReader implements ArtifactReader {
readonly format = "directory" as const;
@@ -35,9 +41,11 @@ export class DirectoryArtifactReader implements ArtifactReader {
/** Enumerate entries, optionally restricting descent while retaining directories. */
async *entries(
signal?: AbortSignal,
- descendInto?: (path: string) => boolean,
+ selection?: DirectoryTraversalOptions | ((path: string) => boolean),
): AsyncIterable {
const root = await this.#resolveRoot();
+ const options =
+ typeof selection === "function" ? { descendInto: selection } : selection;
const pending = [root];
while (pending.length > 0) {
abortIfNeeded(signal);
@@ -50,7 +58,7 @@ export class DirectoryArtifactReader implements ArtifactReader {
directory,
pending,
signal,
- descendInto,
+ options,
))
yield entry;
await withDirectoryIoContext("inspect directory", directory, () =>
@@ -75,7 +83,7 @@ export class DirectoryArtifactReader implements ArtifactReader {
directory: string,
pending: string[],
signal?: AbortSignal,
- descendInto?: (path: string) => boolean,
+ options?: DirectoryTraversalOptions,
): AsyncIterable {
const root = await this.#resolveRoot();
const handle = new OwnedDirectoryHandle(
@@ -103,10 +111,16 @@ export class DirectoryArtifactReader implements ArtifactReader {
);
const path = relative(root, absolute).split(sep).join("/");
const entry = artifactEntry(path, absolute, metadata);
- if (entry === undefined) continue;
+ if (entry === undefined) {
+ options?.onOmission?.({ path, kind: unsupportedEntryKind(metadata) });
+ continue;
+ }
yield entry;
- if (entry.kind === "directory" && (descendInto?.(path) ?? true))
- childDirectories.push(absolute);
+ if (entry.kind === "directory") {
+ if (options?.descendInto?.(path) ?? true)
+ childDirectories.push(absolute);
+ else options?.onOmission?.({ path, kind: "directory-descendants" });
+ }
}
} catch (cause: unknown) {
traversal = {
@@ -256,6 +270,19 @@ export class DirectoryArtifactReader implements ArtifactReader {
}
}
+const unsupportedEntryKind = (
+ metadata: BigIntStats,
+): ArtifactTraversalOmission["kind"] =>
+ metadata.isFIFO()
+ ? "fifo"
+ : metadata.isSocket()
+ ? "socket"
+ : metadata.isBlockDevice()
+ ? "block-device"
+ : metadata.isCharacterDevice()
+ ? "character-device"
+ : "unsupported";
+
const artifactEntry = (
path: string,
absolute: string,
diff --git a/src/artifacts/NativeDmgArtifactReader.ts b/src/artifacts/NativeDmgArtifactReader.ts
index 801ee4daf..93c12ece3 100644
--- a/src/artifacts/NativeDmgArtifactReader.ts
+++ b/src/artifacts/NativeDmgArtifactReader.ts
@@ -8,6 +8,7 @@ import {
ArtifactReaderFailure,
type ArtifactEntry,
type ArtifactReader,
+ type ArtifactTraversalOptions,
} from "./ArtifactReader.js";
import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js";
import {
@@ -149,13 +150,24 @@ export class NativeDmgArtifactReader implements ArtifactReader {
this.host = host ?? createSystemHost(environment);
}
- async *entries(signal?: AbortSignal): AsyncIterable {
+ async *entries(
+ signal?: AbortSignal,
+ options?: ArtifactTraversalOptions,
+ ): AsyncIterable {
await this.#ensureAttached(signal);
const directory = this.#directory;
if (directory === undefined)
throw new ArtifactReaderFailure("unavailable", "DMG is not attached");
const prefix = basename(this.path);
- const entries = directory.entries(signal)[Symbol.asyncIterator]();
+ const entries = directory
+ .entries(signal, {
+ onOmission: (omission) =>
+ options?.onOmission?.({
+ ...omission,
+ path: `${prefix}/${omission.path}`,
+ }),
+ })
+ [Symbol.asyncIterator]();
try {
for (;;) {
if (this.#closed) return;
diff --git a/src/artifacts/apple/MachoLoadCommandReader.test.ts b/src/artifacts/apple/MachoLoadCommandReader.test.ts
index 97bceb672..3dc6334d7 100644
--- a/src/artifacts/apple/MachoLoadCommandReader.test.ts
+++ b/src/artifacts/apple/MachoLoadCommandReader.test.ts
@@ -229,6 +229,27 @@ it("selects matching arm64e subtype slices when tracing a mixed FAT image", asyn
});
describe("Mach-O load command reader: decodes thin-image load commands", () => {
+ it("preserves leading U+FEFF in load-command strings", async () => {
+ const image = machoImage({
+ commands: [
+ dylibCommand(LC.ID_DYLIB, "\uFEFF@rpath/libself.dylib"),
+ dylibCommand(LC.LOAD_DYLIB, "\uFEFF@rpath/libother.dylib"),
+ rpathCommand("\uFEFF@loader_path/Frameworks"),
+ dyldEnvironmentCommand("\uFEFFDYLD_LIBRARY_PATH=/example"),
+ ],
+ });
+ expect(await read(image)).toMatchObject({
+ status: "parsed",
+ slices: [
+ {
+ install_name: "\uFEFF@rpath/libself.dylib",
+ dependencies: [{ install_name: "\uFEFF@rpath/libother.dylib" }],
+ rpaths: ["\uFEFF@loader_path/Frameworks"],
+ dyld_environment: ["\uFEFFDYLD_LIBRARY_PATH=/example"],
+ },
+ ],
+ });
+ });
it("decodes dylib loading commands of a thin image", async () => {
const image = machoImage({
fileType: FILE_TYPE.dylib,
diff --git a/src/artifacts/apple/MachoLoadCommandReader.ts b/src/artifacts/apple/MachoLoadCommandReader.ts
index 27f7ec283..55b134cf5 100644
--- a/src/artifacts/apple/MachoLoadCommandReader.ts
+++ b/src/artifacts/apple/MachoLoadCommandReader.ts
@@ -456,7 +456,8 @@ const commandString = (
`load command ${index} has an unterminated string`,
);
try {
- return new TextDecoder("utf-8", { fatal: true }).decode(
+ // lc_str contains name/path data, so a leading U+FEFF is part of its identity.
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
body.subarray(offset, end),
);
} catch {
diff --git a/src/artifacts/inventory/reader.ts b/src/artifacts/inventory/reader.ts
index f195eac27..bdff4f927 100644
--- a/src/artifacts/inventory/reader.ts
+++ b/src/artifacts/inventory/reader.ts
@@ -1,5 +1,8 @@
import { AsarArtifactReader } from "../AsarArtifactReader.js";
-import type { ArtifactReader } from "../ArtifactReader.js";
+import type {
+ ArtifactReader,
+ ArtifactReaderTraversal,
+} from "../ArtifactReader.js";
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { MachOSliceArtifactReader } from "../MachOSliceArtifactReader.js";
import { NativeDmgArtifactReader } from "../NativeDmgArtifactReader.js";
@@ -7,11 +10,10 @@ import { ZipArtifactReader } from "../ZipArtifactReader.js";
import type {
ArtifactInventoryCoverage,
ArtifactOccurrence,
- IntegrityContradiction,
} from "../../domain/artifactGraph.js";
import {
isZipPackageFormat,
- ZIP_NON_ENTRY_TAIL_LIMITATION,
+ ZIP_NON_ENTRY_LIMITATION,
} from "../../domain/zipPackageFormat.js";
import type { StableRegularFileDescriptor } from "../../filesystem/RegularFile.js";
@@ -40,42 +42,47 @@ export const createReader = (
}
};
-/** Classify source coverage and preserve its user-facing explanation once. */
-export const inventoryAssessment = (
+/** Decide traversal scope and availability once, independently of diagnostic wording. */
+export const inventoryCoverage = (
format: ArtifactOccurrence["artifact_format"],
reader: ArtifactReader | undefined,
- integrityContradictions: readonly IntegrityContradiction[],
- caseCollisionLimitation: string | undefined,
+ traversal: ArtifactReaderTraversal,
): {
- readonly coverage_status: ArtifactInventoryCoverage;
+ readonly coverage: ArtifactInventoryCoverage;
readonly limitations: string[];
} => {
- let coverageStatus: ArtifactInventoryCoverage = "complete";
- const limitations: string[] = [];
- if (reader !== undefined) {
- if (isZipPackageFormat(reader.format))
- limitations.push(ZIP_NON_ENTRY_TAIL_LIMITATION);
- } else if (format === "dmg" || format === "pkg") {
- coverageStatus = "partial";
- limitations.push(
- `${format.toUpperCase()} root hash is observed; child inventory requires a native macOS adapter.`,
- );
- } else if (format === "mach-o-universal" && process.platform !== "darwin") {
- coverageStatus = "partial";
- limitations.push(
- "Universal Mach-O slices require the native macOS lipo adapter.",
- );
- } else {
- // The graph's modeled scope is the root itself for recognized leaf formats.
- limitations.push("Artifact has no child container entries.");
- }
- if (caseCollisionLimitation !== undefined) {
- coverageStatus = "partial";
- limitations.push(caseCollisionLimitation);
- }
- if (integrityContradictions.length > 0)
- limitations.push(
- `${String(integrityContradictions.length)} integrity contradiction(s) were recorded; mismatched content is observed-untrusted.`,
- );
- return { coverage_status: coverageStatus, limitations };
+ if (reader !== undefined)
+ return {
+ coverage: { scope: "container-entries", status: traversal.status },
+ limitations: [
+ ...(isZipPackageFormat(reader.format)
+ ? [ZIP_NON_ENTRY_LIMITATION]
+ : []),
+ ...traversal.limitations,
+ ],
+ };
+ // dmg and pkg reach here only after classifyRootFormat matched their bytes.
+ if (format === "dmg" || format === "pkg")
+ return {
+ coverage: { scope: "container-entries", status: "unknown" },
+ limitations: [
+ `${format.toUpperCase()} root hash is observed; child inventory requires a native macOS adapter.`,
+ ],
+ };
+ if (format === "mach-o-universal")
+ return {
+ coverage: { scope: "container-entries", status: "unknown" },
+ limitations: [
+ "Universal Mach-O slices require the native macOS lipo adapter.",
+ ],
+ };
+ if (isZipPackageFormat(format) || format === "directory" || format === "asar")
+ return {
+ coverage: { scope: "container-entries", status: "unknown" },
+ limitations: [`${format} child inventory reader is unavailable.`],
+ };
+ return {
+ coverage: { scope: "root", status: "complete" },
+ limitations: ["Artifact has no child container entries."],
+ };
};
diff --git a/src/artifacts/inventory/scanCanonical.ts b/src/artifacts/inventory/scanCanonical.ts
index d5234bdf6..2c8679e21 100644
--- a/src/artifacts/inventory/scanCanonical.ts
+++ b/src/artifacts/inventory/scanCanonical.ts
@@ -10,6 +10,7 @@ import type { RootInventorySource } from "./classify.js";
import {
ArtifactReaderFailure,
type ArtifactReader,
+ type ArtifactReaderTraversal,
} from "../ArtifactReader.js";
import type { ArtifactResourceOwner } from "../ArtifactResourceScope.js";
import {
@@ -40,7 +41,7 @@ import {
hashStableRootArtifact,
hashStableRootArtifactHandle,
} from "./hashStableRootArtifact.js";
-import { createReader, inventoryAssessment } from "./reader.js";
+import { createReader, inventoryCoverage } from "./reader.js";
import {
scanReader,
type PendingIntegrityContradiction,
@@ -99,6 +100,7 @@ export const scanCanonicalArtifactInventoryInScope = async (
occurrences,
pendingContradictions,
caseCollisionLimitation,
+ traversal,
} = await scanReader(reader, ownedReaders, options.signal, integrity);
outcome = {
kind: "completed",
@@ -115,6 +117,7 @@ export const scanCanonicalArtifactInventoryInScope = async (
occurrences,
pendingContradictions,
caseCollisionLimitation,
+ traversal,
}),
};
} catch (cause: unknown) {
@@ -196,6 +199,7 @@ interface SnapshotBuildInput {
readonly occurrences: MutableOccurrence[];
readonly pendingContradictions: PendingIntegrityContradiction[];
readonly caseCollisionLimitation: string | undefined;
+ readonly traversal: ArtifactReaderTraversal;
}
const buildInventorySnapshot = async (
@@ -268,12 +272,7 @@ const buildInventorySnapshot = async (
orderedOccurrences,
orderedEdges,
});
- const assessment = inventoryAssessment(
- rootFormat,
- reader,
- integrityContradictions,
- input.caseCollisionLimitation,
- );
+ const traversal = inventoryCoverage(rootFormat, reader, input.traversal);
return {
manifest,
nodes: orderedNodes,
@@ -281,8 +280,14 @@ const buildInventorySnapshot = async (
edges: orderedEdges,
provenance: reader?.provenance() ?? [],
integrity_contradictions: integrityContradictions,
- coverage_status: assessment.coverage_status,
- limitations: assessment.limitations,
+ coverage: traversal.coverage,
+ coverage_status:
+ traversal.coverage.status === "complete" ? "complete" : "partial",
+ limitations: buildLimitations(
+ traversal.limitations,
+ integrityContradictions,
+ input.caseCollisionLimitation,
+ ),
};
};
@@ -395,3 +400,17 @@ const buildManifest = ({
occurrence_count: orderedOccurrences.length,
edge_count: orderedEdges.length,
});
+
+const buildLimitations = (
+ scopeLimitations: readonly string[],
+ integrityContradictions: readonly IntegrityContradiction[],
+ caseCollisionLimitation: string | undefined,
+): string[] => [
+ ...scopeLimitations,
+ ...(caseCollisionLimitation === undefined ? [] : [caseCollisionLimitation]),
+ ...(integrityContradictions.length === 0
+ ? []
+ : [
+ `${String(integrityContradictions.length)} integrity contradiction(s) were recorded; mismatched content is observed-untrusted.`,
+ ]),
+];
diff --git a/src/artifacts/inventory/scanReader.ts b/src/artifacts/inventory/scanReader.ts
index 6873b14d7..38b67d843 100644
--- a/src/artifacts/inventory/scanReader.ts
+++ b/src/artifacts/inventory/scanReader.ts
@@ -9,9 +9,12 @@ import {
ArtifactReaderFailure,
type ArtifactEntry,
type ArtifactReader,
+ type ArtifactReaderTraversal,
+ type ArtifactTraversalOmission,
} from "../ArtifactReader.js";
import { AsarArtifactReader } from "../AsarArtifactReader.js";
import type { ArtifactNode } from "../../domain/artifactGraph.js";
+import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
import {
classifyArtifactContent,
classifyArtifactBytes,
@@ -55,6 +58,7 @@ interface ScanContext {
readonly registry: ArtifactPathRegistry;
readonly expandedContainerIds: Set;
readonly ownedReaders: ArtifactReader[];
+ readonly omissions: ArtifactTraversalOmission[];
}
export const scanReader = async (
@@ -67,6 +71,7 @@ export const scanReader = async (
readonly occurrences: MutableOccurrence[];
readonly pendingContradictions: PendingIntegrityContradiction[];
readonly caseCollisionLimitation: string | undefined;
+ readonly traversal: ArtifactReaderTraversal;
}> => {
const { nodes, occurrences, pendingContradictions } = emptyScan();
if (reader === undefined)
@@ -75,6 +80,7 @@ export const scanReader = async (
occurrences,
pendingContradictions,
caseCollisionLimitation: undefined,
+ traversal: { status: "complete", limitations: [] },
};
const context: ScanContext = {
reader,
@@ -87,6 +93,7 @@ export const scanReader = async (
registry: new ArtifactPathRegistry(),
expandedContainerIds: new Set(),
ownedReaders,
+ omissions: [],
};
await visitArtifactEntries(context, reader, "");
// Archive directories may appear after their children. Resolve containment
@@ -101,9 +108,37 @@ export const scanReader = async (
occurrences,
pendingContradictions,
caseCollisionLimitation: annotateEnglishUnicodeCaseCollisions(occurrences),
+ traversal: {
+ status: context.omissions.length === 0 ? "complete" : "unknown",
+ limitations: [
+ ...new Set(
+ context.omissions.map(
+ ({ path, kind }) =>
+ `Artifact traversal omitted ${kind} at ${JSON.stringify(path)}.`,
+ ),
+ ),
+ ].sort(compareUnicodeCodePoints),
+ },
};
};
+const inventoryEntries = (
+ context: ScanContext,
+ reader: ArtifactReader,
+ prefix: string,
+): AsyncIterator =>
+ reader
+ .entries(context.signal, {
+ onOmission: (omission) =>
+ context.omissions.push({
+ ...omission,
+ path: normalizeArtifactPath(
+ prefix.length === 0 ? omission.path : `${prefix}/${omission.path}`,
+ ),
+ }),
+ })
+ [Symbol.asyncIterator]();
+
const visitArtifactEntries = async (
context: ScanContext,
currentReader: ArtifactReader,
@@ -117,7 +152,7 @@ const visitArtifactEntries = async (
{
reader: currentReader,
prefix,
- iterator: currentReader.entries(context.signal)[Symbol.asyncIterator](),
+ iterator: inventoryEntries(context, currentReader, prefix),
},
];
let failure: { readonly cause: unknown } | undefined;
@@ -191,7 +226,7 @@ const visitArtifactEntries = async (
stack.push({
reader: nested,
prefix: logicalPath,
- iterator: nested.entries(context.signal)[Symbol.asyncIterator](),
+ iterator: inventoryEntries(context, nested, logicalPath),
});
}
}
diff --git a/src/browser/CdpCaptureStorage.ts b/src/browser/CdpCaptureStorage.ts
index 1e88ca74a..9c3eae8c5 100644
--- a/src/browser/CdpCaptureStorage.ts
+++ b/src/browser/CdpCaptureStorage.ts
@@ -1,5 +1,6 @@
import type { InspectWebPageInput } from "../domain/browserObservation.js";
import type { WebPageInspection } from "../domain/browserObservationSchemas.js";
+import type { BrowserExclusionReason } from "../domain/browserCompleteness.js";
import { CdpConnection } from "./CdpConnection.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
import {
@@ -26,6 +27,7 @@ export const captureStorage = async (
limitations: string[],
): Promise<{
readonly value: WebPageInspection["storage"];
+ readonly keysExclusion: BrowserExclusionReason | undefined;
}> => {
const origin = new URL(pageUrl).origin;
const quota = recordValue(
@@ -82,7 +84,18 @@ export const captureStorage = async (
// list is an incomplete enumeration rather than an empty one.
Array.isArray(cacheField) &&
caches.length === cacheField.length;
+ const keysExclusion = !context.input.include_storage_keys
+ ? undefined
+ : local.exclusion === "provider_unavailable" ||
+ session.exclusion === "provider_unavailable" ||
+ indexedRaw === undefined ||
+ cacheRaw === undefined
+ ? "provider_unavailable"
+ : !local.complete || !session.complete || !structuredStorageComplete
+ ? "invalid_protocol_value"
+ : undefined;
return {
+ keysExclusion,
value: {
origin,
usage_bytes: numberValue(quota?.usage) ?? null,
@@ -99,7 +112,11 @@ export const captureStorage = async (
};
};
-const emptyStorageItems: CapturedStorageItems = {
+type StorageItemsCapture = CapturedStorageItems & {
+ readonly exclusion?: "provider_unavailable" | "invalid_protocol_value";
+};
+
+const emptyStorageItems: StorageItemsCapture = {
items: [],
complete: false,
};
@@ -118,7 +135,7 @@ const storageItems = async (
origin: string,
isLocalStorage: boolean,
limitations: string[],
-): Promise => {
+): Promise => {
const raw = await optionalCdpCommand(
context,
"DOMStorage.getDOMStorageItems",
@@ -127,7 +144,12 @@ const storageItems = async (
);
const entries = recordValue(raw)?.entries;
if (raw === undefined || !Array.isArray(entries))
- return { items: [], complete: false };
+ return {
+ items: [],
+ complete: false,
+ exclusion:
+ raw === undefined ? "provider_unavailable" : "invalid_protocol_value",
+ };
const items = entries.flatMap((entry) => {
if (!Array.isArray(entry)) return [];
const key = cdpStringValue(entry[0]);
diff --git a/src/browser/CdpCaptureStorageFingerprints.ts b/src/browser/CdpCaptureStorageFingerprints.ts
index 02a52b3be..ec7a06850 100644
--- a/src/browser/CdpCaptureStorageFingerprints.ts
+++ b/src/browser/CdpCaptureStorageFingerprints.ts
@@ -193,7 +193,9 @@ const addIndexedDb = async (
scope: "indexed_db_schema",
identity: databaseName,
value: stores.map(indexedDbStoreSchema),
- complete: indexedDbSchemaComplete(stores),
+ complete:
+ stores.length === database.objectStores.length &&
+ indexedDbSchemaComplete(stores),
});
for (const store of stores) {
const objectStoreName = cdpStringValue(store.name);
@@ -228,19 +230,37 @@ const indexedDbSchemaComplete = (
stores.every(
(store) =>
cdpStringValue(store.name) !== undefined &&
- stableValueComplete(store.keyPath) &&
+ indexedDbKeyPathComplete(store.keyPath) &&
typeof store.autoIncrement === "boolean" &&
Array.isArray(store.indexes) &&
recordsValue(store.indexes).length === store.indexes.length &&
recordsValue(store.indexes).every(
(index) =>
cdpStringValue(index.name) !== undefined &&
- stableValueComplete(index.keyPath) &&
+ indexedDbKeyPathComplete(index.keyPath) &&
typeof index.unique === "boolean" &&
typeof index.multiEntry === "boolean",
),
);
+const indexedDbKeyPathComplete = (value: unknown): boolean => {
+ const keyPath = recordValue(value);
+ if (keyPath === undefined) return false;
+ switch (keyPath.type) {
+ case "null":
+ return true;
+ case "string":
+ return typeof keyPath.string === "string";
+ case "array":
+ return (
+ Array.isArray(keyPath.array) &&
+ keyPath.array.every((key) => typeof key === "string")
+ );
+ default:
+ return false;
+ }
+};
+
const addIndexedDbRecords = async (
run: FingerprintRun,
input: {
@@ -474,14 +494,49 @@ const stableValueComplete = (value: unknown): boolean => {
const remoteObjectComplete = (value: unknown): boolean => {
const record = recordValue(value);
- if (record === undefined) return true;
- return (
- record.objectId === undefined &&
- record.preview === undefined &&
- record.customPreview === undefined &&
- (record.type !== "object" || record.value !== undefined) &&
- stableValueComplete(record)
- );
+ if (
+ record === undefined ||
+ record.objectId !== undefined ||
+ record.preview !== undefined ||
+ record.customPreview !== undefined ||
+ !stableValueComplete(record)
+ )
+ return false;
+ switch (record.type) {
+ case "undefined":
+ return (
+ record.value === undefined && record.unserializableValue === undefined
+ );
+ case "string":
+ case "boolean":
+ return (
+ typeof record.value === record.type &&
+ record.unserializableValue === undefined
+ );
+ case "number":
+ return record.unserializableValue === undefined
+ ? typeof record.value === "number" && Number.isFinite(record.value)
+ : record.value === undefined &&
+ typeof record.unserializableValue === "string" &&
+ ["-0", "NaN", "Infinity", "-Infinity"].includes(
+ record.unserializableValue,
+ );
+ case "bigint":
+ return (
+ record.value === undefined &&
+ typeof record.unserializableValue === "string" &&
+ /^-?\d+n$/u.test(record.unserializableValue)
+ );
+ case "object":
+ return (
+ record.unserializableValue === undefined &&
+ (record.subtype === "null"
+ ? record.value === null
+ : record.value !== null && typeof record.value === "object")
+ );
+ default:
+ return false;
+ }
};
const decodeBase64 = (value: string): Buffer | null => {
diff --git a/src/browser/CdpPageCapture.ts b/src/browser/CdpPageCapture.ts
index f49d4e186..337f99dfa 100644
--- a/src/browser/CdpPageCapture.ts
+++ b/src/browser/CdpPageCapture.ts
@@ -155,6 +155,12 @@ const captureAuthorizedPage = async (
);
if (!input.include_storage_keys)
state.events.completeness.exclude("storage_keys", "not_approved", null);
+ else if (storageCapture.keysExclusion !== undefined)
+ state.events.completeness.exclude(
+ "storage_keys",
+ storageCapture.keysExclusion,
+ null,
+ );
await report(context.progress, 3, "Normalizing browser evidence");
const completedFrameResult = await authorizedMainFrame({
connection: context.connection,
diff --git a/src/browser/PlaywrightElectronActiveProvider.ts b/src/browser/PlaywrightElectronActiveProvider.ts
index 23379524d..14a98aab4 100644
--- a/src/browser/PlaywrightElectronActiveProvider.ts
+++ b/src/browser/PlaywrightElectronActiveProvider.ts
@@ -96,13 +96,16 @@ export class PlaywrightElectronActiveProvider implements ElectronActiveObservati
await systemProcessOwnershipHost.captureBaseline?.(options.signal);
if (options.signal?.aborted)
throw new BrowserObservationError(OPERATION, "cancelled");
- application = await this.launch({
+ const launchOptions: Parameters[0] = {
executablePath: paths.executable,
cwd: paths.root,
env: safeElectronEnvironment(this.environment, runId),
args: ["-r", hookPath, paths.application, ...input.args],
timeout: Math.max(1, startupDeadline - performance.now()),
- });
+ };
+ application = await containLaunchRejections(() =>
+ this.launch(launchOptions),
+ );
const leaderPid = application.process().pid;
if (
typeof leaderPid !== "number" ||
@@ -377,6 +380,88 @@ const safeElectronEnvironment = (
["REA_PROCESS_RUN_ID", runId] as const,
]);
+const PLAYWRIGHT_INTERNAL_FRAME = /playwright-core[\\/]lib[\\/]/;
+interface LaunchRejectionScope {
+ failure?: unknown;
+}
+const activeLaunches = new Set();
+const heldLaunchRejections = new Map<
+ Promise,
+ {
+ readonly reason: Error;
+ readonly candidates: Set;
+ readonly hadExternalListener: boolean;
+ }
+>();
+
+const holdLaunchRejection = (
+ reason: unknown,
+ promise: Promise,
+): void => {
+ if (
+ !(reason instanceof Error) ||
+ !PLAYWRIGHT_INTERNAL_FRAME.test(reason.stack ?? "")
+ )
+ throw reason;
+ if (!heldLaunchRejections.has(promise))
+ heldLaunchRejections.set(promise, {
+ reason,
+ candidates: new Set(activeLaunches),
+ hadExternalListener: process
+ .listeners("unhandledRejection")
+ .some((listener) => listener !== holdLaunchRejection),
+ });
+};
+
+const forwardLaunchRejection = (
+ reason: Error,
+ hadExternalListener: boolean,
+): void => {
+ // Listeners present at the original event already observed it. When the
+ // holding listener was the only listener, restore Node's fatal behavior.
+ if (!hadExternalListener)
+ process.nextTick(() => {
+ throw reason;
+ });
+};
+
+/** Playwright leaves rejected launch promises unhandled; absorb only those that repeat this launch's failure. */
+const containLaunchRejections = async (
+ launch: () => Promise,
+): Promise => {
+ const scope: LaunchRejectionScope = {};
+ if (activeLaunches.size === 0)
+ process.on("unhandledRejection", holdLaunchRejection);
+ activeLaunches.add(scope);
+ try {
+ return await launch();
+ } catch (cause: unknown) {
+ scope.failure = cause;
+ throw cause;
+ } finally {
+ await new Promise((resolve) => setImmediate(resolve));
+ activeLaunches.delete(scope);
+ if (activeLaunches.size === 0)
+ process.off("unhandledRejection", holdLaunchRejection);
+ for (const [
+ promise,
+ { reason, candidates, hadExternalListener },
+ ] of heldLaunchRejections) {
+ if (!candidates.delete(scope)) continue;
+ if (
+ scope.failure instanceof Error &&
+ reason.message !== "" &&
+ scope.failure.message.includes(reason.message)
+ )
+ heldLaunchRejections.delete(promise);
+ else if (candidates.size === 0) {
+ heldLaunchRejections.delete(promise);
+ forwardLaunchRejection(reason, hadExternalListener);
+ }
+ }
+ }
+};
+
const providerError = (cause: unknown): AnalysisError =>
cause instanceof AnalysisError
? cause
diff --git a/src/browser/WebSourceMapFetcher.ts b/src/browser/WebSourceMapFetcher.ts
index 5c8f86ea0..5dbbe2ad1 100644
--- a/src/browser/WebSourceMapFetcher.ts
+++ b/src/browser/WebSourceMapFetcher.ts
@@ -11,6 +11,7 @@ import { isUrlLikeModuleSpecifier } from "../domain/webBundleAnalyzerAst.js";
import { analyzeParsedJavaScriptReferences } from "../domain/javascript/javascriptSemanticAnalysis.js";
import { traverseJavaScriptAst } from "../domain/javascript/javascriptSemanticTraversal.js";
import { parseJavaScriptSource } from "../domain/javascript/javascriptSourceParser.js";
+import { stringValue } from "../domain/javascript/javascriptStaticAnalysisHelpers.js";
import type {
AnalyzeWebBundleInput,
WebSourceMapItem,
@@ -1049,18 +1050,21 @@ const originalDependency = (
return node.source === null || node.source === undefined
? null
: { kind: "static_import", specifier: node.source.value };
- if (t.isImportExpression(node) && t.isStringLiteral(node.source))
- return { kind: "dynamic_import", specifier: node.source.value };
+ if (t.isImportExpression(node)) {
+ const specifier = stringValue(node.source);
+ return specifier === undefined
+ ? null
+ : { kind: "dynamic_import", specifier };
+ }
if (
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
- t.isIdentifier(node.callee, { name: "require" }) &&
- t.isStringLiteral(node.arguments[0])
- )
- return {
- kind: "require",
- specifier: node.arguments[0].value,
- requireCallee: node.callee,
- };
+ t.isIdentifier(node.callee, { name: "require" })
+ ) {
+ const specifier = stringValue(node.arguments[0]);
+ return specifier === undefined
+ ? null
+ : { kind: "require", specifier, requireCallee: node.callee };
+ }
// `import x = require("m")` is only legal at module top level and always
// refers to the host loader, so it needs no unbound-`require` check. The
// `moduleReference` is an entity name for a local alias instead, which
diff --git a/src/browser/history/QlogCapture.test.ts b/src/browser/history/QlogCapture.test.ts
index bd67d4e80..fb4e10072 100644
--- a/src/browser/history/QlogCapture.test.ts
+++ b/src/browser/history/QlogCapture.test.ts
@@ -186,6 +186,21 @@ it("excludes structural auth material and explicit normalized values without gue
expect(traces[0]?.incomplete_fact_records).toContain(2);
});
+it("omits a hidden derived total for a stream with more contributions than call arguments allow", () => {
+ const events = Array.from({ length: 130 }, (_, index) =>
+ event(
+ Array.from({ length: 1_000 }, (_, offset) =>
+ frame(index * 1_000 + offset, 1),
+ ),
+ ),
+ );
+ const { traces } = facts(capture(events), ["130000"]);
+ expect(traces[0]?.streams).toEqual([]);
+ expect(traces[0]?.incomplete_fact_records).toEqual(
+ Array.from({ length: 130 }, (_, ordinal) => ordinal),
+ );
+});
+
it("hides marked ancestor keys and safe derived sums while still validating hidden subtrees", () => {
const text = capture([event([frame(0, 3), frame(3, 4)])]);
expect(facts(text, ["7"]).traces[0]?.streams).toEqual([]);
diff --git a/src/browser/history/QlogCapture.ts b/src/browser/history/QlogCapture.ts
index 2e8a1f790..8d19fc64f 100644
--- a/src/browser/history/QlogCapture.ts
+++ b/src/browser/history/QlogCapture.ts
@@ -95,10 +95,10 @@ export const decodeQlogCapture = (
...stream.reported_final_sizes,
...stream.covered_ranges.flatMap((r) => [r.start, r.end]),
];
+ // A stream can have more contributions than call arguments allow.
if (numericValues.some(excluded))
- fact.incomplete_fact_records.push(
- ...stream.contributions.map((c) => c.record_ordinal),
- );
+ for (const contribution of stream.contributions)
+ fact.incomplete_fact_records.push(contribution.record_ordinal);
else fact.streams.push(stream);
}
fact.incomplete_fact_records = [...new Set(fact.incomplete_fact_records)];
diff --git a/src/cli.ts b/src/cli.ts
index d1bdf2912..a12d1e327 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -5,6 +5,7 @@ import { snapshotEnvironment } from "./process/snapshotEnvironment.js";
import { createDirectAnalysis } from "./composition/directAnalysis.js";
import { registerEvmCommands } from "./cli/evmCommands.js";
import { registerGoCommands } from "./cli/goCommands.js";
+import { registerRustCommands } from "./cli/rustCommands.js";
import { registerSqliteCommands } from "./cli/sqliteCommands.js";
import { registerAnalysisViewCommands } from "./cli/analysisViewCommands.js";
import { registerBinaryDiagnosticsCommands } from "./cli/binaryDiagnosticsCommands.js";
@@ -48,6 +49,8 @@ import {
streamCliCommandResults,
type CliResultOutput,
} from "./cli/cliResultOutput.js";
+import { registerReverseEngineeringCommands } from "./cli/reverseEngineeringCommands.js";
+import { registerCutterCommands } from "./cli/cutterCommands.js";
/** Describe CLI settings that are invalid before any command runs, as MCP does. */
export const cliConfigurationError = (
@@ -94,6 +97,8 @@ export const createCli = (
});
registerSetupCommands(cli, logger, environment);
+ registerReverseEngineeringCommands(cli, logger, environment);
+ registerCutterCommands(cli, logger, environment);
registerCoreAnalysisCommands(cli, logger, analysis.runDirectAnalysis);
registerUtilityCommands(cli, logger, environment, analysis);
registerWasmCommands(cli, logger, environment);
@@ -101,6 +106,7 @@ export const createCli = (
registerSignalCommands(cli, logger, environment);
registerEvmCommands(cli, logger, environment);
registerGoCommands(cli, logger);
+ registerRustCommands(cli, logger);
registerSqliteCommands(cli, logger, environment);
registerArtifactCommands(cli, logger, analysis.runProviderAnalysis);
registerManagedCommands(cli, logger, analysis.runProviderAnalysis);
diff --git a/src/cli/coreBinaryCommands.ts b/src/cli/coreBinaryCommands.ts
index 8902cf4e9..ff8a86536 100644
--- a/src/cli/coreBinaryCommands.ts
+++ b/src/cli/coreBinaryCommands.ts
@@ -45,6 +45,24 @@ const registerOverviewCommands = (
.describe("Load and update a local analysis snapshot"),
...nativeOpenOptionFields,
});
+ // `analyze` declares the JavaScript integrity policy for its JavaScript
+ // lane. Preserve whether the caller supplied it until the target lane is
+ // known, then apply the JavaScript default only in that lane. The owner
+ // field fills an omission with that default, and an `.optional()` wrapper
+ // keeps reporting it, so every binary target would look like an explicit
+ // policy. Remove the owner's default before making the CLI option optional;
+ // reapply its metadata so help and `--schema` keep describing the option.
+ const ownerIntegrityPolicy =
+ javascriptApplicationOptions.shape.integrityPolicy;
+ const defaultIntegrityPolicy = ownerIntegrityPolicy.parse(undefined);
+ const policyMetadata = ownerIntegrityPolicy.meta();
+ const omittableIntegrityPolicy = ownerIntegrityPolicy
+ .removeDefault()
+ .optional();
+ const laneIntegrityPolicy =
+ policyMetadata === undefined
+ ? omittableIntegrityPolicy
+ : omittableIntegrityPolicy.meta(policyMetadata);
cli.command(CLI_COMMANDS.analyze, {
description: "Get an overview of an app",
args: z.object({
@@ -54,7 +72,7 @@ const registerOverviewCommands = (
.describe("App, program, or analysis database path"),
}),
options: overviewOptions.extend({
- integrityPolicy: javascriptApplicationOptions.shape.integrityPolicy,
+ integrityPolicy: laneIntegrityPolicy,
sourcePath: javascriptApplicationOptions.shape.sourcePath,
}),
run: async ({ args, options }) => {
@@ -67,7 +85,8 @@ const registerOverviewCommands = (
runCliJavaScriptApplicationAnalysis(
{
input_path: resolve(args.path),
- integrity_policy: options.integrityPolicy,
+ integrity_policy:
+ options.integrityPolicy ?? defaultIntegrityPolicy,
...(options.sourcePath === undefined
? {}
: { source_paths: options.sourcePath }),
@@ -90,6 +109,19 @@ const registerOverviewCommands = (
},
],
);
+ if (options.integrityPolicy !== undefined)
+ throw new AnalysisInputError(
+ "analyze_javascript_application",
+ undefined,
+ [
+ {
+ path: ["integrity_policy"],
+ reason: "invalid_value",
+ message:
+ "An integrity policy requires the static JavaScript ASAR/directory analysis lane; binary_overview accepts no input.",
+ },
+ ],
+ );
return runDirectAnalysis(
args.path,
"binary_overview",
diff --git a/src/cli/cutterCommands.ts b/src/cli/cutterCommands.ts
new file mode 100644
index 000000000..2b8ce574b
--- /dev/null
+++ b/src/cli/cutterCommands.ts
@@ -0,0 +1,69 @@
+import { z } from "incur";
+import type { Logger } from "pino";
+
+import { CutterBridgeClient } from "../cutter/CutterBridgeClient.js";
+import { CutterBridgeService } from "../cutter/CutterBridgeService.js";
+import { CLI_COMMANDS } from "../cliCommandNames.js";
+import { logCliCommand } from "../cliLogging.js";
+import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
+import { withCommandCancellation } from "./commandCancellation.js";
+import type { CliInstance } from "./types.js";
+
+/** Register commands that control an already-running Cutter instance. */
+export const registerCutterCommands = (
+ cli: CliInstance,
+ logger: Logger,
+ environment: Readonly,
+): void => {
+ const cutter = new CutterBridgeService(new CutterBridgeClient(environment));
+
+ cli.command(CLI_COMMANDS.listCutterSessions, {
+ description:
+ "List live Cutter instances connected through the REA Python plugin bridge",
+ args: z.object({}),
+ run: () =>
+ logCliCommand(logger, CLI_COMMANDS.listCutterSessions, () =>
+ cutter.listSessions(),
+ ),
+ });
+ cli.command(CLI_COMMANDS.cutterCommand, {
+ description: "Execute one Rizin command in a selected live Cutter instance",
+ args: z.object({
+ sessionId: z
+ .string()
+ .uuid()
+ .describe("Session ID copied from list-cutter-sessions."),
+ expectedGeneration: z.coerce
+ .number()
+ .int()
+ .nonnegative()
+ .describe("Session generation returned by list-cutter-sessions."),
+ command: z
+ .string()
+ .min(1)
+ .describe("One unrestricted Rizin command to run in Cutter.")
+ .refine((value) => !/[\r\n]/u.test(value)),
+ }),
+ options: z.object({
+ json: z
+ .boolean()
+ .default(false)
+ .describe("Return command-specific JSON output when available."),
+ }),
+ run: ({ args, options }) =>
+ withCommandCancellation((signal) =>
+ logCliCommand(logger, CLI_COMMANDS.cutterCommand, async () => {
+ const result = await cutter.execute(
+ {
+ session_id: args.sessionId,
+ expected_generation: args.expectedGeneration,
+ command: args.command,
+ json: options.json,
+ },
+ { signal },
+ );
+ return result.ok ? result.value : projectAnalysisError(result.error);
+ }),
+ ),
+ });
+};
diff --git a/src/cli/managedCommands.ts b/src/cli/managedCommands.ts
index eee4eae5a..782549720 100644
--- a/src/cli/managedCommands.ts
+++ b/src/cli/managedCommands.ts
@@ -56,13 +56,13 @@ const registerInspectManagedMembers = (
): void => {
cli.command(CLI_COMMANDS.inspectManagedMembers, {
description:
- "Inspect PE/CLI metadata members, signatures, and CIL anchors without loading target code",
+ "Inspect PE/CLI metadata members, signatures, CIL anchors, and ldstr string literals without loading target code",
args: z.object({
path: z.string().describe("Managed PE executable or assembly path"),
}),
options: z.object({
type: managedMembersInputSchema.shape.type_names.describe(
- "Inspect only members declared by this exact type full name, such as System.Threading.Monitor; a nested type is named by its bare name, which selects every nested type of that name (repeatable)",
+ "Inspect only members declared by this exact type full name, and the string literals they load, such as System.Threading.Monitor; a nested type is named by its bare name, which selects every nested type of that name (repeatable)",
),
}),
run: ({ args, options }) =>
diff --git a/src/cli/reverseEngineeringCommands.ts b/src/cli/reverseEngineeringCommands.ts
new file mode 100644
index 000000000..1feb9184d
--- /dev/null
+++ b/src/cli/reverseEngineeringCommands.ts
@@ -0,0 +1,367 @@
+import { z } from "incur";
+import type { Logger } from "pino";
+
+import { createReverseEngineeringService } from "../composition/reverseEngineering.js";
+import { AnalysisError } from "../domain/analysisErrorBase.js";
+import {
+ AnalysisInputError,
+ AnalysisOutputError,
+} from "../domain/analysisErrorCore.js";
+import { evidenceSchema, type Evidence } from "../domain/evidence.js";
+import type { Result } from "../domain/result.js";
+import { err } from "../domain/result.js";
+import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
+import { logCliCommand } from "../cliLogging.js";
+import type { CliInstance } from "./types.js";
+import { withCommandCancellation } from "./commandCancellation.js";
+import { CLI_COMMANDS } from "../cliCommandNames.js";
+import type { ReverseEngineeringService } from "../application/reverse/ReverseEngineeringService.js";
+import {
+ GdbSessionManager,
+ gdbOneShotCommandMayInvalidateDetachProof,
+} from "../gdb/GdbSessionManager.js";
+import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js";
+
+const executeWithCleanup = async (
+ service: ReverseEngineeringService,
+ execute: () => Promise>,
+): Promise> => {
+ let result: Result;
+ try {
+ result = await execute();
+ } catch (cause) {
+ await service.close().catch(() => undefined);
+ throw cause;
+ }
+ const hadIncompleteCleanup = !result.ok && result.error.cleanupIncomplete;
+ try {
+ await service.close();
+ } catch (cause) {
+ if (result.ok) return err(asCleanupError(cause));
+ const cleanup = cause instanceof AnalysisError ? cause.cleanup : undefined;
+ const error = new AnalysisOutputError(
+ "reverse_engineering_cleanup",
+ "Provider cleanup remains incomplete after the command failed.",
+ {
+ cause: result.error,
+ ...(cleanup === undefined ? {} : { cleanup }),
+ ...(result.error.capturedOutput === undefined
+ ? {}
+ : { capturedOutput: result.error.capturedOutput }),
+ ...(result.error.partialObservation === undefined
+ ? {}
+ : { partialObservation: result.error.partialObservation }),
+ },
+ );
+ return err(error);
+ }
+ if (!result.ok && hadIncompleteCleanup && !result.error.cleanupIncomplete) {
+ const evidence = cleanupOnlyCompleteEvidence(result.error);
+ if (evidence !== undefined) return { ok: true, value: evidence };
+ }
+ return result;
+};
+
+const cleanupOnlyCompleteEvidence = (
+ error: AnalysisError,
+): Evidence | undefined => {
+ if (
+ !(error instanceof AnalysisOutputError) ||
+ !error.cleanupOnly ||
+ error.partialObservation === undefined
+ )
+ return undefined;
+ const parsed = evidenceSchema.safeParse(error.partialObservation);
+ if (!parsed.success) return undefined;
+ const normalized = parsed.data.normalized_result;
+ return typeof normalized === "object" &&
+ normalized !== null &&
+ !Array.isArray(normalized) &&
+ normalized.completion_status === "complete"
+ ? parsed.data
+ : undefined;
+};
+
+const asCleanupError = (cause: unknown): AnalysisError =>
+ cause instanceof AnalysisError
+ ? cause
+ : new AnalysisOutputError(
+ "reverse_engineering_cleanup",
+ "Provider process cleanup failed.",
+ { cause },
+ );
+
+/** Add one-shot reverse-engineering commands using the shared service layer. */
+export const registerReverseEngineeringCommands = (
+ cli: CliInstance,
+ logger: Logger,
+ environment: Readonly,
+ createGdbManager: (
+ environment: Readonly,
+ ) => GdbSessionManager = (providerEnvironment) =>
+ new GdbSessionManager({ environment: providerEnvironment }),
+ createService: typeof createReverseEngineeringService = createReverseEngineeringService,
+): void => {
+ const service = createService(environment);
+ cli.command(CLI_COMMANDS.inspectWithObjdump, {
+ description:
+ "Inspect a local artifact with GNU objdump and retain its raw output as Evidence",
+ args: z.object({
+ path: z
+ .string()
+ .min(1)
+ .describe("Path to the local artifact to inspect."),
+ operation: z
+ .enum([
+ "file_headers",
+ "section_headers",
+ "symbols",
+ "relocations",
+ "dwarf",
+ "disassemble",
+ "disassemble_all",
+ ])
+ .describe("GNU objdump inspection operation to run."),
+ }),
+ options: z.object({
+ followDebugLinks: z
+ .boolean()
+ .default(false)
+ .describe(
+ "Follow local DWARF debug links. External debug-file identity is not bound to Evidence.",
+ ),
+ }),
+ run: ({ args, options }) =>
+ withCommandCancellation((signal) =>
+ logCliCommand(logger, CLI_COMMANDS.inspectWithObjdump, async () => {
+ const result = await executeWithCleanup(service, () =>
+ service.inspectWithObjdump(
+ {
+ ...args,
+ follow_debug_links: options.followDebugLinks,
+ },
+ { signal },
+ ),
+ );
+ return result.ok ? result.value : projectAnalysisError(result.error);
+ }),
+ ),
+ });
+ cli.command(CLI_COMMANDS.listObjdumpArchitectures, {
+ description: "List BFD targets advertised by the installed GNU objdump",
+ args: z.object({}),
+ run: () =>
+ withCommandCancellation((signal) =>
+ logCliCommand(
+ logger,
+ CLI_COMMANDS.listObjdumpArchitectures,
+ async () => {
+ const result = await executeWithCleanup(service, () =>
+ service.inspectWithObjdump(
+ { operation: "architectures" },
+ { signal },
+ ),
+ );
+ return result.ok
+ ? result.value
+ : projectAnalysisError(result.error);
+ },
+ ),
+ ),
+ });
+ cli.command(CLI_COMMANDS.executeRizinCommand, {
+ description: "Execute one Rizin command against a local artifact",
+ args: z.object({
+ path: z
+ .string()
+ .min(1)
+ .describe("Path to the local artifact to analyze."),
+ command: z.string().min(1).describe("One caller-selected Rizin command."),
+ }),
+ run: ({ args }) =>
+ withCommandCancellation((signal) =>
+ logCliCommand(logger, CLI_COMMANDS.executeRizinCommand, async () => {
+ const result = await executeWithCleanup(service, () =>
+ service.executeRizinCommand(args, { signal }),
+ );
+ return result.ok ? result.value : projectAnalysisError(result.error);
+ }),
+ ),
+ });
+ cli.command(CLI_COMMANDS.debugWithGdb, {
+ description:
+ "Run one unrestricted GDB console command in an ephemeral MI session; provide an inferior TTY so a live target can be safely detached",
+ args: z.object({
+ command: z
+ .string()
+ .min(1)
+ .describe("One unrestricted GDB console command."),
+ }),
+ options: z.object({
+ inferiorTty: z
+ .string()
+ .min(1)
+ .describe(
+ "Caller-selected durable TTY for output from a live GDB inferior.",
+ ),
+ }),
+ run: ({ args, options }) =>
+ withCommandCancellation((signal) =>
+ logCliCommand(logger, CLI_COMMANDS.debugWithGdb, async () => {
+ if (gdbOneShotCommandMayInvalidateDetachProof(args.command))
+ return projectAnalysisError(
+ new AnalysisInputError("gdb_console", undefined, [
+ {
+ path: ["command"],
+ reason: "invalid_value",
+ message:
+ "This command can invalidate the verified inferior output route; use the persistent GDB session workflow.",
+ },
+ ]),
+ );
+ const manager = createGdbManager(environment);
+ let sessionId: string | undefined;
+ try {
+ const started = await manager.start(signal, {
+ inferiorTty: options.inferiorTty,
+ });
+ if (!started.ok) return projectAnalysisError(started.error);
+ sessionId = started.value.session_id;
+ const executed = await manager.execute(
+ sessionId,
+ args.command,
+ signal,
+ );
+ return executed.ok
+ ? executed.value.evidence
+ : projectAnalysisError(executed.error);
+ } finally {
+ let cleanupFailed = false;
+ if (sessionId !== undefined) {
+ try {
+ const closed = await manager.close(sessionId);
+ cleanupFailed = !closed.ok;
+ if (!closed.ok)
+ logger.error(
+ {
+ command: CLI_COMMANDS.debugWithGdb,
+ cleanup: projectAnalysisError(closed.error),
+ },
+ "GDB cleanup failed after collecting command output",
+ );
+ } catch (cause) {
+ cleanupFailed = true;
+ logger.error(
+ {
+ command: CLI_COMMANDS.debugWithGdb,
+ cleanup:
+ cause instanceof Error
+ ? cause.message
+ : "Unknown cleanup failure",
+ },
+ "GDB cleanup failed after collecting command output",
+ );
+ }
+ }
+ try {
+ await manager.closeAll();
+ cleanupFailed = false;
+ } catch (cause) {
+ cleanupFailed = true;
+ logger.error(
+ {
+ command: CLI_COMMANDS.debugWithGdb,
+ cleanup:
+ cause instanceof Error
+ ? cause.message
+ : "Unknown cleanup failure",
+ },
+ "GDB fallback cleanup failed",
+ );
+ }
+ if (cleanupFailed) process.exitCode = 1;
+ }
+ }),
+ ),
+ });
+ cli.command(CLI_COMMANDS.debugWithRizin, {
+ description:
+ "Run one unrestricted Rizin debugger command in an ephemeral session",
+ args: z.object({
+ path: z.string().min(1).describe("Path to the local artifact to debug."),
+ command: z
+ .string()
+ .min(1)
+ .describe("One unrestricted Rizin debugger command.")
+ .refine((value) => !/[\r\n]/u.test(value)),
+ }),
+ options: z.object({
+ inferiorTty: z
+ .string()
+ .min(1)
+ .describe("Caller-selected Linux TTY for inferior stdout and stderr."),
+ backend: z
+ .string()
+ .min(1)
+ .optional()
+ .describe("Optional Rizin IO debugger backend name."),
+ }),
+ run: ({ args, options }) =>
+ withCommandCancellation((signal) =>
+ logCliCommand(logger, CLI_COMMANDS.debugWithRizin, async () => {
+ const manager = new RizinDebugSessionManager(environment);
+ let sessionId: string | undefined;
+ try {
+ const started = await manager.start(
+ {
+ ...args,
+ inferior_tty: options.inferiorTty,
+ ...(options.backend === undefined
+ ? {}
+ : { backend: options.backend }),
+ },
+ signal,
+ );
+ if (!started.ok) return projectAnalysisError(started.error);
+ sessionId = started.value.session_id;
+ const executed = await manager.execute(
+ started.value.session_id,
+ args.command,
+ signal,
+ );
+ return executed.ok
+ ? executed.value.evidence
+ : projectAnalysisError(executed.error);
+ } finally {
+ const closed =
+ sessionId === undefined
+ ? undefined
+ : await manager.close(sessionId);
+ if (closed !== undefined && !closed.ok) {
+ logger.error(
+ {
+ command: CLI_COMMANDS.debugWithRizin,
+ cleanup: projectAnalysisError(closed.error),
+ },
+ "Rizin cleanup failed after collecting command output",
+ );
+ process.exitCode = 1;
+ }
+ await manager.closeAll().catch((cause: unknown) => {
+ process.exitCode = 1;
+ logger.error(
+ {
+ command: CLI_COMMANDS.debugWithRizin,
+ cleanup:
+ cause instanceof Error
+ ? cause.message
+ : "Unknown cleanup failure",
+ },
+ "Rizin fallback cleanup failed",
+ );
+ });
+ }
+ }),
+ ),
+ });
+};
diff --git a/src/cli/rustCommands.ts b/src/cli/rustCommands.ts
new file mode 100644
index 000000000..fc3f8ee85
--- /dev/null
+++ b/src/cli/rustCommands.ts
@@ -0,0 +1,34 @@
+import { z } from "incur";
+import { resolve } from "node:path";
+import { createRustBinaryService } from "../composition/rust.js";
+import { CLI_COMMANDS } from "../cliCommandNames.js";
+import { logCliCommand } from "../cliLogging.js";
+import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
+import type { Logger } from "pino";
+import type { CliInstance } from "./types.js";
+import { withCommandCancellation } from "./commandCancellation.js";
+
+/** Resolve caller paths and inspect embedded Rust dependencies through the shared workflow. */
+export const registerRustCommands = (
+ cli: CliInstance,
+ logger: Logger,
+): void => {
+ const service = createRustBinaryService();
+ cli.command(CLI_COMMANDS.inspectRustBinary, {
+ description:
+ "Inspect an embedded cargo-auditable Rust dependency list without target execution",
+ args: z.object({
+ path: z.string().describe("Local ELF, PE or thin Mach-O image"),
+ }),
+ run: ({ args }) =>
+ withCommandCancellation((signal) =>
+ logCliCommand(logger, CLI_COMMANDS.inspectRustBinary, async () => {
+ const result = await service.inspect(
+ { path: resolve(args.path) },
+ { signal },
+ );
+ return result.ok ? result.value : projectAnalysisError(result.error);
+ }),
+ ),
+ });
+};
diff --git a/src/cliCommandNames.ts b/src/cliCommandNames.ts
index ff208c62e..798451158 100644
--- a/src/cliCommandNames.ts
+++ b/src/cliCommandNames.ts
@@ -49,7 +49,16 @@ export const CLI_COMMANDS = Object.freeze({
inspectManagedArtifact: "inspect-managed-artifact",
inspectRecordedCrash: "inspect-recorded-crash",
inspectBinaryLayout: "inspect-binary-layout",
+ listCutterSessions: "list-cutter-sessions",
+ cutterCommand: "cutter-command",
+ inspectWithObjdump: "inspect-with-objdump",
+ listObjdumpArchitectures: "list-objdump-architectures",
+ executeRizinCommand: "execute-rizin-command",
+ debugWithGdb: "debug-with-gdb",
+ debugWithRizin: "debug-with-rizin",
+
inspectGoBinary: "inspect-go-binary",
+ inspectRustBinary: "inspect-rust-binary",
inspectPeResources: "inspect-pe-resources",
inspectAnalysisView: "inspect-analysis-view",
listFridaDevices: "list-frida-devices",
@@ -163,6 +172,13 @@ export const CLI_COMMAND_TOOL_ALIASES = Object.freeze({
search: ["search_strings", "search_procedures"],
"capture-process": ["capture_process_scenario"],
"compare-bundles": ["compare_bundles"],
+ "list-cutter-sessions": ["list_cutter_sessions"],
+ "cutter-command": ["cutter_command"],
+ "inspect-with-objdump": ["inspect_with_objdump"],
+ "list-objdump-architectures": ["inspect_with_objdump"],
+ "execute-rizin-command": ["execute_rizin_command"],
+ "debug-with-gdb": ["gdb_console"],
+ "debug-with-rizin": ["rizin_debug_command"],
"list-frida-devices": ["list_frida_devices"],
"list-frida-processes": ["list_frida_processes"],
"instrument-with-frida": ["instrument_with_frida"],
@@ -223,6 +239,12 @@ export const MCP_TOOLS_WITHOUT_DEDICATED_CLI = Object.freeze([
"get_evidence_bundle",
"get_navigation_context",
"inspect_address_context",
+ "start_gdb_session",
+ "gdb_session_status",
+ "close_gdb_session",
+ "start_rizin_debug_session",
+ "rizin_debug_session_status",
+ "close_rizin_debug_session",
"start_frida_session",
"load_frida_script",
"resume_frida_session",
diff --git a/src/composition/resolveWindowsProviderExecutable.ts b/src/composition/resolveWindowsProviderExecutable.ts
new file mode 100644
index 000000000..da4079056
--- /dev/null
+++ b/src/composition/resolveWindowsProviderExecutable.ts
@@ -0,0 +1,77 @@
+import { existsSync, statSync } from "node:fs";
+import { win32 } from "node:path";
+
+export interface WindowsExecutableLookup {
+ /** Windows-formatted working directory; defaults to the process CWD. */
+ readonly cwd?: string;
+ /** Test seam for Windows paths when running on a non-Windows host. */
+ readonly isFile?: (path: string) => boolean;
+}
+
+const splitWindowsPath = (value: string): readonly string[] => {
+ const entries: string[] = [];
+ let start = 0;
+ let quoted = false;
+ for (let index = 0; index < value.length; index += 1) {
+ if (value[index] === '"') quoted = !quoted;
+ else if (value[index] === ";" && !quoted) {
+ entries.push(value.slice(start, index));
+ start = index + 1;
+ }
+ }
+ entries.push(value.slice(start));
+ return entries.flatMap((entry) => {
+ if (entry.startsWith('"') && entry.endsWith('"') && entry.length >= 2)
+ return [entry.slice(1, -1)];
+ return entry.includes('"') ? [] : [entry];
+ });
+};
+
+const isRegularFile = (path: string): boolean => {
+ try {
+ return existsSync(path) && statSync(path).isFile();
+ } catch {
+ return false;
+ }
+};
+
+/** Resolve Windows bare command names against the captured child environment. */
+export const resolveWindowsProviderExecutable = (
+ executable: string,
+ environment: Readonly,
+ lookup: WindowsExecutableLookup = {},
+): string => {
+ const cwd = lookup.cwd ?? process.cwd();
+ const absolute = (...paths: string[]): string => win32.resolve(cwd, ...paths);
+ if (
+ win32.isAbsolute(executable) ||
+ executable.includes("/") ||
+ executable.includes("\\") ||
+ win32.parse(executable).root !== ""
+ )
+ return absolute(executable);
+
+ const pathValue = environment.PATH ?? environment.Path ?? environment.path;
+ if (pathValue === undefined) return executable;
+ const pathEntries = splitWindowsPath(pathValue);
+ const explicitExtension = win32.extname(executable).toLowerCase();
+ if (explicitExtension !== "" && ![".com", ".exe"].includes(explicitExtension))
+ return executable;
+ const extensions =
+ explicitExtension !== ""
+ ? [""]
+ : (environment.PATHEXT ?? environment.PathExt ?? ".COM;.EXE")
+ .split(";")
+ .filter((extension) =>
+ [".com", ".exe"].includes(extension.toLowerCase()),
+ );
+
+ for (const entry of pathEntries) {
+ if (entry.length === 0) continue;
+ for (const extension of extensions) {
+ const candidate = absolute(entry, `${executable}${extension}`);
+ if ((lookup.isFile ?? isRegularFile)(candidate)) return candidate;
+ }
+ }
+ return executable;
+};
diff --git a/src/composition/reverseEngineering.ts b/src/composition/reverseEngineering.ts
new file mode 100644
index 000000000..493e86b9c
--- /dev/null
+++ b/src/composition/reverseEngineering.ts
@@ -0,0 +1,59 @@
+import type { ReverseEngineeringServiceRuntimeOptions } from "../application/reverse/ReverseEngineeringService.js";
+import type { ReverseEngineeringCommandPort } from "../application/reverse/ReverseEngineeringCommandPort.js";
+import { ReverseEngineeringService } from "../application/reverse/ReverseEngineeringService.js";
+import {
+ objdumpCommand,
+ OBJDUMP_PROVIDER_IDENTITY,
+} from "../objdump/ObjdumpCommand.js";
+import {
+ rizinCommand,
+ RIZIN_PROVIDER_IDENTITY,
+} from "../rizin/RizinCommand.js";
+import { resolveWindowsProviderExecutable } from "./resolveWindowsProviderExecutable.js";
+
+export const createReverseEngineeringCommandPort = (
+ environment: Readonly,
+ platform: NodeJS.Platform = process.platform,
+): ReverseEngineeringCommandPort => ({
+ objdump: (input) => ({
+ executable: providerExecutable(
+ environment.REA_OBJDUMP_COMMAND ?? "objdump",
+ environment,
+ platform,
+ ),
+ arguments: objdumpCommand(input),
+ identity: OBJDUMP_PROVIDER_IDENTITY,
+ environmentOverrides: { DEBUGINFOD_URLS: "" },
+ }),
+ rizin: (input) => ({
+ executable: providerExecutable(
+ environment.REA_RIZIN_COMMAND ?? "rizin",
+ environment,
+ platform,
+ ),
+ arguments: rizinCommand(input),
+ identity: RIZIN_PROVIDER_IDENTITY,
+ }),
+});
+
+const providerExecutable = (
+ executable: string,
+ environment: Readonly,
+ platform: NodeJS.Platform,
+): string =>
+ platform === "win32"
+ ? resolveWindowsProviderExecutable(executable, environment)
+ : executable;
+
+/** Compose BYO reverse-engineering commands without acquiring tools at import time. */
+export const createReverseEngineeringService = (
+ environment: Readonly = process.env,
+ options: ReverseEngineeringServiceRuntimeOptions = {},
+): ReverseEngineeringService => {
+ const environmentSnapshot = { ...environment };
+ return new ReverseEngineeringService({
+ ...options,
+ environment: environmentSnapshot,
+ commands: createReverseEngineeringCommandPort(environmentSnapshot),
+ });
+};
diff --git a/src/composition/rust.ts b/src/composition/rust.ts
new file mode 100644
index 000000000..5a05eceb3
--- /dev/null
+++ b/src/composition/rust.ts
@@ -0,0 +1,6 @@
+import { RustBinaryService } from "../application/rust/RustBinaryService.js";
+import { RustBinaryProvider } from "../rust/RustBinaryProvider.js";
+
+/** Compose bundled offline inspection without a Rust toolchain or any child process. */
+export const createRustBinaryService = (): RustBinaryService =>
+ new RustBinaryService(new RustBinaryProvider());
diff --git a/src/config.test.ts b/src/config.test.ts
index 67db7e111..efa1b48fc 100644
--- a/src/config.test.ts
+++ b/src/config.test.ts
@@ -249,6 +249,40 @@ describe("configuration failure diagnostics", () => {
},
);
+ it.each(["1", "90000", "2147483647"])(
+ "accepts Swift compile deadline %s without changing Hopper's deadline",
+ (value) => {
+ expect(
+ parseConfig({ REA_SWIFTC_COMPILE_TIMEOUT_MS: value }),
+ ).toMatchObject({
+ ok: true,
+ value: {
+ hopperStartupTimeoutMs: 120_000,
+ },
+ });
+ },
+ );
+
+ it.each(["", "0", "-1", "1.5", "1e3", "invalid", "2147483648"])(
+ "rejects malformed Swift compile deadline %j with its setting",
+ (value) => {
+ const result = parseConfig({ REA_SWIFTC_COMPILE_TIMEOUT_MS: value });
+ if (result.ok) throw new Error("Expected invalid Swift compile deadline");
+ expect(projectAnalysisError(result.error)).toMatchObject({
+ code: "configuration_invalid",
+ category: "invalid_input",
+ details: {
+ settings: [
+ {
+ setting: "REA_SWIFTC_COMPILE_TIMEOUT_MS",
+ constraint: expect.any(String),
+ },
+ ],
+ },
+ });
+ },
+ );
+
it.each([
["GHIDRA_INSTALL_DIR", "./fixture"],
["JAVA_HOME", "./jdk"],
diff --git a/src/config/environment.ts b/src/config/environment.ts
index 7dcd955d0..121d46614 100644
--- a/src/config/environment.ts
+++ b/src/config/environment.ts
@@ -12,6 +12,7 @@ import { ghidraDisabledAnalyzersSchema } from "./ghidraDisabledAnalyzers.js";
import {
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
+ DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
providerStartupTimeoutSchema,
} from "./providerStartupTimeout.js";
import { mcpInputSchemaProfileSchema } from "./mcpInputSchemaProfile.js";
@@ -47,6 +48,9 @@ const environmentSchema = z.object({
REA_HOPPER_STARTUP_TIMEOUT_MS: providerStartupTimeoutSchema.default(
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
),
+ REA_SWIFTC_COMPILE_TIMEOUT_MS: providerStartupTimeoutSchema.default(
+ DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
+ ),
REA_GHIDRA_LANGUAGE_ID: ghidraLanguageIdSchema.optional(),
REA_GHIDRA_COMPILER_SPEC_ID: ghidraCompilerSpecIdSchema.optional(),
REA_GHIDRA_DISABLED_ANALYZERS: ghidraDisabledAnalyzersSchema.optional(),
diff --git a/src/config/providerStartupTimeout.ts b/src/config/providerStartupTimeout.ts
index 27c0116de..eb3b9d52f 100644
--- a/src/config/providerStartupTimeout.ts
+++ b/src/config/providerStartupTimeout.ts
@@ -6,6 +6,10 @@ export const DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS = 330_000;
/** Default absolute deadline for Hopper launch, analysis, bridge, and readiness. */
export const DEFAULT_HOPPER_STARTUP_TIMEOUT_MS = 120_000;
+/** Default deadline for Darwin ownership-helper `swiftc` compilation. */
+/** Fresh Swift SDK cache took ~88s on the supported Intel macOS VM. */
+export const DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS = 120_000;
+
// Node schedules delays above this signed 32-bit boundary as a 1 ms timer.
const MAX_TIMER_DELAY_MS = 2_147_483_647;
diff --git a/src/contracts/adb/adbToolContracts.ts b/src/contracts/adb/adbToolContracts.ts
index b121c8846..e72b603d7 100644
--- a/src/contracts/adb/adbToolContracts.ts
+++ b/src/contracts/adb/adbToolContracts.ts
@@ -127,7 +127,7 @@ export const ADB_TOOL_CONTRACTS = [
name: "push_adb_file",
...toolContractMetadata("push_adb_file"),
description:
- "Push one local file to an absolute device path. The only device-mutating ADB tool: existing device files are refused unless overwrite is explicitly requested, and the transfer is verified by comparing the local digest with the device's own sha256sum when the device provides one.",
+ "Push one local file to an absolute device path. A device-mutating tool: existing device files are refused unless overwrite is explicitly requested, and the transfer is verified by comparing the local digest with the device's own sha256sum when the device provides one.",
kind: "adb-provider",
inputSchema: adbInputSchemas.push_adb_file,
outputSchema: evidenceResultOf(adbResultSchemas.push_adb_file),
diff --git a/src/contracts/cutterToolContracts.ts b/src/contracts/cutterToolContracts.ts
new file mode 100644
index 000000000..e978251b8
--- /dev/null
+++ b/src/contracts/cutterToolContracts.ts
@@ -0,0 +1,96 @@
+import { z } from "zod";
+import { jsonValueSchema } from "../domain/jsonValue.js";
+import type { ToolContract } from "./toolContractTypes.js";
+import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
+import { toolContractMetadata } from "./toolEffects.js";
+
+/** List live Cutter plugin bridge sessions. */
+export const cutterListInputSchema = z.strictObject({});
+
+/** Execute a command in a selected live Cutter instance and observed document generation. */
+export const cutterCommandInputSchema = z.strictObject({
+ session_id: z.string().uuid(),
+ expected_generation: z.number().int().nonnegative(),
+ command: z
+ .string()
+ .min(1)
+ .refine(
+ (value) => !/[\r\n]/u.test(value),
+ "A single Rizin command line is required",
+ ),
+ json: z.boolean().default(false),
+});
+
+export const cutterSessionSchema = z.strictObject({
+ session_id: z.string().uuid(),
+ pid: z.number().int().positive(),
+ document_generation: z.number().int().nonnegative(),
+ current_file: z.string().nullable(),
+ cutter_version: z.string().nullable(),
+ identity_status: z.literal("partial"),
+});
+
+export const cutterCommandResultSchema = z.strictObject({
+ command: z.string(),
+ output: jsonValueSchema.nullable(),
+ execution_state: z.enum(["not_started", "complete", "failed", "unknown"]),
+ error: z.string().nullable(),
+ message: z.string().nullable(),
+ output_truncated: z.boolean(),
+ cutter_version: z.string().nullable(),
+ current_file: z.string().nullable(),
+ document_generation: z.number().int().nonnegative(),
+ identity_status: z.literal("partial"),
+});
+
+export const cutterDiscoverySchema = z.strictObject({
+ sessions: z.array(cutterSessionSchema),
+ bridge_directory: z.string(),
+ discovery_status: z.enum([
+ "sessions_found",
+ "no_live_bridge_found",
+ "bridge_directory_unavailable",
+ "bridge_directory_insecure",
+ "bridge_directory_capacity_exceeded",
+ ]),
+ bridge_directory_security: z.enum([
+ "private_verified",
+ "not_private",
+ "unverified_platform_acl",
+ "not_checked",
+ ]),
+});
+
+/** Live Cutter discovery and arbitrary Rizin command contracts. */
+export const CUTTER_TOOL_CONTRACTS = [
+ {
+ name: "list_cutter_sessions",
+ ...toolContractMetadata("list_cutter_sessions"),
+ kind: "native-provider",
+ description:
+ "List live REA bridge sessions hosted by upstream Cutter instances, including observed document identity and bridge-directory security status. Discovery admits at most 256 descriptor candidates (16 MiB at the descriptor byte limit), reads and probes at most eight concurrently, and reports bridge_directory_capacity_exceeded rather than claiming no sessions when that budget is exceeded.",
+ inputSchema: cutterListInputSchema,
+ outputSchema: cutterDiscoverySchema,
+ examples: [{ title: "List open Cutter instances", input: {} }],
+ },
+ {
+ name: "cutter_command",
+ ...toolContractMetadata("cutter_command"),
+ kind: "native-provider",
+ description:
+ "Run one arbitrary Rizin command in a selected live Cutter session. Commands may edit and persist analysis state or execute plugin-provided effects; completion uncertainty is returned as Evidence and must not be retried automatically. In a shared MCP server, this admitted command closes any active binary-analysis session; after its effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first.",
+ inputSchema: cutterCommandInputSchema,
+ outputSchema: evidenceResultOf(cutterCommandResultSchema),
+ examples: [
+ {
+ title: "Inspect active binary metadata",
+ input: {
+ session_id: "00000000-0000-4000-8000-000000000001",
+ expected_generation: 0,
+ command: "ij",
+ json: true,
+ },
+ },
+ ],
+ },
+] as const satisfies readonly ToolContract[];
diff --git a/src/contracts/gdbToolContracts.ts b/src/contracts/gdbToolContracts.ts
new file mode 100644
index 000000000..f1858a14a
--- /dev/null
+++ b/src/contracts/gdbToolContracts.ts
@@ -0,0 +1,113 @@
+import { z } from "zod";
+
+import type { ToolContract } from "./toolContractTypes.js";
+import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
+import { toolContractMetadata } from "./toolEffects.js";
+
+const sessionId = z.string().uuid();
+const gdbCommandResult = z.strictObject({
+ command: z.string(),
+ mi: z.string(),
+ records: z.array(z.string()),
+ console: z.string(),
+ target: z.string(),
+ log: z.string(),
+ process_exit_observed: z.boolean().optional(),
+ process_exit_code: z.number().int().nullable().optional(),
+ process_exit_signal: z.string().nullable().optional(),
+ output_truncated: z.boolean(),
+ output_invalid_utf8: z.literal(true).optional(),
+ completion_status: z.enum(["complete", "unknown"]),
+});
+
+/** Persistent GDB/MI session contracts hosted by the existing REA MCP server. */
+export const GDB_TOOL_CONTRACTS = [
+ {
+ name: "start_gdb_session",
+ ...toolContractMetadata("start_gdb_session"),
+ kind: "native-provider",
+ description:
+ "Start a BYO GDB process in MI3 mode when available, suppress startup files, and disable auto-loading. Persistent GDB sessions are available only on Linux, where REA can verify live inferior output descriptors through /proc before detaching; macOS descriptor verification is not implemented, and Windows Job Object cleanup can terminate debugger inferiors. Optionally set inferior_tty to a caller-selected terminal for future inferior runs; REA verifies the setting through GDB/MI before the session becomes ready. The selected terminal must remain available after GDB exits. A live inferior can be detached during close only while this durable output sink remains verified. The session persists in this REA MCP server process and receives an explicit session ID.",
+ inputSchema: z.strictObject({
+ inferior_tty: z
+ .string()
+ .min(1)
+ .optional()
+ .describe(
+ "Caller-selected terminal path for future inferior runs. GDB must report the exact configured path before the session is ready.",
+ ),
+ }),
+ outputSchema: z.strictObject({
+ session_id: sessionId,
+ mi_version: z.enum(["mi3", "mi2"]),
+ }),
+ examples: [{ title: "Start GDB", input: {} }],
+ },
+ {
+ name: "gdb_console",
+ ...toolContractMetadata("gdb_console"),
+ kind: "native-provider",
+ description:
+ "Run one unrestricted GDB CLI command through MI interpreter-exec console and return raw MI, console, target, and log records inline with Evidence. A command timeout preserves collected records with completion_status unknown and stops the owned debugger session. Commands can execute shell, alter target state, access files, and use network. Closing the debugger never implies target termination. In a shared MCP server, this admitted command closes any active binary-analysis session; after its effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first.",
+ inputSchema: z.strictObject({
+ session_id: sessionId,
+ command: z.string().min(1),
+ }),
+ outputSchema: evidenceResultOf(gdbCommandResult),
+ examples: [
+ {
+ title: "Inspect loaded symbols",
+ input: {
+ session_id: "00000000-0000-4000-8000-000000000001",
+ command: "info functions",
+ },
+ },
+ ],
+ },
+ {
+ name: "gdb_session_status",
+ ...toolContractMetadata("gdb_session_status"),
+ kind: "native-provider",
+ description:
+ "Return the observed lifecycle state and bounded recent MI records and process diagnostics for one persistent REA GDB session; truncation and invalid-UTF-8 fields report discarded or lossy data.",
+ inputSchema: z.strictObject({ session_id: sessionId }),
+ outputSchema: z.strictObject({
+ session_id: sessionId,
+ mi_version: z.enum(["mi3", "mi2"]),
+ state: z.enum(["ready", "closed"]),
+ recent_mi_records: z.array(z.string()),
+ recent_mi_records_truncated: z.boolean(),
+ mi_invalid_utf8: z.boolean(),
+ stderr: z.string(),
+ stderr_observed_bytes: z.number().int().nonnegative(),
+ stderr_truncated: z.boolean(),
+ stderr_invalid_utf8: z.boolean(),
+ diagnostics_truncated: z.boolean(),
+ inferior_tty: z.string().nullable(),
+ inferior_tty_verified: z.boolean(),
+ }),
+ examples: [
+ {
+ title: "Check GDB session",
+ input: { session_id: "00000000-0000-4000-8000-000000000001" },
+ },
+ ],
+ },
+ {
+ name: "close_gdb_session",
+ ...toolContractMetadata("close_gdb_session"),
+ kind: "native-provider",
+ description:
+ "Close the owned GDB process after verifying the inferior state. When one live thread group has a verified durable output TTY, REA automatically detaches it, which may resume target execution; otherwise close reports that a disposition is required. REA never kills attached or remote targets as part of debugger cleanup. In a shared MCP server, this admitted close retires any active binary-analysis session; after target effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first.",
+ inputSchema: z.strictObject({ session_id: sessionId }),
+ outputSchema: z.strictObject({
+ target_state: z.enum(["none_observed", "unknown"]),
+ }),
+ examples: [
+ {
+ title: "Close GDB session",
+ input: { session_id: "00000000-0000-4000-8000-000000000001" },
+ },
+ ],
+ },
+] as const satisfies readonly ToolContract[];
diff --git a/src/contracts/managed/managedToolContracts.ts b/src/contracts/managed/managedToolContracts.ts
index 1e685a702..2c0088b9b 100644
--- a/src/contracts/managed/managedToolContracts.ts
+++ b/src/contracts/managed/managedToolContracts.ts
@@ -43,7 +43,7 @@ export const managedMembersInputSchema = managedTargetInputSchema.extend({
})
.meta({ uniqueItems: true })
.describe(
- "Inspect only the fields, methods, call edges, and field accesses declared by these exact type full names; omit to inspect every type. Unmatched names are reported in selection.unmatched_type_names.",
+ "Inspect only the fields, methods, call edges, and field accesses declared by these exact type full names, and the user strings their method bodies load; omit to inspect every type. Unmatched names are reported in selection.unmatched_type_names.",
)
.optional(),
});
@@ -87,7 +87,7 @@ export const MANAGED_TOOL_CONTRACTS = [
name: "inspect_managed_members",
...toolContractMetadata("inspect_managed_members"),
description:
- "Inspect PE/CLI metadata members, signatures, raw CIL hashes, decoded-instruction-tuple hashes, separately reported exception regions, call edges, and field-access anchors without loading or executing target code. Select type_names to decode only the members of those exact declaring types, which keeps large assemblies such as a base class library within the representation budget. Metadata tokens are reported as build-local coordinates bound to the artifact SHA-256 and MVID; the tuple hash does not resolve them or fully commit control flow.",
+ "Inspect PE/CLI metadata members, signatures, raw CIL hashes, decoded-instruction-tuple hashes, separately reported exception regions, call edges, field-access anchors, and the #US string literals that ldstr loads, without loading or executing target code. Select type_names to decode only the members of those exact declaring types and the literals their bodies load, which keeps large assemblies such as a base class library within the representation budget. Metadata tokens are reported as build-local coordinates bound to the artifact SHA-256 and MVID; the tuple hash does not resolve them or fully commit control flow.",
kind: "managed-provider",
inputSchema: managedMembersInputSchema,
outputSchema: memberOutputSchema,
diff --git a/src/contracts/managed/managedWorkflowExamples.ts b/src/contracts/managed/managedWorkflowExamples.ts
index 85297d671..6e5d836ec 100644
--- a/src/contracts/managed/managedWorkflowExamples.ts
+++ b/src/contracts/managed/managedWorkflowExamples.ts
@@ -53,6 +53,7 @@ const emptyManagedMembers = (
member_refs: [],
call_edges: [],
field_accesses: [],
+ user_strings: { state: "complete", literals: [], issues: [] },
selection: null,
coverage: { state: "complete", issues: [] },
limitations: [],
diff --git a/src/contracts/managed/managedWorkflowToolContracts.ts b/src/contracts/managed/managedWorkflowToolContracts.ts
index 519c2e125..18d78a310 100644
--- a/src/contracts/managed/managedWorkflowToolContracts.ts
+++ b/src/contracts/managed/managedWorkflowToolContracts.ts
@@ -98,7 +98,7 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [
name: "compare_managed_members",
...toolContractMetadata("compare_managed_members"),
description:
- "Compare two authenticated inspect_managed_members Evidence records using unique-only tiers: exact CIL/signature, exact declared type, name, and raw signature (decoded or not), and decoded structural method shape. Names alone are never a matching basis; a member whose signature was not decoded, or whose same-named counterpart's was not, stays unknown rather than added or removed. Metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.",
+ "Compare two authenticated inspect_managed_members Evidence records using unique-only tiers: exact declared type, name, and CIL/signature; exact declared type, name, and raw signature (decoded or not); exact declared type, name, and decoded structural method shape; then exact CIL/signature or decoded structural method shape alone, which never takes a member whose own declared type and name remain unpaired on the other side or whose declaring type is unresolved. Names alone are never a matching basis; a member whose signature was not decoded, or whose same-named counterpart's was not, stays unknown rather than added or removed. Metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.",
kind: "application",
inputSchema: compareManagedMembersReferenceInputSchema,
outputSchema: comparisonOutputSchema,
diff --git a/src/contracts/reverseEngineeringToolContracts.ts b/src/contracts/reverseEngineeringToolContracts.ts
new file mode 100644
index 000000000..7b6542be1
--- /dev/null
+++ b/src/contracts/reverseEngineeringToolContracts.ts
@@ -0,0 +1,61 @@
+import { z } from "zod";
+
+import {
+ objdumpInputSchema,
+ rizinInputSchema,
+} from "../domain/reverseEngineering.js";
+import type { ToolContract } from "./toolContractTypes.js";
+import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
+import { toolContractMetadata } from "./toolEffects.js";
+
+const commandObservationSchema = z.strictObject({
+ stdout: z.string(),
+ stderr: z.string(),
+ exit_code: z.union([z.string(), z.number(), z.null()]),
+ signal: z.string().nullable(),
+ output_truncated: z.boolean(),
+ completion_status: z.enum(["complete", "unknown"]),
+ artifact_sha256_after: z
+ .string()
+ .regex(/^[a-f0-9]{64}$/u)
+ .nullable(),
+ artifact_changed: z.boolean().nullable(),
+});
+
+/** Caller-selected GNU objdump and Rizin observations. */
+export const REVERSE_ENGINEERING_TOOL_CONTRACTS = [
+ {
+ name: "inspect_with_objdump",
+ ...toolContractMetadata("inspect_with_objdump"),
+ kind: "native-provider",
+ description:
+ "Run caller-selected GNU objdump inspection against a local artifact, or request operation=architectures without a path to list BFD targets advertised by the installed binary. Preserves decoded stdout and stderr inline with SHA-256 Evidence for artifact inspections, up to a shared 16 MiB combined limit; invalid UTF-8 bytes are represented by replacement characters and flagged in Evidence limitations. output_truncated reports output beyond that limit and completion_status is unknown if buffer enforcement terminated the process. The raw output is authoritative and REA does not claim a stable normalized format. DWARF does not follow local debug links by default. Set follow_debug_links to true to follow them; identities of external debug files are not bound to Evidence. REA clears DEBUGINFOD_URLS for objdump so DWARF processing does not make debuginfod network requests. Architecture output is subjectless and does not establish support for any artifact.",
+ inputSchema: objdumpInputSchema,
+ outputSchema: evidenceResultOf(commandObservationSchema),
+ examples: [
+ {
+ title: "Disassemble code sections",
+ input: { path: "/tmp/program", operation: "disassemble" },
+ },
+ {
+ title: "List advertised BFD targets",
+ input: { operation: "architectures" },
+ },
+ ],
+ },
+ {
+ name: "execute_rizin_command",
+ ...toolContractMetadata("execute_rizin_command"),
+ kind: "native-provider",
+ description:
+ "Execute one caller-selected Rizin command against a local artifact using the Rizin CLI profile that suppresses user settings and scripts while retaining plugins. Preserves decoded stdout and stderr inline with SHA-256 Evidence, up to a shared 16 MiB combined limit; invalid UTF-8 bytes are represented by replacement characters and flagged in Evidence limitations. output_truncated reports output beyond that limit and completion_status is unknown if buffer enforcement terminated the process. Rizin commands and retained plugins can execute foreign code, mutate files, access the network, and read files outside the selected artifact; identities and contents of those external inputs are not bound to Evidence. Command effects depend on the Rizin command and loaded plugins; JSON is command-specific and raw output remains authoritative. In a shared MCP server, this admitted command closes any active binary-analysis session; after its effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first.",
+ inputSchema: rizinInputSchema,
+ outputSchema: evidenceResultOf(commandObservationSchema),
+ examples: [
+ {
+ title: "Inspect binary information",
+ input: { path: "/tmp/program", command: "iI" },
+ },
+ ],
+ },
+] as const satisfies readonly ToolContract[];
diff --git a/src/contracts/rizinDebugToolContracts.ts b/src/contracts/rizinDebugToolContracts.ts
new file mode 100644
index 000000000..d638a143e
--- /dev/null
+++ b/src/contracts/rizinDebugToolContracts.ts
@@ -0,0 +1,107 @@
+import { z } from "zod";
+
+import type { ToolContract } from "./toolContractTypes.js";
+import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
+import { toolContractMetadata } from "./toolEffects.js";
+
+const sessionId = z.string().uuid();
+const commandResult = z.strictObject({
+ command: z.string(),
+ output: z.string(),
+ output_scope: z.literal("command_and_interleaved_session_output"),
+ output_truncated: z.boolean(),
+ output_invalid_utf8: z.literal(true).optional(),
+ completion_status: z.enum(["complete", "unknown"]),
+ backend: z.string().nullable(),
+});
+
+/** Persistent Rizin debugger session contracts. */
+export const RIZIN_DEBUG_TOOL_CONTRACTS = [
+ {
+ name: "start_rizin_debug_session",
+ ...toolContractMetadata("start_rizin_debug_session"),
+ kind: "native-provider",
+ description:
+ "Start a persistent Rizin debugger session using `-N` to suppress user settings and scripts while retaining plugins, `-0` for NUL framing, and `-d` with an optional caller-selected IO debugger backend. Requires a caller-selected Linux TTY for inferior stdout and stderr; the target may write directly to that terminal while running and after detach. REA verifies both destinations and `dbg.exitkills=false` before detaching. Backend and target capabilities depend on the installed build. The session lives in this REA MCP process. Persistent sessions require Linux `/proc` descriptor readback. In a shared MCP server, starting this session retires any active binary-analysis session; after target effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first.",
+ inputSchema: z.strictObject({
+ path: z.string().min(1),
+ inferior_tty: z.string().min(1),
+ backend: z.string().min(1).optional(),
+ }),
+ outputSchema: z.strictObject({
+ session_id: sessionId,
+ backend: z.string().nullable(),
+ }),
+ examples: [
+ {
+ title: "Start Rizin debugger",
+ input: { path: "/tmp/program", inferior_tty: "/dev/pts/7" },
+ },
+ ],
+ },
+ {
+ name: "rizin_debug_command",
+ ...toolContractMetadata("rizin_debug_command"),
+ kind: "native-provider",
+ description:
+ "Execute one unrestricted command in a persistent Rizin debugger session. Returns raw output inline with Evidence, including backend output interleaved before the completion marker. If the command exceeds the output or frame limit, `output_truncated` is true and `completion_status` is `unknown`; retained partial output is returned and the owned debugger session is stopped. Unsolicited frames already queued before the command make the session unavailable because correlation is ambiguous. Commands can access local files, shell, network, plugins, and target state; effects depend on the active backend and target. In a shared MCP server, this admitted command closes any active binary-analysis session; after its effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first.",
+ inputSchema: z.strictObject({
+ session_id: sessionId,
+ command: z
+ .string()
+ .min(1)
+ .refine(
+ (value) => !/[\r\n]/u.test(value),
+ "A single command line is required",
+ ),
+ }),
+ outputSchema: evidenceResultOf(commandResult),
+ examples: [
+ {
+ title: "Inspect debugger state",
+ input: {
+ session_id: "00000000-0000-4000-8000-000000000001",
+ command: "dr",
+ },
+ },
+ ],
+ },
+ {
+ name: "rizin_debug_session_status",
+ ...toolContractMetadata("rizin_debug_session_status"),
+ kind: "native-provider",
+ description:
+ "Return observed lifecycle state and bounded recent NUL-framed output and process diagnostics for one Rizin debugger session; truncation fields report discarded data, and output_invalid_utf8 reports whether invalid UTF-8 has been observed in session frame bytes; recent output may contain replacement characters.",
+ inputSchema: z.strictObject({ session_id: sessionId }),
+ outputSchema: z.strictObject({
+ session_id: sessionId,
+ state: z.enum(["ready", "closed"]),
+ inferior_tty: z.string(),
+ recent_output: z.array(z.string()),
+ recent_output_truncated: z.boolean(),
+ diagnostics_truncated: z.boolean(),
+ output_invalid_utf8: z.boolean(),
+ }),
+ examples: [
+ {
+ title: "Check Rizin session",
+ input: { session_id: "00000000-0000-4000-8000-000000000001" },
+ },
+ ],
+ },
+ {
+ name: "close_rizin_debug_session",
+ ...toolContractMetadata("close_rizin_debug_session"),
+ kind: "native-provider",
+ description:
+ "Close the REA-owned Rizin debugger only after verifying no inferior remains or detaching one with a verified caller-selected TTY output sink. A detached target may continue running and write to that TTY; target state is otherwise reported as unknown. REA never signals an inferior based on PID or path. In a shared MCP server, this admitted close retires any active binary-analysis session; after target effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first.",
+ inputSchema: z.strictObject({ session_id: sessionId }),
+ outputSchema: z.strictObject({ target_state: z.literal("unknown") }),
+ examples: [
+ {
+ title: "Close Rizin debugger",
+ input: { session_id: "00000000-0000-4000-8000-000000000001" },
+ },
+ ],
+ },
+] as const satisfies readonly ToolContract[];
diff --git a/src/contracts/rust/rustToolContracts.ts b/src/contracts/rust/rustToolContracts.ts
new file mode 100644
index 000000000..7f827dfbe
--- /dev/null
+++ b/src/contracts/rust/rustToolContracts.ts
@@ -0,0 +1,26 @@
+import {
+ inspectRustBinaryInputSchema,
+ rustBinarySchema,
+} from "../../domain/rust/rustBinary.js";
+import type { ToolContract } from "../toolContractTypes.js";
+import { toolContractMetadata } from "../toolEffects.js";
+import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
+
+/** Static Rust dependency metadata is independent of active native-analysis providers. */
+export const RUST_TOOL_CONTRACTS = [
+ {
+ name: "inspect_rust_binary",
+ ...toolContractMetadata("inspect_rust_binary"),
+ kind: "artifact-provider",
+ description:
+ "Inspect the cargo-auditable crate dependency list embedded in an explicit local ELF, PE or thin Mach-O image. Returns the artifact SHA-256, the .dep-v0 section file offset and size, compressed and uncompressed sizes, format version, packages in their original order, the root package index, and dependency edges by index. Reads zlib-compressed JSON without installing Rust, opening a disassembler, executing the target, writing temporary files or accessing the network. A missing section is null metadata with a limitation and is not proof that the binary has no dependencies. A rustc commit hash is reported only as an inference when a /rustc/<40-hex>/ path is present in read-only data. Resource guards bound files to 256 MiB, dependency sections to 1 MiB, inflated JSON to the smaller of 8 MiB and 64 times the compressed size, and the complete report to 8 MiB. Malformed, unsupported and oversized inputs receive typed failures rather than partial success.",
+ inputSchema: inspectRustBinaryInputSchema,
+ outputSchema: evidenceResultOf(rustBinarySchema),
+ examples: [
+ {
+ title: "Inspect an embedded Rust dependency list",
+ input: { path: "/artifacts/application" },
+ },
+ ],
+ },
+] as const satisfies readonly ToolContract[];
diff --git a/src/contracts/toolContractExamples.ts b/src/contracts/toolContractExamples.ts
index 50a745e75..dc6567e39 100644
--- a/src/contracts/toolContractExamples.ts
+++ b/src/contracts/toolContractExamples.ts
@@ -82,6 +82,11 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
left_bundle_path: "/tmp/left-evidence.json",
right_bundle_path: "/tmp/right-evidence.json",
},
+ inspect_with_objdump: {
+ path: "/tmp/program",
+ operation: "disassemble",
+ },
+ execute_rizin_command: { path: "/tmp/program", command: "iI" },
list_frida_devices: {},
list_frida_processes: { device_id: "local" },
start_frida_session: { mode: "attach", device_id: "local", pid: 1234 },
diff --git a/src/contracts/toolContracts.ts b/src/contracts/toolContracts.ts
index 2fdac1fbe..91627ee5b 100644
--- a/src/contracts/toolContracts.ts
+++ b/src/contracts/toolContracts.ts
@@ -3,6 +3,7 @@ import { SIGNAL_TOOL_CONTRACTS } from "./signals/signalToolContracts.js";
import { WASM_TOOL_CONTRACTS } from "./wasm/wasmToolContracts.js";
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
import { GO_TOOL_CONTRACTS } from "./go/goToolContracts.js";
+import { RUST_TOOL_CONTRACTS } from "./rust/rustToolContracts.js";
import { SQLITE_TOOL_CONTRACTS } from "./sqlite/sqliteToolContracts.js";
import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "./analysisViewToolContracts.js";
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
@@ -28,6 +29,10 @@ import { JAVASCRIPT_RECOVERY_TOOL_CONTRACTS } from "./javascript/javascriptRecov
import { OFFICIAL_TOOL_CONTRACTS } from "./officialToolContracts.js";
import { ENHANCED_TOOL_CONTRACTS } from "./enhancedToolContracts.js";
import { SESSION_TOOL_CONTRACTS } from "./sessionToolContracts.js";
+import { CUTTER_TOOL_CONTRACTS } from "./cutterToolContracts.js";
+import { REVERSE_ENGINEERING_TOOL_CONTRACTS } from "./reverseEngineeringToolContracts.js";
+import { GDB_TOOL_CONTRACTS } from "./gdbToolContracts.js";
+import { RIZIN_DEBUG_TOOL_CONTRACTS } from "./rizinDebugToolContracts.js";
import { FRIDA_TOOL_CONTRACTS } from "./fridaToolContracts.js";
/** Complete ordered public inventory used by registration and verification. */
@@ -40,6 +45,7 @@ export const TOOL_CONTRACTS = [
...ARTIFACT_TOOL_CONTRACTS,
...EVM_TOOL_CONTRACTS,
...GO_TOOL_CONTRACTS,
+ ...RUST_TOOL_CONTRACTS,
...WASM_TOOL_CONTRACTS,
...SQLITE_TOOL_CONTRACTS,
...CAPTURE_TOOL_CONTRACTS,
@@ -62,6 +68,10 @@ export const TOOL_CONTRACTS = [
...WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
...JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
...SESSION_TOOL_CONTRACTS,
+ ...CUTTER_TOOL_CONTRACTS,
+ ...REVERSE_ENGINEERING_TOOL_CONTRACTS,
+ ...GDB_TOOL_CONTRACTS,
+ ...RIZIN_DEBUG_TOOL_CONTRACTS,
...FRIDA_TOOL_CONTRACTS,
] as const;
diff --git a/src/contracts/toolEffects.test.ts b/src/contracts/toolEffects.test.ts
index 3a4acca99..7d23db92d 100644
--- a/src/contracts/toolEffects.test.ts
+++ b/src/contracts/toolEffects.test.ts
@@ -13,12 +13,18 @@ const EXECUTION_WORDING =
/\b(?:run|runs|running|launch|launches|launching|execute|executes|executing|execution|start|starts|install|installs|uninstall|uninstalls)\b/iu;
const EXECUTION_WORDING_EXCEPTIONS: Readonly> = {
+ inspect_with_objdump:
+ "runs a caller-selected inspection tool, not target code",
+ start_gdb_session:
+ "starts a debugger session; execution is attributed to gdb_console",
start_frida_session:
"spawned targets remain suspended until resume_frida_session",
inspect_native_load_image: "states that it does not execute the target",
resolve_native_call_targets: "states that it does not execute code",
inspect_pe_resources: "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",
inspect_sqlite_database:
"uses a trusted inspector worker without executing application code or caller SQL",
inspect_binary_layout:
@@ -127,14 +133,21 @@ describe("tool effects", () => {
"capture_electron_scenario",
"capture_native_ui_scenario",
"capture_process_scenario",
+ "close_gdb_session",
+ "close_rizin_debug_session",
+ "cutter_command",
+ "execute_rizin_command",
+ "gdb_console",
"install_adb_package",
"instrument_with_frida",
"load_frida_script",
"observe_native_calls",
"push_adb_file",
"resume_frida_session",
+ "rizin_debug_command",
"start_adb_activity",
"start_adb_app",
+ "start_rizin_debug_session",
"stop_adb_app",
"uninstall_adb_package",
]);
@@ -167,6 +180,17 @@ describe("tool effects", () => {
});
});
+ it("marks automatic GDB detach as destructive target execution", () => {
+ const effects = TOOL_EFFECTS["close_gdb_session"];
+ if (effects === undefined)
+ throw new Error("close_gdb_session is unaudited");
+ expect(effects).toMatchObject({
+ mutatesTarget: true,
+ executesForeignCode: true,
+ });
+ expect(annotationsFromEffects(effects).destructiveHint).toBe(true);
+ });
+
it("publishes the advertised annotations derived from canonical effects", () => {
for (const contract of TOOL_CONTRACTS)
expect(contract.annotations, contract.name).toEqual(
diff --git a/src/contracts/toolEffects.ts b/src/contracts/toolEffects.ts
index 377905480..c64edefca 100644
--- a/src/contracts/toolEffects.ts
+++ b/src/contracts/toolEffects.ts
@@ -53,7 +53,80 @@ const sessionEvidence = effects({ mutatesSession: true });
/** Explicit effect audit for every public tool. */
export const TOOL_EFFECTS: Readonly> = {
+ list_cutter_sessions: effects({ accessesNetwork: true }),
+ cutter_command: effects({
+ mutatesTarget: true,
+ executesForeignCode: true,
+ mutatesSession: true,
+ writesFilesystem: true,
+ launchesProcess: true,
+ accessesNetwork: true,
+ changesUiState: true,
+ idempotent: false,
+ }),
+ inspect_with_objdump: effects({
+ mutatesSession: true,
+ launchesProcess: true,
+ }),
+ start_gdb_session: effects({
+ launchesProcess: true,
+ mutatesSession: true,
+ idempotent: false,
+ }),
+ gdb_console: effects({
+ mutatesTarget: true,
+ mutatesSession: true,
+ writesFilesystem: true,
+ launchesProcess: true,
+ accessesNetwork: true,
+ executesForeignCode: true,
+ idempotent: false,
+ }),
+ gdb_session_status: effects(),
+ start_rizin_debug_session: effects({
+ mutatesTarget: true,
+ mutatesSession: true,
+ launchesProcess: true,
+ executesForeignCode: true,
+ idempotent: false,
+ }),
+ rizin_debug_command: effects({
+ mutatesTarget: true,
+ mutatesSession: true,
+ executesForeignCode: true,
+ writesFilesystem: true,
+ launchesProcess: true,
+ accessesNetwork: true,
+ changesUiState: true,
+ mayDiscardData: true,
+ idempotent: false,
+ }),
+ rizin_debug_session_status: effects({ mutatesSession: true }),
+ close_rizin_debug_session: effects({
+ mutatesSession: true,
+ launchesProcess: true,
+ mutatesTarget: true,
+ executesForeignCode: true,
+ }),
+ close_gdb_session: effects({
+ launchesProcess: true,
+ mutatesSession: true,
+ mutatesTarget: true,
+ executesForeignCode: true,
+ idempotent: false,
+ }),
+ execute_rizin_command: effects({
+ mutatesTarget: true,
+ mutatesSession: true,
+ writesFilesystem: true,
+ launchesProcess: true,
+ accessesNetwork: true,
+ mayDiscardData: true,
+ executesForeignCode: true,
+ idempotent: false,
+ }),
inspect_go_binary: evidence,
+ inspect_rust_binary: evidence,
inspect_sqlite_database: effects({
mutatesSession: true,
writesFilesystem: true,
diff --git a/src/cutter/CutterBridgeClient.ts b/src/cutter/CutterBridgeClient.ts
new file mode 100644
index 000000000..13973cabf
--- /dev/null
+++ b/src/cutter/CutterBridgeClient.ts
@@ -0,0 +1,891 @@
+import { randomUUID } from "node:crypto";
+import { constants as fsConstants } from "node:fs";
+import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
+import { createConnection } from "node:net";
+import { lstat, open, opendir } from "node:fs/promises";
+import { homedir } from "node:os";
+import { join } from "node:path";
+import { z } from "zod";
+
+import {
+ AnalysisCapabilityUnavailableError,
+ AnalysisOutputError,
+ AnalysisResourceConstraintError,
+} from "../domain/analysisErrorCore.js";
+import type { AnalysisError } from "../domain/analysisErrorBase.js";
+import { err, ok, type Result } from "../domain/result.js";
+import { requireWindowsNativeAuthority } from "../windows/WindowsNativeLoader.js";
+import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
+
+const descriptorSchema = z.object({
+ session_id: z.string().uuid(),
+ pid: z.number().int().positive(),
+ host: z.literal("127.0.0.1"),
+ port: z.number().int().min(1).max(65535),
+ token: z.string().min(20),
+ document_generation: z.number().int().nonnegative(),
+ current_file: z.string().nullable(),
+ cutter_version: z.string().nullable(),
+ identity_status: z.literal("partial"),
+});
+
+export const CUTTER_PROVIDER_IDENTITY = {
+ id: "cutter.python-plugin",
+ name: "Cutter Python plugin",
+ version: null,
+} as const;
+
+const responseSchema = z.object({
+ ok: z.boolean(),
+ session_id: z.string().uuid().optional(),
+ output: z.unknown().optional(),
+ current_file: z.string().nullable().optional(),
+ cutter_version: z.string().nullable().optional(),
+ document_generation: z.number().int().nonnegative().optional(),
+ identity_status: z.literal("partial").optional(),
+ error: z.string().optional(),
+ message: z.string().optional(),
+ execution_state: z
+ .enum(["not_started", "running", "complete", "failed", "unknown"])
+ .optional(),
+ output_truncated: z.boolean().optional(),
+ completed_commands: z
+ .array(
+ z.object({
+ request_id: z.string().uuid(),
+ execution_state: z.enum(["not_started", "complete", "failed"]),
+ }),
+ )
+ .max(64)
+ .optional(),
+ completed_connection: z
+ .string()
+ .regex(/^[0-9a-f]{16}$/)
+ .optional(),
+});
+const acknowledgementSchema = z.object({
+ ok: z.literal(true),
+ acknowledged: z.boolean(),
+});
+const MAX_DESCRIPTOR_BYTES = 64 * 1024;
+const MAX_FRAME_BYTES = 16 * 1024 * 1024;
+const FRAME_PAGE_BYTES = 64 * 1024;
+// Bound admitted descriptor input bytes and concurrent file/socket handles.
+const MAX_DESCRIPTOR_CANDIDATES = MAX_FRAME_BYTES / MAX_DESCRIPTOR_BYTES;
+const DISCOVERY_CONCURRENCY = 8;
+type AuthenticatedRequestBytes = string & {
+ readonly __authenticatedRequestBytes: unique symbol;
+};
+
+const serializeRequest = (
+ descriptor: z.infer,
+ request: Readonly>,
+): AuthenticatedRequestBytes => {
+ const requestBytes = JSON.stringify({
+ ...request,
+ session_id: descriptor.session_id,
+ });
+ let frameBytes =
+ Buffer.byteLength(JSON.stringify({ request: "", proof: "0".repeat(64) })) +
+ Buffer.byteLength(requestBytes);
+ for (const character of requestBytes)
+ if (character === '"' || character === "\\") frameBytes += 1;
+ if (frameBytes > MAX_FRAME_BYTES)
+ throw new AnalysisResourceConstraintError(
+ "cutter_command",
+ "transport",
+ "Cutter request exceeds its authenticated frame limit; no command was sent.",
+ { frame_bytes: frameBytes, max_frame_bytes: MAX_FRAME_BYTES },
+ );
+ return requestBytes as AuthenticatedRequestBytes;
+};
+
+class CutterBridgeResponseFrames {
+ #parts: Buffer[] = [];
+ #page = Buffer.allocUnsafe(FRAME_PAGE_BYTES);
+ #pageLength = 0;
+ #contentBytes = 0;
+ #framesRead = 0;
+
+ push(chunk: Buffer, onFrame: (line: Buffer) => boolean): void {
+ let offset = 0;
+ while (offset < chunk.length) {
+ const delimiter = chunk.indexOf(0x0a, offset);
+ const end = delimiter < 0 ? chunk.length : delimiter;
+ const segment = chunk.subarray(offset, end);
+ if (
+ this.#contentBytes + segment.length + (delimiter < 0 ? 1 : 0) >
+ MAX_FRAME_BYTES
+ )
+ throw new Error(
+ "Cutter bridge response exceeded its framed transport limit",
+ );
+ this.#append(segment);
+ if (delimiter < 0) return;
+ if (++this.#contentBytes > MAX_FRAME_BYTES)
+ throw new Error(
+ "Cutter bridge response exceeded its framed transport limit",
+ );
+ if (this.#pageLength > 0)
+ this.#parts.push(this.#page.subarray(0, this.#pageLength));
+ const line = Buffer.concat(this.#parts, this.#contentBytes - 1);
+ this.#reset();
+ if (++this.#framesRead > 2)
+ throw new Error("Cutter bridge sent more than two protocol frames");
+ offset = delimiter + 1;
+ if (!onFrame(line)) return;
+ }
+ }
+
+ #append(segment: Buffer): void {
+ let copied = 0;
+ while (copied < segment.length) {
+ const length = Math.min(
+ segment.length - copied,
+ FRAME_PAGE_BYTES - this.#pageLength,
+ );
+ segment.copy(this.#page, this.#pageLength, copied, copied + length);
+ this.#pageLength += length;
+ this.#contentBytes += length;
+ copied += length;
+ if (this.#pageLength === FRAME_PAGE_BYTES) {
+ this.#parts.push(this.#page);
+ this.#page = Buffer.allocUnsafe(FRAME_PAGE_BYTES);
+ this.#pageLength = 0;
+ }
+ }
+ }
+
+ #reset(): void {
+ this.#parts = [];
+ this.#page = Buffer.allocUnsafe(FRAME_PAGE_BYTES);
+ this.#pageLength = 0;
+ this.#contentBytes = 0;
+ }
+}
+
+const mapDiscovery = async (
+ inputs: readonly Input[],
+ operation: (input: Input) => Promise,
+): Promise => {
+ const results = new Array(inputs.length);
+ let next = 0;
+ await Promise.all(
+ Array.from(
+ { length: Math.min(inputs.length, DISCOVERY_CONCURRENCY) },
+ async () => {
+ while (next < inputs.length) {
+ const index = next++;
+ results[index] = await operation(inputs[index]!);
+ }
+ },
+ ),
+ );
+ return results;
+};
+export type CutterBridgeExecutionState =
+ | "not_started"
+ | "complete"
+ | "failed"
+ | "unknown";
+export interface CutterBridgeExecution {
+ readonly requestId: string;
+ readonly output: unknown;
+ readonly currentFile: string | null;
+ readonly documentGeneration: number;
+ readonly identityStatus: "partial";
+ readonly cutterVersion: string | null;
+ readonly executionState: CutterBridgeExecutionState;
+ readonly error: string | null;
+ readonly message: string | null;
+ readonly outputTruncated: boolean;
+ readonly completionToken?: string;
+}
+
+class CutterBridgeRequestFailure extends Error {
+ constructor(
+ message: string,
+ readonly requestSent: boolean,
+ ) {
+ super(message);
+ }
+}
+
+export interface WindowsCutterDescriptorReader {
+ verifyDirectory(directory: string): boolean;
+ readDescriptor(directory: string, name: string, maxBytes: number): Buffer;
+}
+interface CutterBridgeClientOptions {
+ readonly platform?: NodeJS.Platform;
+ readonly windowsPrivateReader?: WindowsCutterDescriptorReader;
+}
+
+export type CutterBridgeSession = Omit<
+ z.infer,
+ "token" | "host" | "port"
+>;
+export interface CutterBridgeDiscovery {
+ readonly sessions: readonly CutterBridgeSession[];
+ readonly bridge_directory: string;
+ readonly discovery_status:
+ | "sessions_found"
+ | "no_live_bridge_found"
+ | "bridge_directory_unavailable"
+ | "bridge_directory_insecure"
+ | "bridge_directory_capacity_exceeded";
+ readonly bridge_directory_security:
+ | "private_verified"
+ | "not_private"
+ | "unverified_platform_acl"
+ | "not_checked";
+}
+
+/** Discovers and communicates with REA Python bridge plugins in live Cutter processes. */
+export class CutterBridgeClient {
+ readonly #directory: string;
+ readonly #platform: NodeJS.Platform;
+ readonly #windowsPrivateReader: WindowsCutterDescriptorReader | undefined;
+ readonly #pendingAcknowledgements = new Map>();
+
+ constructor(
+ environment: Readonly = process.env,
+ options: CutterBridgeClientOptions = {},
+ ) {
+ const configured = environment.REA_CUTTER_BRIDGE_DIR;
+ const localAppData = environment.LOCALAPPDATA;
+ this.#platform = options.platform ?? process.platform;
+ this.#windowsPrivateReader =
+ options.windowsPrivateReader ??
+ (this.#platform === "win32"
+ ? {
+ verifyDirectory: (directory) =>
+ z
+ .object({ privateDacl: z.literal(true) })
+ .safeParse(
+ requireWindowsNativeAuthority().call(
+ "cutter_bridge_verify_directory",
+ [directory],
+ ),
+ ).success,
+ readDescriptor: (directory, name, maxBytes) =>
+ z
+ .instanceof(Buffer)
+ .parse(
+ requireWindowsNativeAuthority().call(
+ "cutter_bridge_read_descriptor",
+ [directory, name, maxBytes],
+ ),
+ ),
+ }
+ : undefined);
+ this.#directory =
+ configured ??
+ (this.#platform === "win32" && localAppData
+ ? join(localAppData, "REA", "CutterBridge")
+ : join(homedir(), ".cache", "rea", "cutter-bridge"));
+ }
+
+ async listSessions(): Promise {
+ const loaded = await this.#readDescriptors();
+ if (loaded.error !== undefined)
+ return {
+ sessions: [],
+ bridge_directory: this.#directory,
+ discovery_status: loaded.error,
+ bridge_directory_security: loaded.security,
+ };
+ const descriptors = await mapDiscovery(
+ loaded.descriptors,
+ async (descriptor) => {
+ try {
+ const status = responseSchema.safeParse(
+ await this.#request(
+ descriptor,
+ serializeRequest(descriptor, {
+ kind: "status",
+ session_id: descriptor.session_id,
+ }),
+ ),
+ );
+ if (
+ !status.success ||
+ !status.data.ok ||
+ status.data.session_id !== descriptor.session_id
+ )
+ return undefined;
+ return {
+ session_id: descriptor.session_id,
+ pid: descriptor.pid,
+ document_generation:
+ status.data.document_generation ?? descriptor.document_generation,
+ current_file:
+ status.data.current_file === undefined
+ ? descriptor.current_file
+ : status.data.current_file,
+ cutter_version:
+ status.data.cutter_version ?? descriptor.cutter_version,
+ identity_status: "partial" as const,
+ };
+ } catch {
+ return undefined;
+ }
+ },
+ );
+ const sessions = descriptors.filter(
+ (value): value is CutterBridgeSession => value !== undefined,
+ );
+ return {
+ sessions,
+ bridge_directory: this.#directory,
+ discovery_status:
+ sessions.length > 0 ? "sessions_found" : "no_live_bridge_found",
+ bridge_directory_security: loaded.security,
+ };
+ }
+
+ async execute(input: {
+ readonly sessionId: string;
+ readonly expectedGeneration: number;
+ readonly command: string;
+ readonly json: boolean;
+ readonly signal?: AbortSignal;
+ }): Promise {
+ input.signal?.throwIfAborted();
+ const descriptor = await this.#find(input.sessionId);
+ input.signal?.throwIfAborted();
+ const requestId = randomUUID();
+ const commandRequest = serializeRequest(descriptor, {
+ kind: "command",
+ request_id: requestId,
+ expected_generation: input.expectedGeneration,
+ command: input.command,
+ json: input.json,
+ });
+ await this.#recoverCompletedCommands(descriptor, input.signal);
+ await this.#retryPendingAcknowledgements(descriptor);
+ input.signal?.throwIfAborted();
+ let completionToken: string | undefined;
+ let rawResponse: unknown;
+ try {
+ rawResponse = await this.#request(
+ descriptor,
+ commandRequest,
+ input.signal,
+ (ordinal) => {
+ completionToken = ordinal;
+ },
+ );
+ } catch (cause) {
+ if (!(cause instanceof CutterBridgeRequestFailure) || !cause.requestSent)
+ throw cause;
+ return {
+ requestId,
+ output: null,
+ currentFile: descriptor.current_file,
+ documentGeneration: input.expectedGeneration,
+ identityStatus: "partial",
+ cutterVersion: descriptor.cutter_version,
+ executionState: "unknown",
+ error: "transport-response-missing",
+ message: `${cause.message}; the command may have completed; do not retry automatically`,
+ outputTruncated: false,
+ ...(completionToken === undefined ? {} : { completionToken }),
+ };
+ }
+ const parsed = responseSchema.safeParse(rawResponse);
+ if (!parsed.success)
+ return {
+ requestId,
+ output: null,
+ currentFile: descriptor.current_file,
+ documentGeneration: input.expectedGeneration,
+ identityStatus: "partial",
+ cutterVersion: descriptor.cutter_version,
+ executionState: "unknown",
+ error: "malformed-bridge-response",
+ message:
+ "Cutter may have completed the command, but its response was invalid; do not retry automatically",
+ outputTruncated: false,
+ ...(completionToken === undefined ? {} : { completionToken }),
+ };
+ const response = parsed.data;
+ const executionState: CutterBridgeExecutionState = response.ok
+ ? response.execution_state === undefined ||
+ response.execution_state === "complete"
+ ? "complete"
+ : response.execution_state === "not_started"
+ ? "not_started"
+ : "unknown"
+ : response.execution_state === "failed"
+ ? "failed"
+ : response.execution_state === "not_started"
+ ? "not_started"
+ : "unknown";
+ if (executionState !== "unknown")
+ await this.#acknowledgeCompletion(descriptor, requestId);
+ if (response.error === "active-document-generation-changed")
+ throw new Error(
+ "Cutter's active document generation changed. List sessions again and select the current generation.",
+ );
+ return {
+ requestId,
+ output: response.output ?? null,
+ currentFile:
+ response.current_file === undefined
+ ? descriptor.current_file
+ : response.current_file,
+ documentGeneration:
+ response.document_generation ?? input.expectedGeneration,
+ identityStatus: "partial",
+ cutterVersion: response.cutter_version ?? descriptor.cutter_version,
+ executionState,
+ error: response.ok ? null : (response.error ?? "cutter-command-failed"),
+ message: response.message ?? null,
+ outputTruncated: response.output_truncated ?? false,
+ ...(completionToken === undefined ? {} : { completionToken }),
+ };
+ }
+
+ /** Confirm completion by exact receipt or authenticated connection watermark. */
+ async confirmCommandCompletion(
+ sessionId: string,
+ requestId: string,
+ completionToken?: string,
+ ): Promise> {
+ let descriptor: z.infer;
+ try {
+ descriptor = await this.#find(sessionId);
+ } catch (cause) {
+ return err(
+ new AnalysisCapabilityUnavailableError(
+ "cutter",
+ "cutter_command",
+ cause instanceof Error ? cause.message : "Cutter session unavailable",
+ { cause },
+ ),
+ );
+ }
+ try {
+ const raw = await this.#request(
+ descriptor,
+ serializeRequest(descriptor, {
+ kind: "status",
+ session_id: sessionId,
+ }),
+ );
+ const parsed = responseSchema.safeParse(raw);
+ if (
+ !parsed.success ||
+ !parsed.data.ok ||
+ parsed.data.session_id !== sessionId
+ )
+ return err(
+ new AnalysisOutputError(
+ "cutter_command_status",
+ "The bridge status response did not contain a valid session identity.",
+ { cause: parsed.success ? undefined : parsed.error },
+ ),
+ );
+ const completed = (parsed.data.completed_commands ?? []).some(
+ (receipt) => receipt.request_id === requestId,
+ );
+ if (completed) await this.#acknowledgeCompletion(descriptor, requestId);
+ return ok(
+ completed ||
+ (completionToken !== undefined &&
+ parsed.data.completed_connection !== undefined &&
+ parsed.data.completed_connection >= completionToken),
+ );
+ } catch (cause) {
+ return err(
+ new AnalysisCapabilityUnavailableError(
+ "cutter",
+ "cutter_command_status",
+ cause instanceof Error ? cause.message : "Cutter status failed",
+ { cause },
+ ),
+ );
+ }
+ }
+
+ async #recoverCompletedCommands(
+ descriptor: z.infer,
+ signal?: AbortSignal,
+ ): Promise {
+ const raw = await this.#request(
+ descriptor,
+ serializeRequest(descriptor, {
+ kind: "status",
+ session_id: descriptor.session_id,
+ }),
+ signal,
+ );
+ const status = responseSchema.safeParse(raw);
+ if (
+ !status.success ||
+ !status.data.ok ||
+ status.data.session_id !== descriptor.session_id
+ )
+ throw new AnalysisOutputError(
+ "cutter_command_status",
+ "The bridge status response did not contain a valid session identity.",
+ { cause: status.success ? undefined : status.error },
+ );
+ if (status.data.completed_connection === undefined)
+ throw new AnalysisOutputError(
+ "cutter_command_status",
+ "The bridge did not provide authenticated proof that earlier connections reached a terminal state.",
+ );
+ const pending =
+ this.#pendingAcknowledgements.get(descriptor.session_id) ?? new Set();
+ for (const receipt of status.data.completed_commands ?? [])
+ pending.add(receipt.request_id);
+ if (pending.size > 0)
+ this.#pendingAcknowledgements.set(descriptor.session_id, pending);
+ }
+
+ async #acknowledgeCompletion(
+ descriptor: z.infer,
+ requestId: string,
+ ): Promise {
+ try {
+ const raw = await this.#request(
+ descriptor,
+ serializeRequest(descriptor, {
+ kind: "ack",
+ request_id: requestId,
+ }),
+ );
+ const acknowledged = acknowledgementSchema.safeParse(raw);
+ if (!acknowledged.success)
+ throw new Error(
+ "Cutter did not confirm the completion receipt acknowledgement.",
+ );
+ const pending = this.#pendingAcknowledgements.get(descriptor.session_id);
+ pending?.delete(requestId);
+ if (pending?.size === 0)
+ this.#pendingAcknowledgements.delete(descriptor.session_id);
+ return true;
+ } catch {
+ const pending =
+ this.#pendingAcknowledgements.get(descriptor.session_id) ??
+ new Set();
+ pending.add(requestId);
+ this.#pendingAcknowledgements.set(descriptor.session_id, pending);
+ return false;
+ }
+ }
+
+ async #retryPendingAcknowledgements(
+ descriptor: z.infer,
+ ): Promise {
+ const pending = this.#pendingAcknowledgements.get(descriptor.session_id);
+ if (pending === undefined) return;
+ for (const requestId of pending) {
+ if (!(await this.#acknowledgeCompletion(descriptor, requestId)))
+ throw new AnalysisCapabilityUnavailableError(
+ "cutter",
+ "cutter_command",
+ "A retained Cutter completion receipt could not be acknowledged; no new command was sent.",
+ );
+ }
+ }
+
+ async #find(sessionId: string): Promise> {
+ const loaded = await this.#readDescriptors();
+ if (loaded.error === "bridge_directory_capacity_exceeded")
+ throw new AnalysisResourceConstraintError(
+ "cutter_command",
+ "transport",
+ "Cutter descriptor discovery exceeded its candidate budget; no command was sent.",
+ {
+ max_descriptor_candidates: MAX_DESCRIPTOR_CANDIDATES,
+ bridge_directory: this.#directory,
+ },
+ );
+ if (loaded.error !== undefined)
+ throw new AnalysisCapabilityUnavailableError(
+ "cutter",
+ "cutter_command",
+ `Cutter descriptor discovery failed: ${loaded.error}`,
+ );
+ const descriptor = loaded.descriptors.find(
+ (candidate) => candidate.session_id === sessionId,
+ );
+ if (descriptor === undefined)
+ throw new Error("Cutter bridge session is unavailable or stale");
+ return descriptor;
+ }
+
+ async listDescriptors(): Promise<
+ readonly z.infer[]
+ > {
+ const loaded = await this.#readDescriptors();
+ return loaded.descriptors;
+ }
+
+ async #readDescriptors(): Promise<{
+ readonly descriptors: readonly z.infer[];
+ readonly security:
+ | "private_verified"
+ | "not_private"
+ | "unverified_platform_acl"
+ | "not_checked";
+ readonly error?:
+ | "bridge_directory_unavailable"
+ | "bridge_directory_insecure"
+ | "bridge_directory_capacity_exceeded";
+ }> {
+ let directoryInfo;
+ try {
+ directoryInfo = await lstat(this.#directory);
+ } catch (cause) {
+ const code = (cause as NodeJS.ErrnoException).code;
+ if (code === "ENOENT")
+ return { descriptors: [], security: "not_checked" };
+ return {
+ descriptors: [],
+ security: "not_checked",
+ error: "bridge_directory_unavailable",
+ };
+ }
+ if (!directoryInfo.isDirectory() || directoryInfo.isSymbolicLink())
+ return {
+ descriptors: [],
+ security: "not_private",
+ error: "bridge_directory_insecure",
+ };
+ const currentUid =
+ this.#platform !== "win32" ? process.getuid?.() : undefined;
+ const posix = currentUid !== undefined;
+ if (
+ posix &&
+ (directoryInfo.uid !== currentUid || (directoryInfo.mode & 0o077) !== 0)
+ )
+ return {
+ descriptors: [],
+ security: "not_private",
+ error: "bridge_directory_insecure",
+ };
+ let security:
+ | "private_verified"
+ | "not_private"
+ | "unverified_platform_acl" = posix
+ ? "private_verified"
+ : "unverified_platform_acl";
+ const windowsPrivateReader = this.#windowsPrivateReader;
+ if (!posix) {
+ if (this.#platform !== "win32" || windowsPrivateReader === undefined)
+ return {
+ descriptors: [],
+ security,
+ error: "bridge_directory_insecure",
+ };
+ try {
+ if (!windowsPrivateReader.verifyDirectory(this.#directory))
+ return {
+ descriptors: [],
+ security: "not_private",
+ error: "bridge_directory_insecure",
+ };
+ security = "private_verified";
+ } catch {
+ return {
+ descriptors: [],
+ security,
+ error: "bridge_directory_insecure",
+ };
+ }
+ }
+ const entries: string[] = [];
+ try {
+ const directory = await opendir(this.#directory);
+ for await (const entry of directory) {
+ if (!/^cutter-\d+-[0-9a-f-]{36}\.json$/iu.test(entry.name)) continue;
+ if (entries.length === MAX_DESCRIPTOR_CANDIDATES)
+ return {
+ descriptors: [],
+ security,
+ error: "bridge_directory_capacity_exceeded",
+ };
+ entries.push(entry.name);
+ }
+ } catch {
+ return {
+ descriptors: [],
+ security: "not_checked",
+ error: "bridge_directory_unavailable",
+ };
+ }
+ const loaded = await mapDiscovery(entries, async (entry) => {
+ let file;
+ try {
+ let bytes: Buffer;
+ if (this.#platform === "win32") {
+ bytes = windowsPrivateReader!.readDescriptor(
+ this.#directory,
+ entry,
+ MAX_DESCRIPTOR_BYTES,
+ );
+ } else {
+ const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0);
+ file = await open(join(this.#directory, entry), flags);
+ const info = await file.stat();
+ if (
+ !info.isFile() ||
+ info.size > MAX_DESCRIPTOR_BYTES ||
+ info.uid !== currentUid ||
+ (info.mode & 0o077) !== 0
+ )
+ return undefined;
+ const boundedBytes = await readBoundedFileBytes(
+ file,
+ MAX_DESCRIPTOR_BYTES,
+ );
+ if (boundedBytes === undefined) return undefined;
+ bytes = boundedBytes;
+ }
+ if (bytes.byteLength > MAX_DESCRIPTOR_BYTES) return undefined;
+ const parsed = descriptorSchema.safeParse(
+ JSON.parse(bytes.toString("utf8")) as unknown,
+ );
+ if (
+ !parsed.success ||
+ entry !== `cutter-${parsed.data.pid}-${parsed.data.session_id}.json`
+ )
+ return undefined;
+ return parsed.data;
+ } catch {
+ return undefined;
+ } finally {
+ await file?.close().catch(() => undefined);
+ }
+ });
+ return {
+ descriptors: loaded.filter(
+ (value): value is z.infer =>
+ value !== undefined,
+ ),
+ security,
+ };
+ }
+
+ #request(
+ descriptor: z.infer,
+ requestBytes: AuthenticatedRequestBytes,
+ signal?: AbortSignal,
+ onAuthenticated?: (connectionOrdinal: string) => void,
+ ): Promise {
+ return new Promise((resolve, reject) => {
+ const connection = createConnection({
+ host: descriptor.host,
+ port: descriptor.port,
+ });
+ const frames = new CutterBridgeResponseFrames();
+ let settled = false;
+ let requestSent = false;
+ let authenticated = false;
+ const clientNonce = randomBytes(32).toString("hex");
+ let serverNonce = "";
+ let connectionOrdinal: string | undefined;
+ const finish = (complete: () => void): void => {
+ if (settled) return;
+ settled = true;
+ clearTimeout(timer);
+ signal?.removeEventListener("abort", onAbort);
+ complete();
+ };
+ const rejectRequest = (message: string): void =>
+ finish(() =>
+ reject(new CutterBridgeRequestFailure(message, requestSent)),
+ );
+ const timer = setTimeout(() => {
+ rejectRequest("Cutter bridge request timed out");
+ connection.destroy();
+ }, 25_000);
+ const onAbort = (): void => {
+ rejectRequest("Cutter bridge request was cancelled");
+ connection.destroy();
+ };
+ signal?.addEventListener("abort", onAbort, { once: true });
+ if (signal?.aborted) onAbort();
+ connection.once("connect", () => {
+ connection.write(
+ `${JSON.stringify({ kind: "rea-auth-v2", nonce: clientNonce })}\n`,
+ );
+ });
+ connection.on("data", (chunk: Buffer) => {
+ try {
+ frames.push(chunk, (line) => {
+ const frame: unknown = JSON.parse(line.toString("utf8"));
+ if (!authenticated) {
+ const challenge = z
+ .object({
+ session_id: z.string().uuid(),
+ nonce: z.string().regex(/^[0-9a-f]{64}$/),
+ proof: z.string().regex(/^[0-9a-f]{64}$/),
+ })
+ .parse(frame);
+ if (challenge.session_id !== descriptor.session_id)
+ throw new Error("unexpected peer session");
+ serverNonce = challenge.nonce;
+ connectionOrdinal = challenge.nonce.slice(0, 16);
+ const expected = createHmac("sha256", descriptor.token)
+ .update(
+ `rea-cutter-server-v2:${descriptor.session_id}:${clientNonce}:${challenge.nonce}`,
+ )
+ .digest();
+ if (
+ !timingSafeEqual(expected, Buffer.from(challenge.proof, "hex"))
+ )
+ throw new Error("peer proof failed");
+ onAuthenticated?.(connectionOrdinal);
+ const proof = createHmac("sha256", descriptor.token)
+ .update(
+ `rea-cutter-client-v2:${descriptor.session_id}:${clientNonce}:${challenge.nonce}:${requestBytes}`,
+ )
+ .digest("hex");
+ authenticated = true;
+ requestSent = true;
+ connection.write(
+ `${JSON.stringify({ request: requestBytes, proof })}\n`,
+ );
+ return true;
+ } else {
+ const envelope = z
+ .object({
+ response: z.string(),
+ proof: z.string().regex(/^[0-9a-f]{64}$/),
+ })
+ .parse(frame);
+ const expected = createHmac("sha256", descriptor.token)
+ .update(
+ `rea-cutter-response-v2:${descriptor.session_id}:${clientNonce}:${serverNonce}:${envelope.response}`,
+ )
+ .digest();
+ if (
+ !timingSafeEqual(expected, Buffer.from(envelope.proof, "hex"))
+ )
+ throw new Error("response proof failed");
+ const response: unknown = JSON.parse(envelope.response);
+ connection.end();
+ finish(() => resolve(response));
+ return false;
+ }
+ });
+ } catch {
+ rejectRequest("Cutter bridge peer authentication or response failed");
+ connection.destroy();
+ }
+ });
+ connection.once("error", (cause) =>
+ rejectRequest(`Cutter bridge is unavailable: ${cause.message}`),
+ );
+ connection.once("close", () =>
+ rejectRequest(
+ "Cutter bridge closed before returning a complete response",
+ ),
+ );
+ });
+ }
+}
diff --git a/src/cutter/CutterBridgeService.ts b/src/cutter/CutterBridgeService.ts
new file mode 100644
index 000000000..2d05249f0
--- /dev/null
+++ b/src/cutter/CutterBridgeService.ts
@@ -0,0 +1,192 @@
+import { jsonValueSchema } from "../domain/jsonValue.js";
+import {
+ AnalysisCancelledError,
+ AnalysisCapabilityUnavailableError,
+ AnalysisInputError,
+ AnalysisOutputError,
+} from "../domain/analysisErrorCore.js";
+import { AnalysisError } from "../domain/analysisErrorBase.js";
+import { createEvidence, type Evidence } from "../domain/evidence.js";
+import { err, ok, type Result } from "../domain/result.js";
+import { cutterCommandInputSchema } from "../contracts/cutterToolContracts.js";
+import {
+ CutterBridgeClient,
+ type CutterBridgeDiscovery,
+ type CutterBridgeExecution,
+} from "./CutterBridgeClient.js";
+import { CUTTER_PROVIDER_IDENTITY } from "./CutterBridgeClient.js";
+
+interface CutterBridgePort {
+ listSessions(): Promise;
+ confirmCommandCompletion?(
+ sessionId: string,
+ requestId: string,
+ completionToken?: string,
+ ): Promise>;
+ execute(input: {
+ readonly sessionId: string;
+ readonly expectedGeneration: number;
+ readonly command: string;
+ readonly json: boolean;
+ readonly signal?: AbortSignal;
+ }): Promise;
+}
+
+/** Shared Cutter bridge workflows used by the MCP adapter and CLI. */
+export class CutterBridgeService {
+ #cleanupAttempt: Promise> | undefined;
+ readonly #pendingCleanup = new Map<
+ string,
+ {
+ readonly sessionId: string;
+ readonly completionToken?: string;
+ readonly failure: AnalysisOutputError;
+ }
+ >();
+
+ constructor(readonly client: CutterBridgePort = new CutterBridgeClient()) {}
+
+ /** Release timed-out work only after producer-owned terminal proof. */
+ retryCleanup(): Promise> {
+ if (this.#cleanupAttempt !== undefined) return this.#cleanupAttempt;
+ const attempt = this.#retryPendingCleanup().finally(() => {
+ if (this.#cleanupAttempt === attempt) this.#cleanupAttempt = undefined;
+ });
+ this.#cleanupAttempt = attempt;
+ return attempt;
+ }
+
+ async #retryPendingCleanup(): Promise> {
+ for (const [requestId, pending] of this.#pendingCleanup) {
+ const confirmCommandCompletion = this.client.confirmCommandCompletion;
+ if (confirmCommandCompletion === undefined) return err(pending.failure);
+ const receipt = await confirmCommandCompletion.call(
+ this.client,
+ pending.sessionId,
+ requestId,
+ pending.completionToken,
+ );
+ if (!receipt.ok || !receipt.value) return err(pending.failure);
+ const { failure } = pending;
+ failure.resolveCleanup();
+ this.#pendingCleanup.delete(requestId);
+ }
+ return ok(null);
+ }
+
+ listSessions(): Promise {
+ return this.client.listSessions();
+ }
+
+ async execute(
+ rawInput: unknown,
+ options: { readonly signal?: AbortSignal } = {},
+ ): Promise> {
+ const parsed = cutterCommandInputSchema.safeParse(rawInput);
+ if (!parsed.success) return err(new AnalysisInputError("cutter_command"));
+ if (options.signal?.aborted)
+ return err(new AnalysisCancelledError("cutter_command"));
+ const input = parsed.data;
+ const cleanup = await this.retryCleanup();
+ if (!cleanup.ok) return err(cleanup.error);
+ try {
+ const output = await this.client.execute({
+ sessionId: input.session_id,
+ expectedGeneration: input.expected_generation,
+ command: input.command,
+ json: input.json,
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
+ });
+ const normalizedOutput = jsonValueSchema.parse(output.output);
+ const evidence = createEvidence(
+ undefined,
+ {
+ ...CUTTER_PROVIDER_IDENTITY,
+ version: output.cutterVersion,
+ },
+ {
+ predicateType: "rea.cutter.command-observation",
+ operation: "cutter_command",
+ parameters: {
+ session_id: input.session_id,
+ expected_generation: input.expected_generation,
+ command: input.command,
+ json: input.json,
+ },
+ result: {
+ command: input.command,
+ output: normalizedOutput,
+ execution_state: output.executionState,
+ error: output.error,
+ message: output.message,
+ output_truncated: output.outputTruncated,
+ cutter_version: output.cutterVersion,
+ current_file: output.currentFile,
+ document_generation: output.documentGeneration,
+ identity_status: output.identityStatus,
+ },
+ rawResult: normalizedOutput,
+ subjectUnavailableReason:
+ "Cutter exposes no verified stable identity for the active unsaved document or tab.",
+ limitations: [
+ "Document identity is partial: the bridge uses a plugin instance UUID and observed generation; a same-path tab switch or reload may not be distinguishable.",
+ "Cutter command effects depend on the Rizin command and active plugins.",
+ "The local IPC protocol is implemented by REA and is not an upstream Cutter API.",
+ ...(output.executionState === "unknown"
+ ? [
+ "Command completion or its output could not be confirmed; do not retry automatically because the command may have produced partial or persistent effects.",
+ ]
+ : []),
+ ...(output.outputTruncated
+ ? [
+ "The command completed or may have completed, but output exceeded the bridge transport limit and was omitted.",
+ ]
+ : []),
+ ],
+ },
+ );
+ if (output.executionState === "unknown") {
+ const failure = new AnalysisOutputError(
+ "cutter_command",
+ "Cutter command completion is unknown; the bound binary remains unavailable until the UI dispatcher confirms this request reached a terminal state.",
+ {
+ cleanup: {
+ reason:
+ "The exact UI dispatcher completion receipt has not arrived; the dispatched command may still be running.",
+ resources: [`cutter-session:${input.session_id}`],
+ },
+ partialObservation: evidence,
+ },
+ );
+ this.#pendingCleanup.set(output.requestId, {
+ sessionId: input.session_id,
+ ...(output.completionToken === undefined
+ ? {}
+ : { completionToken: output.completionToken }),
+ failure,
+ });
+ return err(failure);
+ }
+ return ok(evidence);
+ } catch (cause) {
+ if (cause instanceof AnalysisError) return err(cause);
+ const message =
+ cause instanceof Error
+ ? cause.message
+ : "Unknown Cutter bridge failure";
+ if (options.signal?.aborted && !message.includes("may have completed"))
+ return err(new AnalysisCancelledError("cutter_command", { cause }));
+ return err(
+ message.includes("generation changed") ||
+ message.includes("unavailable or stale")
+ ? new AnalysisInputError("cutter_command", { cause })
+ : new AnalysisCapabilityUnavailableError(
+ "cutter",
+ "cutter_command",
+ message,
+ { cause },
+ ),
+ );
+ }
+ }
+}
diff --git a/src/domain/analysisErrorBase.ts b/src/domain/analysisErrorBase.ts
index 60c3c0472..a4c446f71 100644
--- a/src/domain/analysisErrorBase.ts
+++ b/src/domain/analysisErrorBase.ts
@@ -65,7 +65,7 @@ export abstract class AnalysisError extends Error {
readonly diagnostics: Readonly> | undefined;
readonly capturedOutput: AnalysisCapturedOutput | undefined;
readonly cleanup: AnalysisCleanupObservation | undefined;
- readonly cleanupIncomplete: boolean;
+ cleanupIncomplete: boolean;
readonly cleanupResources: readonly string[];
constructor(message: string, options?: AnalysisErrorOptions) {
super(message, options);
@@ -89,6 +89,30 @@ export abstract class AnalysisError extends Error {
partialObservation: AnalysisPartialObservation | undefined;
readonly cleanupReport: ProcessCaptureCleanupReport | undefined = undefined;
+ /** Retain a later lifecycle failure without replacing the original typed error. */
+ retainSecondaryFailure(
+ failure: AnalysisError,
+ diagnostics: Readonly>,
+ ): this {
+ // Secondary resources remain owned by their cleanup lifecycle; preserve
+ // the primary error's live cleanup status and original output.
+ Object.assign(this, {
+ diagnostics: structuredClone({ ...this.diagnostics, ...diagnostics }),
+ cause: new AggregateError(
+ this.cause === undefined ? [failure] : [this.cause, failure],
+ "Analysis failed and a later lifecycle operation also failed.",
+ { cause: this.cause },
+ ),
+ });
+ return this;
+ }
+
+ /** Clear live cleanup status only after its owning resource confirms retry success. */
+ resolveCleanup(): this {
+ this.cleanupIncomplete = false;
+ return this;
+ }
+
/** Retain a completed observation when a later step fails. */
retainPartialObservation(observation: AnalysisPartialObservation): this {
this.partialObservation ??= observation;
diff --git a/src/domain/analysisErrorCore.ts b/src/domain/analysisErrorCore.ts
index 50db91ccd..380b440a7 100644
--- a/src/domain/analysisErrorCore.ts
+++ b/src/domain/analysisErrorCore.ts
@@ -86,13 +86,15 @@ export interface AnalysisInputIssue {
/** Provider output failed provider-neutral application parsing. */
export class AnalysisOutputError extends AnalysisError {
readonly _tag = "AnalysisOutputError";
+ readonly cleanupOnly: boolean;
constructor(
readonly operation: string,
readonly reason: string,
- options?: AnalysisErrorOptions,
+ options?: AnalysisErrorOptions & { readonly cleanupOnly?: boolean },
) {
super(`Invalid analysis output for ${operation}: ${reason}`, options);
+ this.cleanupOnly = options?.cleanupOnly ?? false;
}
}
diff --git a/src/domain/analysisErrorProjection.test.ts b/src/domain/analysisErrorProjection.test.ts
index 0766ca6e0..c0008156b 100644
--- a/src/domain/analysisErrorProjection.test.ts
+++ b/src/domain/analysisErrorProjection.test.ts
@@ -796,3 +796,32 @@ describe("analysis error projection: evidence files", () => {
},
);
});
+
+it("retains the primary failure and both cleanup reports after a secondary failure", () => {
+ const primary = new AnalysisCancelledError("execute_rizin_command", {
+ capturedOutput: {
+ stdout: "partial",
+ stderr: "diagnostic",
+ truncated: false,
+ },
+ cleanup: { reason: "writer remains", resources: ["writer"] },
+ });
+ const secondary = new AnalysisOutputError("open_binary", "cannot rebind", {
+ cleanup: { reason: "provider remains", resources: ["provider", "writer"] },
+ });
+ const enriched = primary.retainSecondaryFailure(secondary, {
+ rebind_error: projectAnalysisError(secondary),
+ });
+ expect(enriched).toBe(primary);
+ expect(enriched._tag).toBe("AnalysisCancelledError");
+ expect(projectAnalysisError(enriched)).toMatchObject({
+ code: "cleanup_incomplete",
+ details: {
+ execution_failure: "cancelled",
+ resources: ["writer"],
+ cleanup_reason: "writer remains",
+ captured_output: { stdout: "partial", stderr: "diagnostic" },
+ diagnostics: { rebind_error: { code: "cleanup_incomplete" } },
+ },
+ });
+});
diff --git a/src/domain/analysisErrorProjection.ts b/src/domain/analysisErrorProjection.ts
index bfdf7eb42..fbd1b5386 100644
--- a/src/domain/analysisErrorProjection.ts
+++ b/src/domain/analysisErrorProjection.ts
@@ -447,8 +447,8 @@ const lifecycleErrorDetails = (
if (error instanceof AnalysisCancelledError)
return {
operation: error.operation,
- cleanup: error.cleanup === undefined ? "complete" : "incomplete",
- ...(error.cleanup === undefined
+ cleanup: error.cleanupIncomplete ? "incomplete" : "complete",
+ ...(error.cleanup === undefined || !error.cleanupIncomplete
? {}
: {
cleanup_reason: error.cleanup.reason,
@@ -462,7 +462,7 @@ const lifecycleErrorDetails = (
return {
operation: error.operation,
timeout_ms: error.timeoutMs,
- ...(error.cleanup === undefined
+ ...(error.cleanup === undefined || !error.cleanupIncomplete
? {}
: {
cleanup: "incomplete",
diff --git a/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.test.ts b/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.test.ts
index 410546543..d75a23b1a 100644
--- a/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.test.ts
+++ b/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.test.ts
@@ -26,6 +26,36 @@ const add = (
if (!result.ok) throw result.error;
};
+it("bounds query parser expansion before publishing or merging observed names", () => {
+ const accumulator = new WebNetworkApiSurfaceAccumulator(64 * 1024, true);
+ add(accumulator, record(0, "/products?kept=1"));
+ const before = accumulator.page(0, 1);
+ expect(
+ accumulator.add(record(1, `/products?${"x=1&".repeat(200)}`)),
+ ).toMatchObject({
+ ok: false,
+ error: { _tag: "AnalysisResourceConstraintError" },
+ });
+ expect(accumulator.page(0, 1)).toEqual(before);
+ add(accumulator, record(2, "/products?after=2"));
+ expect(accumulator.page(0, 1).groups[0]?.query_parameter_names).toEqual([
+ { name: "after", record_ordinals: [2] },
+ { name: "kept", record_ordinals: [0] },
+ ]);
+});
+
+it("charges each retained name once and each name's ordinal once per request", () => {
+ const accumulator = new WebNetworkApiSurfaceAccumulator(1024 * 1024, true);
+ for (let ordinal = 0; ordinal < 1000; ordinal += 1)
+ add(accumulator, record(ordinal, "/products?category=one&category=two"));
+ expect(accumulator.page(0, 1).groups[0]?.query_parameter_names).toEqual([
+ {
+ name: "category",
+ record_ordinals: Array.from({ length: 1000 }, (_, index) => index),
+ },
+ ]);
+});
+
it("admits the capture schema clone and grouping against the same budget", () => {
const sample = record(0, `/${"a".repeat(1200)}`);
const isolated = new WebNetworkApiSurfaceAccumulator(64 * 1024);
diff --git a/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.ts b/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.ts
index 0abc64675..16f3d8efb 100644
--- a/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.ts
+++ b/src/domain/analysisView/WebNetworkApiSurfaceAccumulator.ts
@@ -5,7 +5,10 @@ import {
} from "../analysisErrorCore.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import { compareUnicodeCodePoints as compare } from "../unicodeCodePointOrder.js";
-import { parseRecordedTransportUrl } from "../recordedTransportUrl.js";
+import {
+ parseRecordedTransportUrl,
+ parseUrlQueryParameterNames,
+} from "../recordedTransportUrl.js";
import { err, ok, type Result } from "../result.js";
import { MAX_JSON_DEPTH } from "../jsonValue.js";
import {
@@ -41,6 +44,7 @@ interface GroupState {
readonly ordinals: number[];
readonly statuses: Set;
readonly contentTypes: Set;
+ readonly queryNames?: Map;
}
type Buckets = Map>>>;
@@ -120,7 +124,10 @@ export class WebNetworkApiSurfaceAccumulator {
#retainedBytes = 0;
#temporaryBytes = 0;
- constructor(readonly maximumBytes = WEB_NETWORK_CAPTURE_LIMITS.outputBytes) {}
+ constructor(
+ readonly maximumBytes = WEB_NETWORK_CAPTURE_LIMITS.outputBytes,
+ readonly includeQueryParameterNames = false,
+ ) {}
#retain(bytes: number): boolean {
if (this.#retainedBytes + this.#temporaryBytes + bytes > this.maximumBytes)
@@ -305,7 +312,9 @@ export class WebNetworkApiSurfaceAccumulator {
if (!this.#temporary(RECORD_SCRATCH_BYTES + textBytes(url.length)))
return this.#overBudget();
try {
- const parsed = parseRecordedTransportUrl(url);
+ const parsed = parseRecordedTransportUrl(url, {
+ includeQuery: this.includeQueryParameterNames,
+ });
if (parsed.origin === null)
return this.#exclude(record.ordinal, "url-unparseable");
if (typeof method !== "string" || method.length === 0)
@@ -325,6 +334,8 @@ export class WebNetworkApiSurfaceAccumulator {
return this.#overBudget();
const route = this.#route(parsed.path);
if (!route.ok) return route;
+ const queryNames = this.#queryNames(parsed.query);
+ if (!queryNames.ok) return queryNames;
return this.#append(
record.ordinal,
method,
@@ -332,6 +343,7 @@ export class WebNetworkApiSurfaceAccumulator {
route.value,
typeof status === "number" && status !== 0 ? status : null,
mediaTypeEssence(rawMime),
+ queryNames.value,
);
} finally {
// Per-record strings and descriptors never become cumulative scratch.
@@ -339,6 +351,26 @@ export class WebNetworkApiSurfaceAccumulator {
}
}
+ #queryNames(
+ query: string | undefined,
+ ): Result | undefined, AnalysisError> {
+ if (query === undefined) return ok(undefined);
+ // Reserve form-parser tuples, decoded strings, the iterator and the name
+ // set before URLSearchParams materializes them. Delimiter count is an upper
+ // bound: empty query pairs are ignored by the WHATWG parser.
+ let pairs = query.length > 1 ? 1 : 0;
+ for (const character of query) if (character === "&") pairs += 1;
+ if (
+ !this.#temporary(
+ RECORD_SCRATCH_BYTES +
+ textBytes(query.length) +
+ pairs * ENTRY_BYTES * 4,
+ )
+ )
+ return this.#overBudget();
+ return ok(new Set(parseUrlQueryParameterNames(query)));
+ }
+
#append(
ordinal: number,
method: string,
@@ -346,6 +378,7 @@ export class WebNetworkApiSurfaceAccumulator {
route: Route,
status: number | null,
mime: string | null,
+ queryNames: ReadonlySet | undefined,
): Result