Compare commits

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

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

Signed-off-by: dependabot[bot] <[email protected]>
2026-11-03 01:36:48 +00:00
morluto eef8f44003 feat(website): publish agent discovery and Markdown responses 2026-11-03 09:33:41 +08:00
ShuaixinHuang 510e83eadc fix(firmware): recommend the operation-specific engine (#8668) 2026-11-03 09:17:33 +08:00
2d11fd92d2 fix(javascript): reconcile Node targets with entry assets (#8670)
* fix(javascript): reconcile Node targets with entry assets

* test(inspector): read reconciled evidence payload

* Update tests/boundary/inspector/javascriptRuntimeNodeTarget.test.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* test(inspector): read the workflow evidence payload

* Update tests/boundary/inspector/javascriptRuntimeNodeTarget.test.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* test(javascript): restore Evidence workflow result access

---------

Co-authored-by: morluto <[email protected]>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-11-03 08:54:39 +08:00
morluto 9bea563e30 fix(kuna): qualify parallel project pseudocode (#8694)
* fix(kuna): qualify parallel project pseudocode

* test(kuna): cover parallel caveats through MCP
2026-11-03 03:30:19 +08:00
morluto e749160ab0 fix(mcp): qualify string byte-read guidance (#8704)
* fix(mcp): qualify string byte-read guidance

* fix(mcp): avoid implying complete string references
2026-11-03 03:29:59 +08:00
morluto 920d634e83 ci: exempt designated authors from readiness declaration 2026-11-03 02:18:55 +08:00
morluto 55078b9f57 feat(mcp): page retained incoming reference addresses (#8591) 2026-11-03 02:13:21 +08:00
morluto 75c3cab540 fix(kuna)!: separate verification policy from analysis identity (#8699)
* feat(kuna): verify release 1.989

* fix(kuna)!: separate verification policy from analysis identity

* docs(kuna): show historical evidence recovery

* test(kuna): cover verification warnings through MCP
2026-11-03 01:34:40 +08:00
morluto 3c05b4d3fe fix(ci): remove undeclared dependency and handle PTY chunks 2026-11-03 01:10:56 +08:00
84 changed files with 1337 additions and 167 deletions

No files matched your search

+2 -1
View File
@@ -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.
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
ref: ${{ github.event.workflow_run.head_sha }}
path: triggering-source
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: trusted-retention/.nvmrc
- name: Retain current Turbo snapshots and npm download caches
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+17 -17
View File
@@ -92,7 +92,7 @@ jobs:
with:
ref: ${{ inputs.source_sha || github.sha }}
fetch-depth: 0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Bind qualification to the caller revision
@@ -144,7 +144,7 @@ jobs:
CHANGES_RESULT: ${{ needs.changes.result }}
run: test "${CHANGES_RESULT}" = "success"
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Validate source architecture before dependency installation
@@ -213,7 +213,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -278,7 +278,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -299,7 +299,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -337,7 +337,7 @@ jobs:
shard: ${{ fromJSON(needs.changes.outputs.tier == 'fast' && '["1/8","2/8","3/8","4/8","5/8","6/8","7/8","8/8"]' || '["1/6","2/6","3/6","4/6","5/6","6/6"]') }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -377,7 +377,7 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -428,7 +428,7 @@ jobs:
actions: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -512,7 +512,7 @@ jobs:
run: test "${CHANGES_RESULT}" = "success"
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
if: needs.changes.outputs.code == 'true' && success()
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: needs.changes.outputs.code == 'true' && success()
with:
node-version-file: .nvmrc
@@ -641,7 +641,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
@@ -702,7 +702,7 @@ jobs:
UV_THREADPOOL_SIZE: "2"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -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
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
GHIDRA_RELEASE_SHA256: ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Validate the SDK pin against the provider's verified release
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
config: stryker.semantics.config.mjs
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
COMPlus_GCHeapHardLimit: "30000000"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
timeout-minutes: 30
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Fetch checksum-pinned engine and public APK fixture
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
timeout-minutes: 12
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Install the cross compiler used by the Windows native lane
@@ -53,7 +53,7 @@ jobs:
runner: [self-hosted, cutter-upstream, Windows]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
@@ -65,7 +65,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- run: npm ci
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 25
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -44,7 +44,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
timeout-minutes: 20
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -40,7 +40,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
ca-certificates git python3-venv gcc libc6-dev strace gdb
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
@@ -39,7 +39,7 @@ jobs:
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
+7 -7
View File
@@ -88,7 +88,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
if: steps.editorial.outputs.frozen != 'true'
with:
node-version-file: .nvmrc
@@ -126,7 +126,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -248,7 +248,7 @@ jobs:
persist-credentials: false
- name: Set up Node.js for checkpoint validation
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -323,7 +323,7 @@ jobs:
- name: Set up Node.js for generated documentation
if: inputs.phase == 'prepare' && steps.proposal.outputs.prs_created == 'true'
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -385,7 +385,7 @@ jobs:
with:
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -416,7 +416,7 @@ jobs:
with:
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
@@ -454,7 +454,7 @@ jobs:
with:
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
package-manager-cache: false
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
node: ["^22.23.4", "24.22.1", "^24.22.1", "^26.12.1"]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
check-latest: true
+4
View File
@@ -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
+42 -4
View File
@@ -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")
+2 -2
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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. |
+3
View File
@@ -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
View File
@@ -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.
+6
View File
@@ -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
+5
View File
@@ -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
+1 -1
View File
@@ -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;
+ }
+ }
+2 -1
View File
@@ -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: {
+14
View File
@@ -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(
+3 -1
View File
@@ -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))
+3
View File
@@ -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__":
+109
View File
@@ -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 &lt; b ? "x_y" : "&amp;";\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
View File
@@ -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:
+116
View File
@@ -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"],
+10 -3
View File
@@ -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", []);
+2 -1
View File
@@ -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>>;
+2 -2
View File
@@ -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: {
+2
View File
@@ -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,
};
+2 -2
View File
@@ -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
+6 -1
View File
@@ -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.",
]
: [],
});
}
-1
View File
@@ -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 -1
View File
@@ -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";
+17 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+11
View File
@@ -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
+3
View File
@@ -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
+103
View File
@@ -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);
},
};
+127
View File
@@ -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);
}
});
+4 -1
View File
@@ -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]