mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd3ffa2c44 | ||
|
|
eef8f44003 | ||
|
|
510e83eadc | ||
|
|
2d11fd92d2 | ||
|
|
9bea563e30 | ||
|
|
e749160ab0 | ||
|
|
920d634e83 | ||
|
|
55078b9f57 | ||
|
|
75c3cab540 | ||
|
|
3c05b4d3fe |
No files matched your search
@@ -106,7 +106,8 @@ limits, security/privacy/containment implications, and meaningful proof gaps. --
|
||||
|
||||
## Author readiness
|
||||
|
||||
<!-- The human owner completes these declarations before requesting review.
|
||||
<!-- The human owner completes these declarations before requesting review,
|
||||
unless the PR author is exempt as listed in CONTRIBUTING.md.
|
||||
The PR author is the owner by default. A different owner must explicitly accept
|
||||
ownership on this PR.
|
||||
AI agents must leave these declarations to a human to dictate to them or there could be catastrophic consequences for many users of REA.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -758,7 +758,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
|
||||
@@ -829,7 +829,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
|
||||
@@ -860,7 +860,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
|
||||
@@ -983,7 +983,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
|
||||
@@ -1008,7 +1008,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
|
||||
@@ -1208,7 +1208,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,6 +21,10 @@ jobs:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Set up Node.js for the website Worker checks
|
||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
||||
@@ -54,6 +54,10 @@ jobs:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Set up Node.js for the website Worker checks
|
||||
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -75,10 +79,21 @@ jobs:
|
||||
PY
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
|
||||
# upload-pages-artifact excludes dot directories, including .well-known.
|
||||
- name: Package the website including agent discovery
|
||||
run: |
|
||||
tar --dereference --hard-dereference --directory website/_site \
|
||||
--exclude=.git --exclude=.github -cf "$RUNNER_TEMP/artifact.tar" .
|
||||
tar -tf "$RUNNER_TEMP/artifact.tar" \
|
||||
./.well-known/ard.json ./.well-known/ai-catalog.json \
|
||||
./.well-known/agent-skills/index.json > /dev/null
|
||||
- name: Upload the website
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
path: website/_site
|
||||
name: github-pages
|
||||
path: ${{ runner.temp }}/artifact.tar
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
deploy:
|
||||
needs: prepare
|
||||
@@ -98,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
|
||||
@@ -130,12 +145,13 @@ jobs:
|
||||
PUBLISH_TARGET: ${{ inputs.target }}
|
||||
run: |
|
||||
python3 -u - <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from urllib.error import URLError
|
||||
from urllib.parse import urljoin
|
||||
from urllib.parse import urljoin, urlsplit
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
expected = os.environ["GITHUB_SHA"]
|
||||
@@ -172,6 +188,28 @@ jobs:
|
||||
time.sleep(5)
|
||||
if pending:
|
||||
sys.exit("Could not verify published revision on: " + ", ".join(pending))
|
||||
for name, base in hosts.items():
|
||||
with urlopen(urljoin(base, ".well-known/agent-skills/index.json"), timeout=15) as response:
|
||||
skill, = json.load(response)["skills"]
|
||||
# Fetch from each selected host, including the /rea/ mirror.
|
||||
artifact_url = urljoin(base, urlsplit(skill["url"]).path.lstrip("/"))
|
||||
with urlopen(artifact_url, timeout=15) as response:
|
||||
digest = "sha256:" + hashlib.sha256(response.read()).hexdigest()
|
||||
if digest != skill["digest"]:
|
||||
sys.exit(name + " serves a skill archive with the wrong digest")
|
||||
for manifest in ("ard.json", "ai-catalog.json"):
|
||||
with urlopen(urljoin(base, ".well-known/" + manifest), timeout=15) as response:
|
||||
if not json.load(response).get("entries"):
|
||||
sys.exit(name + " serves an empty discovery manifest")
|
||||
if name == "Cloudflare":
|
||||
request = Request(base, headers={"Accept": "text/markdown"})
|
||||
with urlopen(request, timeout=15) as response:
|
||||
vary = {value.strip().lower() for value in response.headers.get("Vary", "").split(",")}
|
||||
if response.headers.get_content_type() != "text/markdown" or "accept" not in vary:
|
||||
sys.exit("Cloudflare did not negotiate Markdown with Vary: Accept")
|
||||
if 'rel="ard"' not in response.headers.get("Link", ""):
|
||||
sys.exit("Cloudflare is missing its discovery Link header")
|
||||
print(name + " serves the discovery manifests and verified skill archive")
|
||||
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
|
||||
links = " and ".join(f"[{name}]({url})" for name, url in hosts.items())
|
||||
summary.write(f"Published commit `{expected}` to {links}.\n")
|
||||
|
||||
@@ -128,8 +128,8 @@ owner to complete. Never attest to human review on their behalf. Draft status
|
||||
is optional; preserve the PR's current draft status unless the user asks to change
|
||||
it. Later pushes do not invalidate the readiness declaration or require another
|
||||
human sign-off. Keep the PR body and affected validation current as work changes.
|
||||
The readiness check validates
|
||||
the filled declaration; CI and automated review results do not establish that
|
||||
The readiness check validates the filled declaration or the author exemption
|
||||
listed in CONTRIBUTING.md; CI and automated review results do not establish that
|
||||
human review occurred, maintainer scope approval, or merge permission.
|
||||
|
||||
<!-- BEGIN:turborepo-agent-rules -->
|
||||
|
||||
+8
-4
@@ -93,7 +93,8 @@ Before requesting maintainer review, the human owner must:
|
||||
passing. Follow the [verification requirements](#development-feedback-and-pr-verification)
|
||||
for required checks and real-provider coverage.
|
||||
- Complete the **Author readiness** declaration in the PR template, explicitly
|
||||
stating that the PR is ready for initial review.
|
||||
stating that the PR is ready for initial review, unless the PR author is
|
||||
exempt from the declaration as listed below.
|
||||
|
||||
Keep the PR description concise and current. Explain the problem, observable
|
||||
outcome, approach, and useful evidence; link long logs or put them in optional
|
||||
@@ -114,9 +115,12 @@ validation current as work changes. AI agents must leave human declarations to
|
||||
the human owner and must not attest to a human review on their behalf.
|
||||
|
||||
The `PR readiness` check reports pending for drafts and requires a named human
|
||||
owner and all three template declarations for non-drafts. It checks that the
|
||||
declaration is filled, not whether the code or evidence is correct. Existing PRs
|
||||
can copy the **Author readiness** section from the current template.
|
||||
owner and all three template declarations for non-drafts. PRs authored by
|
||||
@morluto, @kaoru0822-kitauji, @N0zoM1z0, or @gensokyo-dev are exempt from
|
||||
the declaration; their human ownership, review, and evidence responsibilities
|
||||
above still apply. The check identifies authors by GitHub account ID. It checks
|
||||
that the declaration is filled, not whether the code or evidence is correct.
|
||||
Existing PRs can copy the **Author readiness** section from the current template.
|
||||
Open PRs must have distinct head commits because GitHub shares commit statuses.
|
||||
|
||||
Maintainers enable merge enforcement after the workflow is merged: run
|
||||
|
||||
@@ -60,7 +60,7 @@ pin inspection sources; they are not included in the npm package.
|
||||
| ghidra-nativeaot | [MIT](https://github.com/Washi1337/ghidra-nativeaot/blob/effeb734fc570c32650f88b159608979dc7b423e/LICENSE.md) | Pinned inspection source; derived parser attribution above. |
|
||||
| JADX | [Apache-2.0](https://github.com/skylot/jadx/blob/v1.5.6/LICENSE) | The audited jadx-headless-mcp source uses JADX 1.5.6 for Android decompilation; the engine JAR is separately supplied. |
|
||||
| jadx-headless-mcp | [Apache-2.0](https://github.com/1013503897/jadx-headless-mcp/blob/5844800a486d2248fa949b74c9896285f5b54de2/LICENSE) | v0.7.1; pinned submodule and user-supplied JAR. |
|
||||
| Kuna | [Apache-2.0](https://github.com/Noelo-Lab/kuna/blob/v1.988/LICENSE), with its [NOTICE](https://github.com/Noelo-Lab/kuna/blob/v1.988/NOTICE) | Bring your own `kuna`; REA verified 1.988 and never downloads, installs or bundles it. REA invokes it as a separate process. |
|
||||
| Kuna | [Apache-2.0](https://github.com/Noelo-Lab/kuna/blob/v1.989/LICENSE), with its [NOTICE](https://github.com/Noelo-Lab/kuna/blob/v1.989/NOTICE) | Bring your own `kuna`; REA verified 1.989 and never downloads, installs or bundles it. REA invokes it as a separate process. |
|
||||
| Apktool | [Apache-2.0](https://github.com/iBotPeaches/Apktool/blob/master/LICENSE.md) | User-supplied Android resource decoder. |
|
||||
| Binwalk | [MIT](https://github.com/ReFirmLabs/binwalk/blob/4fdab3d464d97b68e0af9088df3f9e2e1545b21c/LICENSE) | v3.1.0; pinned submodule and user-supplied firmware engine. |
|
||||
| Unblob | [MIT](https://github.com/onekey-sec/unblob/blob/1fcc7a0a584a70a96c31f5a276c20944d199a089/LICENSE) | 26.6.4; pinned submodule and user-supplied firmware engine. |
|
||||
|
||||
@@ -26,6 +26,9 @@ REA does not install these tools, their dependencies or system packages. It
|
||||
checks the tool's `--version` banner when executing a request. Binwalk must
|
||||
print `binwalk <version>` and Unblob must print the version alone. Inspection and
|
||||
extraction have independent prerequisites; one does not require the other.
|
||||
Capability discovery recommends `REA_BINWALK_COMMAND` for missing inspection
|
||||
and `REA_UNBLOB_COMMAND` for missing extraction. Configuring the other engine
|
||||
does not enable the unavailable operation.
|
||||
Both require util-linux `prlimit`, normally `/usr/bin/prlimit`; override its
|
||||
absolute path with `REA_FIRMWARE_PRLIMIT_COMMAND` if needed. `rea doctor` does
|
||||
not currently inspect firmware readiness. Engine selection/version checks run
|
||||
|
||||
@@ -93,6 +93,10 @@ coverage. It does not print captured source text.
|
||||
Runtime targets, frames, scripts, and workers become capture-scoped graph
|
||||
nodes. REA evaluates compatible static candidates conservatively:
|
||||
|
||||
V8 Inspector targets whose protocol type is `node` use JavaScript asset/module
|
||||
candidates for their verified entry location. A target-only correspondence does
|
||||
not mark that script loaded; load-state evidence requires a script observation.
|
||||
|
||||
1. A captured-source SHA-256 plus one matching mapped location yields
|
||||
a content-and-location match.
|
||||
2. When no static candidate exists at a mapped location, one unique exact
|
||||
|
||||
+23
-1
@@ -1,9 +1,27 @@
|
||||
# Kuna provider
|
||||
|
||||
[Kuna](https://github.com/Noelo-Lab/kuna) is an Apache-2.0 decompiler distributed as one `kuna` executable. For a new installation, use Kuna 1.988, REA's recommended verified release. REA uses a `kuna` you supply; it does not download, install, update or bundle Kuna. REA accepts other releases, which it reports as unverified in `rea doctor` and in each analysis profile. The verified-release flag compares the reported version; it does not authenticate the executable. The 1.988 real-provider checks ran on macOS arm64; Linux and other hosts have not been verified with that release.
|
||||
[Kuna](https://github.com/Noelo-Lab/kuna) is an Apache-2.0 decompiler distributed as one `kuna` executable. For a new installation, use Kuna 1.989, REA's recommended verified release. REA uses a `kuna` you supply; it does not download, install, update or bundle Kuna. REA accepts other releases; `rea doctor` and analysis Evidence report them as unverified. Verification compares the reported version; it does not authenticate the executable. Analysis profiles retain the observed version and executable digest, independently of REA's current verified-release policy. The 1.989 real-provider checks ran on macOS arm64; Linux and other hosts have not been verified with that release.
|
||||
|
||||
Kuna is static: it reads the selected image and never executes it.
|
||||
|
||||
This profile change makes earlier Kuna analysis snapshots and resumable directory
|
||||
exports incompatible, even with the same Kuna executable. Keep the old files:
|
||||
their recorded Evidence remains historical. To continue analysis, open the
|
||||
binary without the old snapshot and save a new one, or choose a new export
|
||||
directory. Existing snapshot Evidence can be extracted from its
|
||||
`evidence_bundle` JSON member and imported as a separate Evidence bundle; that
|
||||
does not restore cached workflow entries or resume the old directory export.
|
||||
For example, using `jq` and a new output path:
|
||||
|
||||
```bash
|
||||
jq -e '.evidence_bundle | select(type == "object")' old-snapshot.json > history.json
|
||||
rea evidence-import history.json --json
|
||||
```
|
||||
|
||||
The CLI command validates the bundle in a one-shot session. To use the records
|
||||
in an MCP session, call `import_evidence_bundle` with the extracted file path.
|
||||
This validates the embedded bundle, not the rest of the snapshot container.
|
||||
|
||||
## Configure
|
||||
|
||||
```bash
|
||||
@@ -30,6 +48,10 @@ completes. Cleanup retries keep both the observations and the exact file owner.
|
||||
| `REA_KUNA_TIMEOUT_MS` | 120 s | Deadline for one single-function decompile, and for the inventory run REA makes when the export ends without one. |
|
||||
| `REA_KUNA_JOBS` | 1 | `decompile-project --jobs`. 1 is Kuna's reference answer; more jobs finish sooner but lose callee-first prototypes, and change about 40–60% of function bodies on the measured binaries. |
|
||||
|
||||
Pseudocode read from a parallel project export includes a prototype-context
|
||||
limitation in its Evidence, including in function dossiers and composed
|
||||
workflows. Early single-function results have their own source limitation.
|
||||
|
||||
A single-function decompile, inventory, string, graph or xref run that exceeds its deadline fails as `provider_timeout`, naming the setting that owns the deadline and that raising it takes a restart. A project export that exceeds `REA_KUNA_PROJECT_TIMEOUT_MS` is an export failure instead: requests that need the export report `capability_unavailable` with the export's own retry guidance.
|
||||
|
||||
Windows is not supported yet. Supported targets are ELF and thin Mach-O executables for x86-64, AArch64 and ARM; PE and universal Mach-O are rejected with a typed reason.
|
||||
|
||||
@@ -292,6 +292,12 @@ limitation and reason IDs index that page's `items.messages` dictionary. Expand
|
||||
those references to recover exact text, repeated messages, and absent versus
|
||||
empty optional fields. Failed nodes retain their original error.
|
||||
|
||||
Retained `xrefs` Evidence also supports `native-observation` address pages and
|
||||
`native-context` source caveats. `xrefs` still returns complete Evidence by
|
||||
default and does not accept `detail`; when its MCP response exceeds the budget,
|
||||
follow the returned continuation to page the retained result. These addresses
|
||||
remain untyped; paging does not add reference kinds or establish completeness.
|
||||
|
||||
These views reference complete source context through `source_context.digest`
|
||||
and caveat counts. Read it using the same parent with
|
||||
`view: {"kind":"native-context","facet":"overview"}`. The other facets are
|
||||
|
||||
@@ -451,6 +451,11 @@ Mach-O fixture and caller-built symbol-bearing and stripped x86-64 ELF fixtures.
|
||||
Linux and other hosts have not been verified with 1.988; this is not a repeat
|
||||
of the larger 1.833 matrix.
|
||||
|
||||
Kuna 1.989 passed the real-provider lane on macOS arm64 with the generated
|
||||
symbol-bearing and stripped Mach-O fixtures, including cold and warm reads,
|
||||
references, and cleanup. Linux, other hosts, and the larger 1.833 target matrix
|
||||
were not verified with 1.989.
|
||||
|
||||
## DOS Ghidra analysis
|
||||
|
||||
`npm run verify:ghidra:dos` requires the supported Ghidra and JDK installation
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
+ existingSha = data.sha;
|
||||
+ }
|
||||
+ catch (error) {
|
||||
+ if (!(error instanceof request_error_1.RequestError && error.status === 404)) {
|
||||
+ if (!(error && error.status === 404)) {
|
||||
+ throw error;
|
||||
+ }
|
||||
+ }
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface ReadinessPullRequest {
|
||||
readonly state: "open" | "closed";
|
||||
readonly draft: boolean;
|
||||
readonly body: string | null;
|
||||
readonly user: { readonly id: number } | null;
|
||||
readonly head: { readonly sha: string };
|
||||
readonly base: { readonly ref: string };
|
||||
}
|
||||
@@ -53,7 +54,7 @@ export interface ReadinessGitHub {
|
||||
>;
|
||||
}
|
||||
export function evaluateReadiness(
|
||||
pull: Pick<ReadinessPullRequest, "body" | "draft">,
|
||||
pull: Pick<ReadinessPullRequest, "body" | "draft" | "user">,
|
||||
template: string,
|
||||
): ReadinessResult;
|
||||
export function runReadinessCheck(input: {
|
||||
|
||||
@@ -2,6 +2,14 @@ import { readFile } from "node:fs/promises";
|
||||
|
||||
const result = (state, description) => ({ state, description });
|
||||
|
||||
// GitHub account IDs remain stable when a login changes.
|
||||
const EXEMPT_AUTHOR_IDS = new Set([
|
||||
76467478, // morluto
|
||||
208281322, // kaoru0822-kitauji
|
||||
161784452, // N0zoM1z0
|
||||
342580529, // gensokyo-dev
|
||||
]);
|
||||
|
||||
const OPEN_HEADS_QUERY = `query ReadinessOpenHeads($owner: String!, $name: String!, $after: String) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
pullRequests(first: 100, after: $after, states: OPEN, orderBy: {field: CREATED_AT, direction: ASC}) {
|
||||
@@ -105,6 +113,11 @@ export function evaluateReadiness(pull, template) {
|
||||
"pending",
|
||||
"Draft PR: complete Author readiness before requesting review.",
|
||||
);
|
||||
if (EXEMPT_AUTHOR_IDS.has(pull.user?.id))
|
||||
return result(
|
||||
"success",
|
||||
"PR author is exempt from the readiness declaration.",
|
||||
);
|
||||
const expected = readinessLines(template).flatMap((line) => {
|
||||
const match = /^- \[ \] (.+)$/u.exec(line);
|
||||
return match ? [match[1]] : [];
|
||||
@@ -210,6 +223,7 @@ export async function runReadinessCheck({ github, context, core }) {
|
||||
latest.head.sha !== pull.head.sha ||
|
||||
latest.body !== pull.body ||
|
||||
latest.draft !== pull.draft ||
|
||||
latest.user?.id !== pull.user?.id ||
|
||||
latest.base.ref !== pull.base.ref
|
||||
)
|
||||
verdict = result(
|
||||
|
||||
@@ -704,7 +704,9 @@ export const CI_LANES = {
|
||||
agentenv: sameJobs(PATH_SCOPES.agentenv, ["agentenv"]),
|
||||
nativeaot: sameJobs(PATH_SCOPES.nativeaot, ["nativeaot"]),
|
||||
ghidra_bridge: sameJobs(PATH_SCOPES.ghidra_bridge, ["ghidra_bridge"]),
|
||||
website: sameJobs(PATH_SCOPES.website, ["website"]),
|
||||
website: sameJobs(PATH_SCOPES.website, ["website"], {
|
||||
earlyInputs: [".agents/skills/reverse-engineer-anything/**"],
|
||||
}),
|
||||
};
|
||||
// An input owner omitted from the registry must never silently lose a lane.
|
||||
for (const [name, paths] of Object.entries(PATH_SCOPES))
|
||||
|
||||
@@ -7,6 +7,7 @@ import xml.etree.ElementTree as ET
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from website import PageMetadata, page_url
|
||||
from website_agents import prepare_discovery, prepare_markdown
|
||||
|
||||
|
||||
EXAMPLE_FILES = (
|
||||
@@ -81,6 +82,8 @@ def prepare_assets(site):
|
||||
prepare_sharing_image(site)
|
||||
prepare_sitemap(site)
|
||||
prepare_notes_example(site)
|
||||
prepare_discovery(Path(__file__).resolve().parent.parent, site)
|
||||
prepare_markdown(site)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Check the public build artifacts and Markdown content contract."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from zipfile import ZipFile
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import yaml
|
||||
|
||||
from website_agents import prepare_discovery, prepare_markdown
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class AgentPublishingChecks(unittest.TestCase):
|
||||
def test_published_skill_digest_covers_every_reference_and_catalogs_resolve(self):
|
||||
site = ROOT / "website/_site"
|
||||
index = json.loads((site / ".well-known/agent-skills/index.json").read_text())
|
||||
self.assertEqual(index["$schema"], "https://schemas.agentskills.io/discovery/0.2.0/schema.json")
|
||||
skill, = index["skills"]
|
||||
self.assertEqual(skill["name"], "reverse-engineer-anything")
|
||||
self.assertEqual(skill["type"], "archive")
|
||||
artifact = site / skill["url"].removeprefix("https://rea.tools/")
|
||||
self.assertEqual(skill["digest"], "sha256:" + hashlib.sha256(artifact.read_bytes()).hexdigest())
|
||||
source = ROOT / ".agents/skills/reverse-engineer-anything"
|
||||
with ZipFile(artifact) as archive:
|
||||
expected = {f"{source.name}/{p.relative_to(source).as_posix()}": p.read_bytes()
|
||||
for p in source.rglob("*.md")}
|
||||
self.assertEqual(set(archive.namelist()), set(expected))
|
||||
for name, data in expected.items():
|
||||
self.assertEqual(archive.read(name), data)
|
||||
manifest = archive.read(f"{source.name}/SKILL.md").decode()
|
||||
metadata = yaml.safe_load(manifest[4:].split("\n---\n", 1)[0])
|
||||
self.assertEqual(skill["description"], metadata["description"])
|
||||
catalog = json.loads((site / ".well-known/ai-catalog.json").read_text())
|
||||
self.assertEqual(catalog["specVersion"], "1.0")
|
||||
entry, = catalog["entries"]
|
||||
self.assertEqual(entry["identifier"], "urn:air:rea.tools:skill:reverse-engineer-anything")
|
||||
self.assertEqual(entry["url"], skill["url"])
|
||||
self.assertNotIn("data", entry)
|
||||
ard = json.loads((site / ".well-known/ard.json").read_text())
|
||||
resource, = ard["entries"]
|
||||
self.assertEqual(resource["url"], skill["url"])
|
||||
self.assertTrue(2 <= len(resource["representativeQueries"]) <= 5)
|
||||
|
||||
def test_reference_changes_invalidate_digest_and_yaml_scalars_are_parsed(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / ".agents/skills/reverse-engineer-anything"
|
||||
source.mkdir(parents=True)
|
||||
(source / "SKILL.md").write_text(
|
||||
'---\nname: reverse-engineer-anything\ndescription: >-\n Inspect programs:\n preserve evidence.\n---\n# REA\n'
|
||||
)
|
||||
(source / "references").mkdir()
|
||||
reference = source / "references/evidence.md"
|
||||
reference.write_text("First revision")
|
||||
site = root / "site"
|
||||
def build():
|
||||
prepare_discovery(root, site)
|
||||
return json.loads((site / ".well-known/agent-skills/index.json").read_text())["skills"][0]
|
||||
before = build()
|
||||
self.assertEqual(before["description"], "Inspect programs: preserve evidence.")
|
||||
self.assertEqual(build(), before)
|
||||
reference.write_text("Second revision")
|
||||
self.assertNotEqual(build()["digest"], before["digest"])
|
||||
|
||||
def test_markdown_preserves_content_and_resources_without_publishing_excluded_pages(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory)
|
||||
page = site / "zh-CN/guides/test/index.html"
|
||||
page.parent.mkdir(parents=True)
|
||||
page.write_text('''<html><head><title>REA: test</title></head><body>
|
||||
<nav>Outside navigation</nav><main><aside>Shortened display paths.</aside>
|
||||
<h1>Inspect a program</h1><nav>Contents navigation</nav>
|
||||
<ol><li><span class="flow-label">Read</span><span class="flow-node"><code>input.exe</code></span><span class="flow-note">Keep evidence.</span></li></ol>
|
||||
<p><a href="../other/?q=a%2Fb#code">Another guide</a></p>
|
||||
<pre><code>const value = a < b ? "x_y" : "&";\n keepIndent();</code></pre>
|
||||
<details><summary>Evidence</summary><p>Hidden evidence remains available.</p></details>
|
||||
<img src="../../../assets/diagram.svg" alt="Investigation diagram">
|
||||
<button>Copy code</button><script>doNotPublish()</script></main></body></html>''')
|
||||
fixture = site / "examples/notes-web/index.html"
|
||||
fixture.parent.mkdir(parents=True)
|
||||
fixture.write_text("<main>Target application</main>")
|
||||
fallback = site / "zh-CN/fallback/index.html"
|
||||
fallback.parent.mkdir(parents=True)
|
||||
fallback.write_text('<head><meta name="robots" content="noindex, follow"></head><main>English fallback notice.</main>')
|
||||
prepare_markdown(site)
|
||||
markdown = page.with_suffix(".md").read_text()
|
||||
self.assertIn('# Inspect a program', markdown)
|
||||
self.assertIn('const value = a < b ? "x_y" : "&";\n keepIndent();', markdown)
|
||||
self.assertIn("Hidden evidence remains available.", markdown)
|
||||
self.assertIn("Shortened display paths.", markdown)
|
||||
self.assertNotIn("Read`input.exe`", markdown)
|
||||
self.assertIn("https://rea.tools/zh-CN/guides/other/?q=a%2Fb#code", markdown)
|
||||
self.assertIn("https://rea.tools/assets/diagram.svg", markdown)
|
||||
for text in ("Outside navigation", "Contents navigation", "Copy code", "doNotPublish"):
|
||||
self.assertNotIn(text, markdown)
|
||||
self.assertFalse(fixture.with_suffix(".md").exists())
|
||||
self.assertFalse(fallback.with_suffix(".md").exists())
|
||||
html = BeautifulSoup(page.read_text(), "html.parser")
|
||||
self.assertEqual(html.find("link", rel="alternate")["href"], "https://rea.tools/zh-CN/guides/test/index.md")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+2
-1
@@ -327,8 +327,9 @@ def main():
|
||||
refresh_contributors(root)
|
||||
elif args.command == "check":
|
||||
sync_pages(root, check=True)
|
||||
for name in ("prepare-website.py", "verify-website.py", "test-website.py"):
|
||||
for name in ("prepare-website.py", "verify-website.py", "test-website.py", "test-website-agents.py"):
|
||||
run_script(root, name)
|
||||
subprocess.run(["node", "--test", "website/worker.test.mjs"], cwd=root, check=True)
|
||||
elif args.command == "links":
|
||||
run_script(root, "audit-website-links.py", ["--report", args.report] if args.report else [])
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Build public agent discovery and Markdown from their maintained sources."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from urllib.parse import urljoin
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from markdownify import MarkdownConverter
|
||||
import yaml
|
||||
|
||||
from website import NOTES_FIXTURES, PageMetadata, SITE_ORIGIN, page_url
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def prepare_discovery(root, site):
|
||||
"""Publish the complete skill, including its relative reference documents."""
|
||||
source = root / ".agents/skills/reverse-engineer-anything"
|
||||
skill = (source / "SKILL.md").read_text(encoding="utf-8")
|
||||
if not skill.startswith("---\n") or "\n---\n" not in skill[4:]:
|
||||
raise ValueError("Public REA skill needs YAML frontmatter")
|
||||
metadata = yaml.safe_load(skill[4:].split("\n---\n", 1)[0])
|
||||
if not isinstance(metadata, dict):
|
||||
raise ValueError("Public REA skill needs YAML metadata")
|
||||
name, description = metadata["name"], metadata["description"]
|
||||
if name != source.name or not isinstance(description, str) or not description.strip():
|
||||
raise ValueError("Invalid public REA skill metadata")
|
||||
discovery = site / ".well-known/agent-skills"
|
||||
discovery.mkdir(parents=True, exist_ok=True)
|
||||
artifact = discovery / f"{name}.zip"
|
||||
with ZipFile(artifact, "w") as archive:
|
||||
for path in [source / "SKILL.md", *sorted(source.glob("references/**/*.md"))]:
|
||||
relative = path.relative_to(source)
|
||||
entry = ZipInfo(f"{name}/{relative.as_posix()}", (1980, 1, 1, 0, 0, 0))
|
||||
entry.create_system = 3
|
||||
entry.external_attr = 0o100644 << 16
|
||||
archive.writestr(entry, path.read_bytes(), compress_type=ZIP_DEFLATED)
|
||||
artifact_url = f"{SITE_ORIGIN}/.well-known/agent-skills/{artifact.name}"
|
||||
write_json(discovery / "index.json", {
|
||||
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
||||
"skills": [{
|
||||
"name": name, "type": "archive", "description": description,
|
||||
"url": artifact_url,
|
||||
"digest": "sha256:" + hashlib.sha256(artifact.read_bytes()).hexdigest(),
|
||||
}],
|
||||
})
|
||||
entry = {
|
||||
"identifier": f"urn:air:rea.tools:skill:{name}",
|
||||
"displayName": "REA reverse engineering skill",
|
||||
"description": description,
|
||||
"type": "application/zip",
|
||||
"url": artifact_url,
|
||||
}
|
||||
write_json(site / ".well-known/ai-catalog.json", {
|
||||
"specVersion": "1.0",
|
||||
"host": {"displayName": "REA", "documentationUrl": f"{SITE_ORIGIN}/get-started/"},
|
||||
"entries": [entry],
|
||||
})
|
||||
write_json(site / ".well-known/ard.json", {"entries": [{
|
||||
**entry,
|
||||
"representativeQueries": [
|
||||
"Explain how a feature works in a shipped executable using REA.",
|
||||
"Trace an Electron application's behavior back to its packaged JavaScript.",
|
||||
"Build an AgentEnv bundle from a web application capture.",
|
||||
],
|
||||
}]})
|
||||
print("Prepared agent skill archive, discovery index, AI Catalog and ARD manifest.")
|
||||
|
||||
|
||||
def prepare_markdown(site):
|
||||
"""Convert rendered reading pages, keeping code and resolving resource URLs."""
|
||||
converter = MarkdownConverter(heading_style="ATX", bullets="-", escape_underscores=False)
|
||||
count = 0
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
if path.relative_to(site).as_posix() in NOTES_FIXTURES:
|
||||
continue
|
||||
source = path.read_text(encoding="utf-8")
|
||||
metadata = PageMetadata()
|
||||
metadata.feed(source)
|
||||
# Drafts and English fallbacks keep their HTML indexing policy intact.
|
||||
if metadata.noindex:
|
||||
continue
|
||||
soup = BeautifulSoup(source, "html.parser")
|
||||
main = soup.find("main")
|
||||
if main is None:
|
||||
continue
|
||||
# Preserve fallback notices and closed disclosures; remove interface chrome.
|
||||
for node in main.select('script, style, nav, button, svg, .home-page-nav, [aria-hidden="true"]'):
|
||||
if node.parent is not None:
|
||||
node.decompose()
|
||||
# These spans are separate lines in the shared flow component's CSS.
|
||||
for node in main.select("span.flow-label, span.flow-node, span.flow-note"):
|
||||
node.name = "div"
|
||||
canonical = page_url(site, path)
|
||||
for node in main.select("[href], [src]"):
|
||||
for attribute in ("href", "src"):
|
||||
if node.has_attr(attribute):
|
||||
node[attribute] = urljoin(canonical, node[attribute])
|
||||
title = metadata.titles[0] if metadata.titles else "REA"
|
||||
header = yaml.safe_dump({"title": title, "url": canonical}, allow_unicode=True, sort_keys=False)
|
||||
markdown = converter.convert_soup(main).strip()
|
||||
path.with_suffix(".md").write_text(f"---\n{header}---\n\n{markdown}\n", encoding="utf-8")
|
||||
alternate = page_url(site, path.with_suffix(".md"))
|
||||
links = (
|
||||
f'<link rel="alternate" type="text/markdown" href="{alternate}" />\n'
|
||||
f'<link rel="ard" type="application/json" href="{SITE_ORIGIN}/.well-known/ard.json" />\n'
|
||||
f'<link rel="ai-catalog" type="application/ai-catalog+json" href="{SITE_ORIGIN}/.well-known/ai-catalog.json" />\n'
|
||||
f'<link rel="related" type="application/json" title="Agent skills index" href="{SITE_ORIGIN}/.well-known/agent-skills/index.json" />\n'
|
||||
)
|
||||
path.write_text(source.replace("</head>", links + "</head>", 1), encoding="utf-8")
|
||||
count += 1
|
||||
print(f"Prepared Markdown for {count} reading pages.")
|
||||
@@ -880,6 +880,62 @@ describe("trace_feature requirements", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("firmware operation recovery", () => {
|
||||
it.each([
|
||||
[false, false],
|
||||
[true, false],
|
||||
[false, true],
|
||||
[true, true],
|
||||
])(
|
||||
"keeps inspection=%s and extraction=%s independent",
|
||||
(inspection, extraction) => {
|
||||
const inventory = buildCapabilityInventory(status(), {
|
||||
processCaptureEnabled: false,
|
||||
firmwareInspectionEnabled: inspection,
|
||||
firmwareExtractionEnabled: extraction,
|
||||
});
|
||||
for (const [name, enabled, command, otherCommand] of [
|
||||
[
|
||||
"inspect_firmware_regions",
|
||||
inspection,
|
||||
"REA_BINWALK_COMMAND",
|
||||
"REA_UNBLOB_COMMAND",
|
||||
],
|
||||
[
|
||||
"extract_firmware",
|
||||
extraction,
|
||||
"REA_UNBLOB_COMMAND",
|
||||
"REA_BINWALK_COMMAND",
|
||||
],
|
||||
] as const) {
|
||||
const tool = inventory.find((candidate) => candidate.name === name);
|
||||
expect(tool).toMatchObject({
|
||||
available: enabled,
|
||||
reason: enabled ? "available" : "provider_missing",
|
||||
});
|
||||
if (enabled) {
|
||||
expect(tool?.remediation).toBeNull();
|
||||
} else {
|
||||
expect(tool?.remediation).toContain(command);
|
||||
expect(tool?.remediation).not.toContain(otherCommand);
|
||||
expect(tool?.remediation).toContain("prlimit");
|
||||
expect(tool?.remediation).toContain("procps ps");
|
||||
expect(tool?.remediation?.includes("external extractor")).toBe(
|
||||
name === "extract_firmware",
|
||||
);
|
||||
}
|
||||
}
|
||||
const byName = (left: ToolAvailability, right: ToolAvailability) =>
|
||||
left.name.localeCompare(right.name);
|
||||
expect(
|
||||
expandToolAvailability(compactToolAvailability(inventory)).toSorted(
|
||||
byName,
|
||||
),
|
||||
).toEqual(inventory.toSorted(byName));
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const HOST_RESTRICTED_TOOLS = [
|
||||
["inspect_packet_capture", "packetCapture", "packetCaptureEnabled"],
|
||||
["inspect_packet_fields", "packetCapture", "packetCaptureEnabled"],
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { PCAP_CAPTURE_UNSUPPORTED_HOST_REASON } from "../domain/pcap/pcapAnalysis.js";
|
||||
import { DEFAULT_CALL_RELATIONSHIP_DIRECTION } from "../contracts/enhancedInputs.js";
|
||||
import { FIRMWARE_TOOL_REQUIREMENTS } from "../contracts/firmware/firmwareToolRequirements.js";
|
||||
import {
|
||||
FIRMWARE_OPERATION_ENGINES,
|
||||
FIRMWARE_TOOL_REQUIREMENTS,
|
||||
} from "../contracts/firmware/firmwareToolRequirements.js";
|
||||
import { CALL_RELATIONSHIP_TRAVERSALS } from "./CallRelationshipTraversal.js";
|
||||
import { NATIVE_DISPATCH_METADATA_SOURCES } from "./native/NativeDispatchMetadataInspection.js";
|
||||
import { NATIVE_UI_ACTION_OPERATIONS } from "./native/NativeUiActionTrace.js";
|
||||
@@ -595,15 +598,19 @@ const workflowAvailabilityFor = ({
|
||||
"On Linux x64, set absolute REA_PWNTOOLS_PYTHON to caller-supplied Python with pwntools 4.15.0. No active binary target is required.",
|
||||
);
|
||||
if (kind === "firmware-provider") {
|
||||
const { binwalk, unblob } = FIRMWARE_TOOL_REQUIREMENTS;
|
||||
const inspection = name === "inspect_firmware_regions";
|
||||
const operation = inspection
|
||||
? "inspect_firmware_regions"
|
||||
: "extract_firmware";
|
||||
const requirement =
|
||||
FIRMWARE_TOOL_REQUIREMENTS[FIRMWARE_OPERATION_ENGINES[operation]];
|
||||
return hostRestrictedAvailability(
|
||||
policy,
|
||||
inspection ? "firmwareInspection" : "firmwareExtraction",
|
||||
inspection
|
||||
? policy.firmwareInspectionEnabled
|
||||
: policy.firmwareExtractionEnabled,
|
||||
`On Linux, provide an absolute ${binwalk.commandEnvironment} (${binwalk.name} ${binwalk.line}, verified with ${binwalk.version}) or ${unblob.commandEnvironment} (${unblob.name} ${unblob.line}, verified with ${unblob.version}) path and util-linux prlimit. Extraction also requires the selected format’s external extractor.`,
|
||||
`On Linux, provide an absolute ${requirement.commandEnvironment} (${requirement.requirement}) path, util-linux prlimit and procps ps.${inspection ? "" : " Extraction also requires the selected format’s external extractor."}`,
|
||||
);
|
||||
}
|
||||
if (kind === "android-provider")
|
||||
|
||||
@@ -121,53 +121,56 @@ const graph = (): NativeCallGraph =>
|
||||
limitations: ["Graph caveat", "Graph caveat"],
|
||||
});
|
||||
|
||||
it("returns useful caller/callee facts with exact source context identity and partial coverage", async () => {
|
||||
const source = parent("procedure_callees", [
|
||||
"ram:0x0010",
|
||||
"ram:0x0010",
|
||||
"ram:0x0020",
|
||||
]);
|
||||
const before = JSON.stringify(source);
|
||||
const summary = await project(source, { kind: "summary" });
|
||||
expect(summary.value).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_evidence_id: source.evidence_id,
|
||||
source_provider: source.provider,
|
||||
source_parameters: source.parameters,
|
||||
source_analysis_profile_digest: source.analysis_profile?.digest,
|
||||
source_context: {
|
||||
evidence_limitation_count: 1,
|
||||
result_limitation_count: null,
|
||||
},
|
||||
summary: { observations: 3 },
|
||||
items: {
|
||||
type: "addresses",
|
||||
rows: [
|
||||
{ source_index: 0, address: "ram:0x0010" },
|
||||
{ source_index: 1, address: "ram:0x0010" },
|
||||
{ source_index: 2, address: "ram:0x0020" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(summary.evidence.relationship_coverage).toEqual(
|
||||
source.relationship_coverage,
|
||||
);
|
||||
const page = await project(source, {
|
||||
kind: "native-observation",
|
||||
offset: 1,
|
||||
limit: 1,
|
||||
});
|
||||
expect(page.value).toMatchObject({
|
||||
items: { rows: [{ source_index: 1, address: "ram:0x0010" }] },
|
||||
coverage: { total: 3, next_offset: 2, exhausted: false },
|
||||
});
|
||||
expect(page.value).toHaveProperty(
|
||||
"source_context.digest",
|
||||
contextDigest(summary.value),
|
||||
);
|
||||
expect(JSON.stringify(summary.value)).not.toContain('"analysis_profile":');
|
||||
expect(JSON.stringify(source)).toBe(before);
|
||||
});
|
||||
it.each(["procedure_callees", "xrefs"])(
|
||||
"pages %s addresses with exact source context and duplicate identity",
|
||||
async (operation) => {
|
||||
const source = parent(operation, [
|
||||
"ram:0x0010",
|
||||
"ram:0x0010",
|
||||
"ram:0x0020",
|
||||
]);
|
||||
const before = JSON.stringify(source);
|
||||
const summary = await project(source, { kind: "summary" });
|
||||
expect(summary.value).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_evidence_id: source.evidence_id,
|
||||
source_provider: source.provider,
|
||||
source_parameters: source.parameters,
|
||||
source_analysis_profile_digest: source.analysis_profile?.digest,
|
||||
source_context: {
|
||||
evidence_limitation_count: 1,
|
||||
result_limitation_count: null,
|
||||
},
|
||||
summary: { observations: 3 },
|
||||
items: {
|
||||
type: "addresses",
|
||||
rows: [
|
||||
{ source_index: 0, address: "ram:0x0010" },
|
||||
{ source_index: 1, address: "ram:0x0010" },
|
||||
{ source_index: 2, address: "ram:0x0020" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(summary.evidence.relationship_coverage).toEqual(
|
||||
source.relationship_coverage,
|
||||
);
|
||||
const page = await project(source, {
|
||||
kind: "native-observation",
|
||||
offset: 1,
|
||||
limit: 1,
|
||||
});
|
||||
expect(page.value).toMatchObject({
|
||||
items: { rows: [{ source_index: 1, address: "ram:0x0010" }] },
|
||||
coverage: { total: 3, next_offset: 2, exhausted: false },
|
||||
});
|
||||
expect(page.value).toHaveProperty(
|
||||
"source_context.digest",
|
||||
contextDigest(summary.value),
|
||||
);
|
||||
expect(JSON.stringify(summary.value)).not.toContain('"analysis_profile":');
|
||||
expect(JSON.stringify(source)).toBe(before);
|
||||
},
|
||||
);
|
||||
|
||||
it("pages complete context fields and changes its digest when source profile or caveats change", async () => {
|
||||
const source = parent("procedure_callers", []);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FIRMWARE_OPERATION_ENGINES } from "../contracts/firmware/firmwareToolRequirements.js";
|
||||
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
||||
import { z } from "incur";
|
||||
import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js";
|
||||
@@ -36,7 +37,7 @@ export const registerFirmwareCommands = (
|
||||
callerPath,
|
||||
() => service.execute(operation, input, { signal }),
|
||||
service,
|
||||
operation === "inspect_firmware_regions" ? "binwalk" : "unblob",
|
||||
FIRMWARE_OPERATION_ENGINES[operation],
|
||||
operation,
|
||||
signal,
|
||||
).then(projectCliAnalysisResult),
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
import type { ToolContract } from "../toolContractTypes.js";
|
||||
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
|
||||
import { toolContractMetadata } from "../toolEffects.js";
|
||||
import { FIRMWARE_TOOL_REQUIREMENTS } from "./firmwareToolRequirements.js";
|
||||
import {
|
||||
FIRMWARE_OPERATION_ENGINES,
|
||||
FIRMWARE_TOOL_REQUIREMENTS,
|
||||
} from "./firmwareToolRequirements.js";
|
||||
|
||||
/** Explicit local firmware observations and extraction, backed by caller-supplied tools. */
|
||||
export const FIRMWARE_TOOL_CONTRACTS = [
|
||||
@@ -13,7 +16,7 @@ export const FIRMWARE_TOOL_CONTRACTS = [
|
||||
name: "inspect_firmware_regions",
|
||||
...toolContractMetadata("inspect_firmware_regions"),
|
||||
kind: "firmware-provider",
|
||||
description: `Inspect signature candidates in a local firmware file with ${FIRMWARE_TOOL_REQUIREMENTS.binwalk.requirement}. Returns offsets, reported lengths, descriptions and raw confidence values. Length validation and runtime addresses remain unknown. No extraction or target execution. Linux, caller-supplied tool, util-linux prlimit and procps ps required; 128 MiB input limit, one worker. Adjust analysis_timeout_ms for the analysis wall-clock budget (default 120000); fixed per-process limits are unchanged.`,
|
||||
description: `Inspect signature candidates in a local firmware file with ${FIRMWARE_TOOL_REQUIREMENTS[FIRMWARE_OPERATION_ENGINES.inspect_firmware_regions].requirement}. Returns offsets, reported lengths, descriptions and raw confidence values. Length validation and runtime addresses remain unknown. No extraction or target execution. Linux, caller-supplied tool, util-linux prlimit and procps ps required; 128 MiB input limit, one worker. Adjust analysis_timeout_ms for the analysis wall-clock budget (default 120000); fixed per-process limits are unchanged.`,
|
||||
inputSchema: firmwareInputSchemas.inspect_firmware_regions,
|
||||
outputSchema: evidenceResultOf(
|
||||
firmwareResultSchemas.inspect_firmware_regions,
|
||||
@@ -29,7 +32,7 @@ export const FIRMWARE_TOOL_CONTRACTS = [
|
||||
name: "extract_firmware",
|
||||
...toolContractMetadata("extract_firmware"),
|
||||
kind: "firmware-provider",
|
||||
description: `Extract caller-selected firmware bytes with ${FIRMWARE_TOOL_REQUIREMENTS.unblob.requirement} into an absent output directory. Returns verified child paths/digests, task derivations, unknown chunks and uncovered selected input ranges, extraction diagnostics and explicit extraction_gaps rows naming unextracted regions, missing extractor dependencies and a continuation_id referencing shared extraction_continuations recovery instructions. Preserves the default sandbox; publishes regular files only. Linux, caller-supplied tool, format-specific extractors, util-linux prlimit and procps ps required. No mounts or extracted-code execution. Select a returned native file with open_binary for Ghidra analysis; decompressed offsets are not original-file addresses.`,
|
||||
description: `Extract caller-selected firmware bytes with ${FIRMWARE_TOOL_REQUIREMENTS[FIRMWARE_OPERATION_ENGINES.extract_firmware].requirement} into an absent output directory. Returns verified child paths/digests, task derivations, unknown chunks and uncovered selected input ranges, extraction diagnostics and explicit extraction_gaps rows naming unextracted regions, missing extractor dependencies and a continuation_id referencing shared extraction_continuations recovery instructions. Preserves the default sandbox; publishes regular files only. Linux, caller-supplied tool, format-specific extractors, util-linux prlimit and procps ps required. No mounts or extracted-code execution. Select a returned native file with open_binary for Ghidra analysis; decompressed offsets are not original-file addresses.`,
|
||||
inputSchema: firmwareInputSchemas.extract_firmware,
|
||||
outputSchema: evidenceResultOf(firmwareResultSchemas.extract_firmware),
|
||||
examples: [
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { FirmwareOperation } from "../../domain/firmware/firmwareAnalysis.js";
|
||||
|
||||
/** Derive public support facts from one audited, stable release number. */
|
||||
const defineToolRequirement = (release: {
|
||||
readonly name: string;
|
||||
@@ -39,3 +41,9 @@ export const FIRMWARE_TOOL_REQUIREMENTS = {
|
||||
|
||||
/** Caller-supplied firmware engine selected by the operation. */
|
||||
export type FirmwareEngineName = keyof typeof FIRMWARE_TOOL_REQUIREMENTS;
|
||||
|
||||
/** The engine required by each existing firmware operation, shared by discovery and execution. */
|
||||
export const FIRMWARE_OPERATION_ENGINES = {
|
||||
inspect_firmware_regions: "binwalk",
|
||||
extract_firmware: "unblob",
|
||||
} as const satisfies Readonly<Record<FirmwareOperation, FirmwareEngineName>>;
|
||||
@@ -224,7 +224,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
),
|
||||
official(
|
||||
"search_strings",
|
||||
`Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, read_bytes and xrefs inspect its bytes and references. Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
|
||||
`Search every provider-reported string object or candidate using literal matching by default or regex when requested. Results are deterministic and complete over that provider inventory, not every text sequence in artifact bytes. Kuna scans mapped sequences of at least five characters, including unterminated fragments, without computing references. Ghidra may omit packed or undefined literals, including Rust strings; an empty result does not prove text is absent. If an address is known, xrefs can look for references; read_bytes can inspect bytes where supported (not available with Kuna). Hopper retains typed byte decoding, native display, and explicit decoding unknowns. Adjacent long-literal fragments match independently. ${hopperRegexDeadline} Select detail summary for counts and retained native-inventory pages; complete remains the default.`,
|
||||
z.object({ ...nativeSearchInput, detail: analysisDetailSchema }),
|
||||
{ evidenceParameters: "omit-detail" },
|
||||
),
|
||||
@@ -260,7 +260,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
),
|
||||
official(
|
||||
"xrefs",
|
||||
"Return analyzed references to a code or data address. Hopper may default to its current cursor; headless providers such as Ghidra require an explicit address. Use to connect strings, globals, selectors, and functions; bare addresses are untyped and indirect references may be incomplete. Use inspect_native_instruction at the target address for typed incoming references where supported.",
|
||||
"Return analyzed references to a code or data address. Hopper may default to its current cursor; headless providers such as Ghidra require an explicit address. Use to connect strings, globals, selectors, and functions; bare addresses are untyped and indirect references may be incomplete. Page a retained result with inspect_analysis_view and native-observation; use native-context for source caveats. Use inspect_native_instruction at the target address for typed incoming references where supported.",
|
||||
z.object({ document, address: optionalAddress }),
|
||||
),
|
||||
] as const satisfies readonly ToolContract[];
|
||||
|
||||
@@ -382,6 +382,7 @@ export const nativeObservationSummaryViewResultSchemas = {
|
||||
procedure_address: nativeObservationSummaryFor("procedure_address"),
|
||||
procedure_callees: nativeObservationSummaryFor("procedure_callees"),
|
||||
procedure_callers: nativeObservationSummaryFor("procedure_callers"),
|
||||
xrefs: nativeObservationSummaryFor("xrefs"),
|
||||
resolve_containing_procedure: nativeObservationSummaryFor(
|
||||
"resolve_containing_procedure",
|
||||
),
|
||||
@@ -393,6 +394,7 @@ const nativeObservationSummaryViewResultSchema = z.discriminatedUnion(
|
||||
nativeObservationSummaryViewResultSchemas.procedure_address,
|
||||
nativeObservationSummaryViewResultSchemas.procedure_callees,
|
||||
nativeObservationSummaryViewResultSchemas.procedure_callers,
|
||||
nativeObservationSummaryViewResultSchemas.xrefs,
|
||||
nativeObservationSummaryViewResultSchemas.resolve_containing_procedure,
|
||||
nativeObservationSummaryViewResultSchemas.get_call_graph,
|
||||
],
|
||||
@@ -412,6 +414,7 @@ const nativeObservationPageViewResultSchema = z.discriminatedUnion(
|
||||
nativeObservationPageFor("procedure_address"),
|
||||
nativeObservationPageFor("procedure_callees"),
|
||||
nativeObservationPageFor("procedure_callers"),
|
||||
nativeObservationPageFor("xrefs"),
|
||||
nativeObservationPageFor("resolve_containing_procedure"),
|
||||
nativeObservationPageFor("get_call_graph"),
|
||||
],
|
||||
|
||||
@@ -158,6 +158,7 @@ export const nativeObservationItemsSchemas = {
|
||||
procedure_address: addresses,
|
||||
procedure_callees: addresses,
|
||||
procedure_callers: addresses,
|
||||
xrefs: addresses,
|
||||
resolve_containing_procedure: resolutions,
|
||||
get_call_graph: nativeGraphViewItemsSchema,
|
||||
};
|
||||
|
||||
@@ -45,7 +45,8 @@ const readSource = (
|
||||
): Result<NativeObservation, AnalysisError> => {
|
||||
switch (parent.operation) {
|
||||
case "procedure_callees":
|
||||
case "procedure_callers": {
|
||||
case "procedure_callers":
|
||||
case "xrefs": {
|
||||
const read = readAddresses(parent);
|
||||
return read.ok
|
||||
? ok({ operation: parent.operation, result: read.value })
|
||||
@@ -138,6 +139,7 @@ const observationRows = (
|
||||
};
|
||||
case "procedure_callees":
|
||||
case "procedure_callers":
|
||||
case "xrefs":
|
||||
return {
|
||||
parent_operation: source.operation,
|
||||
total: source.result.length,
|
||||
|
||||
@@ -57,7 +57,10 @@ const classifyLayer = (
|
||||
const direct = new Map<string, string[]>();
|
||||
const ambiguous = new Set<string>();
|
||||
const incomplete = new Set<string>();
|
||||
for (const match of matches)
|
||||
// A target's entry location establishes correspondence, not script presence.
|
||||
for (const match of matches.filter(
|
||||
({ entity_kind }) => entity_kind === "script",
|
||||
))
|
||||
if (
|
||||
match.status === "matched" &&
|
||||
match.static_layer_id === layer.layerId &&
|
||||
|
||||
@@ -94,7 +94,10 @@ const targetEntity = (
|
||||
frameKey: null,
|
||||
scriptKey: null,
|
||||
location,
|
||||
staticKind: "renderer",
|
||||
staticKind:
|
||||
capture.kind === "v8-inspector" && target.type === "node"
|
||||
? "javascript"
|
||||
: "renderer",
|
||||
sourceSha256: null,
|
||||
label: target.title,
|
||||
properties: {
|
||||
|
||||
@@ -66,6 +66,7 @@ export const NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS = [
|
||||
export const NATIVE_OBSERVATION_OPERATIONS = [
|
||||
...NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS,
|
||||
"get_call_graph",
|
||||
"xrefs",
|
||||
] as const;
|
||||
export type NativeObservationOperation =
|
||||
(typeof NATIVE_OBSERVATION_OPERATIONS)[number];
|
||||
@@ -74,6 +75,7 @@ export const nativeObservationSourceSchemas = {
|
||||
procedure_address: z.string(),
|
||||
procedure_callees: nativeAddressListSchema,
|
||||
procedure_callers: nativeAddressListSchema,
|
||||
xrefs: nativeAddressListSchema,
|
||||
resolve_containing_procedure: containingProcedureResolutionSchema,
|
||||
get_call_graph: nativeCallGraphSchema,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FIRMWARE_OPERATION_ENGINES } from "../contracts/firmware/firmwareToolRequirements.js";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
import { assertArtifactCommandPath } from "../process/ArtifactCommandPath.js";
|
||||
import { join } from "node:path";
|
||||
@@ -332,8 +333,7 @@ export class FirmwareProvider implements FirmwareAnalysisPort {
|
||||
const environment = snapshotEnvironment(this.environment);
|
||||
let root: PrivateRuntimeRoot | undefined;
|
||||
let rootLease: AnalysisCleanupLease | undefined;
|
||||
const engineName =
|
||||
request.operation === "inspect_firmware_regions" ? "binwalk" : "unblob";
|
||||
const engineName = FIRMWARE_OPERATION_ENGINES[request.operation];
|
||||
const withCaller = (phase: FirmwareDeadline) =>
|
||||
options?.signal === undefined
|
||||
? phase.signal
|
||||
|
||||
@@ -350,7 +350,12 @@ export class KunaProjectReader {
|
||||
.slice(text.value.indexOf("\n") + 1)
|
||||
.replace(/\n+$/u, ""),
|
||||
files,
|
||||
limitations: [],
|
||||
limitations:
|
||||
this.project.manifest.inputs.jobs > 1
|
||||
? [
|
||||
"Parallel Kuna project export does not propagate callee-first prototypes between workers; function signatures and call arguments may differ from a serial export.",
|
||||
]
|
||||
: [],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +265,6 @@ export class KunaProvider implements AnalysisProviderCandidate {
|
||||
kuna_sha256: value.sha256,
|
||||
kuna_command: value.configuration.command,
|
||||
kuna_executable: value.executable,
|
||||
verified_release: value.verified,
|
||||
jobs: value.configuration.jobs,
|
||||
export: "decompile-project",
|
||||
reference_graph: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Kuna release whose CLI and project layout REA verified; others report as unverified. */
|
||||
export const KUNA_VERIFIED_VERSION = "1.988";
|
||||
export const KUNA_VERIFIED_VERSION = "1.989";
|
||||
|
||||
/** Stable provider ID shared by readiness, doctor and Evidence. */
|
||||
export const KUNA_PROVIDER_ID = "kuna";
|
||||
|
||||
@@ -89,7 +89,7 @@ import {
|
||||
kunaLimitations,
|
||||
type KunaOperation,
|
||||
} from "./KunaProviderCapabilities.js";
|
||||
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
|
||||
import { KUNA_PROVIDER_ID, KUNA_VERIFIED_VERSION } from "./KunaRelease.js";
|
||||
import { KunaTargetCommands } from "./KunaTargetCommands.js";
|
||||
import { KunaReferenceQueries } from "./KunaReferenceQueries.js";
|
||||
import { KunaQueryWork } from "./KunaQueryWork.js";
|
||||
@@ -285,6 +285,16 @@ const exportUnavailable = (
|
||||
);
|
||||
};
|
||||
|
||||
/** Version verification is observation context, not an analysis input. */
|
||||
const kunaVerificationLimitations = (
|
||||
installation: KunaInstallation,
|
||||
): readonly string[] =>
|
||||
installation.verified
|
||||
? []
|
||||
: [
|
||||
`Kuna ${installation.version} is not REA's verified ${KUNA_VERIFIED_VERSION} release; its project layout is unverified. Version matching does not authenticate the executable.`,
|
||||
];
|
||||
|
||||
/** Kuna names a procedure by its inventory entry or by an exact entry address. */
|
||||
const procedureSelectorSchema = z.string().min(1);
|
||||
|
||||
@@ -652,7 +662,11 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
: { strings: answer.stringSource }),
|
||||
},
|
||||
}),
|
||||
limitations: [...operationLimitations, ...answer.limitations],
|
||||
limitations: [
|
||||
...operationLimitations,
|
||||
...kunaVerificationLimitations(installation),
|
||||
...answer.limitations,
|
||||
],
|
||||
...(answer.relationshipCoverage === undefined
|
||||
? {}
|
||||
: { relationshipCoverage: answer.relationshipCoverage }),
|
||||
@@ -1469,6 +1483,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
this.#deadline,
|
||||
this.#heapRequest,
|
||||
),
|
||||
...kunaVerificationLimitations(installation),
|
||||
...pseudocode.value.limitations,
|
||||
KUNA_RELATIONSHIP_LIMITATION,
|
||||
...(relationships.ok
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { javascriptRuntimeReconciliationResultSchema } from "../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js";
|
||||
import { nodeReconciliationFixture } from "../../fixtures/nodeRuntimeReconciliation.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"reconciles synthetic Node target entry Evidence through %s",
|
||||
async (surface, { cli }) => {
|
||||
const input = nodeReconciliationFixture(false);
|
||||
let document: unknown;
|
||||
if (surface === "CLI") {
|
||||
const root = await createTestTempDirectory(
|
||||
"rea-node-reconciliation-cli-",
|
||||
);
|
||||
const path = join(root, "input.json");
|
||||
await writeFile(path, JSON.stringify(input));
|
||||
const response = await cli.run({
|
||||
arguments: ["reconcile-javascript-runtime", path, "--format", "json"],
|
||||
});
|
||||
expect(response.exitCode, response.stderr).toBe(0);
|
||||
document = response.json;
|
||||
} else {
|
||||
const call = await cli.sharedMcpCall!();
|
||||
const response = await call("reconcile_javascript_runtime", input);
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
document = response.structuredContent;
|
||||
}
|
||||
const result = javascriptRuntimeReconciliationResultSchema.parse(
|
||||
(document as { normalized_result: unknown }).normalized_result,
|
||||
);
|
||||
expect(result.reconciliations).toEqual([
|
||||
expect.objectContaining({
|
||||
entity_kind: "target",
|
||||
status: "matched",
|
||||
reason: "unique-location-match",
|
||||
confidence: "medium",
|
||||
candidate_static_count: 1,
|
||||
}),
|
||||
]);
|
||||
expect(result.summary).toMatchObject({
|
||||
runtime_scripts: 0,
|
||||
static_loaded: 0,
|
||||
static_not_observed: 0,
|
||||
});
|
||||
expect(
|
||||
result.graph.edges.filter(({ relation }) => relation === "observed_as"),
|
||||
).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
@@ -879,7 +879,14 @@ it.each([
|
||||
["install.sh", { code: true, runtime: true, package: true, evm: false }],
|
||||
[
|
||||
".agents/skills/reverse-engineer-anything/SKILL.md",
|
||||
{ code: true, runtime: true, docs: true, package: false, evm: false },
|
||||
{
|
||||
code: true,
|
||||
runtime: true,
|
||||
docs: true,
|
||||
website: true,
|
||||
package: false,
|
||||
evm: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"scripts/verify-real-ghidra.mjs",
|
||||
|
||||
@@ -30,6 +30,7 @@ const makePull = (
|
||||
state: "open",
|
||||
draft: false,
|
||||
body: readyBody,
|
||||
user: { id: 123456 },
|
||||
head: { sha: "a".repeat(40) },
|
||||
base: { ref: "main" },
|
||||
...changes,
|
||||
@@ -94,6 +95,30 @@ function fixture(changes: Partial<ReadinessPullRequest> = {}) {
|
||||
}
|
||||
|
||||
describe("initial author declaration", () => {
|
||||
it.each([76467478, 208281322, 161784452, 342580529])(
|
||||
"exempts the configured author ID %s, but keeps drafts pending",
|
||||
(id) => {
|
||||
expect(
|
||||
evaluateReadiness(makePull({ user: { id }, body: null }), template),
|
||||
).toMatchObject({
|
||||
state: "success",
|
||||
description: expect.stringContaining("exempt"),
|
||||
});
|
||||
expect(
|
||||
evaluateReadiness(
|
||||
makePull({ user: { id }, body: null, draft: true }),
|
||||
template,
|
||||
).state,
|
||||
).toBe("pending");
|
||||
},
|
||||
);
|
||||
|
||||
it("does not exempt unknown or missing author identities", () => {
|
||||
for (const user of [{ id: 123456 }, null])
|
||||
expect(
|
||||
evaluateReadiness(makePull({ user, body: null }), template).state,
|
||||
).toBe("failure");
|
||||
});
|
||||
it("accepts the filled current template and uppercase checkmarks", () => {
|
||||
expect(evaluateReadiness(makePull(), template).state).toBe("success");
|
||||
expect(
|
||||
@@ -212,6 +237,35 @@ describe("initial author declaration", () => {
|
||||
});
|
||||
|
||||
describe("GitHub status boundary", () => {
|
||||
it.each([76467478, 208281322, 161784452, 342580529])(
|
||||
"publishes a labeled exemption for author ID %s after checking the head",
|
||||
async (id) => {
|
||||
const input = fixture({ user: { id }, body: null });
|
||||
expect(await runReadinessCheck(input)).toMatchObject({
|
||||
state: "success",
|
||||
description: expect.stringContaining("exempt"),
|
||||
});
|
||||
expect(input.paginate).toHaveBeenCalledOnce();
|
||||
expect(input.createCommitStatus).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ state: "success", sha: input.pull.head.sha }),
|
||||
);
|
||||
},
|
||||
);
|
||||
it("does not let an exempt author lend success to another PR sharing the head", async () => {
|
||||
const input = fixture({ user: { id: 76467478 }, body: null });
|
||||
input.paginate.mockResolvedValue([input.pull, makePull({ number: 43 })]);
|
||||
expect((await runReadinessCheck(input))?.state).toBe("failure");
|
||||
});
|
||||
|
||||
it("does not publish an exemption if the author changes during the check", async () => {
|
||||
const input = fixture({ user: { id: 76467478 }, body: null });
|
||||
input.get
|
||||
.mockResolvedValueOnce({ data: input.pull })
|
||||
.mockResolvedValueOnce({
|
||||
data: makePull({ user: { id: 123456 }, body: null }),
|
||||
});
|
||||
expect((await runReadinessCheck(input))?.state).toBe("pending");
|
||||
});
|
||||
it("publishes failure on the actual PR head when declarations are unchecked", async () => {
|
||||
const input = fixture({ body: readyBody.replaceAll("[x]", "[ ]") });
|
||||
expect((await runReadinessCheck(input))?.state).toBe("failure");
|
||||
|
||||
@@ -10,7 +10,6 @@ import { createRequire } from "node:module";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { RequestError } from "@octokit/request-error";
|
||||
import { afterEach, beforeAll, expect, it, vi } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -106,15 +105,11 @@ it("creates overflow notes on a new branch without requiring an existing file",
|
||||
createNewBranch,
|
||||
octokit: {
|
||||
repos: {
|
||||
getContent: vi.fn().mockRejectedValue(
|
||||
new RequestError("missing", 404, {
|
||||
request: {
|
||||
method: "GET",
|
||||
url: "https://example.test/notes",
|
||||
headers: {},
|
||||
},
|
||||
}),
|
||||
),
|
||||
getContent: vi
|
||||
.fn()
|
||||
.mockRejectedValue(
|
||||
Object.assign(new Error("missing"), { status: 404 }),
|
||||
),
|
||||
createOrUpdateFileContents: write,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { reconcileJavaScriptRuntimeEvidence } from "../../../src/application/javascript/JavaScriptRuntimeReconciliationService.js";
|
||||
import { javascriptRuntimeReconciliationResultSchema } from "../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js";
|
||||
import { nodeReconciliationFixture } from "../../fixtures/nodeRuntimeReconciliation.js";
|
||||
|
||||
test.each([false, true])(
|
||||
"matches a synthetic Node entry target without inventing script presence: %s",
|
||||
(scriptObserved) => {
|
||||
const reconciled = reconcileJavaScriptRuntimeEvidence(
|
||||
nodeReconciliationFixture(scriptObserved),
|
||||
);
|
||||
if (!reconciled.ok) throw reconciled.error;
|
||||
const result = javascriptRuntimeReconciliationResultSchema.parse(
|
||||
reconciled.value.evidence.normalized_result,
|
||||
);
|
||||
const target = result.reconciliations.find(
|
||||
({ entity_kind }) => entity_kind === "target",
|
||||
);
|
||||
expect(target).toMatchObject({
|
||||
status: "matched",
|
||||
basis: "artifact-path",
|
||||
confidence: "medium",
|
||||
reason: "unique-location-match",
|
||||
candidate_static_count: 1,
|
||||
});
|
||||
expect(
|
||||
result.graph.nodes.find(
|
||||
({ node_id }) => node_id === target?.static_node_id,
|
||||
)?.kind,
|
||||
).toBe("javascript-asset");
|
||||
expect(result.summary).toMatchObject({
|
||||
runtime_scripts: scriptObserved ? 1 : 0,
|
||||
static_loaded: scriptObserved ? 1 : 0,
|
||||
static_not_observed: 0,
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -119,6 +119,33 @@ it("delivers native primitive and graph summaries with exact retained parents an
|
||||
);
|
||||
expect(rejected.isError).toBe(true);
|
||||
}
|
||||
const references = await call("xrefs", { address: "0x401000" });
|
||||
const source = {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: references.evidence_id,
|
||||
};
|
||||
const before = harness.calls.length;
|
||||
const page = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: { kind: "native-observation", offset: 0, limit: 1 },
|
||||
});
|
||||
expect(page.normalized_result).toMatchObject({
|
||||
parent_evidence_id: references.evidence_id,
|
||||
parent_operation: "xrefs",
|
||||
items: {
|
||||
type: "addresses",
|
||||
rows: [{ source_index: 0, address: "0x401001" }],
|
||||
},
|
||||
coverage: { total: 1, exhausted: true },
|
||||
});
|
||||
const caveats = await call("inspect_analysis_view", {
|
||||
source,
|
||||
view: { kind: "native-context", facet: "limitations" },
|
||||
});
|
||||
expect(caveats.normalized_result).toMatchObject({
|
||||
parent_operation: "xrefs",
|
||||
});
|
||||
expect(harness.calls).toHaveLength(before);
|
||||
} finally {
|
||||
await harness.close();
|
||||
}
|
||||
@@ -177,9 +204,12 @@ it("follows native overflow continuations through MCP without repeating provider
|
||||
for (const name of [
|
||||
...NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS,
|
||||
"get_call_graph",
|
||||
"xrefs",
|
||||
] as const) {
|
||||
const input =
|
||||
name === "get_call_graph" || name === "resolve_containing_procedure"
|
||||
name === "get_call_graph" ||
|
||||
name === "resolve_containing_procedure" ||
|
||||
name === "xrefs"
|
||||
? { address: "0x1000" }
|
||||
: { procedure: "fixture_main" };
|
||||
const complete = await client.callTool({ name, arguments: input });
|
||||
|
||||
@@ -115,11 +115,20 @@ itWithCaptureCapability(
|
||||
};
|
||||
|
||||
const expanded = await capture("x".repeat(500));
|
||||
expect(expanded.frames).toHaveLength(0);
|
||||
expect(expanded.truncated).toBe(true);
|
||||
const retainedNormalized = expanded.frames.map(({ data }) => data).join("");
|
||||
const retainedRaw = expanded.frames
|
||||
.map(({ raw_data, data }) => raw_data ?? data)
|
||||
.join("");
|
||||
// PTY read boundaries need not match the child's write. A small chunk can
|
||||
// fit even after another chunk was omitted for exceeding the budget.
|
||||
expect(retainedNormalized).toBe(
|
||||
retainedRaw.replaceAll("a", "x".repeat(500)),
|
||||
);
|
||||
expect(Buffer.byteLength(retainedNormalized)).toBeLessThanOrEqual(1_000);
|
||||
expect(expanded.truncation_details.normalized_terminal).toMatchObject({
|
||||
budget_bytes: 1_000,
|
||||
retained_bytes: 0,
|
||||
retained_bytes: Buffer.byteLength(retainedNormalized),
|
||||
omitted_frames: expect.any(Number),
|
||||
});
|
||||
expect(
|
||||
@@ -128,9 +137,9 @@ itWithCaptureCapability(
|
||||
expect(expanded.truncation_details.raw_terminal).toMatchObject({
|
||||
budget_bytes: 1_000,
|
||||
observed_bytes: expect.any(Number),
|
||||
retained_bytes: 0,
|
||||
retained_bytes: Buffer.byteLength(retainedRaw),
|
||||
observed_frames: expect.any(Number),
|
||||
retained_frames: 0,
|
||||
retained_frames: expanded.frames.length,
|
||||
});
|
||||
expect(
|
||||
expanded.limitations.some((limitation) =>
|
||||
|
||||
@@ -89,6 +89,36 @@ test("keeps each Kuna no-pseudocode observation's limitations on its batch item"
|
||||
})
|
||||
).structuredContent,
|
||||
);
|
||||
// The provider's version policy travels with this observation, while its
|
||||
// profile commits only analysis inputs. Composed and retained views must
|
||||
// preserve the same warning for agents reading those results.
|
||||
const verificationWarning =
|
||||
"Kuna 1.833 is not REA's verified 1.989 release";
|
||||
expect(batch.limitations).toContainEqual(
|
||||
expect.stringContaining(verificationWarning),
|
||||
);
|
||||
expect(broken.analysis_profile?.parameters).not.toHaveProperty(
|
||||
"verified_release",
|
||||
);
|
||||
const summary = parseEvidence(
|
||||
(await call("analyze_function", { procedure: "main", detail: "summary" }))
|
||||
.structuredContent,
|
||||
);
|
||||
const parent = summary.normalized_result as { parent_evidence_id: string };
|
||||
const page = parseEvidence(
|
||||
(
|
||||
await call("inspect_analysis_view", {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: parent.parent_evidence_id,
|
||||
},
|
||||
view: { kind: "native", facet: "pseudocode" },
|
||||
})
|
||||
).structuredContent,
|
||||
);
|
||||
expect(page.limitations).toContainEqual(
|
||||
expect.stringContaining(verificationWarning),
|
||||
);
|
||||
if (batchDecompile === undefined || getEvidenceBundle === undefined)
|
||||
throw new Error(
|
||||
"Missing batch_decompile or get_evidence_bundle contract",
|
||||
|
||||
@@ -34,7 +34,7 @@ it("reports an unconfigured Kuna as a missing optional engine", async () => {
|
||||
it("publishes the caller's explicit settings once the installation validates", async () => {
|
||||
const root = await createTestTempDirectory("rea-kuna-doctor-");
|
||||
const kuna = await writeFakeKuna(root, {
|
||||
version: { stdout: "kuna 1.988\n" },
|
||||
version: { stdout: "kuna 1.989\n" },
|
||||
});
|
||||
const cache = join(root, "cache");
|
||||
|
||||
@@ -48,7 +48,7 @@ it("publishes the caller's explicit settings once the installation validates", a
|
||||
expect(inspection).toMatchObject({
|
||||
configured: true,
|
||||
available: true,
|
||||
providerVersion: "1.988",
|
||||
providerVersion: "1.989",
|
||||
registrationEnvironment: {
|
||||
REA_KUNA_COMMAND: kuna,
|
||||
REA_KUNA_CACHE_DIR: cache,
|
||||
@@ -59,10 +59,10 @@ it("publishes the caller's explicit settings once the installation validates", a
|
||||
name: "installation",
|
||||
ok: true,
|
||||
detail: expect.stringMatching(
|
||||
/reports Kuna 1\.988 \(sha256 [a-f0-9]{64}\)\.$/u,
|
||||
/reports Kuna 1\.989 \(sha256 [a-f0-9]{64}\)\.$/u,
|
||||
),
|
||||
remediation: null,
|
||||
details: { version: "1.988", verified_version: "1.988" },
|
||||
details: { version: "1.989", verified_version: "1.989" },
|
||||
},
|
||||
{
|
||||
name: "cache",
|
||||
@@ -88,7 +88,7 @@ it("flags an unverified release without failing the installation", async () => {
|
||||
"You may continue with the selected unverified release.",
|
||||
),
|
||||
remediation: expect.stringContaining("setting REA_KUNA_COMMAND"),
|
||||
details: { verified_version: "1.988" },
|
||||
details: { verified_version: "1.989" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const it = test.skipIf(process.platform === "win32");
|
||||
it("identifies a caller-selected kuna by version and digest", async () => {
|
||||
const root = await createTestTempDirectory("rea-kuna-install-");
|
||||
const kuna = await writeFakeKuna(root, {
|
||||
version: { stdout: "kuna 1.988\n" },
|
||||
version: { stdout: "kuna 1.989\n" },
|
||||
});
|
||||
const link = join(root, "current-kuna");
|
||||
await symlink(kuna, link);
|
||||
@@ -37,7 +37,7 @@ it("identifies a caller-selected kuna by version and digest", async () => {
|
||||
value: {
|
||||
configuration: { command: link, cacheRoot: join(root, "cache") },
|
||||
executable: await realpath(kuna),
|
||||
version: "1.988",
|
||||
version: "1.989",
|
||||
verified: true,
|
||||
// A `#!` launcher reports its interpreter; ownership checks must not expect the path.
|
||||
script: true,
|
||||
@@ -104,14 +104,14 @@ fs.renameSync(process.argv[1] + ".new", process.argv[1]);
|
||||
it("accepts another release but reports it as unverified", async () => {
|
||||
const root = await createTestTempDirectory("rea-kuna-unverified-");
|
||||
const kuna = await writeFakeKuna(root, {
|
||||
version: { stdout: "kuna 1.986\n" },
|
||||
version: { stdout: "kuna 1.988\n" },
|
||||
});
|
||||
|
||||
const result = await inspectKunaInstallation({ REA_KUNA_COMMAND: kuna });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: { version: "1.986", verified: false },
|
||||
value: { version: "1.988", verified: false },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { AnalysisProviderRegistry } from "../../../../src/application/binary/AnalysisProviderRegistry.js";
|
||||
import { composeBinarySession } from "../../../../src/application/binary/BinarySessionComposition.js";
|
||||
import { parseEvidence } from "../../../../src/domain/evidence.js";
|
||||
import { KunaProvider } from "../../../../src/kuna/KunaProvider.js";
|
||||
import { elfNoteFixture } from "../../../../src/native/elf/ElfNotes.fixture.js";
|
||||
import { createServer } from "../../../../src/server/createServer.js";
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
import { mcpTest } from "../../../support/mcp/mcpFixture.js";
|
||||
import { writeFakeKuna } from "./fakeKuna.js";
|
||||
|
||||
const test = mcpTest.skipIf(process.platform === "win32");
|
||||
const caveat =
|
||||
"Parallel Kuna project export does not propagate callee-first prototypes";
|
||||
|
||||
for (const jobs of [1, 2]) {
|
||||
test(`retains source limitations in batch and paged dossier with ${String(jobs)} worker(s)`, async ({
|
||||
mcp,
|
||||
}) => {
|
||||
const root = await createTestTempDirectory("rea-kuna-parallel-mcp-");
|
||||
const kuna = await writeFakeKuna(root, {
|
||||
functions: [{ name: "main", addr: "0x1000" }],
|
||||
});
|
||||
const target = join(root, "program");
|
||||
await writeFile(target, elfNoteFixture({ bits: 64, type: 2 }));
|
||||
const session = composeBinarySession(
|
||||
new AnalysisProviderRegistry(
|
||||
[
|
||||
new KunaProvider(
|
||||
{
|
||||
REA_KUNA_COMMAND: kuna,
|
||||
REA_KUNA_CACHE_DIR: join(root, "cache"),
|
||||
REA_KUNA_JOBS: String(jobs),
|
||||
},
|
||||
{ platform: process.platform },
|
||||
),
|
||||
],
|
||||
"kuna",
|
||||
),
|
||||
);
|
||||
const client = await mcp.connect(
|
||||
createServer({ kind: "session", session }),
|
||||
);
|
||||
const call = async (name: string, arguments_: Record<string, unknown>) => {
|
||||
const response = await client.callTool({ name, arguments: arguments_ });
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
return response.structuredContent;
|
||||
};
|
||||
await call("open_binary", { path: target });
|
||||
// Assembly waits for the complete project, avoiding timing-based source selection.
|
||||
await call("procedure_assembly", { procedure: "main" });
|
||||
const batch = parseEvidence(
|
||||
await call("batch_decompile", {
|
||||
addresses: ["0x1000"],
|
||||
detail: "complete",
|
||||
}),
|
||||
);
|
||||
expect(batch.limitations.some((item) => item.includes(caveat))).toBe(
|
||||
jobs > 1,
|
||||
);
|
||||
const dossier = parseEvidence(
|
||||
await call("analyze_function", { procedure: "main", detail: "summary" }),
|
||||
);
|
||||
const summary = dossier.normalized_result as { parent_evidence_id: string };
|
||||
const view = parseEvidence(
|
||||
await call("inspect_analysis_view", {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: summary.parent_evidence_id,
|
||||
},
|
||||
view: { kind: "native", facet: "pseudocode" },
|
||||
}),
|
||||
);
|
||||
expect(view.limitations.some((item) => item.includes(caveat))).toBe(
|
||||
jobs > 1,
|
||||
);
|
||||
await call("close_binary", {});
|
||||
});
|
||||
}
|
||||
@@ -182,22 +182,42 @@ it("waits for an aborted pre-session hash before retrying its late close owner",
|
||||
|
||||
it("commits the observed Kuna release and digest to the analysis profile", async () => {
|
||||
const { profile, kuna } = await session("rea-kuna-profile-", {
|
||||
version: { stdout: "kuna 1.988\n" },
|
||||
version: { stdout: "kuna 1.989\n" },
|
||||
});
|
||||
|
||||
expect(profile).toMatchObject({
|
||||
provider: { id: "kuna", name: "Kuna", version: "1.988" },
|
||||
provider: { id: "kuna", name: "Kuna", version: "1.989" },
|
||||
parameters: {
|
||||
kuna_version: "1.988",
|
||||
kuna_version: "1.989",
|
||||
kuna_sha256: expect.stringMatching(/^[0-9a-f]{64}$/u),
|
||||
kuna_command: kuna,
|
||||
verified_release: true,
|
||||
jobs: 1,
|
||||
cache_policy: "live",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reports an unverified release on observations without binding policy into the profile", async () => {
|
||||
const { profile, open } = await session("rea-kuna-policy-", {
|
||||
version: { stdout: "kuna 1.988\n" },
|
||||
});
|
||||
expect(profile.parameters).not.toHaveProperty("verified_release");
|
||||
expect(profile.parameters).toMatchObject({
|
||||
kuna_version: "1.988",
|
||||
kuna_sha256: expect.stringMatching(/^[0-9a-f]{64}$/u),
|
||||
});
|
||||
const client = open();
|
||||
const inventory = await run(client, "list_procedures");
|
||||
const warning = "Kuna 1.988 is not REA's verified 1.989 release";
|
||||
expect(inventory.limitations.join("\n")).toContain(warning);
|
||||
const dossier = await run(client, "analyze_function", { procedure: "main" });
|
||||
expect(dossier.limitations.join("\n")).toContain(warning);
|
||||
expect(
|
||||
(dossier.result as { limitations: string[] }).limitations.join("\n"),
|
||||
).toContain(warning);
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it("lists the selected document without starting target analysis", async () => {
|
||||
const { kuna, target, cacheRoot, open } = await session(
|
||||
"rea-kuna-document-metadata-",
|
||||
@@ -215,6 +235,39 @@ it("lists the selected document without starting target analysis", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("qualifies only parallel project pseudocode, including dossier results", async () => {
|
||||
const { open } = await session(
|
||||
"rea-kuna-parallel-context-",
|
||||
{ delayMs: 300 },
|
||||
{ REA_KUNA_JOBS: "2" },
|
||||
);
|
||||
const client = open();
|
||||
const caveat =
|
||||
"Parallel Kuna project export does not propagate callee-first prototypes";
|
||||
const cold = await run(client, "procedure_pseudo_code", {
|
||||
procedure: "main",
|
||||
});
|
||||
expect(cold.rawResult).toMatchObject({ source: { kind: "single-function" } });
|
||||
expect(cold.limitations.join("\n")).not.toContain(caveat);
|
||||
await run(client, "procedure_assembly", { procedure: "main" });
|
||||
const warm = await run(client, "procedure_pseudo_code", {
|
||||
procedure: "main",
|
||||
});
|
||||
expect(warm.rawResult).toMatchObject({ source: { kind: "project-export" } });
|
||||
expect(warm.limitations.join("\n")).toContain(caveat);
|
||||
const dossier = await run(client, "analyze_function", { procedure: "main" });
|
||||
expect(dossier.limitations.join("\n")).toContain(caveat);
|
||||
expect(
|
||||
(dossier.result as { limitations: string[] }).limitations.join("\n"),
|
||||
).toContain(caveat);
|
||||
const missing = await run(client, "procedure_pseudo_code", {
|
||||
procedure: "broken",
|
||||
});
|
||||
expect(missing.result).toBeNull();
|
||||
expect(missing.limitations.join("\n")).not.toContain(caveat);
|
||||
await client.close();
|
||||
});
|
||||
|
||||
it("answers a cold request from one function, then serves the finished export", async () => {
|
||||
const { kuna, cacheRoot, open } = await session("rea-kuna-cold-", {
|
||||
delayMs: 300,
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { createJavaScriptRuntimeObservationEvidence } from "../../src/application/javascript/JavaScriptRuntimeObservationEvidence.js";
|
||||
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
||||
import { javascriptRuntimeObservationSchema } from "../../src/domain/javascript/javascriptRuntimeObservation.js";
|
||||
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../../src/inspector/providerIdentity.js";
|
||||
|
||||
/** Synthetic, schema-validated capture; this is not a live Inspector observation. */
|
||||
export const nodeReconciliationFixture = (scriptObserved: boolean) => {
|
||||
const location = {
|
||||
kind: "file",
|
||||
file_path: "/Applications/Example.app/Contents/Resources/app/renderer.js",
|
||||
};
|
||||
const runtime = javascriptRuntimeObservationSchema.parse({
|
||||
runtime: {
|
||||
product: "node.js/v24.22.1",
|
||||
protocol_version: "1.3",
|
||||
v8_version: null,
|
||||
},
|
||||
target: {
|
||||
target_id: "synthetic-node",
|
||||
protocol_type: "node",
|
||||
attached: null,
|
||||
location,
|
||||
runtime_kind: "unknown",
|
||||
runtime_kind_authority: "not-declared",
|
||||
},
|
||||
capture: {
|
||||
observation_ms: 100,
|
||||
events_observed: scriptObserved ? 1 : 0,
|
||||
events_retained: scriptObserved ? 1 : 0,
|
||||
events_dropped: 0,
|
||||
metadata_bytes_retained: 0,
|
||||
truncated: false,
|
||||
truncation_reasons: [],
|
||||
},
|
||||
scripts: {
|
||||
items: scriptObserved
|
||||
? [
|
||||
{
|
||||
script_key: `v8_script_${"4".repeat(64)}`,
|
||||
location,
|
||||
execution_context_key: null,
|
||||
cdp_hash: null,
|
||||
length: null,
|
||||
is_module: null,
|
||||
status: "observed-loaded",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
observed_total: scriptObserved ? 1 : 0,
|
||||
excluded: { unsupported_location: 0, invalid_protocol_value: 0 },
|
||||
},
|
||||
execution_contexts: [],
|
||||
directly_observed: [],
|
||||
unavailable_without_instrumentation: [],
|
||||
unknowns: ["Synthetic bounded capture."],
|
||||
limitations: ["Synthetic fixture, not a live runtime."],
|
||||
});
|
||||
return {
|
||||
static_layers: [{ analysis: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE }],
|
||||
runtime_observations: [
|
||||
createJavaScriptRuntimeObservationEvidence(
|
||||
"observe_javascript_runtime",
|
||||
{
|
||||
inspector_endpoint: "http://127.0.0.1:9229",
|
||||
target_id: "synthetic-node",
|
||||
observation_ms: 100,
|
||||
},
|
||||
runtime,
|
||||
V8_INSPECTOR_PROVIDER_IDENTITY,
|
||||
),
|
||||
],
|
||||
};
|
||||
};
|
||||
+57
-1
@@ -486,10 +486,66 @@ The mask SVG is drawn from the seventh decoded mask. It selects zero-based
|
||||
position 21 and compares its character code with 55 (`7`). Keep the figure,
|
||||
solver constants and evidence notes aligned when changing this case.
|
||||
|
||||
## Agent discovery and Markdown
|
||||
|
||||
`scripts/website_agents.py` runs during asset preparation, after translations.
|
||||
It generates these public files in ignored `_site/`:
|
||||
|
||||
- `.well-known/agent-skills/index.json`: the draft v0.2.0 skills discovery index.
|
||||
Its archive contains the maintained
|
||||
`.agents/skills/reverse-engineer-anything/SKILL.md` and Markdown references.
|
||||
The name and description come from YAML frontmatter; SHA-256 covers the
|
||||
complete, reproducible ZIP, so reference changes invalidate the digest too.
|
||||
- `.well-known/ard.json`: an ARD manifest with representative analyst queries.
|
||||
- `.well-known/ai-catalog.json`: an AI Catalog 1.0 document listing that same
|
||||
downloadable skill. These advertise installation instructions for the local
|
||||
tool, without claiming a hosted analysis endpoint.
|
||||
- `index.md` beside each indexable rendered reading page, including translations.
|
||||
Markdown retains code, evidence disclosures and image descriptions, with
|
||||
absolute resource links. Drafts, labeled English fallbacks and target example
|
||||
applications retain their HTML and indexing policy.
|
||||
|
||||
The [skills discovery draft](https://github.com/cloudflare/agent-skills-discovery-rfc),
|
||||
[ARD proposal](https://agenticresourcediscovery.org/spec/) and
|
||||
[AI Catalog proposal](https://ai-catalog.io/) are evolving formats. ARD v0.91
|
||||
uses `ard.json` and `rel="ard"`; AI Catalog uses its separate `ai-catalog.json`
|
||||
envelope. Both entries point to the same ZIP with the registered
|
||||
`application/zip` media type.
|
||||
|
||||
Cloudflare's Worker selects Markdown when an explicit `Accept: text/markdown`
|
||||
has higher quality than HTML. HTML wins ties and remains the default. Successful
|
||||
reading-page responses include `Vary: Accept` and an alternate Markdown Link;
|
||||
Markdown uses `Content-Type: text/markdown; charset=utf-8`. Static `_headers`
|
||||
advertises discovery resources, sets discovery CORS to `*`, and supplies MIME
|
||||
types. Build-generated HTML `<link>` elements also advertise these resources.
|
||||
|
||||
GitHub Pages and the Python preview serve the same files and HTML links, with
|
||||
direct Markdown URLs such as `guides/javascript/index.md`. They do not execute
|
||||
the Worker or apply Cloudflare's `_headers`: content negotiation, custom MIME
|
||||
overrides and discovery CORS headers are Cloudflare behavior. The publish
|
||||
workflow packages `.well-known` explicitly because `upload-pages-artifact`
|
||||
excludes dot directories by default.
|
||||
|
||||
`python3 scripts/website.py check` includes artifact/content regressions and
|
||||
Worker request-contract tests. To check the actual local Cloudflare runtime
|
||||
after preparing assets, use the repository's pinned Node.js and Wrangler:
|
||||
|
||||
```sh
|
||||
npx wrangler@4.149.0 dev --cwd website --env production --local
|
||||
curl -i -H 'Accept: text/markdown' http://localhost:8787/guides/javascript/
|
||||
curl -I http://localhost:8787/.well-known/agent-skills/index.json
|
||||
```
|
||||
|
||||
The `production` environment here skips the preview build and uses the already
|
||||
verified artifact; `--local` runs on your machine. Check HTML and Markdown,
|
||||
GET and HEAD, discovery CORS, the downloaded ZIP digest, and unknown routes.
|
||||
Production remains published only by the manual website workflow.
|
||||
|
||||
## Cloudflare Workers
|
||||
|
||||
`wrangler.toml` serves `website/_site/` through Workers Static Assets. There is
|
||||
no Worker script or frontend framework. Before a preview or deployment, Wrangler
|
||||
one small Worker (`worker.mjs`) for Markdown content negotiation and no frontend
|
||||
framework. Before a preview or deployment, Wrangler
|
||||
runs the Python scripts to build the language editions, prepare the example ZIP and verify the
|
||||
website. Node.js and Python 3 must be available; the commands below use a pinned
|
||||
Wrangler version without adding it to REA's package dependencies.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Link: <https://rea.tools/.well-known/ard.json>; rel="ard", <https://rea.tools/.well-known/ai-catalog.json>; rel="ai-catalog", <https://rea.tools/.well-known/agent-skills/index.json>; rel="related"; type="application/json"; title="Agent skills index"
|
||||
|
||||
/.well-known/*
|
||||
Access-Control-Allow-Origin: *
|
||||
|
||||
/.well-known/ai-catalog.json
|
||||
Content-Type: application/ai-catalog+json
|
||||
|
||||
/*.md
|
||||
Content-Type: text/markdown; charset=utf-8
|
||||
@@ -2,3 +2,6 @@
|
||||
CairoSVG==2.9.1
|
||||
# Standard gettext PO/POT catalogs; no translation service is used by builds.
|
||||
polib==1.2.0
|
||||
# Build Markdown reading pages and parse the maintained skill's YAML metadata.
|
||||
markdownify==1.2.3
|
||||
PyYAML==6.0.3
|
||||
@@ -0,0 +1,103 @@
|
||||
// Negotiate prebuilt Markdown while keeping the static asset service's routing.
|
||||
function prefersMarkdown(accept) {
|
||||
const ranges = (accept ?? "")
|
||||
.toLowerCase()
|
||||
.split(",")
|
||||
.map((range) => {
|
||||
const [type, ...parameters] = range.trim().split(";");
|
||||
const quality = parameters
|
||||
.map((value) => value.trim())
|
||||
.find((value) => value.startsWith("q="));
|
||||
const q = quality === undefined ? 1 : Number(quality.slice(2));
|
||||
return {
|
||||
type: type.trim(),
|
||||
q: Number.isFinite(q) && q >= 0 && q <= 1 ? q : 0,
|
||||
};
|
||||
});
|
||||
const quality = (type) => {
|
||||
for (const match of [type, "text/*", "*/*"]) {
|
||||
const matches = ranges.filter((range) => range.type === match);
|
||||
if (matches.length) return Math.max(...matches.map((range) => range.q));
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
// Wildcards alone never opt a browser into Markdown; HTML wins ties.
|
||||
return (
|
||||
ranges.some((range) => range.type === "text/markdown") &&
|
||||
quality("text/markdown") > 0 &&
|
||||
quality("text/markdown") > quality("text/html")
|
||||
);
|
||||
}
|
||||
|
||||
function advertiseMarkdown(response, path) {
|
||||
const vary = new Set(
|
||||
(response.headers.get("Vary") ?? "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
if (![...vary].some((s) => s.toLowerCase() === "accept" || s === "*"))
|
||||
vary.add("Accept");
|
||||
response.headers.set("Vary", [...vary].join(", "));
|
||||
response.headers.append(
|
||||
"Link",
|
||||
`<${path}>; rel="alternate"; type="text/markdown"`,
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const useMarkdown = prefersMarkdown(request.headers.get("Accept"));
|
||||
// HTML validators describe a different representation. Probe the route first,
|
||||
// then let the asset service evaluate validators/ranges against the Markdown.
|
||||
const probe = new Request(request);
|
||||
if (useMarkdown) {
|
||||
for (const name of [
|
||||
"If-None-Match",
|
||||
"If-Modified-Since",
|
||||
"If-Match",
|
||||
"If-Unmodified-Since",
|
||||
"If-Range",
|
||||
"Range",
|
||||
])
|
||||
probe.headers.delete(name);
|
||||
}
|
||||
const original = await env.ASSETS.fetch(probe);
|
||||
if (
|
||||
![200, 206, 304].includes(original.status) ||
|
||||
!["GET", "HEAD"].includes(request.method) ||
|
||||
!original.headers.get("Content-Type")?.startsWith("text/html")
|
||||
)
|
||||
return original;
|
||||
|
||||
const url = new URL(request.url);
|
||||
if (!url.pathname.endsWith("/") && !url.pathname.endsWith(".html"))
|
||||
return original;
|
||||
const markdown = new URL(url);
|
||||
markdown.pathname = url.pathname.endsWith("/")
|
||||
? `${url.pathname}index.md`
|
||||
: url.pathname.slice(0, -5) + ".md";
|
||||
const candidate = await env.ASSETS.fetch(
|
||||
useMarkdown
|
||||
? new Request(markdown, request)
|
||||
: new Request(markdown, { method: "HEAD" }),
|
||||
);
|
||||
if (!candidate.ok && ![304, 412, 416].includes(candidate.status)) {
|
||||
await candidate.body?.cancel();
|
||||
return original;
|
||||
}
|
||||
const response = new Response(
|
||||
useMarkdown ? candidate.body : original.body,
|
||||
useMarkdown ? candidate : original,
|
||||
);
|
||||
if (useMarkdown) {
|
||||
await original.body?.cancel();
|
||||
if (candidate.status < 400)
|
||||
response.headers.set("Content-Type", "text/markdown; charset=utf-8");
|
||||
} else {
|
||||
await candidate.body?.cancel();
|
||||
}
|
||||
return advertiseMarkdown(response, markdown.pathname);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import worker from "./worker.mjs";
|
||||
|
||||
function assets({ markdownStatus = 200 } = {}) {
|
||||
return {
|
||||
ASSETS: {
|
||||
async fetch(request) {
|
||||
const url = new URL(request.url);
|
||||
const markdown = url.pathname.endsWith("index.md");
|
||||
if (url.pathname === "/missing/")
|
||||
return new Response("Missing", { status: 404 });
|
||||
if (url.pathname === "/guide")
|
||||
return Response.redirect(`${url.origin}/guide/`, 307);
|
||||
const headers = {
|
||||
"Content-Type": markdown ? "text/plain" : "text/html",
|
||||
ETag: markdown ? '"markdown"' : '"html"',
|
||||
Link: '<https://rea.tools/.well-known/ard.json>; rel="ard"',
|
||||
};
|
||||
const status =
|
||||
request.headers.get("If-None-Match") === headers.ETag
|
||||
? 304
|
||||
: markdown
|
||||
? markdownStatus
|
||||
: 200;
|
||||
return new Response(
|
||||
request.method === "HEAD" || status === 304
|
||||
? null
|
||||
: markdown
|
||||
? "# Guide"
|
||||
: "<h1>Guide</h1>",
|
||||
{ headers, status },
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("HTML is the default and explicit Accept preferences select Markdown", async () => {
|
||||
for (const [accept, markdown] of [
|
||||
[null, false],
|
||||
["*/*", false],
|
||||
["text/html", false],
|
||||
["text/markdown", true],
|
||||
["text/markdown;q=0", false],
|
||||
["text/markdown;q=0.5,text/html", false],
|
||||
["text/markdown,text/html;q=0.5", true],
|
||||
["text/markdown,text/html", false],
|
||||
["TEXT/MARKDOWN; q=1,text/*;q=0.5", true],
|
||||
["text/markdown;q=bad", false],
|
||||
["text/markdown;q=0,*/*;q=1", false],
|
||||
]) {
|
||||
const response = await worker.fetch(
|
||||
new Request("https://rea.tools/guide/", {
|
||||
headers: accept === null ? {} : { Accept: accept },
|
||||
}),
|
||||
assets(),
|
||||
);
|
||||
assert.equal(
|
||||
response.headers.get("Content-Type"),
|
||||
markdown ? "text/markdown; charset=utf-8" : "text/html",
|
||||
accept,
|
||||
);
|
||||
assert.equal(
|
||||
await response.text(),
|
||||
markdown ? "# Guide" : "<h1>Guide</h1>",
|
||||
accept,
|
||||
);
|
||||
assert.match(response.headers.get("Vary"), /Accept/);
|
||||
assert.match(response.headers.get("Link"), /rel="ard"/);
|
||||
assert.match(
|
||||
response.headers.get("Link"),
|
||||
/<\/guide\/index.md>; rel="alternate"/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("HEAD and validators use the selected representation", async () => {
|
||||
for (const method of ["GET", "HEAD"]) {
|
||||
const response = await worker.fetch(
|
||||
new Request("https://rea.tools/guide/", {
|
||||
method,
|
||||
headers: { Accept: "text/markdown", "If-None-Match": '"html"' },
|
||||
}),
|
||||
assets(),
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("ETag"), '"markdown"');
|
||||
assert.equal(await response.text(), method === "HEAD" ? "" : "# Guide");
|
||||
}
|
||||
for (const [accept, tag] of [
|
||||
["text/html", '"html"'],
|
||||
["text/markdown", '"markdown"'],
|
||||
]) {
|
||||
const response = await worker.fetch(
|
||||
new Request("https://rea.tools/guide/", {
|
||||
headers: { Accept: accept, "If-None-Match": tag },
|
||||
}),
|
||||
assets(),
|
||||
);
|
||||
assert.equal(response.status, 304);
|
||||
assert.equal(await response.text(), "");
|
||||
assert.match(response.headers.get("Vary"), /Accept/);
|
||||
}
|
||||
});
|
||||
|
||||
test("missing Markdown keeps HTML, and asset redirects and missing routes survive", async () => {
|
||||
const html = await worker.fetch(
|
||||
new Request("https://rea.tools/guide/", {
|
||||
headers: { Accept: "text/markdown" },
|
||||
}),
|
||||
assets({ markdownStatus: 404 }),
|
||||
);
|
||||
assert.equal(await html.text(), "<h1>Guide</h1>");
|
||||
for (const [path, status] of [
|
||||
["/guide", 307],
|
||||
["/missing/", 404],
|
||||
]) {
|
||||
const response = await worker.fetch(
|
||||
new Request(`https://rea.tools${path}`, {
|
||||
headers: { Accept: "text/markdown" },
|
||||
}),
|
||||
assets(),
|
||||
);
|
||||
assert.equal(response.status, status);
|
||||
}
|
||||
});
|
||||
@@ -1,18 +1,21 @@
|
||||
# Serve only the public website. The default deployment is a workers.dev preview.
|
||||
name = "rea-website-preview"
|
||||
main = "worker.mjs"
|
||||
compatibility_date = "2026-10-08"
|
||||
workers_dev = true
|
||||
preview_urls = false
|
||||
|
||||
[assets]
|
||||
directory = "./_site"
|
||||
binding = "ASSETS"
|
||||
run_worker_first = ["/", "/*/", "/*.html"]
|
||||
html_handling = "auto-trailing-slash"
|
||||
not_found_handling = "none"
|
||||
|
||||
[build]
|
||||
cwd = ".."
|
||||
command = "python3 scripts/website.py check"
|
||||
watch_dir = ["./public", "./locales", "./templates"]
|
||||
watch_dir = ["./public", "./locales", "./templates", "../.agents/skills/reverse-engineer-anything"]
|
||||
|
||||
# Public HTTPS depends on zone activation and certificate readiness.
|
||||
[env.production]
|
||||
|
||||
Reference in new issue
Block a user