Merge remote-tracking branch 'origin/main' into HEAD

This commit is contained in:
morluto committed 2026-11-03 12:54:06 +08:00
commit bc962f5c19
62 files changed
+1716 -131

No files matched your search

+3 -3
View File
@@ -55,12 +55,12 @@ jobs:
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- name: Install ELF32 cross toolchains for source-owned fixtures
- name: Install AArch64 and ELF32 cross toolchains for source-owned fixtures
run: |
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
gcc-i686-linux-gnu gcc-arm-linux-gnueabi gcc-mips-linux-gnu gcc-mipsel-linux-gnu \
libc6-dev-i386-cross libc6-dev-armel-cross libc6-dev-mips-cross libc6-dev-mipsel-cross
gcc-aarch64-linux-gnu gcc-i686-linux-gnu gcc-arm-linux-gnueabi gcc-mips-linux-gnu gcc-mipsel-linux-gnu \
libc6-dev-arm64-cross libc6-dev-i386-cross libc6-dev-armel-cross libc6-dev-mips-cross libc6-dev-mipsel-cross
- name: Acquire exact upstream libraries in an isolated runner environment
shell: bash
run: |
+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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Install sharing-image dependencies
run: |
sudo apt-get update
+50 -8
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@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.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
@@ -130,14 +145,24 @@ 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
def open_public(url, accept=None):
headers = {
"Cache-Control": "no-cache",
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
}
if accept:
headers["Accept"] = accept
return urlopen(Request(url, headers=headers), timeout=10)
expected = os.environ["GITHUB_SHA"]
pages_url = os.environ["PAGES_URL"]
if not pages_url:
@@ -153,11 +178,7 @@ jobs:
for name, base in list(pending.items()):
url = urljoin(base, "deployment.json") + "?commit=" + expected
try:
request = Request(url, headers={
"Cache-Control": "no-cache",
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
})
with urlopen(request, timeout=10) as response:
with open_public(url) as response:
actual = json.load(response).get("commit")
if actual == expected:
print(f"{name} serves {expected}: {base}")
@@ -172,6 +193,27 @@ jobs:
time.sleep(5)
if pending:
sys.exit("Could not verify published revision on: " + ", ".join(pending))
for name, base in hosts.items():
with open_public(urljoin(base, ".well-known/agent-skills/index.json")) 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 open_public(artifact_url) 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 open_public(urljoin(base, ".well-known/" + manifest)) as response:
if not json.load(response).get("entries"):
sys.exit(name + " serves an empty discovery manifest")
if name == "Cloudflare":
with open_public(base, accept="text/markdown") 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")
+4 -4
View File
@@ -16,10 +16,10 @@ PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2"
PACKAGES = {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"}
OUTPUT_BYTES = 64 * 1024 * 1024
# (e_machine, ELF class bits, byte order) -> architecture pwntools must also report.
PROFILES = {(62, 64, "little"): "amd64", (3, 32, "little"): "i386", (40, 32, "little"): "arm",
(8, 32, "little"): "mips", (8, 32, "big"): "mips"}
PROFILE_SCOPE = ("Layout profile requires ELF64 x86-64, ELF32 i386, ELF32 ARM little-endian "
"or ELF32 MIPS little- or big-endian objects.")
PROFILES = {(62, 64, "little"): "amd64", (183, 64, "little"): "aarch64", (3, 32, "little"): "i386",
(40, 32, "little"): "arm", (8, 32, "little"): "mips", (8, 32, "big"): "mips"}
PROFILE_SCOPE = ("Layout profile requires ELF64 x86-64, ELF64 AArch64 little-endian, ELF32 i386, "
"ELF32 ARM little-endian or ELF32 MIPS little- or big-endian objects.")
class LayoutFailure(Exception):
+5 -3
View File
@@ -9,9 +9,11 @@ ordinary binary inspection use the same modular capability.
## Bring your own engine
Real verification covers **Linux x64** hosts and **EXEC/DYN/REL** objects in
these profiles: ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32
little- or big-endian MIPS. Other machines, classes and byte orders, including
AArch64 and big-endian ARM, return `unsupported_target`. Provide an absolute
these profiles: ELF64 x86-64, ELF64 little-endian AArch64, ELF32 i386, ELF32
little-endian ARM and ELF32 little- or big-endian MIPS. Other machines, classes
and byte orders, including big-endian ARM, return `unsupported_target`. Other
hosts, including Linux arm64, return `capability_unavailable` naming the
detected host before any target is read. Provide an absolute
Python executable whose environment already contains unchanged pwntools 4.15.0,
pyelftools 0.33 and Unicorn 2.1.2:
+9
View File
@@ -28,6 +28,15 @@ The host must already provide util-linux `prlimit`, normally `/usr/bin/prlimit`;
no system tools and changes no user configuration. [Upstream provenance and
license](https://github.com/morluto/rea/blob/main/third_party/evmole/README.md) are packaged.
Limiter discovery validates the selected path without launching it. A relative
path or directory is reported as `not_configured`, a missing file as
`executable_missing`, and denied access or another filesystem-probe failure as
`runtime_unavailable`. Recovery names the selected path and
`REA_EVM_PRLIMIT_COMMAND`; denied access asks for execute permission and
filesystem access. The original admission or system code stays in diagnostics.
An available filesystem probe does not verify the selected tool's version or
util-linux compatibility, and REA does not substitute another command.
## Evidence semantics
- `encoding` is required: raw bytes or UTF-8 hex, with optional `0x` prefix and
+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
+4
View File
@@ -48,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.
+5
View File
@@ -388,6 +388,11 @@ sites remain distinct. A static route does not establish runtime reachability.
Swift/closure dispatch requires resolved static references; unavailable ABI
forms remain unknown.
Encoded Objective-C method records retain their own locations in dispatch
Evidence. A handler node uses the implementation address with an unknown file
offset unless that function already has an independently located graph node;
the method-record offset does not locate the handler's code.
Incomplete reference kinds do not discard known typed calls or partial
provider call references. A `provider_call` with `observed` resolution retains
the provider-reported call site and endpoint without claiming generic direct,
+7 -4
View File
@@ -421,7 +421,10 @@ the first cold pseudocode answer comes from a single-function run or an export
that already finished, assembly waits for and serves from that export, and
pseudocode and both dossier facets match their recorded file spans and digests,
a fresh server reuses the export without rebuilding it, staging is empty after
a completed export, and no owned process survives. Pass
a completed export, and no owned process survives. A separate forced 1 ms
project deadline checks that a timed-out inventory recommends raising the
project deadline without changing jobs; a fresh server retries with 60 seconds
and the same two-worker setting. Pass
`--targets DIR --functions a,b` to add caller-built binaries laid out as
`DIR/<flavor>/<name>` beside `<name>.stripped`; addresses come from the
symbol-bearing build. `--compare-ghidra` with `GHIDRA_INSTALL_DIR` runs those
@@ -1108,13 +1111,13 @@ coverage limits.
### Offline binary layout
`npm run verify:binary:layout` requires Linux x64, GCC/binutils, the
`i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and `mipsel-linux-gnu`
cross `gcc`/`readelf` toolchains, absolute
`aarch64-linux-gnu`, `i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and
`mipsel-linux-gnu` cross `gcc`/`readelf` toolchains, absolute
`REA_PWNTOOLS_PYTHON` with pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2 and
absolute `REA_VERIFY_STRACE_COMMAND`. It compiles ephemeral source-owned ELF
fixtures and checks public CLI/MCP, lossless addresses/names, file ranges,
mitigation inferences, malformed/unsupported input, original file hashes and
released process ownership. Cross-compiled ELF32 executables and relocatable
released process ownership. Cross-compiled ELF64 AArch64 and ELF32 executables and relocatable
objects check the reported class, machine and byte order, and every section
name, address, offset and size against GNU `readelf`. Exec syscall tracing must identify only the declared
Node/Python launchers; no target binary is executed. Core/debugger claims need
+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()
+21 -16
View File
@@ -38,15 +38,16 @@ if (
!isAbsolute(strace ?? "")
)
throw new Error(
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the AArch64/i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
);
const run = createVerifierRun();
const execute = promisify(execFile);
const ELF32_TOOLCHAINS = [
["i686-linux-gnu", "EM_386", "little"],
["arm-linux-gnueabi", "EM_ARM", "little"],
["mips-linux-gnu", "EM_MIPS", "big"],
["mipsel-linux-gnu", "EM_MIPS", "little"],
const CROSS_TOOLCHAINS = [
["aarch64-linux-gnu", "EM_AARCH64", 64, "little"],
["i686-linux-gnu", "EM_386", 32, "little"],
["arm-linux-gnueabi", "EM_ARM", 32, "little"],
["mips-linux-gnu", "EM_MIPS", 32, "big"],
["mipsel-linux-gnu", "EM_MIPS", 32, "little"],
];
for (const command of [
"gcc",
@@ -54,7 +55,7 @@ for (const command of [
"strip",
"/usr/bin/prlimit",
strace,
...ELF32_TOOLCHAINS.flatMap(([prefix]) => [
...CROSS_TOOLCHAINS.flatMap(([prefix]) => [
`${prefix}-gcc`,
`${prefix}-readelf`,
]),
@@ -347,14 +348,15 @@ try {
cases++;
}
}
// ELF32 profiles: cross-compiled, never executed, checked against GNU readelf.
// Cross profiles: cross-compiled, never executed, checked against GNU readelf.
for (const [
prefix,
machine,
bits,
byteOrder,
kind,
flags,
] of ELF32_TOOLCHAINS.flatMap((toolchain) => [
] of CROSS_TOOLCHAINS.flatMap((toolchain) => [
[...toolchain, "executable", ["-fPIE", "-pie"]],
[...toolchain, "relocatable", ["-c", "-fPIC"]],
])) {
@@ -423,16 +425,18 @@ try {
`readelf relocation count: ${name}`,
);
for (const [entryIndex, [, offset, info]] of rows.entries()) {
// ELF32 r_info uses the low byte for type and the upper 24 bits for
// the symbol index. readelf has already decoded the file's byte order.
// ELF32 r_info packs an 8-bit type under a 24-bit symbol index;
// ELF64 packs a 32-bit type under a 32-bit index. readelf has already
// decoded the file's byte order.
const decodedInfo = BigInt(`0x${info}`);
const typeBits = bits === 32 ? 8n : 32n;
expectedRelocations.push({
section_index: section.index,
entry_index: entryIndex,
reported_offset: BigInt(`0x${offset}`),
type: Number(decodedInfo & 0xffn),
type: Number(decodedInfo & ((1n << typeBits) - 1n)),
symbol_table_index: section.link,
symbol_index: Number(decodedInfo >> 8n),
symbol_index: Number(decodedInfo >> typeBits),
});
}
}
@@ -445,7 +449,7 @@ try {
const value = await inspect(mode, path);
assert.deepEqual(value.architecture, {
machine,
bits: 32,
bits,
byte_order: byteOrder,
});
assert.equal(
@@ -1312,7 +1316,8 @@ try {
}
}
}
unsupported.writeUInt16LE(183, 18);
// EM_RISCV: AArch64 (183) is now an admitted profile.
unsupported.writeUInt16LE(243, 18);
const core = Buffer.from(object);
core.writeUInt16LE(4, 16);
for (const [name, bytes, category] of [
@@ -1337,7 +1342,7 @@ try {
"invalid_input",
],
["undersized-program-header", undersizedProgramHeader, "invalid_input"],
["arm64", unsupported, "unsupported_target"],
["riscv64", unsupported, "unsupported_target"],
["core", core, "unsupported_target"],
]) {
const path = join(root.path, name);
+75 -3
View File
@@ -28,7 +28,10 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { inspectKunaInstallation } from "../dist/kuna/KunaInstallation.js";
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import {
requireMcpOperationResult,
requireMcpToolError,
} from "./lib/mcp-verifier-results.mjs";
import { verifyKunaReferences } from "./lib/kuna-reference-verifier.mjs";
import {
nativeKunaStrings,
@@ -172,7 +175,7 @@ const sampleTree = async (root) => {
};
/** One MCP server with a selected provider, sampled while it runs. */
const openServer = async (provider) => {
const openServer = async (provider, kunaSettings = {}) => {
const owner = Symbol(provider.id);
pendingServers.add(owner);
const transport = new StdioClientTransport({
@@ -185,6 +188,7 @@ const openServer = async (provider) => {
REA_PROCESS_RUN_ID: run.run_id,
REA_KUNA_COMMAND: kuna,
REA_KUNA_CACHE_DIR: cacheRoot,
...kunaSettings,
...(process.env.GHIDRA_INSTALL_DIR === undefined
? {}
: { GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR }),
@@ -229,6 +233,13 @@ const openServer = async (provider) => {
structured: result.structuredContent,
};
};
const callError = async (name, arguments_ = {}) => {
const result = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 3_600_000 },
);
return requireMcpToolError(result);
};
const close = async (priorFailure) => {
clearInterval(timer);
await sample();
@@ -270,7 +281,59 @@ const openServer = async (provider) => {
// A rejected connection/target still owns its transport and workspace.
await close(cause);
}
return { call, close };
return { call, callError, close };
};
/** Verify a forced inventory timeout can be retried without changing jobs. */
const verifyProjectTimeoutRecovery = async (path, address) => {
const before = createHash("sha256")
.update(await readFile(path))
.digest("hex");
const settings = {
REA_KUNA_JOBS: "2",
REA_KUNA_TIMEOUT_MS: "60000",
REA_KUNA_PROJECT_TIMEOUT_MS: "1",
};
const timed = await openServer({ path, id: "kuna" }, settings);
let timedFailure;
try {
const failure = await timed.callError("procedure_assembly", {
procedure: address,
});
assert.equal(failure.code, "capability_unavailable");
assert.match(failure.message, /kuna functions/u);
assert.match(failure.remediation.action, /REA_KUNA_PROJECT_TIMEOUT_MS/u);
assert.doesNotMatch(failure.remediation.action, /REA_KUNA_JOBS/u);
} catch (cause) {
timedFailure = cause;
} finally {
await timed.close(timedFailure);
}
const retry = await openServer(
{ path, id: "kuna" },
{ ...settings, REA_KUNA_PROJECT_TIMEOUT_MS: "60000" },
);
let retryFailure;
try {
const assembly = await retry.call("procedure_assembly", {
procedure: address,
});
assert.equal(typeof assembly.value, "string");
assert.ok(assembly.value.length > 0);
assert.equal(assembly.structured.analysis_profile.parameters.jobs, 2);
} catch (cause) {
retryFailure = cause;
} finally {
await retry.close(retryFailure);
}
assert.equal(
createHash("sha256")
.update(await readFile(path))
.digest("hex"),
before,
"Kuna timeout recovery changed the target",
);
return { forced_deadline_ms: 1, retry_deadline_ms: 60000, jobs: 2 };
};
/** Count structuring and diagnostic markers in provider pseudocode. */
@@ -535,6 +598,7 @@ const verifyVariant = async ({ flavor, directory, symbols }, variant) => {
};
const results = [];
let timeoutRecovery;
const failures = [];
let inspectionResourcesClean = true;
try {
@@ -596,6 +660,11 @@ try {
}),
});
timeoutRecovery = await verifyProjectTimeoutRecovery(
built,
fixtureFunctions.get("rea_checksum"),
);
// Caller-built matrix.
for (const pair of callerPairs)
for (const variant of [pair.file, `${pair.file}.stripped`])
@@ -650,6 +719,9 @@ const report = {
platform: `${process.platform}/${process.arch}`,
verifier_run: lifecycle,
results,
...(timeoutRecovery === undefined
? {}
: { timeout_recovery: timeoutRecovery }),
limitations: [
"Targets are analyzed statically; they are never executed.",
"Memory is the sampled peak resident set of the MCP server process tree; CPU is its sampled cumulative time. A short-lived peak between 200 ms samples is not observed.",
+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")
@@ -58,6 +58,7 @@ it("records the decoded layout once in its Evidence", async () => {
it.each([
[{ machine: "EM_X86_64", bits: 64, byte_order: "little" }, "x86_64"],
[{ machine: "EM_AARCH64", bits: 64, byte_order: "little" }, "arm64"],
[{ machine: "EM_386", bits: 32, byte_order: "little" }, "x86"],
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, "arm"],
[{ machine: "EM_MIPS", bits: 32, byte_order: "big" }, "mips"],
@@ -14,6 +14,7 @@ import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_binary_layout";
const EVIDENCE_ARCHITECTURES = {
EM_X86_64: "x86_64",
EM_AARCH64: "arm64",
EM_386: "x86",
EM_ARM: "arm",
EM_MIPS: "mips",
+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>>;
@@ -245,7 +245,7 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
...toolContractMetadata("inspect_binary_layout"),
kind: "native-provider",
description:
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Profile: EXEC/DYN/REL ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Profile: EXEC/DYN/REL ELF64 x86-64, ELF64 little-endian AArch64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
inputSchema: inspectBinaryLayoutRequestSchema,
outputSchema: analysisDetailOutputSchemaOf(
binaryLayoutSchema,
+1 -1
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" },
),
@@ -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: {
@@ -42,9 +42,11 @@ export const javaScriptSemanticQuerySeedSchema = z.discriminatedUnion("kind", [
kind: z.literal("property"),
name: z.string(),
}),
// Analysis preserves the complete literal endpoint, including the empty
// string, so the seed matches any retained value exactly, as event does.
z.strictObject({
kind: z.literal("endpoint"),
value: z.string().min(1),
value: z.string(),
}),
z.strictObject({
kind: z.literal("event"),
+4 -1
View File
@@ -122,6 +122,9 @@ it("retains high linked values and NOBITS without inventing file backing", () =>
});
it.each([
[{ machine: "EM_AARCH64", bits: 64, byte_order: "little" }, true],
[{ machine: "EM_AARCH64", bits: 64, byte_order: "big" }, false],
[{ machine: "EM_AARCH64", bits: 32, byte_order: "little" }, false],
[{ machine: "EM_386", bits: 32, byte_order: "little" }, true],
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, true],
[{ machine: "EM_MIPS", bits: 32, byte_order: "little" }, true],
@@ -150,7 +153,7 @@ it.each([
if (problem === "file-backed-outside") section.file_backing = "file";
if (problem === "header-outside") section.header_location.offset = "0x80";
if (problem === "wrong-index") section.index = 1;
if (problem === "wrong-machine") value.architecture.machine = "EM_AARCH64";
if (problem === "wrong-machine") value.architecture.machine = "EM_RISCV";
if (problem === "outside-elf64")
value.entry_point.reported_value = "0x10000000000000000";
if (problem === "noncanonical-base64") section.name.bytes_base64 = "AR==";
+1 -1
View File
@@ -35,7 +35,7 @@ const binaryLayoutObjectSchema = z.strictObject({
format: z.literal("elf"),
architecture: z.union([
z.strictObject({
machine: z.literal("EM_X86_64"),
machine: z.enum(["EM_X86_64", "EM_AARCH64"]),
bits: z.literal(64),
byte_order: z.literal("little"),
}),
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { objcSwiftMetadataSchema } from "./objcSwiftMetadata.js";
import {
inspectNativeDispatchMetadata,
objcSwiftMetadataSchema,
} from "./objcSwiftMetadata.js";
import {
nativeInvestigationGraphSchema,
@@ -191,9 +194,18 @@ describe("native investigation graph traces", () => {
});
describe("Interface Builder dispatch joins", () => {
it.each(["BuildViewController", "BuildView", "BuildButton", "BuildHandler"])(
"joins an action to its declared %s receiver class",
(className) => {
it.each([
{
className: "BuildViewController",
recordOffset: 1944,
existingOffset: null,
},
{ className: "BuildView", recordOffset: 6040, existingOffset: null },
{ className: "BuildButton", recordOffset: null, existingOffset: null },
{ className: "BuildHandler", recordOffset: 1944, existingOffset: 1916 },
])(
"joins the declared $className without mixing record and implementation offsets",
({ className, recordOffset, existingOffset }) => {
const ui = buildInterfaceBuilderAnalysis({
targetSha256: "b".repeat(64),
toolVersion: "test",
@@ -221,38 +233,80 @@ describe("Interface Builder dispatch joins", () => {
],
limits: interfaceBuilderLimitsSchema.parse({}),
});
const joined = joinInterfaceBuilderDispatch(ui.graph, {
objc_classes: [],
objc_protocols: [],
objc_categories: [],
swift_decls: [],
objc_ivars: [],
objc_protocol_records: [],
const recordLocation = {
address: recordOffset === null ? "0x1000" : "0x2000",
file_offset: recordOffset,
};
const recordEvidence = [
{
kind: recordOffset === null ? "symbol" : "binary_metadata",
description: "Explicit synthetic method-record location",
location: recordLocation,
artifact_path: "/synthetic/Fixture",
artifact_sha256: "b".repeat(64),
},
];
const mappedEvidence = [
{
...recordEvidence[0],
kind: "reference",
description: "Explicit independently mapped handler",
location: { address: "0x1000", file_offset: existingOffset },
},
];
const inputGraph =
existingOffset === null
? ui.graph
: nativeInvestigationGraphSchema.parse({
...ui.graph,
nodes: [
...ui.graph.nodes,
{
id: "native:function:0x1000",
kind: "function",
name: "Already mapped handler",
location: { address: "0x1000", file_offset: existingOffset },
attributes: {},
evidence: mappedEvidence,
},
],
});
const metadata = objcSwiftMetadataSchema.parse({
...inspectNativeDispatchMetadata([], 1),
objc_dispatch_implementations: [
{
class_name: className,
selector: "buildTapped:",
method_type: "instance",
implementation_address: "0x1000",
location: { address: "0x1000", file_offset: 0 },
location: recordLocation,
decode: { status: "decoded", reason: null },
evidence,
evidence: recordEvidence,
},
],
swift_conformances: [],
swift_dispatch_slots: [],
swift_symbols: [],
relative_pointers: [],
coverage: [],
db_save_result: null,
});
const joined = joinInterfaceBuilderDispatch(inputGraph, metadata);
expect(joined.edges).toContainEqual(
expect.objectContaining({
relation: "objc_dispatch",
resolution: "resolved",
to: "native:function:0x1000",
evidence: expect.arrayContaining(recordEvidence),
}),
);
const handler = joined.nodes.find(
({ id }) => id === "native:function:0x1000",
);
expect(handler?.location).toEqual({
address: "0x1000",
file_offset: existingOffset,
});
expect(handler?.evidence).toEqual(
existingOffset === null ? recordEvidence : mappedEvidence,
);
expect(metadata.objc_dispatch_implementations[0]?.location).toEqual(
recordLocation,
);
},
);
});
@@ -391,7 +391,8 @@ export const joinInterfaceBuilderDispatch = (
name: `${implementation.class_name} ${selector}`,
location: {
address: implementation.implementation_address,
file_offset: implementation.location.file_offset,
// The implementation location identifies the method record, not its code.
file_offset: null,
},
attributes: {
class_name: implementation.class_name,
+29 -4
View File
@@ -1,4 +1,5 @@
import { EvmAdmissionError } from "./EvmAdmissionError.js";
import type { ProviderRejectionCode } from "../domain/providerSelection.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { safeParseUtf8Json } from "../domain/safeJson.js";
@@ -115,6 +116,7 @@ type EvmLimiterSelection = {
readonly diagnostics: Readonly<Record<string, JsonValue>>;
};
type EvmLimiterUnavailable = EvmLimiterSelection & {
readonly code: ProviderRejectionCode;
readonly reason: string;
readonly cause: unknown;
};
@@ -159,16 +161,39 @@ const selectEvmLimiter = async (
if (cause instanceof EvmAdmissionError)
return err({
path,
code: "not_configured",
diagnostics: {
...diagnostics,
code: cause.code,
...cause.detail,
},
reason: cause.code,
reason: `Set REA_EVM_PRLIMIT_COMMAND to an absolute executable regular file for caller-supplied util-linux prlimit. The selected path ${path} ${cause.code === "prlimit_not_absolute" ? "is not absolute" : "is not a regular file"}.`,
cause,
});
const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${path}; ${cause instanceof Error ? cause.message : String(cause)}`;
return err({ path, diagnostics, reason, cause });
const systemCode =
cause instanceof Error &&
"code" in cause &&
typeof cause.code === "string"
? cause.code
: undefined;
const permissionDenied = systemCode === "EACCES" || systemCode === "EPERM";
const missing = systemCode === "ENOENT" || systemCode === "ENOTDIR";
const recovery = permissionDenied
? "Grant execute permission and filesystem access to the selected file, or set REA_EVM_PRLIMIT_COMMAND to an accessible absolute executable regular file for util-linux prlimit."
: missing
? "Set REA_EVM_PRLIMIT_COMMAND to an existing absolute executable regular file for util-linux prlimit."
: "Check the selected file and filesystem access, or set REA_EVM_PRLIMIT_COMMAND to an accessible absolute executable regular file for util-linux prlimit.";
const reason = `Offline EVM interface inspection requires caller-supplied util-linux prlimit: ${path}; ${cause instanceof Error ? cause.message : String(cause)}. ${recovery}`;
return err({
path,
code: missing ? "executable_missing" : "runtime_unavailable",
diagnostics: {
...diagnostics,
...(systemCode === undefined ? {} : { system_code: systemCode }),
},
reason,
cause,
});
}
};
@@ -193,7 +218,7 @@ export const evmLimiterAvailability = async (
}
: {
status: "unavailable",
code: "executable_missing",
code: selected.error.code,
reason: selected.error.reason,
// The limiter owner does not yet author a separate repair (#8508).
remediation: selected.error.reason,
+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.",
]
: [],
});
}
+19 -2
View File
@@ -258,7 +258,7 @@ const exportUnavailable = (
"REA reads Kuna settings when it starts, so restart REA, then reopen the target.";
const next =
failure instanceof AnalysisTimeoutError
? `To retry, set REA_KUNA_PROJECT_TIMEOUT_MS above ${String(failure.timeoutMs)} ms or raise REA_KUNA_JOBS in the environment that starts REA. ${restart}`
? `To retry, set REA_KUNA_PROJECT_TIMEOUT_MS above ${String(failure.timeoutMs)} ms in the environment that starts REA. ${restart}`
: failure instanceof AnalysisResourceConstraintError &&
failure.remediationAction !== undefined
? // A fixed buffer (`memory`) is no setting, so a restart cannot help.
@@ -1057,6 +1057,9 @@ export class KunaSessionClient implements AnalysisClient {
err(
new AnalysisInputError(operation, {
userMessage: kunaDataEntryMessage(entry, index.data.get(entry)!),
conformsToInputContract: true,
remediationAction:
"Inspect the data address with address_name or xrefs, or select a procedure entry from list_procedures. Do not repeat the unchanged call.",
}),
);
const selected = (entry: KunaInventoryFunction) => {
@@ -1076,6 +1079,9 @@ export class KunaSessionClient implements AnalysisClient {
return err(
new AnalysisInputError(operation, {
userMessage: `${address} is not a procedure entry Kuna found; list_procedures shows the entries it found.`,
conformsToInputContract: true,
remediationAction:
"Inspect list_procedures and select a discovered entry if it answers your question. An unlisted address is not proof of a function; REA cannot use it as a Kuna procedure in this session. Do not repeat the unchanged call.",
}),
);
return refused(entry) ? dataRefusal(entry) : selected(entry);
@@ -1088,9 +1094,20 @@ export class KunaSessionClient implements AnalysisClient {
return dataRefusal(named[0]);
return err(
new AnalysisInputError(operation, {
conformsToInputContract: true,
remediationAction:
matches.length === 0 && named.length === 0
? field === "procedure"
? "Use search_procedures or list_procedures to inspect Kuna's discovered entries, then select a matching entry. Do not repeat the unchanged call."
: "Inspect the selected target for the intended address and use that address explicitly. Do not repeat the unchanged call."
: matches.length === 0
? "Inspect the data addresses with address_name or xrefs, or select a procedure entry from list_procedures. Do not repeat the unchanged call."
: "Select the intended entry by its exact address. Do not repeat the unchanged call.",
userMessage:
matches.length === 0 && named.length === 0
? `Kuna found no procedure named ${selector}; search_procedures finds similar names.`
? field === "procedure"
? `Kuna found no procedure named ${selector}; search_procedures finds similar names.`
: `Kuna found no inventory entry named ${selector}.`
: matches.length === 0
? `${selector} names ${String(named.length)} Kuna entries that lie in Mach-O data regions (${named.map(({ address }) => address).join(", ")}); they are data, not procedures.`
: `${selector} names ${String(matches.length)} Kuna entries (${matches.map(({ address }) => address).join(", ")}); select one by address.`,
+14 -5
View File
@@ -69,8 +69,12 @@ import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js";
import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js";
import { pwntoolsInputBytes } from "./PwntoolsInputLimit.js";
const HOST_REQUIREMENT =
"The initial real-verified pwntools artifact profile supports Linux x64 only.";
/** Names the detected host so a host refusal is not read as a target refusal. */
export const pwntoolsHostRequirement = (
platform: NodeJS.Platform,
arch: string,
): string =>
`The real-verified pwntools artifact profile runs only on a Linux x64 host; this host is ${platform}/${arch}. The selected target was not read.`;
/** The pwntools profile runs only on Linux x64; discovery and execution share this check. */
export const pwntoolsHostSupported = (
@@ -290,13 +294,18 @@ export class PwntoolsDecoder<
throw new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
});
if (!pwntoolsHostSupported(process.platform, process.arch))
if (!pwntoolsHostSupported(process.platform, process.arch)) {
const requirement = pwntoolsHostRequirement(
process.platform,
process.arch,
);
throw new AnalysisCapabilityUnavailableError(
this.identity.id,
OPERATION,
HOST_REQUIREMENT,
{ userMessage: HOST_REQUIREMENT },
requirement,
{ userMessage: requirement },
);
}
if (!isAbsolute(selectedPath))
throw pwntoolsUnavailable(
"Set REA_PWNTOOLS_PYTHON to an absolute caller-supplied Python executable with pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2; REA never installs Python or packages.",
@@ -2,6 +2,10 @@ import { expect, it } from "vitest";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { OwnedCommandFailure } from "../../process/OwnedCommand.js";
import {
pwntoolsHostRequirement,
pwntoolsHostSupported,
} from "./PwntoolsDecoder.js";
import { pwntoolsDecoderFailure } from "./PwntoolsFailures.js";
it.each([undefined, null, false, 0, "stop", { stop: true }, new Error("stop")])(
@@ -149,3 +153,11 @@ it("retains legacy owner causes without guessing cancellation from error text",
"ProviderAdapterError",
);
});
it("names the refused host instead of implying the target was refused", () => {
expect(pwntoolsHostSupported("linux", "arm64")).toBe(false);
const message = pwntoolsHostRequirement("linux", "arm64");
expect(message).toContain("Linux x64 host");
expect(message).toContain("this host is linux/arm64");
expect(message).toContain("The selected target was not read.");
});
+25 -4
View File
@@ -12,6 +12,7 @@ import {
type AnalysisErrorProjection,
} from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { NoBinaryOpenError } from "../domain/configurationErrors.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
import { estimateJsonStorage } from "../application/JsonAuthenticationHeadroom.js";
@@ -108,12 +109,22 @@ export class ToolResultDelivery {
selected?: JsonValue,
viewSource?: JsonValue,
) => Readonly<Record<string, JsonValue>> = () => ({}),
private readonly noTargetRecovery?: string,
) {
this.maximumResponseBytes = maximumResponseBytes;
this.resultBudgetBytes = maximumResponseBytes - 1024;
Object.freeze(this);
}
/** Bind scope-specific recovery without mutating a shared delivery policy. */
withNoTargetRecovery(recovery: string): ToolResultDelivery {
return new ToolResultDelivery(
this.maximumResponseBytes,
this.nativeContinuation,
recovery,
);
}
/** Serialize a plain application result or its actionable error. */
toCallToolResult(
result: Result<JsonValue, AnalysisError>,
@@ -193,10 +204,20 @@ export class ToolResultDelivery {
error: AnalysisError,
targetContent: boolean,
): CallToolResult {
return this.projectedErrorResult(
projectAnalysisError(error),
targetContent,
);
const projected = projectAnalysisError(error);
if (
error instanceof NoBinaryOpenError &&
this.noTargetRecovery !== undefined
)
return this.projectedErrorResult(
{
...projected,
message: this.noTargetRecovery,
remediation: { action: this.noTargetRecovery },
},
targetContent,
);
return this.projectedErrorResult(projected, targetContent);
}
/** Retain valid partial Evidence and reference it only after recording succeeds. */
+8 -1
View File
@@ -3,6 +3,7 @@ import {
type CapabilityServiceOptions,
} from "../composition/capabilityServices.js";
import { registerToolFamilies } from "../contracts/toolFamilyIds.js";
import { SCOPED_NO_TARGET_RECOVERY } from "./recoveryInstructions.js";
import { AnalysisSettings } from "../application/AnalysisSettings.js";
import { parseMcpInputBudget } from "../config/mcpInputBudget.js";
import { registerAnalysisSettingsTools } from "./registerAnalysisSettingsTools.js";
@@ -174,7 +175,13 @@ export const createServer = (
options.providerEnvironment,
environment,
);
const delivery = selectToolResultDelivery(environment, options.delivery);
const baseDelivery = selectToolResultDelivery(environment, options.delivery);
const delivery =
session !== undefined &&
advertisedToolFamilies !== undefined &&
!advertisedToolFamilies.has("session")
? baseDelivery.withNoTargetRecovery(SCOPED_NO_TARGET_RECOVERY)
: baseDelivery;
const capabilities = createCapabilityServices({
environment,
providerEnvironment,
+4
View File
@@ -16,3 +16,7 @@ export const recoveryInstructions = (
runtimeSettingsAvailable: boolean,
): string =>
`${RECOVERY_START}${runtimeSettingsAvailable ? RUNTIME_SETTINGS_RECOVERY : LAUNCH_SETTINGS_RECOVERY}${EXPLICIT_ARGUMENT_RECOVERY}${RECOVERY_END}`;
/** Recovery when a scoped session cannot advertise the target lifecycle. */
export const SCOPED_NO_TARGET_RECOVERY =
"No target is open, and this MCP connection does not advertise open_binary. Preserve needed outputs before restarting; retained in-memory Evidence and Unknown history will not carry over. Add session to REA_MCP_TOOL_FAMILIES in the environment that starts REA and restart the server; then call open_binary with the chosen local path.";
@@ -162,3 +162,64 @@ cliTest(
},
120_000,
);
cliTest(
"traces a retained empty literal endpoint by its preserved value",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-empty-endpoint-seed-");
await writeFile(
join(root, "main.js"),
'export const load = async () => (await fetch("")).json();\n',
);
const analyzed = await cli.run({
arguments: [
"analyze-javascript-application",
root,
"--artifact-format",
"directory",
"--json",
],
timeoutMs: 60_000,
});
expect(analyzed.exitCode, analyzed.stderr).toBe(0);
const application = toolContract(
"analyze_javascript_application",
).outputSchema.parse(analyzed.json);
const graph = javascriptApplicationAnalysisResultSchema.parse(
application.normalized_result,
).semantic_graph;
// Analysis keeps the complete literal endpoint, here the empty string.
const retained = new Set(
graph.nodes
.filter(
(node) =>
(node.kind === "request" || node.kind === "response") &&
node.properties.endpoint === "",
)
.map((node) => node.node_id),
);
expect(retained.size).toBeGreaterThan(0);
const request = join(root, "endpoint-query.json");
await writeFile(
request,
JSON.stringify({
application,
query: {
seed: { kind: "endpoint", value: "" },
direction: "forward-influence",
},
}),
);
const traced = await cli.run({
arguments: ["trace-javascript-semantics", request, "--json"],
timeoutMs: 60_000,
});
expect(traced.exitCode, traced.stdout).toBe(0);
const trace = javaScriptSemanticTraceResultSchema.parse(
toolContract("trace_javascript_semantics").outputSchema.parse(traced.json)
.normalized_result,
);
expect(new Set(trace.seed_node_ids)).toEqual(retained);
},
120_000,
);
@@ -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);
},
);
@@ -677,6 +677,11 @@ it.runIf(unsupportedHost)(
const projected = projectAnalysisError(result.error);
expect(projected.message).toContain("Linux x64");
expect(projected.remediation.action).toContain("Linux x64");
// The refusal names this host, not the target, which was never read.
expect(projected.message).toContain(
`this host is ${process.platform}/${process.arch}`,
);
expect(projected.message).toContain("The selected target was not read.");
},
);
@@ -1,4 +1,7 @@
import { expect, it } from "vitest";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs";
import { buildCapabilityInventory } from "../../../src/application/CapabilityInventory.js";
@@ -41,33 +44,43 @@ it("probes the default limiter with executable and regular-file checks", async (
]);
});
it("uses the exact absolute override and reports a failed executable check", async () => {
const limiter = "/fixture/util-linux/prlimit";
const availability = await evmLimiterAvailability(
{ REA_EVM_PRLIMIT_COMMAND: limiter },
{
...linux,
fs: {
access: async (path) => {
expect(path).toBe(limiter);
throw Object.assign(new Error("permission denied"), {
code: "EACCES",
});
},
stat: async () => {
throw new Error("stat must not run after access failure");
it.each(["EACCES", "EPERM"])(
"uses the exact absolute override and reports denied access: %s",
async (code) => {
const limiter = "/fixture/util-linux/prlimit";
const availability = await evmLimiterAvailability(
{ REA_EVM_PRLIMIT_COMMAND: limiter },
{
...linux,
fs: {
access: async (path) => {
expect(path).toBe(limiter);
throw Object.assign(new Error("permission denied"), {
code,
});
},
stat: async () => {
throw new Error("stat must not run after access failure");
},
},
},
},
);
);
expect(availability).toMatchObject({
status: "unavailable",
code: "executable_missing",
reason: expect.stringContaining(limiter),
diagnostics: { limiter_path: limiter },
});
});
expect(availability).toMatchObject({
status: "unavailable",
code: "runtime_unavailable",
reason: expect.stringContaining(limiter),
diagnostics: { limiter_path: limiter, system_code: code },
});
if (availability === undefined) throw new Error("Expected limiter probe");
const entry = buildCapabilityInventory(status, {
...basePolicy,
evmLimiterAvailability: availability,
}).find(({ name }) => name === "inspect_evm_interface");
expect(entry?.remediation).toContain("REA_EVM_PRLIMIT_COMMAND");
expect(entry?.remediation).toContain("execute permission");
},
);
it("rejects a relative override and non-file without probing an alternate command", async () => {
const relative = await evmLimiterAvailability(
@@ -84,7 +97,8 @@ it("rejects a relative override and non-file without probing an alternate comman
);
expect(relative).toMatchObject({
status: "unavailable",
reason: "prlimit_not_absolute",
code: "not_configured",
reason: expect.stringContaining("absolute executable regular file"),
diagnostics: {
limiter_path: "prlimit",
code: "prlimit_not_absolute",
@@ -104,11 +118,53 @@ it("rejects a relative override and non-file without probing an alternate comman
);
expect(directory).toMatchObject({
status: "unavailable",
reason: "prlimit_not_file",
code: "not_configured",
reason: expect.stringContaining("absolute executable regular file"),
diagnostics: expect.objectContaining({ code: "prlimit_not_file" }),
});
});
it("delivers selected-path recovery from real filesystem probes to capability output", async () => {
const root = await createTestTempDirectory("rea-evm-limiter-recovery-");
const executable = join(root, "caller-selected-limiter");
await writeFile(executable, "not executed by the readiness probe", {
mode: 0o700,
});
for (const [path, expectedCode, expectedReason] of [
["prlimit", "not_configured", "absolute executable regular file"],
[root, "not_configured", "absolute executable regular file"],
[
join(root, "missing-limiter"),
"executable_missing",
"REA_EVM_PRLIMIT_COMMAND",
],
[executable, null, null],
] as const) {
const availability = await evmLimiterAvailability(
{ REA_EVM_PRLIMIT_COMMAND: path },
linux,
);
expect(availability?.code).toBe(expectedCode);
expect(availability?.diagnostics.limiter_path).toBe(path);
if (availability === undefined) throw new Error("Expected limiter probe");
const entry = buildCapabilityInventory(status, {
...basePolicy,
evmLimiterAvailability: availability,
}).find(({ name }) => name === "inspect_evm_interface");
if (expectedReason === null) {
expect(entry).toMatchObject({ available: true, remediation: null });
} else {
expect(entry).toMatchObject({
available: false,
reason: "provider_missing",
});
expect(entry?.remediation).toContain(expectedReason);
expect(entry?.remediation).toContain(path);
expect(entry?.remediation).toContain("util-linux prlimit");
}
}
});
it("rechecks on each observation so inventory follows the selected executable's current state", async () => {
let accessible = true;
const fs = {
@@ -970,7 +970,7 @@ it.runIf(!unsupportedHost).each(["directory", "missing-interpreter"])(
});
if (scenario === "directory")
expect(result.error).toMatchObject({
reason: "prlimit_not_file",
reason: expect.stringContaining("absolute executable regular file"),
diagnostics: {
limiter_path: limiter,
code: "prlimit_not_file",
@@ -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",
@@ -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,
});
},
);
@@ -14,6 +14,11 @@ import {
} from "../../../src/domain/analysisErrorCore.js";
import { inspectNativeDispatchMetadata } from "../../../src/domain/native/objcSwiftMetadata.js";
import { nativeInvestigationTraceSchema } from "../../../src/domain/native/nativeInvestigationGraph.js";
import {
buildInterfaceBuilderAnalysis,
interfaceBuilderLimitsSchema,
} from "../../../src/domain/apple/interfaceBuilderGraph.js";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import { err, ok, type Result } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
@@ -111,6 +116,12 @@ const noClassifiedReferences = {
};
type Plan = {
readonly ui?: ReturnType<typeof buildInterfaceBuilderAnalysis>;
readonly implementations?: ReturnType<
typeof inspectNativeDispatchMetadata
>["objc_dispatch_implementations"];
readonly action?: string;
readonly maxDepth?: number;
readonly references?: unknown;
readonly referencesFailure?: AnalysisError;
readonly uiFailure?: AnalysisError;
@@ -142,6 +153,12 @@ function publicTrace(plan: Plan, maxNodes = 12, maxEdges = 500) {
plan.referencesFailure !== undefined
)
return err(plan.referencesFailure);
if (operation === "decode_interface_builder" && plan.ui !== undefined)
return ok(
createAnalysisExecution(jsonValueSchema.parse(plan.ui), identity, {
subject,
}),
);
if (operation === "decode_interface_builder")
return err(
new AnalysisCapabilityUnavailableError(
@@ -157,7 +174,12 @@ function publicTrace(plan: Plan, maxNodes = 12, maxEdges = 500) {
target_sha256: sha256,
provider: identity,
analysis_profile_digest: null,
result: inspectNativeDispatchMetadata([], 1),
result: {
...inspectNativeDispatchMetadata([], 1),
...(plan.implementations === undefined
? {}
: { objc_dispatch_implementations: plan.implementations }),
},
},
identity,
{ subject },
@@ -220,10 +242,10 @@ function publicTrace(plan: Plan, maxNodes = 12, maxEdges = 500) {
const result = await client.callTool({
name: "trace_native_ui_action",
arguments: {
action: "0x1000",
action: plan.action ?? "0x1000",
max_nodes: maxNodes,
max_edges: maxEdges,
max_depth: 1,
max_depth: plan.maxDepth ?? 1,
},
});
return result;
@@ -237,6 +259,93 @@ const traceFrom = (result: { structuredContent?: unknown }) =>
})
.parse(result.structuredContent).normalized_result;
it.each([
{ label: "thin record", recordOffset: 1944 },
{ label: "selected-slice container record", recordOffset: 6040 },
{ label: "symbol-only record", recordOffset: null },
])(
"keeps handler offsets distinct from $label Evidence through public MCP",
async ({ recordOffset }) => {
// Explicit synthetic decoded facts; no AppKit, Ghidra or Kuna runtime claim.
const ui = buildInterfaceBuilderAnalysis({
targetSha256: sha256,
toolVersion: "synthetic",
documents: [
{
relativePath: "Main.storyboardc/scene.nib/objects.nib",
archiveSha256: "b".repeat(64),
documentKind: "storyboard_scene",
raw: {
"com.apple.ibtool.document.objects": {
receiver: { customClass: "AuditController" },
button: { class: "UIButton" },
},
"com.apple.ibtool.document.connections": {
button: [
{
type: "action",
label: "runAction:",
destinationId: "receiver",
},
],
},
},
},
],
limits: interfaceBuilderLimitsSchema.parse({}),
});
const recordLocation = {
address: recordOffset === null ? "0x10000077c" : "0x100000798",
file_offset: recordOffset,
};
const recordEvidence = [
{
kind:
recordOffset === null
? ("symbol" as const)
: ("binary_metadata" as const),
description: "Explicit synthetic method entry",
location: recordLocation,
artifact_path: subject.path,
artifact_sha256: sha256,
},
];
const response = await publicTrace({
ui,
action: "runAction:",
maxDepth: 4,
references: completeReferences([]),
callees: [],
implementations: [
{
class_name: "AuditController",
selector: "runAction:",
method_type: "instance",
implementation_address: "0x10000077c",
location: recordLocation,
decode: { status: "decoded", reason: null },
evidence: recordEvidence,
},
],
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const trace = traceFrom(response);
const handler = trace.nodes.find(
({ id }) => id === "native:function:0x10000077c",
);
expect(handler?.location).toEqual({
address: "0x10000077c",
file_offset: null,
});
expect(handler?.evidence).toEqual(recordEvidence);
expect(
trace.edges.find(({ relation }) => relation === "objc_dispatch")
?.evidence,
).toEqual(expect.arrayContaining(recordEvidence));
expect(recordEvidence[0]?.location).toEqual(recordLocation);
},
);
it("keeps typed callsite evidence and distinct fallback-provider routes for mixed reference kinds", async () => {
const response = await publicTrace({});
expect(response.isError).not.toBe(true);
@@ -1,4 +1,8 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { serverInstructions } from "../../../src/server/serverInstructions.js";
import { SCOPED_NO_TARGET_RECOVERY } from "../../../src/server/recoveryInstructions.js";
import { NoBinaryOpenError } from "../../../src/domain/configurationErrors.js";
import { UNTRUSTED_CONTENT_INSTRUCTIONS } from "../../../src/server/toolResultEncoding.js";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type {
@@ -8,6 +12,8 @@ import type {
import { afterEach, describe, expect, it } from "vitest";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { peExportImage } from "../../fixtures/binaryDiagnostics/nativeExports.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { err, ok } from "../../../src/domain/result.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
@@ -272,3 +278,83 @@ describe.each([
},
);
});
it.each([
{ scope: "direct", sessionAvailable: false },
{ scope: "session,direct", sessionAvailable: true },
{ scope: undefined, sessionAvailable: true },
])(
"guides a missing target using the advertised scope ($scope)",
async ({ scope, sessionAvailable }) => {
const environment = { ...process.env };
if (scope === undefined) delete environment.REA_MCP_TOOL_FAMILIES;
else environment.REA_MCP_TOOL_FAMILIES = scope;
const client = await connect(environment);
const names = (await client.listTools()).tools.map(({ name }) => name);
expect(names.includes("open_binary")).toBe(sessionAvailable);
const { error } = parseMcpToolError(
await client.callTool({
name: "read_bytes",
arguments: { address: "0x1000", length: 4 },
}),
);
expect(error.code).toBe("target_unavailable");
if (sessionAvailable) {
expect(error.remediation.action).toBe(new NoBinaryOpenError().message);
} else {
expect(error.message).toBe(SCOPED_NO_TARGET_RECOVERY);
expect(error.remediation.action).toBe(SCOPED_NO_TARGET_RECOVERY);
expect(error.remediation.action).toContain("REA_MCP_TOOL_FAMILIES");
expect(error.remediation.action).toContain("open_binary");
await expect(
client.callTool({ name: "open_binary", arguments: { path: "/tmp/x" } }),
).rejects.toThrow(/disabled/u);
}
},
);
it("warns that retained evidence does not survive the scoped restart", async () => {
const directory = await createTestTempDirectory("rea-scoped-retained-");
const path = join(directory, "fixture.exe");
await writeFile(path, peExportImage({}));
const before = await connect({
...process.env,
REA_MCP_TOOL_FAMILIES: "direct,native,application",
});
const observed = await before.callTool({
name: "inspect_native_exports",
arguments: { path, detail: "summary" },
});
expect(observed.isError).not.toBe(true);
const summary = observed.structuredContent as {
normalized_result: { parent_evidence_id: string };
};
const view = {
source: {
kind: "retained-evidence",
evidence_id: summary.normalized_result.parent_evidence_id,
},
view: { kind: "native-exports", collection: "exports" },
};
expect(
(await before.callTool({ name: "inspect_analysis_view", arguments: view }))
.isError,
).not.toBe(true);
const failure = parseMcpToolError(
await before.callTool({
name: "read_bytes",
arguments: { address: "0x1000", length: 4 },
}),
);
expect(failure.error.remediation.action).toBe(SCOPED_NO_TARGET_RECOVERY);
await before.close();
const after = await connect({
...process.env,
REA_MCP_TOOL_FAMILIES: "session,direct,native,application",
});
const missing = parseMcpToolError(
await after.callTool({ name: "inspect_analysis_view", arguments: view }),
);
expect(missing.error.code).toBe("evidence_integrity_mismatch");
});
@@ -201,7 +201,7 @@ test("traces a procedure found only by its alias through the composed feature tr
test("omits a data entry an alias names while keeping the code entry that shares it", async ({
mcp,
}) => {
const { search, observe } = await aliasSession(
const { client, search, observe } = await aliasSession(
mcp,
"rea-kuna-alias-macho-",
{
@@ -234,4 +234,52 @@ test("omits a data entry an alias names while keeping the code entry that shares
),
);
expect(await search({ pattern: "global_count" })).toEqual([]);
const { error } = parseMcpToolError(
await client.callTool({
name: "procedure_address",
arguments: { procedure: "global_count" },
}),
);
expect(error.recovery).toBeUndefined();
expect(error.remediation.action).toContain("address_name");
});
test("reports lookup failures with recovery for the selected target, not the input schema", async ({
mcp,
}) => {
const { client } = await aliasSession(mcp, "rea-kuna-lookup-recovery-", {
functions: VERSIONED,
});
for (const [procedure, guidance] of [
["0x1335", "list_procedures"],
["absent_name", "search_procedures"],
["audit_transform", "exact address"],
]) {
const response = await client.callTool({
name: "procedure_address",
arguments: { procedure },
});
const { error } = parseMcpToolError(response);
expect(error.code).toBe("invalid_request");
expect(error.recovery).toBeUndefined();
expect(error.remediation.action).toContain(guidance);
expect(error.remediation.action).not.toContain("input schema");
}
const assemblyError = parseMcpToolError(
await client.callTool({
name: "procedure_assembly",
arguments: { procedure: "0x1335" },
}),
).error;
expect(assemblyError.remediation.action).toContain("Kuna procedure");
expect(assemblyError.remediation.action).not.toContain("decompile");
const missingAddress = parseMcpToolError(
await client.callTool({
name: "xrefs",
arguments: { address: "absent_name" },
}),
).error;
expect(missingAddress.recovery).toBeUndefined();
expect(missingAddress.message).toContain("no inventory entry named");
expect(missingAddress.remediation.action).toContain("address");
});
@@ -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", {});
});
}
@@ -235,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,
@@ -866,7 +899,7 @@ it.each([
behavior: { project: "hang" } as const,
environment: { REA_KUNA_PROJECT_TIMEOUT_MS: "300" },
cause: "AnalysisTimeoutError",
next: "To retry, set REA_KUNA_PROJECT_TIMEOUT_MS above 300 ms or raise REA_KUNA_JOBS in the environment that starts REA. REA reads Kuna settings when it starts, so restart REA, then reopen the target.",
next: "To retry, set REA_KUNA_PROJECT_TIMEOUT_MS above 300 ms in the environment that starts REA. REA reads Kuna settings when it starts, so restart REA, then reopen the target.",
details: {
timeout_ms: 300,
captured_output: { truncated: expect.any(Boolean) },
+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 },
);
},
},
};
}
await 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"/,
);
}
});
await 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/);
}
});
await 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);
}
});
+9 -1
View File
@@ -1,18 +1,26 @@
# 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]