Compare commits

..
Author SHA1 Message Date
dependabot[bot] 3264619ebe chore(deps): bump actions/setup-node from 4.4.0 to 7.1.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4.4.0 to 7.1.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4.4.0...949feb2413d6458794dcd2491c4babbbce0c15c1)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-11-13 21:57:11 +00:00
171 changed files with 1921 additions and 2955 deletions

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
ref: ${{ github.event.workflow_run.head_sha }}
path: triggering-source
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: trusted-retention/.nvmrc
- name: Retain current Turbo snapshots and npm download caches
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+17 -17
View File
@@ -92,7 +92,7 @@ jobs:
with:
ref: ${{ inputs.source_sha || github.sha }}
fetch-depth: 0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Bind qualification to the caller revision
@@ -144,7 +144,7 @@ jobs:
CHANGES_RESULT: ${{ needs.changes.result }}
run: test "${CHANGES_RESULT}" = "success"
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Validate source architecture before dependency installation
@@ -213,7 +213,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -278,7 +278,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -299,7 +299,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -337,7 +337,7 @@ jobs:
shard: ${{ fromJSON(needs.changes.outputs.tier == 'fast' && '["1/8","2/8","3/8","4/8","5/8","6/8","7/8","8/8"]' || '["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
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -377,7 +377,7 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -428,7 +428,7 @@ jobs:
actions: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -512,7 +512,7 @@ jobs:
run: test "${CHANGES_RESULT}" = "success"
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
if: needs.changes.outputs.code == 'true' && success()
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: needs.changes.outputs.code == 'true' && success()
with:
node-version-file: .nvmrc
@@ -641,7 +641,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
@@ -702,7 +702,7 @@ jobs:
UV_THREADPOOL_SIZE: "2"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -760,7 +760,7 @@ jobs:
timeout-minutes: 25
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -831,7 +831,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -862,7 +862,7 @@ jobs:
timeout-minutes: 25
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -986,7 +986,7 @@ jobs:
tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts
tests/boundary/filesystem/javascriptPackagePrecedence.test.ts
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22.23.4
- name: Verify native path boundaries on Node 22
@@ -1011,7 +1011,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -1211,7 +1211,7 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Require every selected lane to pass
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
GHIDRA_RELEASE_SHA256: ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Validate the SDK pin against the provider's verified release
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
config: stryker.semantics.config.mjs
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
COMPlus_GCHeapHardLimit: "30000000"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Fetch checksum-pinned engine and public APK fixture
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
timeout-minutes: 12
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Install the cross compiler used by the Windows native lane
@@ -53,7 +53,7 @@ jobs:
runner: [self-hosted, cutter-upstream, Windows]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
@@ -65,7 +65,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- run: npm ci
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 25
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -44,7 +44,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -40,7 +40,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
ca-certificates git python3-venv gcc libc6-dev strace gdb
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -39,7 +39,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
+7 -7
View File
@@ -88,7 +88,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: steps.editorial.outputs.frozen != 'true'
with:
node-version-file: .nvmrc
@@ -126,7 +126,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -248,7 +248,7 @@ jobs:
persist-credentials: false
- name: Set up Node.js for checkpoint validation
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -323,7 +323,7 @@ jobs:
- name: Set up Node.js for generated documentation
if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -385,7 +385,7 @@ jobs:
with:
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -416,7 +416,7 @@ jobs:
with:
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -454,7 +454,7 @@ jobs:
with:
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
node: ["^22.23.4", "24.22.1", "^24.22.1", "^26.12.1"]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
check-latest: true
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
cache: pip
cache-dependency-path: website/requirements.txt
- name: Set up Node.js for the website Worker checks
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Install sharing-image dependencies
+2 -2
View File
@@ -55,7 +55,7 @@ jobs:
cache: pip
cache-dependency-path: website/requirements.txt
- name: Set up Node.js for the website Worker checks
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Install sharing-image dependencies
@@ -113,7 +113,7 @@ jobs:
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Set up Node.js for Wrangler
if: inputs.target == 'both'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Download the verified website
+30 -15
View File
@@ -357,17 +357,11 @@ The process restarts when the environment changes, after 32 commands, or when
the worker reports a resident set of at least 1 GiB. A fatal worker failure
or a broken session frame retires that process and waits for it to exit before
the next command starts.
In a `cliTest` file, `cli.run` uses that shared process when it reports the
command exactly as a fresh `rea` process would. Both run the same one-shot
owner (`src/cliOneShot.ts`), and the shared process presents the launcher's
entrypoint. A command starts a fresh process instead when it supplies stdin or
an explicit deadline, enters MCP or the MCP doctor (which the launcher routes
first), sets `NODE_OPTIONS`, or enables logging, which writes outside the
per-command capture. Pass `freshProcess: true` for process-level facts such as
startup, signals, or exit, and keep one fresh check beside reused cases where
dispatch itself is under test. `createTestCli` drivers always start a fresh
process. File teardown stops the shared CLI process, including startup still in
flight, and does not open another after close.
`cli.run` still starts a fresh process, including stdin and explicit deadlines.
`runFileCli` uses the file session for commands that have neither. Go binary,
IL2CPP metadata, and dylib-resolution commands use that session. File teardown
stops the shared CLI process, including startup still in flight, and does not
open another after close.
`exportedFunctionBindingsCli.test.ts` uses the shared compiled CLI session for
its file and compares its JSON command results with the MCP session. It selects
20-second startup and command deadlines; other callers retain their existing
@@ -1097,11 +1091,32 @@ existing four-minute timing collection plus the bounded diagnostic stages.
Fast built shards target under ten minutes of uncontended shard-job execution.
That figure is the `test-shard` job. The job waits for classification and the
build, and the portable-test merge runs after the slowest shard. Runner queue is
separate and grows when more shard jobs compete for runners. The npm download
cache stays the setup cache rather than a shared `node_modules` artifact.
build, and the portable-test merge runs after the slowest shard. On 2026-10-20,
run `39451548945` measured those surrounding jobs with runner queue excluded:
classification 15 seconds, build 49 seconds, and the portable-test merge 29
seconds. Shard 1/5 of that run spent 22 seconds before built Vitest and 3
seconds after it, including `npm ci` at 9 seconds. `node_modules` is about 674
MiB, so publishing it as a shared artifact would transfer more data than that
install saves. The npm download cache stays the setup cache.
File timings from that run put the built suite at 7944 seconds of module time.
The sequencer keeps each file's full duration on one worker. Extra workers
overlap other files in the same sequence group, and `process-boundary` runs
after that group. Scheduling the unsplit inventory onto five shards peaks at
820 seconds of tests. That run's slowest built shard job lasted 925 seconds
(shard 4/5), and shard 1/5 spent 874 seconds in built Vitest, so hosted job
time sits above the file schedule. Six shards of the same inventory peak at
684 seconds of tests. Eight shards, after the 578-second loader file is split
by case count (54 cases stay in that file and 46 move into each of three
files), peak at 513 seconds of tests. With the measured 25-second shard setup,
that shard job is about 538 seconds. Adding the measured classification, build,
and merge makes an uncontended portable critical path of about 631 seconds,
still before the gap between this file schedule and hosted Vitest. Runner queue
is separate and grows when more shard jobs compete for runners.
`process-boundary` stays serial because process-tree sampling shares the host.
Hosted CI jobs use four workers; other hosts stay at two
Raising the worker cap from two to four on this four-core host cut a six-file
acceptance sample from 96–101 seconds to 71 seconds and did not remove the
single-file floor. Hosted CI jobs use four workers; other hosts stay at two
because they are shared with other checks. Full qualification keeps six covered
shards; its longer instrumentation is outside this fast budget.
-6
View File
@@ -96,10 +96,6 @@ Persistent command Evidence retains the caller-selected and resolved startup pat
Session status retains up to 64 recent protocol frames. Each history entry keeps a complete-character UTF-8 prefix of at most 16 KiB, plus a truncation marker when needed; `recent_output_truncated` reports omitted history. This history budget also accounts for replacement characters from invalid UTF-8. It does not shorten command Evidence, whose output budget defaults to 16 MiB. Rizin debug startup waits 30000 ms with no runtime override. Each command frame defaults to 120000 ms; its deadline, command-output budget, session capacity and pending-command queue use the [shared debugger launch settings](#debugger-launch-settings). The 16777216-byte frame cap and 4096-frame command cap remain fixed. Evidence names the cap that stopped the command. The one-shot `command_timeout_ms` setting does not change a debug session.
### Stopped debugger sessions
A Rizin or GDB session stops accepting commands when REA stops it, for example after an output or frame limit, a command deadline, cancellation or a protocol failure, or when the debugger process exits. REA keeps the stopped session and the first reason it observed until `close_rizin_debug_session` or `close_gdb_session` releases it, including after a retried cleanup succeeds. `rizin_debug_session_status` and `gdb_session_status` then report `state: "closed"` with that reason in `closed_reason`. A later `rizin_debug_command` or `gdb_console` on the session fails with `capability_unavailable`; `details.reason` carries the same reason, and the remediation is to close the session and start a new one. A `session_id` that this server never returned, or that close already released, fails with `invalid_request` instead.
### Debugger launch settings
GDB and Rizin share these settings in the environment that launches REA:
@@ -140,8 +136,6 @@ rea debug-with-rizin --inferior-tty /dev/pts/7 ./program dr
rea execute-rizin-command ./program iI
```
Rizin exits with status 0 even when its shell refuses a statement, so the exit code does not show whether the command ran. Rizin 0.6 through 0.9.1 log one of four stderr lines for such a refusal: `ERROR: Command '…' does not exist.`, ``ERROR: Wrong number of arguments passed to `…`…``, `ERROR: core: Error while executing command: …` (an invalid statement), or `ERROR: core: Error while parsing command: …`. Rizin then skips the refused statement and every later one in the command. When the completed process's stderr contains one of these lines, REA reports `completion_status: "rejected"` and adds a limitation naming the refusal. stdout then holds only the output of earlier statements, so empty stdout is not an empty result. Other `ERROR:` lines, such as `ERROR: Cannot find function at …`, come from commands that ran; REA keeps them in stderr and reports `complete`. A command that lowers `log.level` or redirects stderr hides refusals from REA.
Captured stdout and stderr use the same `max_output_bytes` (CLI: `--max-output-bytes`) admission and recovery described above, default 16 MiB. One-shot commands independently use `command_timeout_ms`. Raising output admission cannot recover an earlier capture; command effects and cleanup must be checked before another invocation.
### Decoder and map inputs
+8 -2
View File
@@ -842,8 +842,14 @@ branches are not producers. A tier with no qualifying artifact leaves history
unset, and Vitest uses its own assignment. A later schedule that reuses
qualification without uploading timings leaves the previous artifact in place.
Each tier keeps its own history because stale weights from another tier, or
from before a large test change, unbalance the shards.
On 2026-10-21 the newest full history was run
`39545047294`, recorded before compiled-CLI session reuse. Fast run
`39552964873` packed its eight built shards from that history, and those shard
jobs executed for 319–591 seconds. Replaying that run's file durations through
the sequencer's two-worker schedule balances every shard at about 404 seconds
of file time. The same replay with the stale history weights leaves the slowest
shard near 547 seconds and the fastest near 293. That spread is file-schedule
time, not runner queue, which is why each tier keeps its own history.
CI assigns discovered files by historical duration rather than dividing only
by file count. A file with no recorded duration, such as a newly added or
renamed test, is weighted at the median recorded duration until a published
-1
View File
@@ -3,7 +3,6 @@
"entry": [
"src/main.ts",
"src/cli.ts",
"src/cliOneShot.ts",
"src/cliOutput.ts",
"src/cliBuiltinArguments.ts",
"src/cli/cliResultOutput.ts",
-1
View File
@@ -3,7 +3,6 @@
"entry": [
"src/main.ts!",
"src/cli.ts!",
"src/cliOneShot.ts!",
"src/cliOutput.ts!",
"src/cliBuiltinArguments.ts!",
"src/cli/cliResultOutput.ts!",
+1 -1
View File
@@ -24,7 +24,7 @@ export const bundleRuntime = async (root) => {
"cli-runtime",
{
cli: "cli.js",
cliOneShot: "cliOneShot.js",
cliOutput: "cliOutput.js",
cliBuiltinArguments: "cliBuiltinArguments.js",
cliResultOutput: "cli/cliResultOutput.js",
mcpDoctor: "mcpDoctor.js",
+1 -1
View File
@@ -54,7 +54,7 @@ if (await pathExists(join(root, "src"))) {
"dist/main.js",
"dist/mcp/main.js",
"dist/cli-runtime/cli.js",
"dist/cli-runtime/cliOneShot.js",
"dist/cli-runtime/cliOutput.js",
"dist/cli-runtime/cliBuiltinArguments.js",
"dist/cli-runtime/cliResultOutput.js",
"dist/cli-runtime/mcpDoctor.js",
+142 -54
View File
@@ -64,7 +64,7 @@ const runtimeFiles = [
? ["dist/mcp/main.js", "dist/cli-runtime/mcpDoctor.js"]
: [
"dist/cli-runtime/cli.js",
"dist/cli-runtime/cliOneShot.js",
"dist/cli-runtime/cliOutput.js",
"dist/cli-runtime/cliResultOutput.js",
]),
];
@@ -104,66 +104,154 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
);
}
} else {
const { runOneShotCli } = await import("../dist/cli-runtime/cliOneShot.js");
const {
cliConfigurationError,
renderCliOutputArgumentError,
renderEmptyFilteredCliOutput,
renderVersionReport,
parseCliOutputArguments,
} = await import("../dist/cli-runtime/cliOutput.js");
guardClosedStdout();
const cliArgs = isShellCompletion
? args
: builtinArguments.normalizeCliArguments(args);
// Completion words are transport data, including partial output flags.
const outputOptions = parseCliOutputArguments(
isShellCompletion ? [] : cliArgs,
);
const outputArguments = outputOptions.validation;
// `--version` with an explicit format reports the runtime as a document;
// plain `--version` stays Incur's bare version string.
const versionOutput = renderVersionReport(
outputOptions,
{
packageName: packageJson.name,
packageVersion: packageJson.version,
versions: process.versions,
platform: process.platform,
architecture: process.arch,
},
process.stdout.isTTY === true,
);
const configurationError = cliConfigurationError(process.env);
let dispatchFailure;
let closeCliResources;
// Incur can exit from the callback below before this function returns.
// That callback owns the status, including a closed-pipe exit of 141.
let exiting = false;
try {
await runOneShotCli(args, {
environment: process.env,
runtime: {
packageName: packageJson.name,
packageVersion: packageJson.version,
versions: process.versions,
platform: process.platform,
architecture: process.arch,
},
stdoutIsTerminal: process.stdout.isTTY === true,
resultDestination: process.stdout,
closedStdoutStatus: SIGPIPE_EXIT_STATUS,
writeStdout,
writeStderr: (text) => {
process.stderr.write(text);
},
stdoutClosed: () => stdoutClosed,
exitCode: () =>
typeof process.exitCode === "number" ? process.exitCode : 0,
setExitCode: (code) => {
process.exitCode = code;
},
ownCliResources: (close) => {
closeCliResources = close;
},
exitRequested: () => exiting,
// A command failure caused by the closed pipe keeps the pipe's status.
// Worker shutdown is shared with the dispatch finally below. A failed
// shutdown is reported here because this callback is what exits.
exit: (code, finishOutput) => {
exiting = true;
void (async () => {
let status = code;
try {
await closeCliResources();
} catch (cleanup) {
process.stderr.write(
`Failed to close CLI resources: ${cleanupFailureText(cleanup)}\n`,
);
if (status === 0) status = 1;
}
try {
await finishOutput(status);
} catch (cause) {
if (!stdoutClosed)
process.stderr.write(`${cleanupFailureText(cause)}\n`);
if (status === 0) status = 1;
}
process.exit(stdoutClosed ? SIGPIPE_EXIT_STATUS : status);
})();
},
});
// Doctor is the readiness report for this failure. Other commands still stop
// here so they do not run with an unusable log level.
if (
configurationError !== undefined &&
(isShellCompletion ||
builtinArguments.splitIncurBuiltinArguments(cliArgs).command[0] !==
"doctor")
) {
process.stderr.write(`${configurationError}\n`);
process.exitCode = 1;
} else if (!outputArguments.ok) {
writeStdout(renderCliOutputArgumentError(outputArguments));
process.exitCode = 1;
} else if (versionOutput !== undefined) {
writeStdout(versionOutput);
} else if (
!isShellCompletion &&
builtinArguments.isProjectScopedAmpRegistration(cliArgs)
) {
// Amp has global settings only. Share the parsed scope fact with the
// registration workflow and refuse before loading the command runtime.
//
// A caller that asked for a structured document gets the refusal in
// that document, through the same pre-execution error renderer the
// output-argument branch uses; anything else keeps the plain message.
const refusalMessage =
"Amp does not support project-scoped registration. Run without --no-global to register Amp globally, or select a client with project settings. No client settings were changed.";
const refusalFormat = outputOptions.format;
if (
refusalFormat === "json" ||
refusalFormat === "jsonl" ||
refusalFormat === "yaml"
) {
writeStdout(
renderCliOutputArgumentError({
ok: false,
format: refusalFormat,
code: "AMP_PROJECT_SCOPE_UNSUPPORTED",
message: refusalMessage,
}),
);
} else {
process.stderr.write(`${refusalMessage}\n`);
}
process.exitCode = 1;
} else {
const cliRuntime = await import("../dist/cli-runtime/cli.js");
closeCliResources = cliRuntime.closeCliResources;
const { createCli } = cliRuntime;
const { createCliResultOutput } =
await import("../dist/cli-runtime/cliResultOutput.js");
let wroteOutput = false;
const resultOutput = createCliResultOutput(outputOptions, process.stdout);
try {
await createCli(process.env, resultOutput).serve(cliArgs, {
stdout: (output) => {
if (resultOutput?.handled) {
if (resultOutput.failed && !stdoutClosed)
process.stderr.write(output);
return;
}
if (output.length > 0) wroteOutput = true;
if (resultOutput?.captureSdkOutput(output)) return;
writeStdout(output);
},
// A command failure caused by the closed pipe keeps the pipe's status.
// Worker shutdown is shared with the dispatch finally below. A failed
// shutdown is reported here because this callback is what exits.
exit: (code) => {
exiting = true;
void (async () => {
let status = typeof code === "number" ? code : 0;
try {
await closeCliResources();
} catch (cleanup) {
process.stderr.write(
`Failed to close CLI resources: ${cleanupFailureText(cleanup)}\n`,
);
if (status === 0) status = 1;
}
try {
await resultOutput?.finishSdkOutput(status);
} catch (cause) {
if (!stdoutClosed)
process.stderr.write(`${cleanupFailureText(cause)}\n`);
if (status === 0) status = 1;
}
process.exit(stdoutClosed ? SIGPIPE_EXIT_STATUS : status);
})();
},
});
} catch (cause) {
if (wroteOutput) throw cause;
const recovered = await resultOutput
?.recoverFormattingFailure(cause)
.catch((writeFailure) => {
// Recovery writes outside Incur's command boundary. A reader that
// declined this document keeps the same quiet SIGPIPE outcome.
if (!stdoutClosed) throw writeFailure;
return true;
});
if (!recovered) throw cause;
process.exitCode = stdoutClosed ? SIGPIPE_EXIT_STATUS : 1;
}
if (!wroteOutput && !resultOutput?.handled)
writeStdout(renderEmptyFilteredCliOutput(outputOptions) ?? "");
// An SDK failure exits through the callback, which owns its pending write.
if (!exiting)
await resultOutput?.finishSdkOutput(
typeof process.exitCode === "number" ? process.exitCode : 0,
);
}
} catch (cause) {
if (cause?.code === "EPIPE") {
stdoutClosed = true;
@@ -11,11 +11,6 @@ export interface ReverseEngineeringProviderCommand {
readonly identity: EvidenceProvider;
/** What the provider cannot attest about the arguments it built. */
readonly limitations?: readonly string[];
/**
* Explain a provider refusal of the command that a completed process
* reported only in its diagnostics, or return undefined.
*/
readonly rejection?: (stderr: string) => string | undefined;
readonly environmentOverrides?: Readonly<NodeJS.ProcessEnv>;
}
@@ -599,7 +599,7 @@ export class ReverseEngineeringService {
let stderrInvalidUtf8 = false;
let cleanupIncomplete = false;
let cleanupOnlyFailure = false;
let completionStatus: "complete" | "rejected" | "unknown" = "complete";
let completionStatus: "complete" | "unknown" = "complete";
let outputLimitReached = false;
let failure: AnalysisError | undefined;
let cleanupOwner: ProviderProcessSupervisor | undefined;
@@ -1076,15 +1076,6 @@ export class ReverseEngineeringService {
: digestAfter !== digest;
const outputBudgetSource =
parameters.max_output_bytes === undefined ? "default" : "request";
// Only a completed process's diagnostics can establish a refusal; partial
// or failed captures keep their unknown completion.
const rejection =
completionStatus === "complete" &&
(failure === undefined ||
(failure instanceof AnalysisOutputError && failure.cleanupOnly))
? profile.rejection?.(stderr)
: undefined;
if (rejection !== undefined) completionStatus = "rejected";
const result = {
stdout,
stderr,
@@ -1125,7 +1116,6 @@ export class ReverseEngineeringService {
]
: []),
...(profile.limitations ?? []),
...(rejection === undefined ? [] : [rejection]),
...(stdoutInvalidUtf8 || stderrInvalidUtf8
? [
`Captured ${[
+7 -7
View File
@@ -22,7 +22,7 @@ import { parseAnalysisInput } from "../application/AnalysisInput.js";
import { androidJniSelectionSchema } from "../domain/android/androidJniBindings.js";
import { androidJniDeliverySchema } from "../domain/android/androidJniQuery.js";
import { androidInputSchemas } from "../domain/android/androidAnalysis.js";
import { cliDecimalField } from "./options.js";
import { cliIntegerArgument } from "./options.js";
/** Expose the same explicit APK requests through one-shot CLI commands. */
export const registerAndroidCommands = (
@@ -223,11 +223,11 @@ export const registerAndroidCommands = (
),
}),
options: z.object({
overloadIndex: cliDecimalField(
androidInputSchemas.inspect_android_method.shape.overload_index.describe(
overloadIndex: cliIntegerArgument(0)
.optional()
.describe(
"Explicit zero-based index among same-name methods; required when ambiguous",
),
),
mappingPath,
...analysisEvidenceDeliveryOptions,
}),
@@ -255,11 +255,11 @@ export const registerAndroidCommands = (
options: z.object({
methodName:
androidInputSchemas.trace_android_references.shape.method_name,
overloadIndex: cliDecimalField(
androidInputSchemas.trace_android_references.shape.overload_index.describe(
overloadIndex: cliIntegerArgument(0)
.optional()
.describe(
"Index among defined methods or observed external prototypes listed by an ambiguous query",
),
),
}),
run: ({ args, options }) =>
execute(
+3 -6
View File
@@ -8,8 +8,7 @@ import { logCliCommand } from "../cliLogging.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import { withCommandCancellation } from "./commandCancellation.js";
import type { CliInstance } from "./types.js";
import { cliDecimalField } from "./options.js";
import { cutterCommandInputSchema } from "../contracts/cutterToolContracts.js";
import { cliIntegerArgument } from "./options.js";
/** Register commands that control an already-running Cutter instance. */
export const registerCutterCommands = (
@@ -44,10 +43,8 @@ export const registerCutterCommands = (
.string()
.uuid()
.describe("Session ID copied from list-cutter-sessions."),
expectedGeneration: cliDecimalField(
cutterCommandInputSchema.shape.expected_generation.describe(
"Session generation returned by list-cutter-sessions.",
),
expectedGeneration: cliIntegerArgument(0).describe(
"Session generation returned by list-cutter-sessions.",
),
command: z
.string()
+6 -8
View File
@@ -38,7 +38,7 @@ import { inspectWebNetworkCaptureInputSchema } from "../domain/webNetworkCapture
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import type { CliInstance } from "./types.js";
import { cliDecimalField, publishArgvInput } from "./options.js";
import { cliDecimalField } from "./options.js";
/** Same envelope label as the other JSON-input commands and `parseCliJsonInput`. */
const cliError = (error: AnalysisError): JsonValue => ({
@@ -689,13 +689,11 @@ export const registerEnvironmentCommands = (
host: mcpHttpHostSchema
.optional()
.describe("Listener address; default MCP_HOST, else 127.0.0.1"),
port: publishArgvInput(
mcpHttpPortSchema
.optional()
.describe(
"Listener port; default MCP_PORT, else 0 for an ephemeral port",
),
),
port: mcpHttpPortSchema
.optional()
.describe(
"Listener port; default MCP_PORT, else 0 for an ephemeral port",
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
+11 -6
View File
@@ -24,6 +24,13 @@ type DecimalWrapper =
readonly kind: "optional" | "exactOptional" | "nullable" | "readonly";
};
/** Parse decimal integer tokens before Number coercion can erase their syntax. */
export const cliIntegerArgument = (min: number) =>
z
.string()
.regex(DECIMAL_INTEGER_TOKEN, "Expected a decimal integer")
.pipe(z.coerce.number<string>().safe().min(min));
const decimalToken = (domain: z.ZodNumber) =>
z
.string()
@@ -35,12 +42,10 @@ const unwrapSchema = (field: { unwrap: () => unknown }): z.ZodType =>
field.unwrap() as z.ZodType;
/**
* Incur advertises Zod's output view. Publish the input view so an argv token
* grammar that transforms text survives `--schema` and other SDK conversions.
* Incur advertises Zod's output view. Publish the input view so the decimal
* token guard survives `--schema` and other SDK conversions.
*/
export const publishArgvInput = <Field extends z.ZodType>(
field: Field,
): Field => {
const publishDecimalInput = (field: z.ZodType): z.ZodType => {
const { $schema: _dialect, ...inputSchema } = z.toJSONSchema(field, {
io: "input",
});
@@ -144,7 +149,7 @@ export const cliDecimalField = <Field extends z.ZodType>(
numeric.description === undefined
? admitted
: admitted.describe(numeric.description);
return publishArgvInput(described) as z.ZodType<z.output<Field>>;
return publishDecimalInput(described) as z.ZodType<z.output<Field>>;
};
/** Adapt each numeric property of a shared shape; other properties stay unchanged. */
+1 -1
View File
@@ -80,7 +80,7 @@ const cleanupOnlyCompleteEvidence = (
return typeof normalized === "object" &&
normalized !== null &&
!Array.isArray(normalized) &&
normalized.completion_status !== "unknown"
normalized.completion_status === "complete"
? parsed.data
: undefined;
};
+14 -28
View File
@@ -1,6 +1,6 @@
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
import { z } from "incur";
import { cliDecimalField } from "./options.js";
import { cliDecimalField, cliIntegerArgument } from "./options.js";
import type { DirectAnalysis } from "../composition/directAnalysis.js";
import { createHistoricalSourceSummary } from "../domain/referenceSourceSummary.js";
@@ -21,10 +21,7 @@ import {
} from "../domain/analysisErrorCore.js";
import { projectInputIssues } from "../domain/inputIssueProjection.js";
import { safeParseJson } from "../domain/safeJson.js";
import {
nativeUiObservationInputSchema,
nativeUiScenarioInputSchema,
} from "../domain/native/nativeUiObservation.js";
import { nativeUiScenarioInputSchema } from "../domain/native/nativeUiObservation.js";
import {
nativeCommandTimeoutField,
nativeUiDeadlineField,
@@ -52,16 +49,11 @@ export const registerUtilityCommands = (
registerReferenceSourceCommand(cli, logger, environment);
};
// Decimal tokens only; shared fields own their bounds and defaults.
const nativeUiScopeFields = nativeUiObservationInputSchema.shape;
// Decimal tokens only; shared fields own their bounds and help.
const nativeUiScopeOptions = {
pid: cliDecimalField(
nativeUiScopeFields.pid.describe("Existing application PID"),
),
windowId: cliDecimalField(
nativeUiScopeFields.window_id.describe(
"Exact selected application window ID",
),
pid: cliIntegerArgument(1).describe("Existing application PID"),
windowId: cliIntegerArgument(1).describe(
"Exact selected application window ID",
),
screenshot: z
.boolean()
@@ -71,11 +63,9 @@ const nativeUiScopeOptions = {
.boolean()
.default(true)
.describe("Read the selected window accessibility tree"),
maxNodes: cliDecimalField(
nativeUiScopeFields.max_nodes.describe(
"Maximum accessibility nodes per capture",
),
),
maxNodes: cliIntegerArgument(1)
.default(500)
.describe("Maximum accessibility nodes per capture"),
nativeUiDeadlineMs: cliDecimalField(nativeUiDeadlineField),
};
@@ -374,15 +364,11 @@ const registerReferenceSourceCommand = (
.describe(
"Complete committed graph or compact historical-source summary",
),
outlineDepth: cliDecimalField(
z
.int()
.min(0)
.optional()
.describe(
"Summary outline path depth (default: 2; 0 omits entries); requires --detail summary",
),
),
outlineDepth: cliIntegerArgument(0)
.optional()
.describe(
"Summary outline path depth (default: 2; 0 omits entries); requires --detail summary",
),
})
.superRefine((options, context) => {
if (options.outlineDepth !== undefined && options.detail !== "summary")
+3 -5
View File
@@ -6,7 +6,7 @@ import { logCliCommand } from "../cliLogging.js";
import { webModuleTraceInputSchema } from "../domain/webModuleTrace.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { cliDecimalField } from "./options.js";
import { cliIntegerArgument } from "./options.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
@@ -25,10 +25,8 @@ export const registerWebModuleCommands = (
"Trace one captured script's native imports to URLs and captured source candidates",
args: z.object({
manifestPath: webModuleTraceInputSchema.shape.manifest_path,
scriptIndex: cliDecimalField(
webModuleTraceInputSchema.shape.script_index.describe(
"Zero-based manifest script index",
),
scriptIndex: cliIntegerArgument(0).describe(
"Zero-based manifest script index",
),
}),
options: z.object({
+2 -5
View File
@@ -8,7 +8,7 @@ import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { cliDecimalField } from "./options.js";
import { cliDecimalField, cliIntegerArgument } from "./options.js";
import { withCommandCancellation } from "./commandCancellation.js";
import {
analysisEvidenceDeliveryOptions,
@@ -18,9 +18,6 @@ import { localPathStringSchema } from "../domain/localPath.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { explicitSensitiveValueOptions } from "./explicitSensitiveArguments.js";
const recordOrdinal =
inspectWebNetworkCaptureInputSchema.shape.record_ordinals.unwrap().element;
/** Expose offline historical capture inspection with the same selection contract as MCP. */
export const registerWebNetworkCaptureCommands = (
cli: CliInstance,
@@ -54,7 +51,7 @@ export const registerWebNetworkCaptureCommands = (
.optional()
.describe("Fully qualified message type in the descriptor set"),
record: z
.array(cliDecimalField(recordOrdinal))
.array(cliIntegerArgument(0))
.optional()
.describe("Zero-based producer ordinals; repeat to select several"),
...explicitSensitiveValueOptions,
+6 -13
View File
@@ -16,11 +16,10 @@ import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { cliDecimalField } from "./options.js";
import { cliDecimalField, cliIntegerArgument } from "./options.js";
import { withCommandCancellation } from "./commandCancellation.js";
const sourceMapSelection = webSourceLocationInputSchema.shape.source_map;
const generatedPosition = webSourceLocationInputSchema.shape.generated_position;
/** Expose the same explicit selected map/point contract through the CLI. */
export const registerWebSourceLocationCommands = (
@@ -34,20 +33,14 @@ export const registerWebSourceLocationCommands = (
"Trace one retained script position through an explicitly selected local source map",
args: z.object({
manifestPath: webSourceLocationInputSchema.shape.manifest_path,
scriptIndex: cliDecimalField(
webSourceLocationInputSchema.shape.script_index.describe(
"Zero-based manifest script index",
),
scriptIndex: cliIntegerArgument(0).describe(
"Zero-based manifest script index",
),
sourceMapPath: sourceMapSelection.shape.path,
sourceMapUrl: sourceMapSelection.shape.url,
line: cliDecimalField(
generatedPosition.shape.line.describe("One-based generated line"),
),
column: cliDecimalField(
generatedPosition.shape.column.describe(
"Zero-based UTF-16 generated column",
),
line: cliIntegerArgument(1).describe("One-based generated line"),
column: cliIntegerArgument(0).describe(
"Zero-based UTF-16 generated column",
),
}),
options: z.object({
-169
View File
@@ -1,169 +0,0 @@
import type { Writable } from "node:stream";
import {
cliCompletionShell,
isProjectScopedAmpRegistration,
normalizeCliArguments,
splitIncurBuiltinArguments,
} from "./cliBuiltinArguments.js";
import {
cliConfigurationError,
parseCliOutputArguments,
renderCliOutputArgumentError,
renderEmptyFilteredCliOutput,
renderVersionReport,
} from "./cliOutput.js";
import type { VersionReportHost } from "./domain/versionReport.js";
/**
* Process facilities one command needs. The executable binds them to its own
* streams and exit; a driver that serves several commands from one process
* binds them to per-command capture.
*/
export interface OneShotCliHost {
readonly environment: NodeJS.ProcessEnv;
readonly runtime: VersionReportHost;
readonly stdoutIsTerminal: boolean;
/** Destination for result documents the command streams itself. */
readonly resultDestination: Writable;
/** Status reported after a recovered failure when the reader has closed stdout. */
readonly closedStdoutStatus: number;
writeStdout(text: string): void;
writeStderr(text: string): void;
stdoutClosed(): boolean;
/** Status the process would report if it ended now. */
exitCode(): number;
setExitCode(code: number): void;
/** Called once when the command runtime loads, before any command runs. */
ownCliResources(close: () => Promise<void>): void;
/**
* The command asked to end the process with `code`. The host owns that
* exit and must call `finishOutput` with the status it reports.
*/
exit(code: number, finishOutput: (status: number) => Promise<void>): void;
/** Whether `exit` has run. Its host then owns the pending result write. */
exitRequested(): boolean;
}
const AMP_PROJECT_SCOPE_REFUSAL =
"Amp does not support project-scoped registration. Run without --no-global to register Amp globally, or select a client with project settings. No client settings were changed.";
/**
* Run one CLI command: admit its output arguments and configuration, answer
* the documents that need no command runtime, then dispatch. Failures the
* command did not report propagate to the host.
*/
export const runOneShotCli = async (
arguments_: readonly string[],
host: OneShotCliHost,
): Promise<void> => {
const isShellCompletion = cliCompletionShell(host.environment) !== undefined;
const cliArguments = isShellCompletion
? [...arguments_]
: normalizeCliArguments([...arguments_]);
// Completion words are transport data, including partial output flags.
const outputOptions = parseCliOutputArguments(
isShellCompletion ? [] : cliArguments,
);
const outputArguments = outputOptions.validation;
// `--version` with an explicit format reports the runtime as a document;
// plain `--version` stays Incur's bare version string.
const versionOutput = renderVersionReport(
outputOptions,
host.runtime,
host.stdoutIsTerminal,
);
const configurationError = cliConfigurationError(host.environment);
// Doctor is the readiness report for this failure. Other commands still stop
// here so they do not run with an unusable log level.
if (
configurationError !== undefined &&
(isShellCompletion ||
splitIncurBuiltinArguments(cliArguments).command[0] !== "doctor")
) {
host.writeStderr(`${configurationError}\n`);
host.setExitCode(1);
return;
}
if (!outputArguments.ok) {
host.writeStdout(renderCliOutputArgumentError(outputArguments));
host.setExitCode(1);
return;
}
if (versionOutput !== undefined) {
host.writeStdout(versionOutput);
return;
}
if (!isShellCompletion && isProjectScopedAmpRegistration(cliArguments)) {
// Amp has global settings only. Share the parsed scope fact with the
// registration workflow and refuse before loading the command runtime.
//
// A caller that asked for a structured document gets the refusal in
// that document, through the same pre-execution error renderer the
// output-argument branch uses; anything else keeps the plain message.
const refusalFormat = outputOptions.format;
if (
refusalFormat === "json" ||
refusalFormat === "jsonl" ||
refusalFormat === "yaml"
) {
host.writeStdout(
renderCliOutputArgumentError({
format: refusalFormat,
code: "AMP_PROJECT_SCOPE_UNSUPPORTED",
message: AMP_PROJECT_SCOPE_REFUSAL,
}),
);
} else {
host.writeStderr(`${AMP_PROJECT_SCOPE_REFUSAL}\n`);
}
host.setExitCode(1);
return;
}
// Loaded only now: the documents above answer without the command catalog.
const { closeCliResources, createCli } = await import("./cli.js");
host.ownCliResources(closeCliResources);
const { createCliResultOutput } = await import("./cli/cliResultOutput.js");
let wroteOutput = false;
const resultOutput = createCliResultOutput(
outputOptions,
host.resultDestination,
);
try {
await createCli(host.environment, resultOutput).serve(cliArguments, {
stdout: (output) => {
if (resultOutput?.handled) {
if (resultOutput.failed && !host.stdoutClosed())
host.writeStderr(output);
return;
}
if (output.length > 0) wroteOutput = true;
if (resultOutput?.captureSdkOutput(output)) return;
host.writeStdout(output);
},
exit: (code) => {
host.exit(typeof code === "number" ? code : 0, async (status) =>
resultOutput?.finishSdkOutput(status),
);
},
});
} catch (cause) {
if (wroteOutput) throw cause;
const recovered = await resultOutput
?.recoverFormattingFailure(cause)
.catch((writeFailure: unknown) => {
// Recovery writes outside Incur's command boundary. A reader that
// declined this document keeps the same quiet closed-pipe outcome.
if (!host.stdoutClosed()) throw writeFailure;
return true;
});
if (!recovered) throw cause;
host.setExitCode(host.stdoutClosed() ? host.closedStdoutStatus : 1);
}
if (!wroteOutput && !resultOutput?.handled)
host.writeStdout(renderEmptyFilteredCliOutput(outputOptions) ?? "");
// A requested exit is finished by the host, which owns its pending write.
if (!host.exitRequested())
await resultOutput?.finishSdkOutput(host.exitCode());
};
+1 -10
View File
@@ -366,18 +366,9 @@ export const renderEmptyFilteredCliOutput = ({
return undefined;
};
/** A refusal reported as a structured document before any command runs. */
export interface CliPreExecutionError {
readonly format: StructuredOutputFormat;
readonly code:
| Exclude<CliOutputArgumentValidation, { readonly ok: true }>["code"]
| "AMP_PROJECT_SCOPE_UNSUPPORTED";
readonly message: string;
}
/** Render one complete structured error before command execution. */
export const renderCliOutputArgumentError = (
error: CliPreExecutionError,
error: Exclude<CliOutputArgumentValidation, { readonly ok: true }>,
): string => {
const value = {
ok: false,
-2
View File
@@ -6,7 +6,6 @@ import { OBJDUMP_PROVIDER_IDENTITY } from "../objdump/ObjdumpProviderIdentity.js
import {
rizinCommand,
rizinCommandLimitations,
rizinCommandRejection,
} from "../rizin/RizinCommand.js";
import { RIZIN_PROVIDER_IDENTITY } from "../rizin/RizinProviderIdentity.js";
import { resolveWindowsProviderExecutable } from "./resolveWindowsProviderExecutable.js";
@@ -33,7 +32,6 @@ export const createReverseEngineeringCommandPort = (
),
arguments: rizinCommand(input),
limitations: rizinCommandLimitations(input),
rejection: rizinCommandRejection,
identity: RIZIN_PROVIDER_IDENTITY,
}),
});
+1 -7
View File
@@ -63,7 +63,7 @@ export const GDB_TOOL_CONTRACTS = [
...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; closed_reason says why a closed session stopped, and truncation and invalid-UTF-8 fields report discarded or lossy data.",
"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,
@@ -79,12 +79,6 @@ export const GDB_TOOL_CONTRACTS = [
diagnostics_truncated: z.boolean(),
inferior_tty: z.string().nullable(),
inferior_tty_verified: z.boolean(),
closed_reason: z
.string()
.exactOptional()
.describe(
"Present when state is closed: why the session stopped accepting commands. Close it and start a new session to continue.",
),
}),
examples: [
{
@@ -39,7 +39,7 @@ export const REVERSE_ENGINEERING_TOOL_CONTRACTS = [
...toolContractMetadata("execute_rizin_command"),
kind: "native-provider",
description:
"Run one caller-selected Rizin command on a local artifact. Commands/plugins can execute code, mutate files, access network and read outside the artifact; those external inputs are not Evidence-bound. Suppresses user settings/scripts but retains plugins. Raw stdout/stderr is authoritative, JSON is command-specific, and UTF-8 loss/truncation is explicit; output-limit termination leaves completion unknown. Rizin exits 0 even when its shell refuses a statement; completion_status rejected marks that refusal, and later statements did not run. Other command failures can still exit 0, so inspect stderr before treating empty output as absence. For raw images, asm_arch/asm_bits/asm_cpu select decoding and base_address maps offset 0; base_address also passes -n to skip loaders even on ELF, without REA confirming either. An admitted command retires shared binary analysis: close Ghidra explicitly first, then open_binary after effects settle. command_timeout_ms is per-phase and max_output_bytes bounds combined output (16 MiB default); both apply next call without restart and do not raise client receive limits or persistent debugger deadlines.",
"Run one caller-selected Rizin command on a local artifact. Commands/plugins can execute code, mutate files, access network and read outside the artifact; those external inputs are not Evidence-bound. Suppresses user settings/scripts but retains plugins. completion_status reports process completion; Rizin can reject a command and exit with code 0. Inspect stdout/stderr before treating empty output as absence. Raw stdout/stderr is authoritative, JSON is command-specific, and UTF-8 loss/truncation is explicit; output-limit termination leaves completion unknown. For raw images, asm_arch/asm_bits/asm_cpu select decoding and base_address maps offset 0; base_address also passes -n to skip loaders even on ELF, without REA confirming either. An admitted command retires shared binary analysis: close Ghidra explicitly first, then open_binary after effects settle. command_timeout_ms is per-phase and max_output_bytes bounds combined output (16 MiB default); both apply next call without restart and do not raise client receive limits or persistent debugger deadlines.",
inputSchema: rizinInputSchema,
outputSchema: evidenceResultOf(commandObservationSchema),
examples: [
+1 -7
View File
@@ -72,7 +72,7 @@ export const RIZIN_DEBUG_TOOL_CONTRACTS = [
...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; closed_reason says why a closed session stopped, 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.",
"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,
@@ -82,12 +82,6 @@ export const RIZIN_DEBUG_TOOL_CONTRACTS = [
recent_output_truncated: z.boolean(),
diagnostics_truncated: z.boolean(),
output_invalid_utf8: z.boolean(),
closed_reason: z
.string()
.exactOptional()
.describe(
"Present when state is closed: why the session stopped accepting commands. Close it and start a new session to continue.",
),
}),
examples: [
{
-32
View File
@@ -1,32 +0,0 @@
import { expect, it } from "vitest";
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";
import {
toolInputSchemaWithMetadata,
toolOutputSchemaWithMetadata,
} from "./toolSchemaMetadata.js";
// Each MCP server registers every contract. The SDK reprojects registered
// schemas on every tools/list, so a wrapper rebuilt per server would repeat
// the whole catalog's JSON Schema conversion for each new server.
it("shares one advertised wrapper and projection per contract across registrations", () => {
const contract = ELECTRON_TOOL_CONTRACTS.find(
({ name }) => name === "analyze_javascript_application",
);
if (contract === undefined) throw new Error("Missing contract fixture");
const options = { target: "draft-2020-12" } as const;
const firstInput = toolInputSchemaWithMetadata(contract);
const secondInput = toolInputSchemaWithMetadata(contract);
expect(secondInput).toBe(firstInput);
expect(secondInput["~standard"].jsonSchema.input(options)).toBe(
firstInput["~standard"].jsonSchema.input(options),
);
const firstOutput = toolOutputSchemaWithMetadata(contract);
const secondOutput = toolOutputSchemaWithMetadata(contract);
expect(secondOutput).toBe(firstOutput);
expect(secondOutput["~standard"].jsonSchema.output(options)).toBe(
firstOutput["~standard"].jsonSchema.output(options),
);
});
+51 -77
View File
@@ -63,65 +63,42 @@ const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
type JsonSchemaProjection = z.ZodType["~standard"]["jsonSchema"]["input"];
// Contracts are immutable module values. Build each advertised wrapper once so
// its memoized projection serves every server in the process, not only the
// server that registered it.
const advertisedInputSchemas = new WeakMap<ToolContract, z.ZodType>();
const advertisedOutputSchemas = new WeakMap<ToolContract, z.ZodType>();
const oncePerContract = <
Contract extends ToolContract,
Schema extends z.ZodType,
>(
cache: WeakMap<ToolContract, z.ZodType>,
contract: Contract,
build: () => Schema,
): Schema => {
const cached = cache.get(contract);
if (cached !== undefined) return cached as Schema;
const built = build();
cache.set(contract, built);
return built;
};
/** Attach caller guidance to a canonical schema for the SDK wire projection. */
export const toolInputSchemaWithMetadata = <Contract extends ToolContract>(
contract: Contract,
): Contract["inputSchema"] =>
oncePerContract(advertisedInputSchemas, contract, () =>
withAdvertisedJsonSchema(
contract.inputSchema,
"input",
(project) => (options) => {
const inline = project({
...options,
libraryOptions: { ...options.libraryOptions, reused: "inline" },
});
const examples = advertisedInputExamples(contract);
const presented = presentInputJsonSchema(
inline,
fallbackPropertyDescription,
);
const sourceGuidance = getEvidenceInputContract(contract.inputSchema)
.hasEvidence
? 'Declared Evidence fields accept complete records or {"kind":"retained-evidence","evidence_id":"ev_..."}. For a complete source, use the summary\'s parent_evidence_id.'
: undefined;
return {
// Keep advertised parameters self-contained for clients that do not
// resolve local references. The registered canonical schema remains
// responsible for runtime validation.
...presented,
...(sourceGuidance === undefined
? {}
: {
description: [presented.description, sourceGuidance]
.filter((text) => typeof text === "string")
.join(" "),
}),
...(examples.length === 0 ? {} : { examples }),
};
},
),
withAdvertisedJsonSchema(
contract.inputSchema,
"input",
(project) => (options) => {
const inline = project({
...options,
libraryOptions: { ...options.libraryOptions, reused: "inline" },
});
const examples = advertisedInputExamples(contract);
const presented = presentInputJsonSchema(
inline,
fallbackPropertyDescription,
);
const sourceGuidance = getEvidenceInputContract(contract.inputSchema)
.hasEvidence
? 'Declared Evidence fields accept complete records or {"kind":"retained-evidence","evidence_id":"ev_..."}. For a complete source, use the summary\'s parent_evidence_id.'
: undefined;
return {
// Keep advertised parameters self-contained for clients that do not
// resolve local references. The registered canonical schema remains
// responsible for runtime validation.
...presented,
...(sourceGuidance === undefined
? {}
: {
description: [presented.description, sourceGuidance]
.filter((text) => typeof text === "string")
.join(" "),
}),
...(examples.length === 0 ? {} : { examples }),
};
},
);
/** Return the full-profile advertised input JSON Schema registered on MCP. */
@@ -136,28 +113,25 @@ export const advertisedInputJsonSchema = <Contract extends ToolContract>(
export const toolOutputSchemaWithMetadata = <Contract extends ToolContract>(
contract: Contract,
): Contract["outputSchema"] =>
oncePerContract(advertisedOutputSchemas, contract, () =>
withAdvertisedJsonSchema(
contract.outputSchema,
"output",
(project) => (options) => {
const shared = project({
...options,
libraryOptions: { reused: "ref", ...options.libraryOptions },
});
// An explicit ID owns the reference base. Relative external references
// also depend on that base, so leave those projections unchanged.
if ("$id" in shared || hasRelativeSchemaReference(shared))
return shared;
return {
...shared,
$id: `urn:rea:tool-output:sha256:${digestCanonicalValue(
{ target: options.target, schema: shared },
"Tool output schema",
)}`,
};
},
),
withAdvertisedJsonSchema(
contract.outputSchema,
"output",
(project) => (options) => {
const shared = project({
...options,
libraryOptions: { reused: "ref", ...options.libraryOptions },
});
// An explicit ID owns the reference base. Relative external references
// also depend on that base, so leave those projections unchanged.
if ("$id" in shared || hasRelativeSchemaReference(shared)) return shared;
return {
...shared,
$id: `urn:rea:tool-output:sha256:${digestCanonicalValue(
{ target: options.target, schema: shared },
"Tool output schema",
)}`,
};
},
);
/**
+1 -5
View File
@@ -9,11 +9,7 @@ export const commandObservationSchema = z.strictObject({
output_budget_bytes: z.int().positive().exactOptional(),
output_budget_source: z.enum(["default", "request"]).exactOptional(),
output_truncated: z.boolean(),
completion_status: z
.enum(["complete", "rejected", "unknown"])
.describe(
"complete: the provider process completed. rejected: it completed, but the provider reported that it refused the command; limitations say what ran. unknown: the capture or process ended early.",
),
completion_status: z.enum(["complete", "unknown"]),
artifact_sha256_after: z
.string()
.regex(/^[a-f0-9]{64}$/u)
-58
View File
@@ -1,58 +0,0 @@
import {
AnalysisCapabilityUnavailableError,
AnalysisInputError,
} from "./analysisErrorCore.js";
/** Tool names and wording for one persistent debugger's session errors. */
export interface DebuggerSessionTools {
readonly providerId: string;
readonly name: string;
readonly start: string;
readonly close: string;
}
/** A session_id this server does not hold, unlike a retained stopped session. */
export const unknownDebuggerSessionError = (
tools: DebuggerSessionTools,
operation: string,
): AnalysisInputError =>
new AnalysisInputError(
operation,
{
conformsToInputContract: true,
userMessage: `No ${tools.name} debug session has this session_id in this REA server.`,
remediationAction: `Use a session_id that ${tools.start} returned in this server process; ${tools.close} releases a session's ID. Call ${tools.start} to begin a new session.`,
},
[
{
path: ["session_id"],
reason: "invalid_value",
message: `No ${tools.name} debug session has this session_id in this REA server.`,
},
],
);
/** Refuse a command on a retained session that stopped, naming why it stopped. */
export const stoppedDebuggerSessionError = (
tools: DebuggerSessionTools,
operation: string,
reason: string,
): AnalysisCapabilityUnavailableError =>
new AnalysisCapabilityUnavailableError(tools.providerId, operation, reason, {
userMessage: `The ${tools.name} debug session stopped and accepts no more commands. ${reason}`,
remediationAction: `Call ${tools.close} with this session_id to release the stopped session, then call ${tools.start} to begin a new one. Earlier commands may have changed the target, so confirm their effects before repeating them.`,
});
export const RIZIN_DEBUG_SESSION_TOOLS = {
providerId: "rizin",
name: "Rizin",
start: "start_rizin_debug_session",
close: "close_rizin_debug_session",
} as const satisfies DebuggerSessionTools;
export const GDB_SESSION_TOOLS = {
providerId: "gdb",
name: "GDB",
start: "start_gdb_session",
close: "close_gdb_session",
} as const satisfies DebuggerSessionTools;
@@ -961,15 +961,22 @@ const createGlobalPathRead = (
};
byNamespace.set(namespace, effects);
}
const escapePrecedesRead = effects.escapes.some(({ node }) => {
// Selecting this callee precedes exposing its own receiver. Other reads,
// including arguments, need activation-aware ordering across repetitions.
// The call that escapes a receiver contains the callee being identified.
// That escape cannot have replaced the callee before the call selects it.
const escapePrecedesRead = effects.escapes.some((effect) => {
const escape = effect.node;
if (
(t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
node.callee === site
escape !== undefined &&
escape !== site &&
typeof escape.start === "number" &&
typeof escape.end === "number" &&
typeof site.start === "number" &&
typeof site.end === "number" &&
site.start >= escape.start &&
site.end <= escape.end
)
return false;
return semanticEffectTiming(site, node, state) !== "after";
return semanticEffectTiming(site, escape, state) !== "after";
});
if (
(escapesCovered && effects.escapes.length === 0) ||
@@ -1,67 +0,0 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
const spawns = (source: string) =>
analyzeJavaScriptSemantics(source).childProcessSpawns;
it.each(["spawn", "fork"])("rejects function-valued %s options", (method) => {
expect(
spawns(`
import { ${method} } from "node:child_process";
${method}("worker.js", () => {});
${method}("worker.js", [], function options() {});
${method}("worker.js", [], class Options {});
`),
).toEqual([]);
});
it("rejects a literal exec callback after explicit options", () => {
expect(
spawns(`
import { exec } from "node:child_process";
exec("node --version", {}, false);
exec("node --version", null, {});
`),
).toEqual([]);
});
it("selects execFile callbacks with and without an argv array", () => {
expect(
spawns(`
import { execFile } from "node:child_process";
execFile("node", {}, { cwd: "." });
execFile("node", [], {}, false);
execFile("node", null, {}, "callback");
`),
).toEqual([]);
});
it("keeps callback overloads and nullish callbacks", () => {
expect(
spawns(`
import { exec, execFile } from "node:child_process";
exec("node --version", () => {}, false);
exec("node --version", {}, null);
execFile("node", () => {}, { cwd: false });
execFile("node", [], () => {}, false);
execFile("node", {}, null);
execFile("node", [], {}, undefined);
`),
).toHaveLength(6);
});
it("leaves uncertain callback slots and spread positions undecided", () => {
expect(
spawns(`
import { exec, execFile, spawn } from "node:child_process";
function run(options, callback, args) {
exec("node --version", options, false);
exec("node --version", {}, callback);
execFile("node", options, false);
execFile("node", [], options, false);
execFile("node", ...args, {}, false);
spawn("node", ...args, () => {});
}
`),
).toHaveLength(6);
});
@@ -1,189 +0,0 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
const operations = (source: string) =>
analyzeJavaScriptSemantics(source).timerOperations;
it.each(["setTimeout", "setInterval"])(
"does not create a %s handle when delay conversion must throw",
(method) => {
const result = operations(`
import { ${method}, clearTimeout } from "node:timers";
const positive = ${method}(() => {}, 1n);
const negative = ${method}(() => {}, -1n);
const zero = ${method}(() => {}, 0n);
const symbol = ${method}(() => {}, Symbol("delay"));
const converted = ${method}(() => {}, BigInt("1"));
clearTimeout(positive);
`);
expect(result.filter(({ kind }) => kind === "schedule")).toEqual([]);
expect(result).toMatchObject([
{ kind: "cancel", resolution: "unresolved", linkedTimerId: null },
]);
},
);
it("keeps supported delays and the immediate callback's BigInt argument", () => {
const result = operations(`
import { setTimeout, setInterval, setImmediate } from "node:timers";
setTimeout(() => {}, "2");
setInterval(() => {}, null);
setTimeout(() => {}, !1n);
setTimeout(() => {}, void 1n);
setImmediate(() => {}, 1n);
`);
expect(result.map(({ method }) => method)).toEqual([
"setTimeout",
"setInterval",
"setTimeout",
"setTimeout",
"setImmediate",
]);
});
it("retains uncertain delays, shifted arguments and shadowed constructors", () => {
const result = operations(`
import { setTimeout } from "node:timers";
function schedule(delay, args, Symbol, BigInt) {
setTimeout(() => {}, delay);
setTimeout(...args, 1n);
setTimeout(() => {}, ...args);
setTimeout(() => {}, Symbol("delay"));
setTimeout(() => {}, BigInt("1"));
}
`);
expect(result).toHaveLength(5);
});
it.each([
'eval("Symbol = () => 1; BigInt = () => 1");',
'(0, eval)("Symbol = () => 1; BigInt = () => 1");',
'Function("Symbol = () => 1; BigInt = () => 1")();',
])(
"retains delays after global code may replace constructors: %s",
(effect) => {
expect(
operations(`
import { setTimeout, setInterval } from "node:timers";
${effect}
setTimeout(() => {}, Symbol("delay"));
setInterval(() => {}, BigInt("1"));
`).map(({ method }) => method),
).toEqual(["setTimeout", "setInterval"]);
},
);
it("keeps literal rejection independent of global code effects", () => {
expect(
operations(`
import { setTimeout } from "node:timers";
eval(code);
setTimeout(() => {}, 1n);
`),
).toEqual([]);
});
it("ignores later global code and shadowed eval for constructor identity", () => {
expect(
operations(`
import { setTimeout } from "node:timers";
setTimeout(() => {}, Symbol("delay"));
eval("Symbol = () => 1");
`),
).toEqual([]);
expect(
operations(`
import { setTimeout } from "node:timers";
function run(eval) {
eval("Symbol = () => 1");
setTimeout(() => {}, Symbol("delay"));
}
`),
).toEqual([]);
});
it.each([
["Symbol", "configure(globalThis);"],
["BigInt", "configure(globalThis);"],
["Symbol", "const host = globalThis; configure(host);"],
["BigInt", "configure({ host: globalThis });"],
])("retains %s delays and cancellation after %s", (constructor, effect) => {
const result = operations(`
import configure from "./configure.js";
import { setTimeout, clearTimeout } from "node:timers";
${effect}
const handle = setTimeout(() => {}, ${constructor}("1"));
clearTimeout(handle);
`);
expect(result).toMatchObject([
{ kind: "schedule", resolution: "complete" },
{ kind: "cancel", resolution: "complete" },
]);
expect(result[1]?.linkedTimerId).toBe(result[0]?.timerId);
});
it("still rejects literal BigInt delays after the global object escapes", () => {
expect(
operations(`
import configure from "./configure.js";
import { setTimeout, setInterval } from "node:timers";
configure(globalThis);
setTimeout(() => {}, 1n);
setInterval(() => {}, -1n);
`),
).toEqual([]);
});
it("does not let later or unrelated escapes change constructor identity", () => {
expect(
operations(`
import configure from "./configure.js";
import { setTimeout, setInterval } from "node:timers";
configure({});
setTimeout(() => {}, Symbol("1"));
setInterval(() => {}, BigInt("1"));
configure(globalThis);
`),
).toEqual([]);
});
it("selects a constructor before an argument can escape the global object", () => {
expect(
operations(`
import configure from "./configure.js";
import { setTimeout } from "node:timers";
setTimeout(() => {}, Symbol(configure(globalThis)));
`),
).toEqual([]);
});
it.each(["Symbol", "BigInt"])(
"retains %s identity uncertainty inside an escaped callback",
(constructor) => {
const result = operations(`
import configure from "./configure.js";
import { setTimeout, clearTimeout } from "node:timers";
configure(globalThis, () => {
const handle = setTimeout(() => {}, ${constructor}("1"));
clearTimeout(handle);
});
`);
expect(result).toMatchObject([
{ kind: "schedule", resolution: "complete" },
{ kind: "cancel", resolution: "complete" },
]);
expect(result[1]?.linkedTimerId).toBe(result[0]?.timerId);
},
);
it.each([
'for (let i=0;i<2;i++) configure(globalThis, i ? setTimeout(()=>{}, Symbol("1")) : null);',
'function run(first) { configure(globalThis, first ? null : setTimeout(()=>{}, BigInt("1"))); } run(true); run(false);',
'let first=true; while (next()) { configure(globalThis, first ? null : setTimeout(()=>{}, Symbol("1"))); first=false; }',
])("retains constructor uncertainty across repeated escapes: %s", (source) => {
expect(
operations(
`import configure from "./configure.js"; import {setTimeout} from "node:timers"; ${source}`,
),
).toMatchObject([{ kind: "schedule" }]);
});
@@ -19,14 +19,8 @@ import {
} from "./javascriptSemanticState.js";
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
import {
createGlobalReferenceReader,
isUnmodifiedGlobalReference,
} from "./javascriptSemanticAmbientWrites.js";
import {
semanticOpaqueGlobalMayHaveChanged,
semanticOpaqueNameMayHaveChanged,
} from "./javascriptSemanticOpaqueEffects.js";
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
import { semanticOpaqueNameMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import {
builtinDataEffectIdentity,
@@ -115,10 +109,6 @@ interface TimerCollectionInput {
readonly ownerCallableId: string | null;
readonly state: JavaScriptSemanticAnalysisState;
readonly output: TimerCandidate[];
readonly delayConstructors: ReadonlyMap<
string,
ReturnType<typeof createGlobalReferenceReader>
>;
readonly ancestors: readonly t.Node[];
}
@@ -130,10 +120,6 @@ export const collectJavaScriptSemanticAsyncEffects = (
): AsyncEffectAnalysis => {
const events: JavaScriptSemanticEventOperation[] = [];
const timers: TimerCandidate[] = [];
const delayConstructors = new Map([
["Symbol", createGlobalReferenceReader(state, ["Symbol"])],
["BigInt", createGlobalReferenceReader(state, ["BigInt"])],
]);
const callableIds = new Set(callables.map(({ callableId }) => callableId));
const callableStack = createSemanticCallableIdStack(callableIds);
const ancestors: t.Node[] = [];
@@ -148,7 +134,6 @@ export const collectJavaScriptSemanticAsyncEffects = (
ownerCallableId,
state,
output: timers,
delayConstructors,
ancestors,
});
}
@@ -391,38 +376,6 @@ const schedulesWithNonFunctionCallback = (
return t.isUnaryExpression(value);
};
// Node converts timeout and interval delays to numbers before creating a handle.
// BigInt and Symbol values throw there. A spread can shift the delay slot, and
// shadowed or replaced constructors cannot establish the argument's type.
const schedulesWithInvalidDelay = (
node: t.CallExpression | t.OptionalCallExpression,
state: JavaScriptSemanticAnalysisState,
delayConstructors: TimerCollectionInput["delayConstructors"],
): boolean => {
if (
node.arguments.slice(0, 2).some((argument) => t.isSpreadElement(argument))
)
return false;
const argument = node.arguments[1];
if (argument === undefined) return false;
const value = unwrapJavaScriptExpression(argument).node;
if (t.isBigIntLiteral(value)) return true;
if (
t.isUnaryExpression(value) &&
["+", "-", "~"].includes(value.operator) &&
t.isBigIntLiteral(unwrapJavaScriptExpression(value.argument).node)
)
return true;
return (
t.isCallExpression(value) &&
t.isIdentifier(value.callee) &&
delayConstructors.get(value.callee.name)?.(value.callee).status ===
"unmodified" &&
!semanticOpaqueNameMayHaveChanged(value.callee, state) &&
!semanticOpaqueGlobalMayHaveChanged(value.callee, state)
);
};
const collectTimer = (input: TimerCollectionInput): void => {
const { node, ownerCallableId, state, output, ancestors } = input;
const identity = timerMethod(node.callee, state);
@@ -431,9 +384,7 @@ const collectTimer = (input: TimerCollectionInput): void => {
if (
TIMER_SCHEDULE_METHODS.some((candidate) => candidate === method) &&
nodeBuiltinTimerMethod(node.callee, state) !== null &&
(schedulesWithNonFunctionCallback(node, state) ||
((method === "setTimeout" || method === "setInterval") &&
schedulesWithInvalidDelay(node, state, input.delayConstructors)))
schedulesWithNonFunctionCallback(node, input.state)
)
return;
const kind = TIMER_SCHEDULE_METHODS.some((candidate) => candidate === method)
@@ -79,8 +79,7 @@ export const collectJavaScriptSemanticChildProcesses = (
!forkRejectsStdioArray(candidate, context) &&
!rejectsStdioLiteral(candidate, context) &&
!rejectsProcessOptionLiteral(candidate, context) &&
!rejectsOptionsShape(candidate, context, state) &&
!rejectsCallback(candidate, context),
!rejectsOptionsShape(candidate, context, state),
);
const childProcessSpawns = candidates.map((candidate) =>
immutableSpawn(candidate, context, state),
@@ -648,13 +647,6 @@ const rejectsOptionsShape = (
if (options === undefined) return false;
const value = unwrapJavaScriptExpression(options).node;
if (context.isArray(value)) return true;
if (
candidate.method !== "execFile" &&
(t.isFunctionExpression(value) ||
t.isArrowFunctionExpression(value) ||
t.isClassExpression(value))
)
return true;
const literal = readExactJavaScriptLiteral(value);
if (literal.found)
return !(candidate.method !== "spawn" && literal.value === null);
@@ -670,52 +662,6 @@ const rejectsOptionsShape = (
return isUnpricedPrimitiveLiteral(value);
};
/** Select only callback slots whose overload is established by source syntax. */
const rejectsCallback = (
candidate: SpawnCandidate,
context: ChildSpawnContext,
): boolean => {
if (candidate.method !== "exec" && candidate.method !== "execFile")
return false;
const args = candidate.node.arguments;
if (args.slice(0, 4).some((argument) => t.isSpreadElement(argument)))
return false;
const second = args[1] && unwrapJavaScriptExpression(args[1]).node;
const third = args[2] && unwrapJavaScriptExpression(args[2]).node;
let callback: t.Node | undefined;
if (candidate.method === "exec") {
// A function in slot 1 replaces slot 2, even when slot 2 is not callable.
if (
second !== undefined &&
!context.isNullish(second) &&
!rejectsSemanticEventListener(second)
)
return false;
callback = third;
} else if (t.isObjectExpression(second) || t.isRegExpLiteral(second)) {
callback = third;
} else if (
second === undefined ||
context.isArray(second) ||
context.isNullish(second)
) {
// With argv present, a third-slot function replaces the fourth callback.
if (
third !== undefined &&
!context.isNullish(third) &&
!rejectsSemanticEventListener(third)
)
return false;
callback = args[3];
} else {
return false;
}
return (
callback !== undefined &&
!context.isNullish(callback) &&
rejectsSemanticEventListener(callback)
);
};
/**
* The exact-value reader deliberately refuses to price a BigInt, a non-finite
* numeric literal such as `1e400`, and a negated numeric literal such as `-0`.
+36 -52
View File
@@ -13,11 +13,6 @@ import {
AnalysisInputError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import {
GDB_SESSION_TOOLS,
stoppedDebuggerSessionError,
unknownDebuggerSessionError,
} from "../domain/debuggerSessionErrors.js";
import {
AnalysisError,
type AnalysisErrorOptions,
@@ -547,8 +542,6 @@ interface GdbSession {
noInferiorVerified: boolean;
/** Sticky admission seal once destructive cleanup starts, including failed stops. */
cleanupStarted: boolean;
/** close_gdb_session started cleanup, so a successful retry releases the ID. */
closeRequested: boolean;
stopAttempt: ReturnType<typeof stopOwnedDebuggerProcess> | undefined;
readonly pending: Map<number, PendingCommand>;
readonly cleanupFailures: Set<AnalysisError>;
@@ -573,8 +566,17 @@ interface GdbSession {
recentRecordsTruncated: boolean;
}
const gdbConsoleUnavailable = (): AnalysisInputError =>
new AnalysisInputError("gdb_console", undefined, [
{
path: ["session_id"],
reason: "invalid_value",
message: "GDB session is unavailable or closed.",
},
]);
const gdbCloseUnavailable = (): AnalysisInputError =>
unknownDebuggerSessionError(GDB_SESSION_TOOLS, "close_gdb_session");
new AnalysisInputError("close_gdb_session");
/** Owns persistent GDB/MI sessions independently from the active binary target. */
export class GdbSessionManager {
@@ -767,7 +769,7 @@ export class GdbSessionManager {
"start_gdb_session",
"memory",
`GDB session capacity reached (${String(this.#maxSessions.value)} sessions).`,
"Close a GDB session before starting another; a stopped session that gdb_session_status reports closed still holds its slot until close_gdb_session releases it.",
"Close a GDB session before starting another.",
this.#maxSessions,
{
max_sessions: this.#maxSessions.value,
@@ -901,7 +903,6 @@ export class GdbSessionManager {
// during startup, so no inferior can exist until execute() is called.
noInferiorVerified: true,
cleanupStarted: false,
closeRequested: false,
stopAttempt: undefined,
pending: new Map(),
cleanupFailures: new Set(),
@@ -1255,13 +1256,15 @@ export class GdbSessionManager {
>
> {
const session = this.#sessions.get(sessionId);
if (session === undefined)
return err(unknownDebuggerSessionError(GDB_SESSION_TOOLS, "gdb_console"));
const stopped = this.#stopReason(session);
if (stopped !== undefined)
return err(
stoppedDebuggerSessionError(GDB_SESSION_TOOLS, "gdb_console", stopped),
);
if (
this.#closing ||
session === undefined ||
!session.ready ||
session.cleanupStarted ||
session.exited ||
session.protocolError !== undefined
)
return err(gdbConsoleUnavailable());
if (signal?.aborted)
return err(cancelledWithCallerReason("gdb_console", signal));
try {
@@ -1270,16 +1273,14 @@ export class GdbSessionManager {
async () => {
// Retained cleanup ownership and a delayed exit event do not imply
// command readiness. Recheck admission when this queued turn starts.
// Every path that releases a session first records why it stopped.
const stoppedWhileQueued = this.#stopReason(session);
if (stoppedWhileQueued !== undefined)
return err(
stoppedDebuggerSessionError(
GDB_SESSION_TOOLS,
"gdb_console",
stoppedWhileQueued,
),
);
if (
this.#closing ||
this.#sessions.get(sessionId) !== session ||
session.cleanupStarted ||
session.exited ||
session.protocolError !== undefined
)
return err(gdbConsoleUnavailable());
session.noInferiorVerified = false;
session.knownInferiorIdentities.clear();
if (commandMayChangeInferiorOutput(command))
@@ -1454,17 +1455,21 @@ export class GdbSessionManager {
readonly diagnostics_truncated: boolean;
readonly inferior_tty: string | null;
readonly inferior_tty_verified: boolean;
readonly closed_reason?: string;
}
| undefined {
const session = this.#sessions.get(sessionId);
if (session === undefined) return undefined;
const diagnostics = session.supervisor.snapshot();
const stopped = this.#stopReason(session);
return {
session_id: session.id,
mi_version: session.miVersion,
state: stopped === undefined ? "ready" : "closed",
state:
session.cleanupStarted ||
session.exited ||
session.protocolError !== undefined ||
!session.ready
? "closed"
: "ready",
recent_mi_records: [...session.lines],
recent_mi_records_truncated: session.recentRecordsTruncated,
mi_invalid_utf8: session.invalidUtf8Observed,
@@ -1476,24 +1481,9 @@ export class GdbSessionManager {
diagnostics_truncated: diagnostics.diagnosticTruncated === true,
inferior_tty: session.inferiorTty ?? null,
inferior_tty_verified: session.inferiorTtyVerified,
...(stopped === undefined ? {} : { closed_reason: stopped }),
};
}
/** Why a retained session accepts no commands, or undefined while it is ready. */
#stopReason(session: GdbSession): string | undefined {
if (session.protocolError !== undefined) return session.protocolError;
if (this.#closing)
return "REA is shutting down and closing every GDB session.";
if (session.closeRequested)
return "close_gdb_session started releasing this session, and its owned cleanup has not completed.";
if (session.exited)
return `The GDB process exited (exit code ${String(session.exitCode)}, signal ${String(session.exitSignal)}); a command such as quit, or a crash, can end it.`;
if (session.cleanupStarted || !session.ready)
return "REA stopped this GDB session.";
return undefined;
}
async close(
sessionId: string,
): Promise<
@@ -1559,7 +1549,6 @@ export class GdbSessionManager {
// Refusing inferior disposition above leaves console remediation
// available; from here onward only owned cleanup may be retried.
session.cleanupStarted = true;
session.closeRequested = true;
if (groupCount === 0) {
session.noInferiorVerified = true;
targetState = "none_observed";
@@ -1599,7 +1588,6 @@ export class GdbSessionManager {
async #finishSessionCleanup(
session: GdbSession,
): Promise<Result<{ readonly target_state: "unknown" }, AnalysisError>> {
session.closeRequested = true;
const stopped = await this.#stopDebuggerOnly(session);
if (stopped.status === "incomplete")
return err(
@@ -1626,10 +1614,7 @@ export class GdbSessionManager {
stopped.reason,
),
);
// The caller still holds a published session's ID; keep it addressable
// so commands and status report why it stopped until it is closed.
if (!session.ready || session.closeRequested)
this.#sessions.delete(session.id);
this.#sessions.delete(session.id);
}
return ok(null);
}
@@ -2224,7 +2209,6 @@ export class GdbSessionManager {
"The unrestricted GDB console can access local shell, filesystem, network, and target controls.",
],
});
session.protocolError ??= `A GDB command exceeded its ${String(acceptedDeadline.value)} ms deadline; owned-session shutdown was requested because output correlation can no longer be guaranteed.`;
void this.#stopDebuggerOnly(session).then((stopped) => {
if (
stopped.status === "incomplete" &&
+1 -41
View File
@@ -25,7 +25,7 @@ export const rizinCommand = (input: RizinInput): readonly string[] => [
export const rizinCommandLimitations = (
input: RizinInput,
): readonly string[] => [
"completion_status covers process completion and Rizin shell refusals only. A statement that ran can still fail while Rizin returns exit_code 0, so inspect stderr before treating an empty response as absence.",
"completion_status reports process completion. Command validity remains unverified: Rizin can reject a command while returning exit_code 0. Inspect stdout and stderr before treating an empty response as absence.",
...(input.asm_arch === undefined &&
input.asm_bits === undefined &&
input.asm_cpu === undefined
@@ -39,43 +39,3 @@ export const rizinCommandLimitations = (
"With base_address, REA passed -n and -m so that Rizin maps file offset 0 at that address as a raw image; REA does not confirm the map, which oml shows. -n skips Rizin's format loaders, so expect no sections, symbols or entry point, and arch, bits and byte order stay at Rizin's defaults unless the call selects them.",
]),
];
/**
* Rizin's shell logs exactly these lines when it refuses a statement
* (librz/core/cmd/cmd.c, unchanged from 0.6 through 0.9.1). Rizin then skips
* the remaining statements and still exits 0. Other ERROR lines come from
* commands that ran, so they are diagnostics rather than refusals.
*/
const RIZIN_SHELL_REFUSALS: readonly {
readonly line: RegExp;
readonly refused: (match: RegExpExecArray) => string;
}[] = [
{
line: /^ERROR: Command '(.*)' does not exist\.$/u,
refused: (match) => `the unknown command ${JSON.stringify(match[1])}`,
},
{
line: /^ERROR: Wrong number of arguments passed to `(.*)`, see its help with `.*\?`$/u,
refused: (match) =>
`a wrong argument count for ${JSON.stringify(match[1])}`,
},
{
line: /^ERROR: core: Error while executing command: /u,
refused: () => "an invalid statement",
},
{
line: /^ERROR: core: Error while parsing command: /u,
refused: () => "input that failed to parse",
},
];
/** Explain the first Rizin shell refusal in a completed process's stderr. */
export const rizinCommandRejection = (stderr: string): string | undefined => {
for (const text of stderr.split(/\r?\n/u))
for (const { line, refused } of RIZIN_SHELL_REFUSALS) {
const match = line.exec(text);
if (match !== null)
return `Rizin refused ${refused(match)} (ERROR line on stderr). It skips a refused statement and every later one, so stdout holds at most the output of earlier statements; empty stdout is not an empty result. Correct the statement, using <command>? for Rizin's help, and run it again.`;
}
return undefined;
};
+38 -87
View File
@@ -22,11 +22,6 @@ import {
AnalysisInputError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import {
RIZIN_DEBUG_SESSION_TOOLS,
stoppedDebuggerSessionError,
unknownDebuggerSessionError,
} from "../domain/debuggerSessionErrors.js";
import type {
AnalysisError,
AnalysisErrorOptions,
@@ -229,18 +224,9 @@ interface RizinDebugSession {
frameChunks: Buffer[];
bufferedFrameBytes: number;
ready: boolean;
/** start returned this session's ID, so the caller must close it. */
published: boolean;
closeRequested: boolean;
exited: boolean;
commandTail: Promise<void>;
protocolError: string | undefined;
/**
* Why the session stopped accepting commands. Cleanup synchronization
* clears protocolError once the transport is correlated again, but the
* session stays stopped, so the diagnosis is retained here.
*/
stopReason: string | undefined;
cleanupSynchronization:
| {
readonly marker: string;
@@ -378,7 +364,7 @@ export class RizinDebugSessionManager {
"start_rizin_debug_session",
"memory",
`Rizin session capacity reached (${String(this.#maxSessions.value)} sessions).`,
"Close a Rizin debug session before starting another; a stopped session that rizin_debug_session_status reports closed still holds its slot until close_rizin_debug_session releases it.",
"Close a Rizin debug session before starting another.",
this.#maxSessions,
{
max_sessions: this.#maxSessions.value,
@@ -623,12 +609,9 @@ export class RizinDebugSessionManager {
frameChunks: [],
bufferedFrameBytes: 0,
ready: false,
published: false,
closeRequested: false,
exited: false,
commandTail: Promise.resolve(),
protocolError: undefined,
stopReason: undefined,
cleanupSynchronization: undefined,
commandCaptureActive: false,
commandOutputDropped: false,
@@ -668,7 +651,6 @@ export class RizinDebugSessionManager {
if (session.protocolError !== undefined)
throw new Error(session.protocolError);
session.ready = true;
session.published = true;
return ok({ session_id: id, backend: input.backend ?? null });
} catch (caught) {
const protocolFailure = session.protocolError;
@@ -737,22 +719,15 @@ export class RizinDebugSessionManager {
>
> {
const session = this.#sessions.get(sessionId);
if (session === undefined)
return err(
unknownDebuggerSessionError(
RIZIN_DEBUG_SESSION_TOOLS,
"rizin_debug_command",
),
);
const stopped = this.#stopReason(session);
if (stopped !== undefined)
return err(
stoppedDebuggerSessionError(
RIZIN_DEBUG_SESSION_TOOLS,
"rizin_debug_command",
stopped,
),
);
if (
session === undefined ||
!session.ready ||
session.exited ||
session.protocolError !== undefined ||
session.closing ||
this.#closingAll
)
return err(new AnalysisInputError("rizin_debug_command"));
if (signal?.aborted)
return err(rizinCancellation("rizin_debug_command", signal));
if (session.pendingCommandCount >= this.#maxPendingCommands.value)
@@ -807,15 +782,14 @@ export class RizinDebugSessionManager {
acquired = true;
if (signal?.aborted)
return err(rizinCancellation("rizin_debug_command", signal));
const stoppedWhileQueued = this.#stopReason(session);
if (stoppedWhileQueued !== undefined)
return err(
stoppedDebuggerSessionError(
RIZIN_DEBUG_SESSION_TOOLS,
"rizin_debug_command",
stoppedWhileQueued,
),
);
if (
!session.ready ||
session.exited ||
session.closing ||
this.#closingAll ||
session.protocolError !== undefined
)
return err(new AnalysisInputError("rizin_debug_command"));
if (hasUnclaimedRizinFrames(session)) {
const message =
"Unsolicited Rizin frames arrived before a command; command output can no longer be safely correlated.";
@@ -1094,40 +1068,27 @@ export class RizinDebugSessionManager {
readonly recent_output_truncated: boolean;
readonly diagnostics_truncated: boolean;
readonly output_invalid_utf8: boolean;
readonly closed_reason?: string;
}
| undefined {
const session = this.#sessions.get(sessionId);
if (session === undefined) return undefined;
const stopped = this.#stopReason(session);
return {
session_id: session.id,
state: stopped === undefined ? "ready" : "closed",
inferior_tty: session.inferiorTty,
recent_output: [...session.history],
recent_output_truncated: session.historyTruncated,
diagnostics_truncated:
session.supervisor.snapshot().diagnosticTruncated === true,
output_invalid_utf8: session.invalidUtf8Observed,
...(stopped === undefined ? {} : { closed_reason: stopped }),
};
}
/** Why a retained session accepts no commands, or undefined while it is ready. */
#stopReason(session: RizinDebugSession): string | undefined {
const diagnosed = session.stopReason ?? session.protocolError;
if (diagnosed !== undefined) return diagnosed;
if (this.#closingAll)
return "REA is shutting down and closing every Rizin debug session.";
if (session.closeRequested)
return "close_rizin_debug_session started releasing this session, and its owned cleanup has not completed.";
if (session.exited) {
const { exitCode, signalCode } = session.launched.process;
return `The Rizin process exited (exit code ${String(exitCode)}, signal ${String(signalCode)}); a command such as q, or a crash, can end it.`;
}
if (session.closing || !session.ready)
return "REA stopped this Rizin debug session.";
return undefined;
return session === undefined
? undefined
: {
session_id: session.id,
state:
session.exited ||
session.protocolError !== undefined ||
session.closing ||
!session.ready
? "closed"
: "ready",
inferior_tty: session.inferiorTty,
recent_output: [...session.history],
recent_output_truncated: session.historyTruncated,
diagnostics_truncated:
session.supervisor.snapshot().diagnosticTruncated === true,
output_invalid_utf8: session.invalidUtf8Observed,
};
}
async close(
@@ -1137,13 +1098,7 @@ export class RizinDebugSessionManager {
return err(new AnalysisInputError("close_rizin_debug_session"));
const session = this.#sessions.get(sessionId);
if (session === undefined)
return err(
unknownDebuggerSessionError(
RIZIN_DEBUG_SESSION_TOOLS,
"close_rizin_debug_session",
),
);
session.closeRequested = true;
return err(new AnalysisInputError("close_rizin_debug_session"));
session.closing = true;
const stopped = await this.#stopDebuggerOnly(session);
if (stopped.status === "incomplete")
@@ -1186,10 +1141,7 @@ export class RizinDebugSessionManager {
stopped.reason,
),
);
// The caller still holds a published session's ID; keep it addressable
// so commands and status report why it stopped until it is closed.
if (!session.published || session.closeRequested)
this.#sessions.delete(session.id);
this.#sessions.delete(session.id);
}
return ok(null);
}
@@ -1564,7 +1516,6 @@ export class RizinDebugSessionManager {
clearTimeout(timer);
session.cleanupSynchronization = undefined;
if (cause === undefined) {
session.stopReason ??= session.protocolError;
session.protocolError = undefined;
session.commandOutputDropped = false;
resolve();
+5 -10
View File
@@ -2,10 +2,6 @@ import type { McpServer, ServerContext } from "@modelcontextprotocol/server";
import type { Logger } from "pino";
import type { GdbSessionManager } from "../gdb/GdbSessionManager.js";
import {
GDB_SESSION_TOOLS,
unknownDebuggerSessionError,
} from "../domain/debuggerSessionErrors.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import {
@@ -14,6 +10,7 @@ import {
} from "../application/analysisView/AnalysisViewService.js";
import { toolContract } from "../contracts/toolContracts.js";
import { err, ok } from "../domain/result.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import {
runOwnedCleanupWithExternalMutationAdmission,
runWithExternalMutationAdmission,
@@ -120,12 +117,7 @@ export const registerGdbTools = (
const result = manager.status(input.session_id);
if (result === undefined)
return delivery.toCallToolResult(
err(
unknownDebuggerSessionError(
GDB_SESSION_TOOLS,
"gdb_session_status",
),
),
err(invalidSession("gdb_session_status")),
status,
);
return delivery.toCallToolResult(
@@ -158,3 +150,6 @@ export const registerGdbTools = (
},
);
};
const invalidSession = (operation: string): AnalysisInputError =>
new AnalysisInputError(operation);
+2 -11
View File
@@ -3,10 +3,7 @@ import type { Logger } from "pino";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import {
RIZIN_DEBUG_SESSION_TOOLS,
unknownDebuggerSessionError,
} from "../domain/debuggerSessionErrors.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import type { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js";
import {
runOwnedCleanupWithExternalMutationAdmission,
@@ -102,13 +99,7 @@ export const registerRizinDebugTools = (
const result = manager.status(input.session_id);
if (result === undefined)
return delivery.toCallToolResult(
{
ok: false,
error: unknownDebuggerSessionError(
RIZIN_DEBUG_SESSION_TOOLS,
status.name,
),
},
{ ok: false, error: new AnalysisInputError(status.name) },
status,
);
return delivery.toCallToolResult(
+1 -1
View File
@@ -12,7 +12,7 @@ import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest, type TestCli } from "../support/cli/cliFixture.js";
const inspectBoth = async (cli: TestCli, path: string) => {
const output = await cli.run({
const output = await cli.sharedCliRun!({
arguments: [
"inspect-apk-signature",
path,
@@ -6,7 +6,7 @@ import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
const SERIAL = "listing-numbers-device";
const UNSAFE = "9007199254740993";
@@ -224,10 +224,9 @@ process.stdout.write(require('node:fs').readFileSync(path));
arguments: [operation.replaceAll("_", "-"), SERIAL, "--json"],
environment,
};
const output = await cli.run({
...invocation,
freshProcess: variant === "safe",
});
const output = await (variant === "safe"
? cli.run(invocation)
: runFileCli(cli, invocation));
expect(output.exitCode, output.stderr).toBe(0);
const evidence = contract.outputSchema.parse(output.json);
expect(evidence.parameters).toMatchObject({ serial: SERIAL });
@@ -9,7 +9,7 @@ import { adbDisplayPartialObservationSchema } from "../../../src/domain/adb/adbD
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
const SERIAL = "numeric-declarations-device";
const PACKAGE = "com.example.numeric";
@@ -224,10 +224,9 @@ cliTest.skipIf(process.platform === "win32")(
arguments: ["inspect-adb-display", SERIAL, "--json"],
environment: fixture.environment,
};
const output = await cli.run({
...invocation,
freshProcess: scenario === displayScenarios[0],
});
const output = await (scenario === displayScenarios[0]
? cli.run(invocation)
: runFileCli(cli, invocation));
expect(output.exitCode, `${scenario.name}: ${output.stderr}`).toBe(0);
const evidence = contract.outputSchema.parse(output.json);
expect(evidence.parameters).toEqual({ serial: SERIAL });
@@ -282,10 +281,9 @@ cliTest.skipIf(process.platform === "win32")(
arguments: ["inspect-adb-display", SERIAL, "--json"],
environment: fixture.environment,
};
const output = await cli.run({
...invocation,
freshProcess: value === null,
});
const output = await (value === null
? cli.run(invocation)
: runFileCli(cli, invocation));
expect(output.exitCode, output.stderr).toBe(1);
const projected = analysisErrorProjectionSchema.parse(output.json);
expect(projected.code).toBe("unreadable_output");
@@ -456,10 +454,9 @@ cliTest.skipIf(process.platform === "win32")(
arguments: ["inspect-adb-package", SERIAL, PACKAGE, "--json"],
environment: fixture.environment,
};
const output = await cli.run({
...invocation,
freshProcess: scenario === packageScenarios[0],
});
const output = await (scenario === packageScenarios[0]
? cli.run(invocation)
: runFileCli(cli, invocation));
expect(output.exitCode, `${scenario.name}: ${output.stderr}`).toBe(0);
const evidence = contract.outputSchema.parse(output.json);
expect(evidence.parameters).toEqual({ serial: SERIAL, package: PACKAGE });
@@ -8,7 +8,7 @@ import { toolContract } from "../../../src/contracts/toolContracts.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
const PACKAGE_LINE = "package:/data/app/example/base.apk=com.example.app";
@@ -139,10 +139,9 @@ process.exit(91);
arguments: [command, "emulator-5554", ...tail, "--json"],
environment,
};
const output = await cli.run({
...invocation,
freshProcess: count === 0,
});
const output = await (count === 0
? cli.run(invocation)
: runFileCli(cli, invocation));
expect(output.exitCode, output.stderr).toBe(0);
const evidence = parseEvidence(output.json);
expect(
@@ -131,7 +131,7 @@ const checkCliPublication = async (
},
) => {
const { root, path, parent, summary, selected } = context;
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const complete = await run({
arguments: ["inspect-api-contract", path, "--json"],
});
@@ -395,7 +395,7 @@ cliTest(
coverage: { total: 3 },
source_coverage: { status: "partial" },
});
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const cliResult = await run({
arguments: ["inspect-api-contract", path, "--json"],
});
@@ -442,7 +442,7 @@ cliTest(
details: { partial_observation: snapshot },
});
expect(JSON.stringify(response)).not.toContain("u:p@h");
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const result = await run({
arguments: ["inspect-api-contract", path, "--json"],
});
@@ -7,7 +7,7 @@ import { parseEvidence } from "../../../src/domain/evidence.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { applicationVersionComparisonResultSchema } from "../../../src/domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
cliTest(
"keeps unavailable source-selection scopes unknown in version comparisons",
@@ -72,7 +72,7 @@ cliTest(
) => {
const inputPath = join(root, `${name}.json`);
await writeFile(inputPath, JSON.stringify({ left, right }));
const response = await cli.run({
const response = await runFileCli(cli, {
arguments: ["compare-application-versions", inputPath, "--json"],
environment: { REA_LOG_LEVEL: "silent" },
});
@@ -492,7 +492,7 @@ cliTest(
const compare = async (left: unknown, right: unknown) => {
const inputPath = join(root, `comparison-${Math.random()}.json`);
await writeFile(inputPath, JSON.stringify({ left, right }));
const response = await cli.run({
const response = await runFileCli(cli, {
arguments: ["compare-application-versions", inputPath, "--json"],
environment: { REA_LOG_LEVEL: "silent" },
});
@@ -107,7 +107,7 @@ cliTest(
const root = await createTestTempDirectory("rea-beam-views-");
const path = join(root, "app.beam");
await writeFile(path, erlangBeamFixture("views"));
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const complete = await run({
arguments: ["inspect-erlang-beam", path, "--json"],
});
@@ -312,7 +312,7 @@ cliTest(
async ({ cli }) => {
const root = await createTestTempDirectory("rea-beam-invalid-view-");
const output = join(root, "unused.json");
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const view = JSON.stringify({ kind: "beam", collection: "exports" });
for (const [flags, key] of [
[["--detail", "summary"], "evidence_output"],
@@ -137,7 +137,7 @@ cliTest(
},
}),
);
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const result = await run({
arguments: ["compare-artifacts", requestPath, "--json"],
});
@@ -35,7 +35,7 @@ const checkCliPublication = async (
},
) => {
const { root, parent, selected } = context;
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const args = [
"inspect-api-contract",
"./SDL 雪.graphql",
@@ -271,7 +271,7 @@ cliTest(
},
}),
);
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const offline = await run({
arguments: ["inspect-analysis-view", queryPath, "--json"],
});
@@ -339,7 +339,7 @@ cliTest(
source_coverage: { status: "partial", operations: "unavailable" },
},
});
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const cliResult = await run({
arguments: [
"inspect-api-contract",
@@ -392,7 +392,7 @@ cliTest(
items: [{ code: "schema-parse-refused", source: { role: "schema" } }],
});
await writeFile(path, text);
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const result = await run({
arguments: [
"inspect-api-contract",
@@ -9,7 +9,11 @@ import { interfaceBuilderAnalysisSchema } from "../../../src/domain/apple/interf
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
import {
cliTest,
runFileCli,
type TestCli,
} from "../../support/cli/cliFixture.js";
const unknownMixedInteger = {
$plist_type: "unknown",
@@ -35,7 +39,7 @@ const inspectArchive = (cli: TestCli, app: string, fresh: boolean) => {
arguments: ["decode-interface-builder", app, "--json"],
environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto" },
};
return cli.run({ ...invocation, freshProcess: fresh });
return fresh ? cli.run(invocation) : runFileCli(cli, invocation);
};
cliTest.for(scenarios)(
@@ -1,7 +1,8 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
@@ -9,7 +10,6 @@ import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/j
import { applicationVersionComparisonResultSchema } from "../../../src/domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
@@ -53,93 +53,109 @@ cliTest(
"analyze_javascript_application",
).outputSchema.parse(cliResponse.json);
const { client } = await connectLocalToolsMcp();
const response = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, format: "directory" },
const client = new Client({ name: "endpoint-payload-e2e", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "" },
stderr: "pipe",
});
expect(response.isError).not.toBe(true);
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(response.structuredContent);
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
const text = response.content.find((item) => item.type === "text");
if (text?.type !== "text") throw new Error("Missing MCP text result");
const textEvidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(JSON.parse(text.text));
expect(textEvidence.evidence_id).toBe(evidence.evidence_id);
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expect(result.graph.coverage.limits).toContainEqual(
expect.objectContaining({
name: "javascript_semantic_primitive_bytes",
}),
);
expect(
result.semantic_graph.nodes.some(
({ kind, properties }) =>
kind === "literal" && properties.value === endpoint,
),
).toBe(false);
const endpoints = result.graph.nodes.filter(
(node) => node.kind === "endpoint",
);
const nodeFor = (value: string) => {
const node = endpoints.find((candidate) =>
candidate.observations.some(
({ properties }) => properties.value === value,
),
transport.stderr?.on("data", () => undefined);
try {
await client.connect(transport);
const response = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, format: "directory" },
});
expect(response.isError).not.toBe(true);
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(response.structuredContent);
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
const text = response.content.find((item) => item.type === "text");
if (text?.type !== "text") throw new Error("Missing MCP text result");
const textEvidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(JSON.parse(text.text));
expect(textEvidence.evidence_id).toBe(evidence.evidence_id);
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
if (node === undefined) throw new Error("Complete endpoint was lost");
return node;
};
const endpointNode = nodeFor(endpoint);
expect(
new Set(
[endpoint, digestLookalike, surrogate, replacement].map(
(value) => nodeFor(value).node_id,
expect(result.graph.coverage.limits).toContainEqual(
expect.objectContaining({
name: "javascript_semantic_primitive_bytes",
}),
);
expect(
result.semantic_graph.nodes.some(
({ kind, properties }) =>
kind === "literal" && properties.value === endpoint,
),
).size,
).toBe(4);
const repeatedNode = nodeFor("/ok");
expect(
endpoints.filter((node) =>
node.observations.some(({ properties }) => properties.value === "/ok"),
),
).toHaveLength(1);
expect(repeatedNode.observations).toHaveLength(2);
expect(repeatedNode.identity).toMatchObject({
key: JSON.stringify(["literal", "/ok"]),
});
expect(
repeatedNode.observations.every(({ label }) => label === "/ok"),
).toBe(true);
expect(endpointNode.identity).toMatchObject({
artifact_sha256: createHash("sha256").update(source).digest("hex"),
});
expect(endpointNode.observations[0]?.evidence.location).toMatchObject({
available: true,
value: { kind: "source-range", source: "main.js" },
});
const requestNodes = result.semantic_graph.nodes.filter(
(node) =>
node.kind === "request" && node.properties.endpoint === endpoint,
);
expect(requestNodes).toHaveLength(1);
).toBe(false);
const endpoints = result.graph.nodes.filter(
(node) => node.kind === "endpoint",
);
const nodeFor = (value: string) => {
const node = endpoints.find((candidate) =>
candidate.observations.some(
({ properties }) => properties.value === value,
),
);
if (node === undefined) throw new Error("Complete endpoint was lost");
return node;
};
const endpointNode = nodeFor(endpoint);
expect(
new Set(
[endpoint, digestLookalike, surrogate, replacement].map(
(value) => nodeFor(value).node_id,
),
).size,
).toBe(4);
const repeatedNode = nodeFor("/ok");
expect(
endpoints.filter((node) =>
node.observations.some(
({ properties }) => properties.value === "/ok",
),
),
).toHaveLength(1);
expect(repeatedNode.observations).toHaveLength(2);
expect(repeatedNode.identity).toMatchObject({
key: JSON.stringify(["literal", "/ok"]),
});
expect(
repeatedNode.observations.every(({ label }) => label === "/ok"),
).toBe(true);
expect(endpointNode.identity).toMatchObject({
artifact_sha256: createHash("sha256").update(source).digest("hex"),
});
expect(endpointNode.observations[0]?.evidence.location).toMatchObject({
available: true,
value: { kind: "source-range", source: "main.js" },
});
const requestNodes = result.semantic_graph.nodes.filter(
(node) =>
node.kind === "request" && node.properties.endpoint === endpoint,
);
expect(requestNodes).toHaveLength(1);
await verifyEndpointWorkflows({
client,
evidenceId: evidence.evidence_id,
endpoint,
endpointNodeId: endpointNode.node_id,
requestNodeIds: requestNodes.map(({ node_id }) => node_id),
root,
source,
sourcePath,
});
await client.ping();
await verifyEndpointWorkflows({
client,
evidenceId: evidence.evidence_id,
endpoint,
endpointNodeId: endpointNode.node_id,
requestNodeIds: requestNodes.map(({ node_id }) => node_id),
root,
source,
sourcePath,
});
await client.ping();
} finally {
await client.close();
await transport.close();
}
},
);
@@ -1,12 +1,13 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
@@ -41,106 +42,123 @@ cliTest(
const cliEvidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(cliResponse.json);
const { client } = await connectLocalToolsMcp();
const response = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, format: "directory" },
const client = new Client({ name: "event-payload-e2e", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "" },
stderr: "pipe",
});
expect(response.isError).not.toBe(true);
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(response.structuredContent);
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
const text = response.content.find((item) => item.type === "text");
if (text?.type !== "text") throw new Error("Missing MCP text result");
expect(
toolContract("analyze_javascript_application").outputSchema.parse(
JSON.parse(text.text),
).evidence_id,
).toBe(evidence.evidence_id);
const graph = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
).semantic_graph;
const events = graph.nodes.filter((node) => node.kind === "event");
const selected = events.filter(
(node) => node.properties.event_name === name,
);
expect(selected).toHaveLength(1);
const event = selected[0];
if (event === undefined) throw new Error("Complete event was lost");
const listeners = graph.nodes.filter(
(node) => node.kind === "listener" && node.properties.event_name === name,
);
expect(new Set(listeners.map((node) => node.properties.method))).toEqual(
new Set(["on", "off"]),
);
const listenerIds = new Set(listeners.map((node) => node.node_id));
expect(
graph.relations
.filter(
(relation) =>
relation.source_node_id === event.node_id &&
listenerIds.has(relation.target_node_id),
)
.map((relation) => relation.relation),
).toEqual(
expect.arrayContaining(["registers-listener", "removes-listener"]),
);
const lookalikeEvent = events.find(
(node) => node.properties.event_name === lookalike,
);
expect(lookalikeEvent).toBeDefined();
expect(lookalikeEvent?.node_id).not.toBe(event.node_id);
const shortEvents = events.filter(
(node) => node.properties.event_name === "ok",
);
expect(shortEvents).toHaveLength(2);
expect(
new Set(shortEvents.map((node) => node.properties.emitter_key)).size,
).toBe(2);
expect(
shortEvents.every(
transport.stderr?.on("data", () => undefined);
try {
await client.connect(transport);
const response = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, format: "directory" },
});
expect(response.isError).not.toBe(true);
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(response.structuredContent);
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
const text = response.content.find((item) => item.type === "text");
if (text?.type !== "text") throw new Error("Missing MCP text result");
expect(
toolContract("analyze_javascript_application").outputSchema.parse(
JSON.parse(text.text),
).evidence_id,
).toBe(evidence.evidence_id);
const graph = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
).semantic_graph;
const events = graph.nodes.filter((node) => node.kind === "event");
const selected = events.filter(
(node) => node.properties.event_name === name,
);
expect(selected).toHaveLength(1);
const event = selected[0];
if (event === undefined) throw new Error("Complete event was lost");
const listeners = graph.nodes.filter(
(node) =>
node.label === "ok" && node.identity.role_key.endsWith("\0ok"),
),
).toBe(true);
const dynamic = events.find((node) => node.properties.event_name === null);
expect(dynamic?.label).toBeNull();
expect(
graph.unknowns.some(
(unknown) =>
unknown.node_id === dynamic?.node_id && unknown.family === "event",
),
).toBe(true);
const traceResponse = await client.callTool({
name: "trace_javascript_semantics",
arguments: {
application: {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
node.kind === "listener" && node.properties.event_name === name,
);
expect(new Set(listeners.map((node) => node.properties.method))).toEqual(
new Set(["on", "off"]),
);
const listenerIds = new Set(listeners.map((node) => node.node_id));
expect(
graph.relations
.filter(
(relation) =>
relation.source_node_id === event.node_id &&
listenerIds.has(relation.target_node_id),
)
.map((relation) => relation.relation),
).toEqual(
expect.arrayContaining(["registers-listener", "removes-listener"]),
);
const lookalikeEvent = events.find(
(node) => node.properties.event_name === lookalike,
);
expect(lookalikeEvent).toBeDefined();
expect(lookalikeEvent?.node_id).not.toBe(event.node_id);
const shortEvents = events.filter(
(node) => node.properties.event_name === "ok",
);
expect(shortEvents).toHaveLength(2);
expect(
new Set(shortEvents.map((node) => node.properties.emitter_key)).size,
).toBe(2);
expect(
shortEvents.every(
(node) =>
node.label === "ok" && node.identity.role_key.endsWith("\0ok"),
),
).toBe(true);
const dynamic = events.find(
(node) => node.properties.event_name === null,
);
expect(dynamic?.label).toBeNull();
expect(
graph.unknowns.some(
(unknown) =>
unknown.node_id === dynamic?.node_id && unknown.family === "event",
),
).toBe(true);
const traceResponse = await client.callTool({
name: "trace_javascript_semantics",
arguments: {
application: {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
},
query: {
seed: { kind: "event", name },
direction: "forward-influence",
},
},
query: {
seed: { kind: "event", name },
direction: "forward-influence",
},
},
});
expect(traceResponse.isError).not.toBe(true);
const traceEvidence = toolContract(
"trace_javascript_semantics",
).outputSchema.parse(traceResponse.structuredContent);
const trace = javaScriptSemanticTraceResultSchema.parse(
traceEvidence.normalized_result,
);
expect(new Set(trace.seed_node_ids)).toEqual(
new Set([event.node_id, ...listenerIds]),
);
expect(
trace.nodes
.filter((node) => listenerIds.has(node.node_id))
.map((node) => node.properties.event_name),
).toEqual(listeners.map(() => name));
await client.ping();
});
expect(traceResponse.isError).not.toBe(true);
const traceEvidence = toolContract(
"trace_javascript_semantics",
).outputSchema.parse(traceResponse.structuredContent);
const trace = javaScriptSemanticTraceResultSchema.parse(
traceEvidence.normalized_result,
);
expect(new Set(trace.seed_node_ids)).toEqual(
new Set([event.node_id, ...listenerIds]),
);
expect(
trace.nodes
.filter((node) => listenerIds.has(node.node_id))
.map((node) => node.properties.event_name),
).toEqual(listeners.map(() => name));
await client.ping();
} finally {
await client.close();
await transport.close();
}
},
);
@@ -6,7 +6,7 @@ import { expect } from "vitest";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
cliTest(
"pages only module units for a CommonJS file that exports symbols",
@@ -23,7 +23,7 @@ cliTest(
"exports.beta = function beta() { return 2; };",
].join("\n"),
);
const analyzed = await cli.run({
const analyzed = await runFileCli(cli, {
arguments: ["analyze-javascript-application", root, "--format", "json"],
});
expect(analyzed.exitCode, analyzed.stdout + analyzed.stderr).toBe(0);
@@ -54,7 +54,7 @@ cliTest(
traceInput,
JSON.stringify({ application: evidence, seed }),
);
const traced = await cli.run({
const traced = await runFileCli(cli, {
arguments: ["trace-application-feature", traceInput, "--json"],
});
expect(traced.exitCode, traced.stdout + traced.stderr).toBe(0);
@@ -86,7 +86,7 @@ cliTest(
view,
}),
);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-analysis-view", queryPath, "--json"],
});
expect(result.exitCode, result.stdout + result.stderr).toBe(0);
@@ -13,6 +13,7 @@ import { findApplicationFeatureSeeds } from "../../../src/domain/javascript/java
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
cliTest,
runFileCli,
type SharedMcpCall,
type TestCli,
} from "../../support/cli/cliFixture.js";
@@ -169,7 +170,7 @@ const traceThroughCli = async (
const invocation = {
arguments: ["trace-application-feature", input, "--json"],
};
const response = await cli.run(invocation);
const response = await runFileCli(cli, invocation);
expect(response.exitCode, `${scenario.name} via CLI`).toBe(0);
const evidence = toolContract("trace_application_feature").outputSchema.parse(
response.json,
@@ -1,9 +1,10 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
@@ -57,15 +58,34 @@ cliTest(
0,
),
).toBe(29);
const { client } = await connectLocalToolsMcp();
const response = await client.callTool({
name: contract.name,
arguments: { input_path: root, detail: "complete", ...options },
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" },
stderr: "pipe",
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const mcp = contract.outputSchema.parse(response.structuredContent);
expect(mcp.parameters).toEqual(evidence.parameters);
expect(mcp.evidence_id).toBe(evidence.evidence_id);
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
const client = new Client({
name: "unknown-capacity-regression",
version: "1",
});
try {
await client.connect(transport);
const response = await client.callTool({
name: contract.name,
arguments: { input_path: root, detail: "complete", ...options },
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const mcp = contract.outputSchema.parse(response.structuredContent);
expect(mcp.parameters).toEqual(evidence.parameters);
expect(mcp.evidence_id).toBe(evidence.evidence_id);
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
},
);
@@ -8,7 +8,7 @@ import { afterAll, expect } from "vitest";
import { parse as parseYaml } from "yaml";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
afterAll(() => disposeMdItInstance());
@@ -45,10 +45,9 @@ cliTest.for([
const invocation = {
arguments: ["inspect-keyed-archive", path, ...args],
};
return cli.run({
...invocation,
freshProcess: label === "high" || label === "pair",
});
return label === "high" || label === "pair"
? cli.run(invocation)
: runFileCli(cli, invocation);
};
for (const format of ["json", "yaml"] as const) {
const output = await run(["--format", format]);
@@ -121,7 +120,9 @@ cliTest(
const invocation = {
arguments: ["inspect-keyed-archive", path, ...args],
};
return cli.run({ ...invocation, freshProcess: label === "nul" });
return label === "nul"
? cli.run(invocation)
: runFileCli(cli, invocation);
};
const json = await run(["--json"]);
expect(json.exitCode, json.stderr).toBe(0);
@@ -8,7 +8,7 @@ import { expect } from "vitest";
import { keyedArchiveResultSchema } from "../../../src/domain/apple/keyedArchive.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
cliTest.for([
{ name: "plain real", element: "<real>1.25</real>", expected: 1.25 },
@@ -74,7 +74,7 @@ cliTest.for([
await writeFile(path, bytes);
const call = await cli.sharedMcpCall?.();
if (call === undefined) throw new Error("Missing file-scoped MCP session");
const output = await cli.run({
const output = await runFileCli(cli, {
arguments: [
"inspect-keyed-archive",
path,
@@ -155,7 +155,7 @@ cliTest(
}).replace("<string>__VALUE__</string>", element),
);
await writeFile(path, bytes);
const output = await cli.run({
const output = await runFileCli(cli, {
arguments: ["inspect-keyed-archive", path, "--json"],
environment: { REA_LOG_LEVEL: "silent" },
});
@@ -19,7 +19,7 @@ import { parseEvidence } from "../../../src/domain/evidence.js";
import { harmonyApplicationProjectionResultSchema } from "../../../src/domain/harmony/harmonyApplication.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest, type CliInvocation } from "../../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
const harmony = {
family: "HarmonyOS",
@@ -146,8 +146,10 @@ cliTest.for(fixtures)(
const { call } = await connectLocalToolsMcp(environment);
for (const mode of ["directories", "files", "nested"] as const) {
// Keep one fresh dispatcher check per family; reuse later commands.
const run = (invocation: CliInvocation) =>
cli.run({ ...invocation, freshProcess: mode === "directories" });
const run =
mode === "directories"
? cli.run.bind(cli)
: runFileCli.bind(undefined, cli);
const writer = new ZipWriter(new Uint8ArrayWriter());
for (const path of fixture.paths) {
if (mode !== "files")
@@ -322,8 +324,9 @@ cliTest.for([
const environment = { HOME: root, USERPROFILE: root };
const { call } = await connectLocalToolsMcp(environment);
for (const encrypted of [false, true]) {
const run = (invocation: CliInvocation) =>
cli.run({ ...invocation, freshProcess: !encrypted });
const run = encrypted
? runFileCli.bind(undefined, cli)
: cli.run.bind(cli);
const writer = new ZipWriter(new Uint8ArrayWriter());
for (const member of fixture.paths)
await writer.add(
@@ -40,7 +40,7 @@ cliTest.for(["pe", "elf"] as const)(
versionDefinitions: ["fixture", "V1"],
});
await writeFile(path, bytes);
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const complete = await run({
arguments: ["inspect-native-exports", path, "--json"],
});
+116 -94
View File
@@ -1,15 +1,19 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { peResourcesSchema } from "../../../src/domain/native/peResources.js";
import { peResourceFixture } from "../../../src/native/pe/PeResources.fixture.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
import {
cliTest,
runFileCli,
type TestCli,
} from "../../support/cli/cliFixture.js";
type PeResourceCall = (
arguments_: Record<string, unknown>,
@@ -22,7 +26,7 @@ const expectPeInputFailureAcrossAdapters = async (input: {
readonly reason: "invalid_format" | "invalid_value";
}) => {
const { cli, call, path, reason } = input;
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-pe-resources", path, "--json"],
});
expect(result.exitCode).toBe(1);
@@ -113,7 +117,7 @@ const expectPeWordAlignedRecordsAcrossAdapters = async (input: {
);
const path = join(root, `word-aligned-${plus}-${index}.exe`);
await writeFile(path, fixture.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-pe-resources", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
@@ -204,7 +208,7 @@ const expectPeMappingOwnershipAcrossAdapters = async (
});
continue;
}
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-pe-resources", path, "--json"],
});
expect(result.exitCode, name).toBe(0);
@@ -262,7 +266,9 @@ const expectPeInventoriesAcrossAdapters = async (
};
// Keep an independent public-wrapper process for both PE32 and PE32+.
// The empty-payload variants reuse only compiled-command startup.
const result = await (empty ? cli.run(invocation) : cli.run(invocation));
const result = await (empty
? runFileCli(cli, invocation)
: cli.run(invocation));
expect(result.exitCode).toBe(0);
const evidence = contract.outputSchema.parse(result.json);
const report = peResourcesSchema.parse(evidence.normalized_result);
@@ -342,7 +348,7 @@ const expectPeEmptyBranchesAcrossAdapters = async (
shared ? "shared-empty64.exe" : "empty-branches32.exe",
);
await writeFile(path, fixture.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-pe-resources", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
@@ -416,97 +422,113 @@ cliTest(
"inspects PE32/PE32+ resources through compiled CLI and real stdio MCP without an active target",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-pe-resource-acceptance-");
const { client } = await connectLocalToolsMcp();
const inventory = await client.listTools();
const definition = inventory.tools.find(
({ name }) => name === contract.name,
);
if (definition === undefined)
throw new Error("Missing PE resource tool contract");
expect(inventory.tools).toContainEqual(
expect.objectContaining({
name: contract.name,
annotations: expect.objectContaining({
readOnlyHint: false,
openWorldHint: false,
}),
}),
);
const call: PeResourceCall = (arguments_) =>
client.callTool(
{ name: definition.name, arguments: arguments_ },
{ toolDefinition: definition },
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" },
stderr: "pipe",
});
const client = new Client({ name: "pe-resource-acceptance", version: "1" });
try {
await client.connect(transport);
const inventory = await client.listTools();
const definition = inventory.tools.find(
({ name }) => name === contract.name,
);
const sessionDefinition = inventory.tools.find(
({ name }) => name === "binary_session",
);
if (sessionDefinition === undefined)
throw new Error("Missing binary_session tool contract");
const session = await client.callTool(
{ name: sessionDefinition.name, arguments: {} },
{ toolDefinition: sessionDefinition },
);
expect(session.isError, JSON.stringify(session)).not.toBe(true);
const status = toolContract("binary_session").outputSchema.parse(
session.structuredContent,
).result;
expect(
status.tool_availability.groups.find((group) =>
group.names.includes("inspect_pe_resources"),
),
).toMatchObject({ available: true });
await expectPeInventoriesAcrossAdapters(cli, call, root);
await expectPeEmptyBranchesAcrossAdapters(cli, call, root);
await expectPeWordAlignedRecordsAcrossAdapters({
cli,
call,
root,
});
await expectPeMappingOwnershipAcrossAdapters(cli, call, root);
await expectPePaddingFailureAcrossAdapters({
cli,
call,
path: join(root, "raw-padding.exe"),
});
await expectPeInputFailuresAcrossAdapters({
cli,
call,
root,
});
const absent = peResourceFixture();
absent.bytes.fill(0, absent.directoryAt, absent.directoryAt + 8);
const path = join(root, "absent.exe");
await writeFile(path, absent.bytes);
const empty = await call({ path });
expect(empty.isError).not.toBe(true);
expect(
contract.outputSchema.parse(empty.structuredContent).normalized_result,
).toMatchObject({
directory: null,
directories: [],
resources: [],
coverage: { status: "complete", resources: 0 },
});
for (const arguments_ of [
{ path, max_file_bytes: 64 },
{ path: "relative.exe" },
{ path, extra: true },
]) {
const rejected = await call(arguments_);
if (definition === undefined)
throw new Error("Missing PE resource tool contract");
expect(inventory.tools).toContainEqual(
expect.objectContaining({
name: contract.name,
annotations: expect.objectContaining({
readOnlyHint: false,
openWorldHint: false,
}),
}),
);
const call: PeResourceCall = (arguments_) =>
client.callTool(
{ name: definition.name, arguments: arguments_ },
{ toolDefinition: definition },
);
const sessionDefinition = inventory.tools.find(
({ name }) => name === "binary_session",
);
if (sessionDefinition === undefined)
throw new Error("Missing binary_session tool contract");
const session = await client.callTool(
{ name: sessionDefinition.name, arguments: {} },
{ toolDefinition: sessionDefinition },
);
expect(session.isError, JSON.stringify(session)).not.toBe(true);
const status = toolContract("binary_session").outputSchema.parse(
session.structuredContent,
).result;
expect(
status.tool_availability.groups.find((group) =>
group.names.includes("inspect_pe_resources"),
),
).toMatchObject({ available: true });
await expectPeInventoriesAcrossAdapters(cli, call, root);
await expectPeEmptyBranchesAcrossAdapters(cli, call, root);
await expectPeWordAlignedRecordsAcrossAdapters({
cli,
call,
root,
});
await expectPeMappingOwnershipAcrossAdapters(cli, call, root);
await expectPePaddingFailureAcrossAdapters({
cli,
call,
path: join(root, "raw-padding.exe"),
});
await expectPeInputFailuresAcrossAdapters({
cli,
call,
root,
});
const absent = peResourceFixture();
absent.bytes.fill(0, absent.directoryAt, absent.directoryAt + 8);
const path = join(root, "absent.exe");
await writeFile(path, absent.bytes);
const empty = await call({ path });
expect(empty.isError).not.toBe(true);
expect(
contract.outputSchema.parse(empty.structuredContent).normalized_result,
).toMatchObject({
directory: null,
directories: [],
resources: [],
coverage: { status: "complete", resources: 0 },
});
for (const arguments_ of [
{ path, max_file_bytes: 64 },
{ path: "relative.exe" },
{ path, extra: true },
]) {
const rejected = await call(arguments_);
expect(rejected.isError).toBe(true);
expect(
contract.outputSchema.safeParse(rejected.structuredContent).success,
).toBe(false);
}
const damaged = peResourceFixture();
damaged.bytes.writeUInt32LE(17, damaged.directoryAt + 4);
const badPath = join(root, "damaged.exe");
await writeFile(badPath, damaged.bytes);
const rejected = await call({ path: badPath });
expect(rejected.isError).toBe(true);
expect(
contract.outputSchema.safeParse(rejected.structuredContent).success,
).toBe(false);
await client.ping();
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
const damaged = peResourceFixture();
damaged.bytes.writeUInt32LE(17, damaged.directoryAt + 4);
const badPath = join(root, "damaged.exe");
await writeFile(badPath, damaged.bytes);
const rejected = await call({ path: badPath });
expect(rejected.isError).toBe(true);
expect(
contract.outputSchema.safeParse(rejected.structuredContent).success,
).toBe(false);
await client.ping();
},
);
@@ -73,7 +73,7 @@ cliTest(
(await call("inspect_playwright_trace", { path: fixture.path, view }))
.structuredContent,
).toEqual(selected);
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const result = await run({
arguments: ["inspect-playwright-trace", fixture.path, "--json"],
});
@@ -102,7 +102,7 @@ const checkCliPublication = async ({
call: McpCall;
}) => {
const source = { kind: "retained-evidence", evidence_id: parent.evidence_id };
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const fullCli = await run({
arguments: ["inspect-playwright-trace", fixture.path, "--json"],
});
@@ -420,7 +420,7 @@ cliTest(
expect(
parseEvidence(full.structuredContent).normalized_result,
).toMatchObject({ items: [{ text: message }] });
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const result = await run({
arguments: ["inspect-playwright-trace", fixture.path, "--json"],
});
@@ -74,7 +74,7 @@ cliTest(
const root = await createTestTempDirectory("rea-pyc-views-");
const path = join(root, "module.pyc");
await writeSameNamePyc(path);
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const complete = await run({
arguments: ["inspect-python-pyc", path, "--json"],
});
@@ -222,7 +222,7 @@ cliTest(
async ({ cli }) => {
const root = await createTestTempDirectory("rea-pyc-invalid-delivery-");
const output = join(root, "unused.json");
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const view = JSON.stringify({
kind: "python-pyc",
collection: "code_objects",
@@ -47,7 +47,7 @@ const prepareInputs = async (cli: TestCli) => {
"f0e7d2c869c811d4272a7b2916c6531c8c6cc7938336a35575e2a6b7b6346714",
);
await writeFile(harPath, harBytes);
const run = cli.run.bind(cli);
const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli);
const analyzed = await run({
arguments: [
"analyze-javascript-application",
@@ -8,9 +8,6 @@ import { parseNativeDmgInfo } from "../../src/artifacts/NativeDmgOwnership.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
// Each command verifies the whole image before attaching; hosted macOS runners
// take about three times as long as a local host. The project deadline bounds a
// hang, so no step here hard-codes host speed.
cliTest.skipIf(process.platform !== "darwin")(
"DMG recovery preserves the caller's real attachment and succeeds after its owner releases it",
async ({ cli, processes }) => {
@@ -19,19 +16,23 @@ cliTest.skipIf(process.platform !== "darwin")(
await mkdir(source);
await writeFile(join(source, "fixture.txt"), "caller-owned image\n");
const image = join(root, "fixture.dmg");
const created = await processes.run("/usr/bin/hdiutil", [
"create",
"-quiet",
"-srcfolder",
source,
"-volname",
"ReaRecoveryFixture",
"-size",
"20m",
"-format",
"UDZO",
image,
]);
const created = await processes.run(
"/usr/bin/hdiutil",
[
"create",
"-quiet",
"-srcfolder",
source,
"-volname",
"ReaRecoveryFixture",
"-size",
"20m",
"-format",
"UDZO",
image,
],
{ timeoutMs: 30_000 },
);
expect(created.exitCode, created.stderr).toBe(0);
const digest = createHash("sha256")
.update(await readFile(image))
@@ -40,7 +41,9 @@ cliTest.skipIf(process.platform !== "darwin")(
const owner = new NativeDmgArtifactReader(image, process.env);
let mountRoot: string | undefined;
const attachedImages = async () => {
const info = await processes.run("/usr/bin/hdiutil", ["info", "-plist"]);
const info = await processes.run("/usr/bin/hdiutil", ["info", "-plist"], {
timeoutMs: 10_000,
});
expect(info.exitCode, info.stderr).toBe(0);
return parseNativeDmgInfo(info.stdout).images;
};
@@ -77,9 +80,8 @@ cliTest.skipIf(process.platform !== "darwin")(
expect(before.length).toBeGreaterThan(0);
expect(await attachedImagePaths()).toContain(resolvedImage);
const busy = await cli.run({
// Each command must release its own attachment when its process ends.
freshProcess: true,
arguments: ["inspect-artifact", image, "--json"],
timeoutMs: 30_000,
});
expect(busy.exitCode, busy.stdout).toBe(1);
expect(busy.json).toMatchObject({
@@ -117,8 +119,8 @@ cliTest.skipIf(process.platform !== "darwin")(
}
expect(await mountedDevices()).toEqual([]);
const control = await cli.run({
freshProcess: true,
arguments: ["inspect-artifact", image, "--json"],
timeoutMs: 30_000,
});
expect(control.exitCode, control.stdout).toBe(0);
expect(control.json).toMatchObject({
@@ -2,6 +2,7 @@ import { estimateTokenCount } from "tokenx";
import { expect } from "vitest";
import { workspaceCliTest } from "../support/cli/workspaceCliFixture.js";
import { runFileCli } from "../support/cli/cliFixture.js";
workspaceCliTest(
"applies token controls to SDK manifest and schema documents",
@@ -11,15 +12,12 @@ workspaceCliTest(
const freshTokenCount = arguments_.join(" ") === "--llms --token-count";
const freshTokenLimit =
arguments_.join(" ") === "--llms-full --token-limit 1";
const result = await cli.run({
...invocation,
freshProcess: !(
!freshTokenCount &&
!freshTokenLimit &&
(arguments_.includes("--token-count") ||
arguments_.includes("--token-limit"))
),
});
const result = await (!freshTokenCount &&
!freshTokenLimit &&
(arguments_.includes("--token-count") ||
arguments_.includes("--token-limit"))
? runFileCli(cli, invocation)
: cli.run(invocation));
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stderr).toBe("");
return result.stdout;
+1 -1
View File
@@ -36,7 +36,7 @@ cliTest.for([
bytes[8] = version;
await writeFile(path, bytes);
const hash = createHash("sha256").update(bytes).digest("hex");
const output = await cli.run({
const output = await cli.sharedCliRun!({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(output.exitCode, output.stderr).toBe(0);
+1 -1
View File
@@ -51,7 +51,7 @@ for (const { bits, ownership, machine } of [
});
await writeFile(path, bytes);
const output = await cli.run({
const output = await cli.sharedCliRun!({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(output.exitCode, output.stderr).toBe(0);
+1 -1
View File
@@ -65,7 +65,7 @@ for (const target of [
}),
);
const cliOut = await cli.run({
const cliOut = await cli.sharedCliRun!({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(cliOut.exitCode, cliOut.stderr).toBe(0);
+1 -1
View File
@@ -152,7 +152,7 @@ cliTest(
});
await writeFile(path, bytes);
const output = await cli.run({
const output = await cli.sharedCliRun!({
arguments: ["inspect-elf-notes", path, "--json"],
});
expect(output.exitCode, output.stderr).toBe(0);
+20 -20
View File
@@ -21,7 +21,7 @@ import {
} from "../fixtures/go/image.js";
import { createGoRodataStringFixture } from "../fixtures/go/rodataStringImage.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../support/cli/cliFixture.js";
import { parseEvidence } from "../../src/domain/evidence.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { expectCliMcpEvidenceParity } from "../fixtures/cliMcpEvidenceParity.js";
@@ -47,7 +47,7 @@ cliTest(
] as const) {
const path = join(root, `${item.name}.macho`);
await writeFile(path, item.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
const response = await call("inspect_go_binary", { path });
@@ -130,7 +130,7 @@ cliTest(
);
const path = join(root, `${item.name}.elf`);
await writeFile(path, item.fixture.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
const response = await call("inspect_go_binary", { path });
@@ -199,12 +199,12 @@ cliTest(
const path = join(root, "application.elf");
const output = join(root, "source.json");
await writeFile(path, createGoBinaryFixture().bytes);
const complete = await cli.run({
const complete = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(complete.exitCode, complete.stderr).toBe(0);
const full = evidenceResultOf(goBinarySchema).parse(complete.json);
const summary = await cli.run({
const summary = await runFileCli(cli, {
arguments: [
"inspect-go-binary",
path,
@@ -233,7 +233,7 @@ cliTest(
.map((record) => parseEvidence(record))
.find((record) => record.evidence_id === full.evidence_id),
).toEqual(full);
const refused = await cli.run({
const refused = await runFileCli(cli, {
arguments: [
"inspect-go-binary",
join(root, "missing"),
@@ -256,7 +256,7 @@ cliTest(
const root = await createTestTempDirectory("rea-go-overlap-");
const path = join(root, "overlapping.elf");
await writeFile(path, createGoOverlappingElfFixture());
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode).toBe(1);
@@ -289,7 +289,7 @@ cliTest(
const fixture = createGoSplitElfFixture({ sectionless });
const path = join(root, sectionless ? "sectionless.elf" : "original.elf");
await writeFile(path, fixture.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
@@ -324,7 +324,7 @@ for (const bits of [32, 64] as const) {
] as const) {
const path = join(root, `${label}.exe`);
await writeFile(path, bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
@@ -379,7 +379,7 @@ cliTest(
path,
machoImageWithUnknownCommand(fixture.wide, fixture.size),
);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
const response = await call("inspect_go_binary", { path });
@@ -418,7 +418,7 @@ for (const expectedErrno of ["ELOOP", "ENAMETOOLONG"] as const) {
root,
expectedErrno,
);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode).toBe(1);
@@ -449,7 +449,7 @@ cliTest(
const path = join(root, "byte-strings.elf");
const fixture = createGoBinaryByteStringFixture();
await writeFile(path, fixture.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
@@ -467,7 +467,7 @@ for (const fixture of createGoBinaryDiagnosticFixtures()) {
const root = await createTestTempDirectory("rea-go-cli-mz-");
const path = join(root, `${fixture.name}.exe`);
await writeFile(path, fixture.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode).toBe(1);
@@ -488,7 +488,7 @@ cliTest(
const root = await createTestTempDirectory("rea-go-cli-");
const path = join(root, "text.bin");
await writeFile(path, "This is not an executable container.");
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode).toBe(1);
@@ -508,7 +508,7 @@ for (const format of ["elf", "pe", "macho"] as const) {
const fixture = createGoBinaryFixture({ format, encoding });
const path = join(root, "application.bin");
await writeFile(path, fixture.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", "application.bin", "--json"],
cwd: root,
});
@@ -582,7 +582,7 @@ cliTest("preserves MIPS identity in Go CLI Evidence", async ({ cli }) => {
fixture.bytes.writeUInt16LE(8, 18); // ELF EM_MIPS.
const path = join(root, "application.elf");
await writeFile(path, fixture.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
@@ -601,7 +601,7 @@ cliTest(
const root = await createTestTempDirectory("rea-go-cli-");
const path = join(root, "metadata-free.elf");
await writeFile(path, createGoBinaryFixture({ omitBuildInfo: true }).bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode).toBe(0);
@@ -625,7 +625,7 @@ cliTest(
const root = await createTestTempDirectory("rea-go-gopath-cli-");
const path = join(root, "application.elf");
await writeFile(path, createGoBinaryFixture({ moduleText }).bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
@@ -662,7 +662,7 @@ cliTest(
});
await writeFile(path, fixture.bytes);
const standaloneCap = await cli.run({
const standaloneCap = await runFileCli(cli, {
arguments: [
"inspect-go-binary",
path,
@@ -680,7 +680,7 @@ cliTest(
},
});
const cliResult = await cli.run({
const cliResult = await runFileCli(cli, {
arguments: [
"inspect-go-binary",
path,
@@ -5,7 +5,7 @@ import { evidenceResultOf } from "../../src/contracts/toolOutputSchemaPrimitives
import { goBinarySchema } from "../../src/domain/go/goBinary.js";
import { createGoBinaryFixture, GO_MODULE_TEXT } from "../fixtures/go/image.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../support/cli/cliFixture.js";
cliTest(
"inspects ELF64 Go metadata with four-byte amd64p32 pointers through CLI",
@@ -20,7 +20,7 @@ cliTest(
});
const path = join(root, "nacl-amd64p32.nexe");
await writeFile(path, fixture.bytes);
const result = await cli.run({
const result = await runFileCli(cli, {
arguments: ["inspect-go-binary", path, "--json"],
});
Loaded 100 of 171 files, more files were not shown because too many files have changed in this diff. Show more