Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e65a13f54d | ||
|
|
c11a75cbe9 | ||
|
|
22326e8199 | ||
|
|
3f6dcbc096 | ||
|
|
89a7ca8486 | ||
|
|
15cc7ce6a6 | ||
|
|
d0b60a97ab | ||
|
|
e3d4ac12b3 | ||
|
|
b5e4920823 |
@@ -2,7 +2,7 @@
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
metadata:
|
||||
version: "39"
|
||||
version: "38"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
@@ -9,10 +9,7 @@ provenance; inputs and recovered code are never executed. Recovery requires a
|
||||
caller-supplied Wakaru on Linux x64; see [the recovery guide](https://github.com/morluto/rea/blob/main/docs/javascript-recovery.md).
|
||||
|
||||
Use `analyze_javascript_application` directly on the operator-supplied ASAR or
|
||||
extracted tree. Exact `#alias` specifiers resolve through the importer's
|
||||
enclosing package `imports` map to package-local files; nested `node_modules`
|
||||
packages use their own map. Wildcard, recursive, and external-package import
|
||||
targets stay unresolved with that limitation. When size is unknown or large and the connected schema advertises
|
||||
extracted tree. When size is unknown or large and the connected schema advertises
|
||||
`detail`, request `detail: "summary"`. This returns an `inspect_analysis_view`
|
||||
Evidence record with a distinct ID and a `normalized_result.parent_evidence_id`
|
||||
for the complete retained application analysis. Source-map contents are part of
|
||||
|
||||
@@ -150,13 +150,6 @@ For headerless DOS COM, explicitly open with `format: "dos-com"` (CLI
|
||||
analysis: the entry is `0x10100`, file offset zero, with imposed real-mode segment
|
||||
context. PSP/stack/device state remains unmodeled. See [DOS guide](https://github.com/morluto/rea/blob/main/docs/ghidra-dos.md).
|
||||
|
||||
For other headerless images (embedded firmware without ELF/Mach-O/PE/MZ
|
||||
headers), open with `format: "raw-binary"`, `architecture` (`x86`, `x86_64`,
|
||||
`arm`, or `arm64`), and `entry_point` (`0x` hex). File offset 0 is mapped at
|
||||
that address. Hopper and IDA refuse this interpretation; Ghidra uses
|
||||
`BinaryLoader` with a little-endian generic language. Big-endian, Thumb-only,
|
||||
and Cortex-M variants are unsupported. See [raw binary guide](https://github.com/morluto/rea/blob/main/docs/raw-binary.md).
|
||||
|
||||
### Function annotations in Ghidra
|
||||
|
||||
Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic and session-scoped; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits until close. CLI `annotate-native-function` returns the updated analysis before discarding the session. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations. For naming only, `set_address_name` / `set_addresses_names` also work with Ghidra: they name function entries or data addresses (primary labels) and return compact per-address success booleans instead of a dossier.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**See a feature you like. Understand how it works, down to the binary level.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#what-you-can-analyze)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
@@ -244,9 +245,7 @@ Ghidra and IDA use your existing installations. See [provider setup](docs/instal
|
||||
<summary><strong>Do I need to start Hopper first?</strong></summary>
|
||||
|
||||
REA starts Hopper when an operation needs it. On macOS, a first-run dialog may
|
||||
ask you to choose demo mode or activate your license. Complete that prompt in the
|
||||
same user session before your first CLI/MCP operation or unattended job. Open
|
||||
Hopper manually if needed. See
|
||||
ask you to choose demo mode or activate your license. See
|
||||
[Hopper startup and troubleshooting](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**وجدت ميزة تعجبك؟ افهم طريقة عملها حتى مستوى الشيفرة الثنائية.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Eine Funktion gefällt dir? Verstehe, wie sie funktioniert – bis hinunter auf die Binärebene.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**¿Ves una función que te gusta? Entiende cómo funciona, hasta el nivel binario.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**قابلیتی دیدهاید که دوستش دارید؟ سازوکارش را تا سطح باینری درک کنید.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Repérez une fonctionnalité qui vous plaît. Comprenez son fonctionnement, jusqu'au niveau du binaire.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Melihat fitur yang Anda sukai. Pahami cara kerjanya hingga ke tingkat biner.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Vedi una funzionalità che ti piace. Capisci come funziona, fino al livello binario.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#cosa-puoi-analizzare)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**気に入った機能を見つけたら、その仕組みをバイナリのレベルまで理解する。**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**마음에 드는 기능을 발견했다면, 바이너리 수준까지 작동 원리를 이해하세요.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Widzisz funkcję, która Ci się podoba? Dowiedz się, jak działa, aż do poziomu kodu binarnego.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Viu uma funcionalidade de que gostou? Entenda como ela funciona, até o nível binário.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Нашли интересную функцию? Разберитесь, как она работает, вплоть до двоичного кода.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**เห็นฟีเจอร์ที่ชอบแล้วอยากรู้ว่าทำงานอย่างไร? เข้าใจได้ลึกถึงระดับไบนารี**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Beğendiğiniz bir özellik mi gördünüz? İkili kod düzeyine kadar nasıl çalıştığını anlayın.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Побачили цікаву функцію? Зрозумійте, як вона працює, аж до рівня двійкового коду.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**Thấy một tính năng bạn thích? Hiểu cách nó hoạt động, đến tận mức mã nhị phân.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**看到喜歡的功能,弄清它的工作原理,深入到二進位層面。**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
**看到喜欢的功能,弄清它的工作原理,深入到二进制层面。**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](#current-status)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
|
||||
@@ -126,20 +126,6 @@ class ReaJadxBridge {
|
||||
return result;
|
||||
}
|
||||
|
||||
private JadxSession.SmartCode methodBody(JadxSession current, ClassSnapshot cls,
|
||||
MethodSnapshot selected, JavaMethod method) {
|
||||
if (!selected.shortId().equals("<clinit>()V"))
|
||||
return current.getMethodBodySmart(method, current.getMaxSourceBytes(), true);
|
||||
// Java code generation can fold all or part of <clinit> into field declarations.
|
||||
// Its method text cannot establish coverage, even when a static block remains.
|
||||
JadxSession.MethodSmali smali = current.getMethodSmali(cls.node(), method.getName());
|
||||
if (!smali.getFound() || smali.getBlocks().size() != 1)
|
||||
throw new IllegalArgumentException("Static initializer disassembly requires one exact method: " + selected.shortId());
|
||||
return new JadxSession.SmartCode(
|
||||
TextUtilKt.truncateToBytes(smali.getBlocks().get(0), current.getMaxSourceBytes()),
|
||||
"smali", true, List.of("static_initializer_disassembly"));
|
||||
}
|
||||
|
||||
private CallToolResult handle(String name, JsonObject args) {
|
||||
if (name.equals("rea_jvm_status")) return json(Map.of(
|
||||
"engine_reported_version", BuildInfo.VERSION,
|
||||
@@ -164,7 +150,7 @@ class ReaJadxBridge {
|
||||
int index = integer(args, "overload_index", 0);
|
||||
if (index >= overloads.size()) throw new InputFailure("overload_index", "overload_index out of range: " + index);
|
||||
JavaMethod method = method(cls, overloads.get(index));
|
||||
JadxSession.SmartCode code = methodBody(current, cls, overloads.get(index), method);
|
||||
JadxSession.SmartCode code = current.getMethodBodySmart(method, current.getMaxSourceBytes(), true);
|
||||
return json(Map.of("class_name", cls.summary().get("full_name"),
|
||||
"method_name", overloads.get(index).name(), "full_name", method.getFullName(),
|
||||
"overload_index", index, "overload_count", overloads.size(),
|
||||
@@ -225,7 +211,7 @@ class ReaJadxBridge {
|
||||
String description = switch (name) {
|
||||
case "list_classes" -> "Indexed pages of all top-level class names, with offset and limit.";
|
||||
case "get_class_summary" -> "Parsed member metadata for one exact class name; no source generation.";
|
||||
case "get_method_body" -> "Source for an exact class and stable same-name overload index; static initializers use smali to preserve folded field initialization.";
|
||||
case "get_method_body" -> "Source for an exact class and stable same-name overload index, with smali fallback.";
|
||||
default -> "Complete static references to an exact class or unique method; source lines are unresolved.";
|
||||
};
|
||||
Tool narrowed = new Tool(name, new ToolSchema(null, new JsonObject(properties),
|
||||
|
||||
@@ -182,17 +182,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
sessionDefaultAddressSpace =
|
||||
currentProgram.getAddressFactory().getDefaultAddressSpace();
|
||||
if (currentProgram.getExecutableFormat().equals("Raw Binary")) {
|
||||
boolean dosCom = currentProgram.getLanguageID().getIdAsString()
|
||||
.equals("x86:LE:16:Real Mode");
|
||||
String preparation = dosCom ? "REA DOS COM prepared" : "REA raw binary prepared";
|
||||
if (!currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getBoolean(preparation, false)) {
|
||||
throw new IllegalStateException(
|
||||
dosCom ? "REA DOS COM entry/context preparation failed" :
|
||||
"REA raw binary entry preparation failed"
|
||||
);
|
||||
}
|
||||
if (currentProgram.getExecutableFormat().equals("Raw Binary") &&
|
||||
!currentProgram.getOptions(ghidra.program.model.listing.Program.PROGRAM_INFO)
|
||||
.getBoolean("REA DOS COM prepared", false)) {
|
||||
throw new IllegalStateException("REA DOS COM entry/context preparation failed");
|
||||
}
|
||||
if (!Application.getApplicationVersion().equals(descriptor.providerVersion)) {
|
||||
throw new IllegalStateException("Ghidra provider version does not match the session");
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import org.osgi.framework.BundleActivator;
|
||||
import org.osgi.framework.BundleContext;
|
||||
|
||||
// Compile the no-op activator with REA's scripts. Otherwise Ghidra generates
|
||||
// one afterward and opens a second Java compiler and file manager.
|
||||
public final class ReaGhidraBundleActivator implements BundleActivator {
|
||||
@Override
|
||||
public void start(BundleContext context) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop(BundleContext context) {
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
|
||||
/** Seed the caller-supplied raw-binary entry before default auto-analysis. */
|
||||
public final class ReaGhidraPrepareRaw extends GhidraScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
String[] arguments = getScriptArgs();
|
||||
if (arguments.length != 2) {
|
||||
throw new IllegalArgumentException(
|
||||
"Raw binary preparation requires the committed language and entry address"
|
||||
);
|
||||
}
|
||||
String expectedLanguage = arguments[0];
|
||||
String entryHex = arguments[1];
|
||||
if (!currentProgram.getLanguageID().getIdAsString().equals(expectedLanguage) ||
|
||||
!currentProgram.getExecutableFormat().equals("Raw Binary")) {
|
||||
throw new IllegalArgumentException(
|
||||
"Raw binary preparation requires the committed language and BinaryLoader import"
|
||||
);
|
||||
}
|
||||
Address entry = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(entryHex);
|
||||
Address start = currentProgram.getMinAddress();
|
||||
if (entry == null || !entry.equals(start)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Raw binary import must map file offset 0 at the caller-supplied entry"
|
||||
);
|
||||
}
|
||||
addEntryPoint(entry);
|
||||
if (!disassemble(entry) || createFunction(entry, "entry") == null) {
|
||||
throw new IllegalStateException("Raw binary entry cannot be decoded as a function");
|
||||
}
|
||||
currentProgram.getOptions(Program.PROGRAM_INFO).setBoolean("REA raw binary prepared", true);
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,6 @@ export default defineConfig({
|
||||
{ text: "IDA", link: "/ida-provider" },
|
||||
{ text: "Ghidra on Windows", link: "/windows-ghidra-p0" },
|
||||
{ text: "Ghidra DOS analysis", link: "/ghidra-dos" },
|
||||
{ text: "Ghidra raw binary", link: "/raw-binary" },
|
||||
{ text: "Ghidra NativeAOT", link: "/ghidra-nativeaot" },
|
||||
{ text: "Ghidra semantics", link: "/ghidra-provider" },
|
||||
],
|
||||
|
||||
@@ -94,12 +94,6 @@ fallback for an overloaded method instead of attributing all bodies to one
|
||||
selected overload. Native/abstract methods with no decompiled body report
|
||||
`body_status: "not_available"` without inferring which implementation flag caused it.
|
||||
|
||||
Static initializers (`<clinit>`) return smali with the
|
||||
`static_initializer_disassembly` marker. Java decompilation can move their
|
||||
instructions into field declarations, leaving an empty or incomplete method
|
||||
body. The selected method's disassembly preserves those instructions and retains
|
||||
the same source byte limit and explicit truncation reporting.
|
||||
|
||||
## Interpret the result
|
||||
|
||||
- Package inspection retains decoded manifest XML, summary declarations,
|
||||
|
||||
+1
-2
@@ -59,8 +59,7 @@ alone, use `rea instructions`. A macOS `.app` bundle can be supplied directly.
|
||||
Run `rea --help` or a command's `--help` for arguments and output options.
|
||||
The [generated catalog](product-catalog.json) lists all CLI commands and MCP
|
||||
tools. Provider-specific target support is described in the
|
||||
[native guide](native-investigation.md), [DOS guide](ghidra-dos.md),
|
||||
[raw binary guide](raw-binary.md), and
|
||||
[native guide](native-investigation.md), [DOS guide](ghidra-dos.md), and
|
||||
[Windows Ghidra guide](windows-ghidra-p0.md).
|
||||
|
||||
## Choose a provider
|
||||
|
||||
+2
-18
@@ -352,12 +352,6 @@ Other integrations, including Qwen Code, use `~/.agents/skills`.
|
||||
A mixed selection plans each distinct destination. Selecting the skill without a client uses the shared
|
||||
directory. Setup leaves existing skill copies in other locations untouched.
|
||||
|
||||
`rea setup --skill-client claude_code --yes --json` refreshes and verifies the
|
||||
selected skill destination independently of MCP registration selection. The
|
||||
final `doctor.scope.skill_clients` records which skill destinations were
|
||||
checked; `doctor.scope.clients` continues to describe required registrations.
|
||||
An unselected shared skill does not affect a client-only refresh.
|
||||
|
||||
`doctor --skill --json` verifies the selected copies against the bundled
|
||||
instructions and references. Consumers should use `identity.skill.state`,
|
||||
`installed_version`, and `installed_tool_count` for skill readiness. The
|
||||
@@ -567,13 +561,8 @@ need Xvfb, Python 3, X11 and XTEST; approved setup installs those dependencies
|
||||
on supported distributions. Add the curl installer's reported executable
|
||||
directory, commonly `~/.local/bin`, to your shell `PATH` if needed.
|
||||
|
||||
REA starts Hopper when needed. On macOS, complete its demo or license prompt in
|
||||
the same user session before the first CLI/MCP operation or unattended job. Open
|
||||
Hopper manually if needed; its launcher can also bring the dialog forward.
|
||||
If startup reports `Cannot launch Hopper! Connection is invalid.` or waits for
|
||||
bridge readiness, check Hopper for a pending demo, license, or loader dialog.
|
||||
Complete the dialog and retry. If REA reports `cleanup_incomplete`, follow the
|
||||
retained-resource guidance below before reopening the target.
|
||||
REA starts Hopper when needed. On macOS, its launcher can bring a window or
|
||||
first-run dialog forward; choose demo mode or activate your existing license.
|
||||
Hopper serializes analysis requests. Cancelling a wait can leave provider work
|
||||
running, which the session reports. Successful decompilation is cached until
|
||||
a relevant rename or comment changes it.
|
||||
@@ -602,11 +591,6 @@ if document closure is unconfirmed, REA retains the backing image and reports
|
||||
MCP-exit behavior.
|
||||
Startup deadlines report missing bridge readiness and preserve the launcher
|
||||
outcome; a successful helper exit does not prove that a loader dialog completed.
|
||||
If an owned provider process exits, request failures preserve its observed exit
|
||||
code or signal and bounded output in `details.launcher`. An exit can precede
|
||||
output-stream closure; `output_closed: false` keeps that uncertainty explicit.
|
||||
Later requests retain the known exit instead of reducing it to socket loss.
|
||||
An external GUI's helper exit does not establish that the GUI itself exited.
|
||||
Cursor navigation returns the observed object start when Hopper snaps an interior
|
||||
address; adjacent-object navigation rejects unmapped inputs and document ends.
|
||||
Native API text rejects NUL characters and unpaired Unicode surrogates before
|
||||
|
||||
@@ -159,11 +159,9 @@ completed partial Evidence. MCP failure details return
|
||||
after recording succeeds, so `inspect_analysis_view` can open that analysis
|
||||
directly without serializing the entire partial graph into the error response.
|
||||
CLI failure output retains the complete partial Evidence.
|
||||
Cancellation after source analysis begins likewise retains completed facts,
|
||||
including cancellation during analyzer cleanup, graph construction, or Evidence
|
||||
sealing. REA finishes projecting those owned facts cooperatively to return partial
|
||||
Evidence. An SDK client may reject its cancelled request before receiving that
|
||||
reply. On the same MCP connection, inspect the Evidence bundle
|
||||
Cancellation after source analysis begins likewise
|
||||
retains completed facts; an SDK client may reject its cancelled request before
|
||||
receiving that reply. On the same MCP connection, inspect the Evidence bundle
|
||||
and use the retained analysis ID with `inspect_analysis_view`. Owned analyzer
|
||||
cleanup is verified before its private files are released. Cleanup uncertainty
|
||||
is reported separately and retains ownership for retry.
|
||||
@@ -233,11 +231,6 @@ unknown instead of silently treating them as absent or verified.
|
||||
Plain `.ts` artifact sources use TypeScript syntax without JSX, including
|
||||
angle-bracket type assertions. `.tsx` and `.jsx` retain JSX parsing.
|
||||
|
||||
Relative TypeScript imports can name their emitted `.js`, `.mjs`, or `.cjs`
|
||||
counterparts while resolving to inventoried source files. This substitution
|
||||
requires import or require semantics. Literal Electron preload, renderer, and
|
||||
utility-process runtime paths retain the runtime filename selected by the caller.
|
||||
|
||||
Selected bounded text is then parsed as inert data to recover:
|
||||
|
||||
- `package.json` metadata and declared main or renderer entrypoints;
|
||||
@@ -330,44 +323,15 @@ substitute version-dependent Electron defaults for omitted `webPreferences`.
|
||||
Dynamic option objects, bridge keys, API objects, and IPC channel expressions
|
||||
remain unknown and make coverage partial.
|
||||
|
||||
Electron API identity follows proven lexical module aliases. Namespace interop
|
||||
also preserves identity when the complete Rolldown helper operations, lexical
|
||||
roles, live getters, and captured intrinsic origins match, or when a stable
|
||||
helper simply returns its module parameter. Harmless single-statement control
|
||||
blocks preserve recognition; added operations still require proof. Literal
|
||||
member and destructuring projections are retained from proven results.
|
||||
Omitted or literal node-mode arguments preserve named exports; a module-valued
|
||||
default projection requires a truthy literal node mode. Computed or extra
|
||||
arguments remain unresolved.
|
||||
Recognition uses structure rather than emitted names; other helper bodies and ambiguous
|
||||
runtime fallbacks remain unresolved. Captured function identities survive later global-property
|
||||
replacement, including copies through initialized aliases. Reads before an
|
||||
alias initializer remain unresolved. Visible intrinsic writes before capture or
|
||||
conversion are checked in their owning callable scope, including identifier leaves in nested/default/rest
|
||||
destructuring and loop targets. Property keys and shadowed local bindings do not
|
||||
overwrite the global intrinsic identity. Cross-call side effects, including
|
||||
writes inside directly invoked local functions, are not evaluated. Other
|
||||
reassignments, dynamic selections, and visibly overwritten namespace members
|
||||
are unresolved.
|
||||
Known Object/Reflect mutators remain recognizable through literal destructuring
|
||||
of intrinsic objects and stable bound-function aliases. Calls retain bound
|
||||
argument prefixes and the original bound receiver through `call`, `apply`, and
|
||||
`Reflect.apply`, including generic `Function.prototype` invocation and binding
|
||||
creation. Arguments follow each actual callable invocation rather than property
|
||||
chain depth; rebinding retains the original bound receiver.
|
||||
Visibly replaced invocation intrinsics produce unknown effects instead
|
||||
of using their former call semantics; captured function aliases retain their
|
||||
earlier identity. Unexamined targets remain unresolved. `Reflect.set` examines
|
||||
its explicit receiver as well as its target, retaining unknown receiver effects.
|
||||
Creating an unused binding does not itself mutate an intrinsic. Variable
|
||||
redeclarations retain a single unconditional initializer
|
||||
when they introduce no assignment or second initializer.
|
||||
Stable identifier chains to fresh object, array, function,
|
||||
undecorated class or RegExp syntax keep those targets separate from intrinsic
|
||||
objects when initialized before use; opaque, decorated, reassigned or
|
||||
not-yet-initialized targets remain unresolved. Writes during class initialization
|
||||
are still retained. Function declarations are available before their textual
|
||||
position; variable and class initializers must precede use.
|
||||
Electron API identity follows proven lexical module aliases. Bundled CommonJS
|
||||
namespace imports are also followed through the interop helpers that bundlers
|
||||
and compilers emit (`__toESM`, `__importStar`, `_interopRequireWildcard`),
|
||||
including the `electron = __toESM(electron)` rewrap that Rolldown emits. This
|
||||
step is inferred: a helper is recognized by its emitted name and a single
|
||||
program-scope function definition, and its body is not evaluated, so a local
|
||||
function that reuses one of these names is trusted to pass the module through.
|
||||
Other reassigned bindings, dynamic selections, and visibly overwritten namespace
|
||||
members are unresolved.
|
||||
Direct and destructuring assignment, loop, update, and delete targets are checked
|
||||
through namespace aliases; unrelated member writes do not discard observed API
|
||||
identity. This static check does
|
||||
@@ -461,40 +425,16 @@ such as `./web3:app.js` still names a local artifact.
|
||||
Package `exports` fallback arrays are supported both at the top level and under
|
||||
the root `"."` entry. The resolver selects targets in declared order using the
|
||||
same conditional and invalid-entry handling as nested exports arrays.
|
||||
Nonempty arrays containing only inactive conditions remain unmatched, allowing
|
||||
the enclosing condition object to continue to a later active condition.
|
||||
Empty arrays and explicit null targets keep their blocking outcome.
|
||||
Refusal evidence distinguishes those source values and retains condition names
|
||||
from unmatched nested targets rather than reporting array indices.
|
||||
For imports and requires, the active Node conditions include `module-sync`;
|
||||
earlier active conditions retain precedence over later ones.
|
||||
|
||||
Exact package-private `#alias` specifiers resolve against the importer's
|
||||
enclosing `package.json` `imports` map — the nearest same-container package
|
||||
scope, stopping at a `node_modules` directory so a nested package cannot inherit
|
||||
an ancestor's aliases. Selected targets are package-local `./` files with no
|
||||
extension or index fallback, using the same condition, array, and null selection
|
||||
as exports subpaths. A missing key, explicit null, or out-of-scope alias keeps
|
||||
an unresolved edge with that reason. Wildcard keys (`#internal/*`), recursive
|
||||
`#` targets, and external-package import targets remain unresolved.
|
||||
|
||||
Exact keys such as `#/x` are literal lookup identities. `#` and names ending
|
||||
in `/` are invalid. Imports arrays skip invalid parent-relative, absolute, and
|
||||
URL targets before selecting a later package-local file; an unsupported bare
|
||||
package or recursive alias keeps its unresolved outcome rather than selecting
|
||||
a later fallback.
|
||||
|
||||
ESM relative module paths and selected package exports targets use URL suffix handling
|
||||
and one percent-decoding pass. CommonJS relative paths and legacy package main
|
||||
fields retain literal filename punctuation. Exports targets must start with
|
||||
`./` and exclude `.`, `..`, and `node_modules` path segments, including their
|
||||
encoded forms, before URL normalization. Arrays skip invalid exports targets
|
||||
in declared order, including URLs that escape the package root after normalization.
|
||||
Selected package targets use the normalized URL path for lookup; tabs and
|
||||
newlines in the URL text are removed by the URL parser. CommonJS package targets
|
||||
reject encoded separators anywhere in the selected URL; ESM checks its pathname.
|
||||
URL decoding and file lookup failures for a selected target do not select a
|
||||
later entry. Rejected references retain the package metadata
|
||||
in declared order; URL decoding and file lookup failures for a selected target
|
||||
do not select a later entry. Rejected references retain the package metadata
|
||||
path, original target, failed constraint, and importing source location.
|
||||
|
||||
HTML script references resolve to exact inventoried files after applying the
|
||||
|
||||
@@ -126,24 +126,6 @@ Member inspection binds build-local tokens, signatures, row offsets, method
|
||||
bodies, and decoded relationships to that artifact and MVID. Missing or malformed
|
||||
facts remain explicit in coverage and per-member status.
|
||||
|
||||
Metadata table streams may carry the CLR-compatible `ExtraData` extension
|
||||
(`HeapSizes` flag `0x40`), which Microsoft readers honor although it is not
|
||||
specified by ECMA-335. REA skips its fixed four-byte word after the row counts
|
||||
before decoding table rows; the word's value is not a length. An incomplete
|
||||
word is reported as a metadata coverage failure at its file offset, without
|
||||
reading beyond the declared table stream.
|
||||
|
||||
For unoptimized `#-` metadata, nonempty `FieldPtr` and `MethodPtr` tables
|
||||
translate TypeDef list positions to physical member definitions. REA resolves
|
||||
these indirections for member ownership and local attribute-constructor types;
|
||||
field and method tokens still identify the original definition rows.
|
||||
`field_list` and `method_list` retain the metadata's list coordinates, which
|
||||
refer to the pointer table when present rather than necessarily contiguous
|
||||
definition rows. Malformed or ambiguous pointers leave the owner unknown with
|
||||
partial coverage while preserving readable member and attribute facts.
|
||||
The retained inverse indexes are admitted against the existing representation
|
||||
budget before allocation.
|
||||
|
||||
`exported_types` reports ExportedType rows as declared. Facade assemblies such
|
||||
as .NET `System.Runtime.dll` provide their types this way: `forwarder: true`
|
||||
marks a type forwarder, and `implementation_token` names the AssemblyRef, File,
|
||||
|
||||
+9
-30
@@ -150,15 +150,13 @@ structured data and matching text projection. Oversized errors are retained
|
||||
as Evidence before delivery of a bounded diagnostic with an export reference;
|
||||
if retention is unavailable or fails, the diagnostic reports that reason.
|
||||
|
||||
REA accepts ordinary `tools/call` progress tokens. The numeric `progress` counts
|
||||
emitted status observations and strictly increases, including when a waiting
|
||||
phase has completed no additional work. Updates are rate-bounded to at most one
|
||||
intermediate update per 100 ms and always allow a terminal update. `total` is
|
||||
omitted because the number of future status observations is unknown. Read
|
||||
`message` for the phase and observed work; REA does not fabricate percentages.
|
||||
REA accepts ordinary `tools/call` progress tokens. Updates are monotonic,
|
||||
rate-bounded to at most one intermediate update per 100 ms, and always allow a
|
||||
terminal update. Unknown totals are omitted; REA does not fabricate percentages.
|
||||
`capture_process_scenario` uses that boundary only when the client supplies a
|
||||
progress token. Notifications put the lifecycle phase, elapsed time, and final
|
||||
cleanup status in `message`. A client
|
||||
progress token. Notifications set `progress` to the count of collected terminal
|
||||
frames, process samples, and interaction events, omit `total`, and put the
|
||||
lifecycle phase, elapsed time, and final cleanup status in `message`. A client
|
||||
that does not send a progress token receives no progress notifications. The
|
||||
tool result is unchanged either way.
|
||||
Provider calls receive the request cancellation signal. Artifact traversal,
|
||||
@@ -232,18 +230,6 @@ engine readiness or a percentage of Ghidra auto-analysis. A longer server
|
||||
startup allowance does not extend the client's deadline; progress also does not
|
||||
extend it unless the client explicitly implements that policy.
|
||||
|
||||
While Ghidra startup is pending, REA sends a waiting notification every ten
|
||||
seconds when the request has a progress token. Its message gives elapsed waiting
|
||||
time; import, auto-analysis and bridge readiness may all still be pending.
|
||||
Waiting notifications stop when startup settles or the caller cancels. They do
|
||||
not measure engine work completed.
|
||||
|
||||
The pinned SDK can reset the request's inactivity timeout on progress with
|
||||
`resetTimeoutOnProgress: true`. Also set `maxTotalTimeout` to bound the complete
|
||||
request; the provider's startup deadline still applies. These options belong in
|
||||
the same second argument of `callTool`. Without that explicit client policy, the
|
||||
default 60-second request deadline still expires despite waiting notifications.
|
||||
|
||||
The 240-second setting is a measured example, not a universal timeout: a
|
||||
controlled Linux x64/WSL2 fixture took about 64 seconds for its first overview
|
||||
with Ghidra 12.1.4 on a mounted NTFS installation, native JDK 21, two CPUs, and a
|
||||
@@ -279,10 +265,9 @@ observed process-family member remained after final close. This was a small
|
||||
1,024-byte fixture, so the longer request setting remains an example, not a
|
||||
guarantee for larger targets. No macOS cold-start recovery was exercised.
|
||||
|
||||
With a supplied progress token, that earlier Windows run received an operation-start
|
||||
With a supplied progress token, the Windows run received an operation-start
|
||||
notification before timeout and start/completion notifications for the
|
||||
successful retry, before startup waiting notifications were added. It received
|
||||
no intermediate import or auto-analysis progress.
|
||||
successful retry. It received no intermediate import or auto-analysis progress.
|
||||
These operation markers do not measure analysis work completed or extend the
|
||||
client's deadline. Each new provider session still imports the target into a
|
||||
fresh temporary project.
|
||||
@@ -450,13 +435,7 @@ Tool annotations describe effects and are hints, not authorization controls
|
||||
([MCP ToolAnnotations](https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations)).
|
||||
`readOnlyHint` includes session state: an analysis call that records additive
|
||||
Evidence is marked non-read-only even when it leaves the target unchanged.
|
||||
`destructiveHint` is true when a tool may discard data (overwriting or clearing
|
||||
analyst metadata, replacing files, dropping session state), runs or drives code
|
||||
REA does not own (`executesForeignCode`: process, LLDB, Electron, browser, and
|
||||
native UI scenarios), or changes an attached Android device (`mutatesDevice`:
|
||||
push, install, uninstall, start, and stop). Ordinary Evidence recording is not
|
||||
destructive. The effect record in `binary_session` and the generated catalog
|
||||
carries each flag separately.
|
||||
`destructiveHint` describes possible data loss, not ordinary Evidence recording.
|
||||
Effect metadata covers possible behavior across supported inputs: DMG inventory
|
||||
can launch `hdiutil` and create an owned temporary mount directory; extraction
|
||||
creates a fresh output directory on every call. Comparing supplied web captures
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
# Headerless raw-binary analysis with Ghidra
|
||||
|
||||
REA admits a headerless executable when the caller supplies the CPU family and
|
||||
the address where file offset 0 should be mapped. That interpretation is not
|
||||
inferred from a `.bin` suffix or from unrecognized bytes.
|
||||
|
||||
## Open and inspect
|
||||
|
||||
```bash
|
||||
rea inspect /absolute/path/to/ecu.bin --target-format raw-binary --architecture arm --entry-point 0x8000000 --provider ghidra --json
|
||||
rea function /absolute/path/to/ecu.bin 0x8000000 --target-format raw-binary --architecture arm --entry-point 0x8000000 --provider ghidra --json
|
||||
rea inspect-native-load-image /absolute/path/to/ecu.bin --target-format raw-binary --architecture arm --entry-point 0x8000000 --provider ghidra --json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "open_binary",
|
||||
"arguments": {
|
||||
"path": "/absolute/path/to/ecu.bin",
|
||||
"format": "raw-binary",
|
||||
"architecture": "arm",
|
||||
"entry_point": "0x8000000",
|
||||
"provider_id": "ghidra"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`architecture` is one of `x86`, `x86_64`, `arm`, or `arm64`. `entry_point` is a
|
||||
hexadecimal address of the form `0x…`. The complete file is mapped starting at
|
||||
that address, and Ghidra seeds an `entry` function there before auto-analysis.
|
||||
ARM and AArch64 entries must be 4-byte aligned; x86 entries may be byte-aligned.
|
||||
`open_binary` and an open `binary_session` return the canonical `entry_point`
|
||||
for raw targets so callers can verify the selected mapping directly.
|
||||
Omitted `format` keeps header-based detection; architecture and entry without
|
||||
`format=raw-binary` are rejected.
|
||||
|
||||
Ghidra imports a private snapshot with `BinaryLoader` and these little-endian
|
||||
generic languages:
|
||||
|
||||
| architecture | language_id |
|
||||
| ------------ | ------------------- |
|
||||
| x86 | `x86:LE:32:default` |
|
||||
| x86_64 | `x86:LE:64:default` |
|
||||
| arm | `ARM:LE:32:v7` |
|
||||
| arm64 | `AARCH64:LE:64:v8A` |
|
||||
|
||||
Those settings, the caller-supplied entry, and `language_selection:
|
||||
caller-supplied-v1` are committed in the analysis profile. A different entry
|
||||
or architecture is a different profile; snapshots from header-detected targets
|
||||
do not replay.
|
||||
|
||||
The mapped start is independent of Ghidra's program image base, which remains
|
||||
`0x0` for BinaryLoader. Ghidra may report additional language-defined entry
|
||||
points, including unmapped ARM vectors. Load-image verification requires the
|
||||
caller-supplied entry and preserves those other entries as observations.
|
||||
Independent raw verification streams the complete private snapshot digest and
|
||||
extent with bounded memory and supports cancellation during the read.
|
||||
|
||||
Hopper and IDA refuse raw-binary targets. Windows Ghidra P0 continues to admit
|
||||
PE applications only.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Headers are not discovered from file bytes. This increment does not guess
|
||||
load addresses, vector tables, or memory maps.
|
||||
- Big-endian, Thumb-only, Cortex-M, and other Ghidra processor variants are
|
||||
unsupported.
|
||||
- Reset vectors, memory-mapped I/O, and firmware boot sequences are not
|
||||
emulated.
|
||||
- Independent load-image verification covers source identity, mapped file
|
||||
bytes, the bound language, and the caller-supplied entry. Original bytes
|
||||
stay unchanged.
|
||||
+8
-21
@@ -139,11 +139,8 @@ directory, so default OS-home discovery remains unverified. Live model APIs and
|
||||
native Windows are also unverified.
|
||||
|
||||
`verify:claude-client` requires installed Claude Code (verified with
|
||||
`@anthropic-ai/claude-code@2.1.296` on POSIX and native Windows); select it
|
||||
with `REA_VERIFY_CLAUDE_COMMAND`. On Windows, select
|
||||
`claude.exe`: npm installs put only a `claude.cmd` shim on `PATH`, and the real
|
||||
executable is under `node_modules/@anthropic-ai/claude-code/bin`.
|
||||
The optional lane uses native `CLAUDE_CONFIG_DIR` discovery, an independent
|
||||
`@anthropic-ai/claude-code@2.1.296`); select it with `REA_VERIFY_CLAUDE_COMMAND`.
|
||||
The optional POSIX lane uses native `CLAUDE_CONFIG_DIR` discovery, an independent
|
||||
Git workspace, user settings and default built-in tools. It preserves caller
|
||||
preferences and backups, checks idempotent setup, waits through the native
|
||||
`WaitForMcpServers` tool when discovery is pending, and validates the complete
|
||||
@@ -152,8 +149,8 @@ native `Skill`, checks its full body and verifies named-schema JavaScript Eviden
|
||||
for a Unicode path. Use `-- chat` for ordinary chat with REA enabled, or
|
||||
`REA_VERIFY_RUNTIME_ROOT` for a production-only installed package. The local
|
||||
Anthropic Messages/SSE fixture preserves native resource-hint envelopes in its
|
||||
request artifacts. Usage is synthetic; live Anthropic API and bare-mode
|
||||
personal-skill activation remain unverified by this lane.
|
||||
request artifacts. Usage is synthetic; live Anthropic API, native Windows and
|
||||
bare-mode personal-skill activation remain unverified by this lane.
|
||||
|
||||
`verify:copilot-client` requires installed GitHub Copilot CLI (verified with
|
||||
`@github/copilot@1.0.95`); select it with `REA_VERIFY_COPILOT_COMMAND`.
|
||||
@@ -282,12 +279,10 @@ establish live DeepSeek API or native Windows coverage. Receipts include
|
||||
request artifacts, result digests and owned-process lineage.
|
||||
|
||||
Full E2E tests invoke the production command dispatcher and real providers,
|
||||
without fake launchers, runners or responses. `verify:keyed-archive` writes
|
||||
Foundation binary, XML, and integer archives using one run-owned Swift module
|
||||
cache. It checks CLI and separate stdio MCP parity, pagination, escaping-path
|
||||
rejection, an XML graph golden, and numeric meaning for safe controls, unsafe
|
||||
integers, boxed numbers, and integral-valued reals. `verify:asset-catalog`
|
||||
compiles source-owned colors with
|
||||
without fake launchers, runners or responses. `verify:keyed-archive` writes an
|
||||
actual Foundation binary archive, runs the CLI and a separate stdio MCP
|
||||
subprocess, checks parity and pagination, rejects an escaping path and checks
|
||||
an XML graph golden. `verify:asset-catalog` compiles source-owned colors with
|
||||
`actool`, invokes real `assetutil`, then checks CLI/MCP results, exact catalog
|
||||
digest, every raw metadata field, pagination and malformed input rejection.
|
||||
Neither artifact workflow requires Hopper or launches it. Both run in macOS CI.
|
||||
@@ -798,14 +793,6 @@ truncation constraint, while leading-zero coordinates still resolve correctly.
|
||||
It has the same Ghidra/JDK prerequisites as the MZ lane. Neither lane claims DOS
|
||||
runtime or PC-98 device execution.
|
||||
|
||||
`npm run verify:ghidra:raw` uses a generated ARM little-endian headerless image
|
||||
through the compiled CLI and stdio MCP server. With the same Ghidra/JDK
|
||||
prerequisites, it checks preparation and bridge startup, verified load mappings,
|
||||
instruction and byte readback, canonical-entry session reuse, replacement when
|
||||
the entry changes without `provider_id`, unchanged source bytes, and owned
|
||||
cleanup. Other raw CPU families remain covered by deterministic tests rather
|
||||
than this real-provider lane. See [raw binary analysis](raw-binary.md).
|
||||
|
||||
## Apple Interface Builder archives
|
||||
|
||||
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# REA Code Mode experiment
|
||||
|
||||
Frozen baseline: `fb94d9dde0ccda83ae520ac6fb22b1d283562cf3`, fetched from
|
||||
`origin/main` on 2026-10-11. Branch: `experiment/codemode-bridge-20261011`.
|
||||
This is a local experiment, not a change to the published CLI or MCP API.
|
||||
|
||||
The analyst question is whether composing REA's existing named tools in code
|
||||
improves investigation efficiency enough to justify an optional bridge. Existing
|
||||
users must keep their current names, input schemas, Evidence and session behavior.
|
||||
Provider analysis settings must stay identical between comparison profiles.
|
||||
|
||||
The bridge owns one upstream stdio MCP connection and discovers its complete live
|
||||
catalog. Only discovery and JavaScript execution are exposed downstream. Code
|
||||
calls `tools.<original_name>(original_arguments)` and receives the original MCP
|
||||
result envelope. REA still validates input, chooses providers, retains Evidence,
|
||||
and owns analysis resources. There is no second tool implementation or schema
|
||||
catalog to keep synchronized. A fresh QuickJS WASM context exposes only those
|
||||
tool calls, JSON helpers and explicit output; it has no host filesystem, network,
|
||||
process or module loader.
|
||||
|
||||
Start the bridge without changing an existing registration:
|
||||
|
||||
```bash
|
||||
node bridge.mjs /absolute/path/to/node /absolute/path/to/rea/scripts/rea.mjs mcp
|
||||
```
|
||||
|
||||
Register that command as a separate, temporary MCP server for a trial. Existing
|
||||
REA registrations keep their original tools. `rea_search` accepts a query or exact
|
||||
names; use `schemas: false` for the complete lightweight name/description list.
|
||||
Query discovery returns names/descriptions by default; exact-name discovery
|
||||
returns original input schemas and derived input/output TypeScript declarations.
|
||||
These declarations help write code; they do not replace JSON validation contracts.
|
||||
`schemas` explicitly overrides that choice.
|
||||
Start with one domain word and request all selected exact names in one call.
|
||||
Request `output_schemas: true` when a
|
||||
result contract is needed; the original output schema is then returned unchanged.
|
||||
`catalog_sha256` identifies the SDK catalog; it must not be substituted for REA's
|
||||
`server_identity.catalog.digests` fields.
|
||||
`rea_execute` accepts a JavaScript async function body. For example:
|
||||
|
||||
```js
|
||||
const session = json(await tools.binary_session({})).result;
|
||||
return { open: session.open, identity: session.server_identity };
|
||||
```
|
||||
|
||||
The returned projection is not a new Evidence record. `calls` preserves operation,
|
||||
argument/result digests, upstream errors and actual producer Evidence IDs when
|
||||
provided, including original `evidence_links`. The upstream ledger retains complete original Evidence for later views
|
||||
and exports. Non-Evidence session responses have no invented Evidence ID.
|
||||
Calls and executions sharing one active target are serialized; `Promise.all`
|
||||
currently provides composition rather than parallel provider execution.
|
||||
|
||||
`npm test` runs actual upstream and downstream stdio MCP against this checkout's
|
||||
compiled runtime. The standalone experiment's twelve tests cover discovery parity,
|
||||
original results/errors, bulk promise settlement, host capability absence, CPU
|
||||
deadline recovery, retained Evidence after an error or provider timeout,
|
||||
queued-call cancellation, unresolved promises and the
|
||||
two-tool downstream boundary, guest heap exhaustion/reuse, and SDK-converted
|
||||
schemas validated as [JSON Schema 2020-12](https://modelcontextprotocol.io/specification/2025-11-25/basic/index#schema-dialect).
|
||||
Execution rejects delays above Node's signed 32-bit timer range instead of
|
||||
allowing Node to coerce them to 1 ms. These tests do not establish real native/Android/
|
||||
firmware provider behavior. A lifecycle regression revealed that the QuickJS job
|
||||
pump can allocate an additional context: the execution scope now owns every
|
||||
context at creation and releases them before the runtime. Increasing memory did
|
||||
not fix that regression.
|
||||
|
||||
Install experimental dependencies separately with `npm ci` in this directory.
|
||||
They do not change the production package or lockfile. Use the repository's pinned
|
||||
Node and npm versions. No binary, provider database or raw third-party source is
|
||||
committed. Corpus identities and upstream download locations are in `corpus.json`.
|
||||
|
||||
Measure three baselines: normal full-catalog MCP, discovery-only MCP, and Code
|
||||
Mode. Count discovery, input code, intermediate results, final output, errors and
|
||||
retries. Report actual wall time separately from model-facing turns and local
|
||||
token estimates. Existing summary/page/workflow tools are fair alternatives;
|
||||
do not manufacture a gain by forcing full output on the direct-tool baseline.
|
||||
Replay measurements establish mechanism costs, not model task accuracy or
|
||||
inference latency. Real-agent trials must additionally verify supported facts,
|
||||
source identity, omissions and preserved Evidence references.
|
||||
|
||||
`benchmark.mjs` replays a fixed question against real providers with identical
|
||||
original arguments. For example:
|
||||
|
||||
```bash
|
||||
python3 resource-run.py -- node benchmark.mjs --artifact newpipe.apk \
|
||||
--mode bridge --artifact-root /absolute/path/artifacts \
|
||||
--providers /absolute/path/selected-provider-settings.json \
|
||||
--output /absolute/path/new-result-directory
|
||||
```
|
||||
|
||||
Modes are `direct`, `discovery` and `bridge`. The discovery mode isolates catalog
|
||||
cost while retaining original results; it is not a new production MCP client.
|
||||
Raw responses remain outside Git. Result summaries separate complete envelopes
|
||||
from single text content and count code explicitly. Measured findings and failures
|
||||
are recorded in [results.md](results.md).
|
||||
|
||||
`--artifact obsidian.asar --workflow obsidian-inventory` asks a distinct package
|
||||
inventory question using existing tools. It must not be compared as an equivalent
|
||||
completion of the JavaScript AST question. Workflow names and original arguments
|
||||
are retained in replay summaries.
|
||||
|
||||
`agent-trial.py` runs a real local Codex CLI with a disposable account/config and
|
||||
one MCP registration. It copies authentication only, removes the profile on normal
|
||||
completion, timeout and handled termination, and retains JSONL events, the answer
|
||||
and measured API usage outside the repo.
|
||||
It defaults to `gpt-5.6-terra` with high reasoning, as requested. Run one task under
|
||||
the resource wrapper:
|
||||
|
||||
```bash
|
||||
python3 resource-run.py -- python3 agent-trial.py \
|
||||
--mode direct --prompt /absolute/path/question.txt \
|
||||
--output /absolute/path/new-trial-directory
|
||||
```
|
||||
|
||||
Reproducible questions are in [questions/](questions/). Supply a template with
|
||||
`--prompt questions/android.txt --artifact-path /absolute/path/NewPipe_v0.29.1.apk`
|
||||
(or another matching corpus target). The runner preserves the supplied path in
|
||||
the rendered prompt and records template and rendered-prompt digests. Native's
|
||||
entry-address seed is derived from this corpus's jq header and is not reusable
|
||||
for another build. Run comparisons serially with the same question and provider
|
||||
settings; retain failures as well as successful repetitions.
|
||||
|
||||
Use `--mode bridge` for the experimental server; `--mode client-code` requests
|
||||
Codex's own Code Mode. Optional `--providers` selects a JSON object of explicit
|
||||
provider settings. No ambient environment snapshot is retained. A configured flag
|
||||
or a model's `tool_mode` does not prove the model actually composed tools or
|
||||
filtered an intermediate result. Inspect the transcript, and mark accuracy as
|
||||
unassessed until the requested facts and source support have been reviewed.
|
||||
`grade-trial.py` checks Android (default), managed, native, firmware or
|
||||
electron-inventory (`--task NAME`) against a successful
|
||||
real-provider replay and verifies cited IDs in producer values, their original
|
||||
Evidence links or the bridge audit. This verifies references observed in the
|
||||
transcript; the grader does not resolve every cited payload. It accepts
|
||||
JSON or one complete JSON fence, without repairing prose or partial values. This
|
||||
checks requested producer facts and reference existence, not runtime behavior or
|
||||
independent decompiler correctness. Unparseable result text is reported explicitly;
|
||||
IDs in a truncated JSON fragment cannot satisfy the automated reference check.
|
||||
The Android projection includes the provider's overload index, so a supported
|
||||
signature with that field omitted fails this stronger assertion.
|
||||
An omitted DEX descriptor differs from an explicit null, and booleans differ from
|
||||
numeric counts. Native containing-procedure reports compare resolved identity;
|
||||
the full analyzed procedure is verified separately.
|
||||
Firmware's run-local region UUIDs remain in answers but are excluded from parity
|
||||
against an independently acquired report. Classification evidence, explicit
|
||||
unknowns and coverage remain part of the requested fact checks.
|
||||
The question comparison accepts grouped original path records, extra known
|
||||
coverage counts and explicitly named firmware length/coverage report fields.
|
||||
Derived extent ends must agree with offset plus reported length. It verifies
|
||||
engine version and executable digest rather than display capitalization; a manual
|
||||
review is still needed for limitations and claims beyond these assertions.
|
||||
Grades record the grader source digest, and earlier strict grades are retained
|
||||
when the comparison changes. These are report parsers, not REA API aliases.
|
||||
|
||||
The completed serial cohort is in [agent-results.json](agent-results.json), with
|
||||
source/configuration fingerprints, measured API usage and sampled cgroup stats.
|
||||
The recommendation and migration boundaries are in [decision.md](decision.md).
|
||||
Raw transcripts and original replay replies remain outside Git.
|
||||
|
||||
The supplied feedback is a research lead. Its Ghidra timing claim also changed
|
||||
auto-analysis options, so it does not isolate Code Mode's effect. Relevant primary
|
||||
sources: [Cloudflare Code Mode](https://blog.cloudflare.com/code-mode/),
|
||||
[MCP patterns](https://developers.cloudflare.com/agents/model-context-protocol/codemode/),
|
||||
[Anthropic code execution](https://www.anthropic.com/engineering/code-execution-with-mcp),
|
||||
and [QuickJS WASM](https://github.com/justjake/quickjs-emscripten).
|
||||
|
||||
Run expensive commands with `python3 resource-run.py -- COMMAND ARGS...` on Linux
|
||||
with a user systemd manager. It uses a transient scope with one CPU, a 4 GiB hard
|
||||
memory ceiling, a 3 GiB early-stop threshold, no swap, and a task ceiling. It
|
||||
refuses to start when less than 8 GiB is available and stops if host available
|
||||
memory drops below 4 GiB. These limits protect the host; a resource abort is a
|
||||
failed measurement, never a successful benchmark. Also cap Node and Java heaps.
|
||||
The task ceiling is 192. The wrapper records observed memory/task events and
|
||||
stops a trial that touches the task limit. A limit event invalidates a measurement
|
||||
even when the application later returns successfully.
|
||||
@@ -0,0 +1,995 @@
|
||||
{
|
||||
"baseline_sha": "fb94d9dde0ccda83ae520ac6fb22b1d283562cf3",
|
||||
"scope": "real_cli_requested_fact_parity_and_observed_references_not_independent_runtime_truth",
|
||||
"runtime_source_commit": "22326e8199975f31d8b7c99aaf5fd64140fbe00e",
|
||||
"artifact_ids": "corpus.json",
|
||||
"resource_limits": {
|
||||
"cpu_affinity": [15],
|
||||
"cpu_quota_percent": 100,
|
||||
"hard_memory_bytes": 4294967296,
|
||||
"early_stop_bytes": 3221225472,
|
||||
"tasks_max": 192
|
||||
},
|
||||
"notes": [
|
||||
"Profiles differ in required mode-specific instructions; original model-selected arguments may differ.",
|
||||
"Two Android/managed repetitions are descriptive samples, without confidence intervals or a causal speed claim.",
|
||||
"Managed classification container omissions fail a stronger complete-object assertion; remaining requested facts match.",
|
||||
"Normal-MCP managed JSONL truncation leaves one ID unavailable to the complete-envelope reference parser.",
|
||||
"Uncached input is input minus cached input, not a currency cost estimate."
|
||||
],
|
||||
"trials": [
|
||||
{
|
||||
"trial": "android-direct-5",
|
||||
"task": "android",
|
||||
"mode": "direct",
|
||||
"artifact": "newpipe.apk",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 67.329,
|
||||
"input_tokens": 107007,
|
||||
"cached_input_tokens": 83712,
|
||||
"uncached_input_tokens": 23295,
|
||||
"output_tokens": 1670,
|
||||
"completed_mcp_calls": 3,
|
||||
"original_operations": [
|
||||
"inspect_android_package",
|
||||
"search_android_classes",
|
||||
"inspect_android_class"
|
||||
],
|
||||
"fact_assertions": 6,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "7129418f2d4428d86cc737124b53636ba4a46de522e702e8861180fc175acb23",
|
||||
"developer_instructions_sha256": "eaddc85aa5b141538ec280d640c62db799231b394679a17f2e6cc6bc2146a95c",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "3424dd6ba375f96d88fd0ac38102998948fa0fc46e08af8fae414572ba97a62f",
|
||||
"peak_memory_bytes_sampled": 1336168448,
|
||||
"peak_tasks_sampled": 108,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 23974584,
|
||||
"user_usec": 21485565,
|
||||
"system_usec": 2489019,
|
||||
"nr_periods": 636,
|
||||
"nr_throttled": 45,
|
||||
"throttled_usec": 67840,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "android-bridge-7",
|
||||
"task": "android",
|
||||
"mode": "bridge",
|
||||
"artifact": "newpipe.apk",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 48.231,
|
||||
"input_tokens": 113256,
|
||||
"cached_input_tokens": 91648,
|
||||
"uncached_input_tokens": 21608,
|
||||
"output_tokens": 1456,
|
||||
"completed_mcp_calls": 3,
|
||||
"original_operations": [
|
||||
"inspect_android_package",
|
||||
"search_android_classes",
|
||||
"inspect_android_class"
|
||||
],
|
||||
"fact_assertions": 6,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "7129418f2d4428d86cc737124b53636ba4a46de522e702e8861180fc175acb23",
|
||||
"developer_instructions_sha256": "247ca3314340f748d1479cc1a0a1f33a234231f17b5768044034bdfe517299c6",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "27e234285e493b50929c0f98938284771d0d4a3787045872f2f517b2297aefeb",
|
||||
"peak_memory_bytes_sampled": 1659629568,
|
||||
"peak_tasks_sampled": 126,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 27717810,
|
||||
"user_usec": 25042151,
|
||||
"system_usec": 2675658,
|
||||
"nr_periods": 458,
|
||||
"nr_throttled": 50,
|
||||
"throttled_usec": 73614,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "managed-bridge-2",
|
||||
"task": "managed",
|
||||
"mode": "bridge",
|
||||
"artifact": "sharex.dll",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 73.145,
|
||||
"input_tokens": 116974,
|
||||
"cached_input_tokens": 92672,
|
||||
"uncached_input_tokens": 24302,
|
||||
"output_tokens": 2065,
|
||||
"completed_mcp_calls": 3,
|
||||
"original_operations": [
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_native_boundaries"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": ["classification"],
|
||||
"strict_grade_passed": false,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "fe5a74687126fca7aee78b3ba51f03401983ffeae593924b7c738d50d0f04e61",
|
||||
"developer_instructions_sha256": "247ca3314340f748d1479cc1a0a1f33a234231f17b5768044034bdfe517299c6",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "06cbdda60ac930d00ade7e115cd4ba234527e297e6cfd931eedc175e00efa1c7",
|
||||
"peak_memory_bytes_sampled": 939290624,
|
||||
"peak_tasks_sampled": 103,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 12114716,
|
||||
"user_usec": 9701516,
|
||||
"system_usec": 2413200,
|
||||
"nr_periods": 630,
|
||||
"nr_throttled": 16,
|
||||
"throttled_usec": 15944,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "managed-direct-2",
|
||||
"task": "managed",
|
||||
"mode": "direct",
|
||||
"artifact": "sharex.dll",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 33.15,
|
||||
"input_tokens": 108096,
|
||||
"cached_input_tokens": 75776,
|
||||
"uncached_input_tokens": 32320,
|
||||
"output_tokens": 2315,
|
||||
"completed_mcp_calls": 2,
|
||||
"original_operations": [
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_native_boundaries"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": false,
|
||||
"cited_ids_observed": false,
|
||||
"unreadable_result_texts": 1,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "fe5a74687126fca7aee78b3ba51f03401983ffeae593924b7c738d50d0f04e61",
|
||||
"developer_instructions_sha256": "eaddc85aa5b141538ec280d640c62db799231b394679a17f2e6cc6bc2146a95c",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "5d6957541bd979252d019817e631f5d889ff2dc3efb7f03b9de1033fe34d8430",
|
||||
"peak_memory_bytes_sampled": 688373760,
|
||||
"peak_tasks_sampled": 108,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 5863642,
|
||||
"user_usec": 4359666,
|
||||
"system_usec": 1503976,
|
||||
"nr_periods": 282,
|
||||
"nr_throttled": 4,
|
||||
"throttled_usec": 6216,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "electron-inventory-direct-1",
|
||||
"task": "electron-inventory",
|
||||
"mode": "direct",
|
||||
"artifact": "obsidian.asar",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 89.494,
|
||||
"input_tokens": 266121,
|
||||
"cached_input_tokens": 238848,
|
||||
"uncached_input_tokens": 27273,
|
||||
"output_tokens": 3215,
|
||||
"completed_mcp_calls": 3,
|
||||
"original_operations": [
|
||||
"open_binary",
|
||||
"inspect_artifact",
|
||||
"close_binary"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "22941dc5d77ce06824e40b01abd3f725aa773d131648d7bba2e607b410ccbef9",
|
||||
"developer_instructions_sha256": "eaddc85aa5b141538ec280d640c62db799231b394679a17f2e6cc6bc2146a95c",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "fcd0df1a3ae0b5103143addaa4922323354ddee7ba3d0e3020de64f05b6fe3d3",
|
||||
"peak_memory_bytes_sampled": 703275008,
|
||||
"peak_tasks_sampled": 115,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 7720494,
|
||||
"user_usec": 5691854,
|
||||
"system_usec": 2028640,
|
||||
"nr_periods": 782,
|
||||
"nr_throttled": 7,
|
||||
"throttled_usec": 15453,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "electron-inventory-bridge-1",
|
||||
"task": "electron-inventory",
|
||||
"mode": "bridge",
|
||||
"artifact": "obsidian.asar",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 145.577,
|
||||
"input_tokens": 462809,
|
||||
"cached_input_tokens": 407040,
|
||||
"uncached_input_tokens": 55769,
|
||||
"output_tokens": 5624,
|
||||
"completed_mcp_calls": 8,
|
||||
"original_operations": [
|
||||
"open_binary",
|
||||
"inspect_artifact",
|
||||
"close_binary",
|
||||
"open_binary",
|
||||
"inspect_artifact",
|
||||
"close_binary"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "22941dc5d77ce06824e40b01abd3f725aa773d131648d7bba2e607b410ccbef9",
|
||||
"developer_instructions_sha256": "247ca3314340f748d1479cc1a0a1f33a234231f17b5768044034bdfe517299c6",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "ab7e25e4345a0e9bba6a06bb0a79aa39e1bf1308b6a66f8173a62bbc9e2d9fc9",
|
||||
"peak_memory_bytes_sampled": 1029238784,
|
||||
"peak_tasks_sampled": 112,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 14650718,
|
||||
"user_usec": 12106416,
|
||||
"system_usec": 2544302,
|
||||
"nr_periods": 1224,
|
||||
"nr_throttled": 24,
|
||||
"throttled_usec": 30318,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "firmware-bridge-1",
|
||||
"task": "firmware",
|
||||
"mode": "bridge",
|
||||
"artifact": "openwrt.bin",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 81.944,
|
||||
"input_tokens": 152886,
|
||||
"cached_input_tokens": 126976,
|
||||
"uncached_input_tokens": 25910,
|
||||
"output_tokens": 2192,
|
||||
"completed_mcp_calls": 3,
|
||||
"original_operations": ["inspect_firmware_regions"],
|
||||
"fact_assertions": 5,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "93c94e26dc4a4b1985b3f8c4246ca5aff886f75dc2349a717ee940243dc73b80",
|
||||
"developer_instructions_sha256": "247ca3314340f748d1479cc1a0a1f33a234231f17b5768044034bdfe517299c6",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "a5604d0d3140f6232851a81a952e51daf43c8e7f12ba062fac68a8fae0bef708",
|
||||
"peak_memory_bytes_sampled": 927666176,
|
||||
"peak_tasks_sampled": 104,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 11575373,
|
||||
"user_usec": 9366624,
|
||||
"system_usec": 2208749,
|
||||
"nr_periods": 744,
|
||||
"nr_throttled": 18,
|
||||
"throttled_usec": 26000,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "firmware-direct-1",
|
||||
"task": "firmware",
|
||||
"mode": "direct",
|
||||
"artifact": "openwrt.bin",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 30.161,
|
||||
"input_tokens": 103224,
|
||||
"cached_input_tokens": 74752,
|
||||
"uncached_input_tokens": 28472,
|
||||
"output_tokens": 1761,
|
||||
"completed_mcp_calls": 1,
|
||||
"original_operations": ["inspect_firmware_regions"],
|
||||
"fact_assertions": 5,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "93c94e26dc4a4b1985b3f8c4246ca5aff886f75dc2349a717ee940243dc73b80",
|
||||
"developer_instructions_sha256": "eaddc85aa5b141538ec280d640c62db799231b394679a17f2e6cc6bc2146a95c",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "c0bbd1aec08c9c30061f92064355c3213f680fab545a050271096e85db771a71",
|
||||
"peak_memory_bytes_sampled": 686739456,
|
||||
"peak_tasks_sampled": 109,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 5700866,
|
||||
"user_usec": 4167696,
|
||||
"system_usec": 1533169,
|
||||
"nr_periods": 266,
|
||||
"nr_throttled": 6,
|
||||
"throttled_usec": 3978,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "native-direct-1",
|
||||
"task": "native",
|
||||
"mode": "direct",
|
||||
"artifact": "jq",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 327.15,
|
||||
"input_tokens": 151343,
|
||||
"cached_input_tokens": 122368,
|
||||
"uncached_input_tokens": 28975,
|
||||
"output_tokens": 2458,
|
||||
"completed_mcp_calls": 4,
|
||||
"original_operations": [
|
||||
"open_binary",
|
||||
"resolve_containing_procedure",
|
||||
"analyze_function",
|
||||
"close_binary"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "234bfe93d09f9c76c1a64383c11cf7c174eafa4e4ad563124f705a7d5c8920aa",
|
||||
"developer_instructions_sha256": "eaddc85aa5b141538ec280d640c62db799231b394679a17f2e6cc6bc2146a95c",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "681f38e4ba9b441e7816e868b30e347005fe074907349cd0022fc44302c5b14a",
|
||||
"peak_memory_bytes_sampled": 1328496640,
|
||||
"peak_tasks_sampled": 109,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 263737951,
|
||||
"user_usec": 256864795,
|
||||
"system_usec": 6873156,
|
||||
"nr_periods": 3192,
|
||||
"nr_throttled": 456,
|
||||
"throttled_usec": 756678,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "native-bridge-1",
|
||||
"task": "native",
|
||||
"mode": "bridge",
|
||||
"artifact": "jq",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 334.642,
|
||||
"input_tokens": 285961,
|
||||
"cached_input_tokens": 245504,
|
||||
"uncached_input_tokens": 40457,
|
||||
"output_tokens": 2129,
|
||||
"completed_mcp_calls": 4,
|
||||
"original_operations": [
|
||||
"open_binary",
|
||||
"analyze_function",
|
||||
"close_binary"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "234bfe93d09f9c76c1a64383c11cf7c174eafa4e4ad563124f705a7d5c8920aa",
|
||||
"developer_instructions_sha256": "247ca3314340f748d1479cc1a0a1f33a234231f17b5768044034bdfe517299c6",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "6c64953708df4a06b69fb57e5d70141147e4495adabd7340e87e727fcd340742",
|
||||
"peak_memory_bytes_sampled": 1441775616,
|
||||
"peak_tasks_sampled": 123,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 271095966,
|
||||
"user_usec": 263888347,
|
||||
"system_usec": 7207618,
|
||||
"nr_periods": 3267,
|
||||
"nr_throttled": 450,
|
||||
"throttled_usec": 784719,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "android-bridge-8",
|
||||
"task": "android",
|
||||
"mode": "bridge",
|
||||
"artifact": "newpipe.apk",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 134.555,
|
||||
"input_tokens": 159370,
|
||||
"cached_input_tokens": 134400,
|
||||
"uncached_input_tokens": 24970,
|
||||
"output_tokens": 3250,
|
||||
"completed_mcp_calls": 7,
|
||||
"original_operations": [
|
||||
"inspect_android_package",
|
||||
"search_android_classes",
|
||||
"inspect_android_class",
|
||||
"inspect_android_package",
|
||||
"inspect_android_package",
|
||||
"search_android_classes",
|
||||
"inspect_android_class"
|
||||
],
|
||||
"fact_assertions": 6,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "7129418f2d4428d86cc737124b53636ba4a46de522e702e8861180fc175acb23",
|
||||
"developer_instructions_sha256": "247ca3314340f748d1479cc1a0a1f33a234231f17b5768044034bdfe517299c6",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "f1743a341d334bd41760b54980aa70d5f3c8c1aed57c3ae9e470be63449c594d",
|
||||
"peak_memory_bytes_sampled": 1659744256,
|
||||
"peak_tasks_sampled": 115,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 29619173,
|
||||
"user_usec": 26495167,
|
||||
"system_usec": 3124005,
|
||||
"nr_periods": 1229,
|
||||
"nr_throttled": 60,
|
||||
"throttled_usec": 84762,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "android-direct-6",
|
||||
"task": "android",
|
||||
"mode": "direct",
|
||||
"artifact": "newpipe.apk",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 71.624,
|
||||
"input_tokens": 153031,
|
||||
"cached_input_tokens": 117504,
|
||||
"uncached_input_tokens": 35527,
|
||||
"output_tokens": 1501,
|
||||
"completed_mcp_calls": 3,
|
||||
"original_operations": [
|
||||
"inspect_android_package",
|
||||
"search_android_classes",
|
||||
"inspect_android_class"
|
||||
],
|
||||
"fact_assertions": 6,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "7129418f2d4428d86cc737124b53636ba4a46de522e702e8861180fc175acb23",
|
||||
"developer_instructions_sha256": "eaddc85aa5b141538ec280d640c62db799231b394679a17f2e6cc6bc2146a95c",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "b8d0422eec6bfd31a9a6ed79ac29e9487c2e68c47ff200904ec239daee9720c3",
|
||||
"peak_memory_bytes_sampled": 1348767744,
|
||||
"peak_tasks_sampled": 115,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 23033331,
|
||||
"user_usec": 20566262,
|
||||
"system_usec": 2467069,
|
||||
"nr_periods": 674,
|
||||
"nr_throttled": 40,
|
||||
"throttled_usec": 61573,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "managed-direct-3",
|
||||
"task": "managed",
|
||||
"mode": "direct",
|
||||
"artifact": "sharex.dll",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 75.229,
|
||||
"input_tokens": 108593,
|
||||
"cached_input_tokens": 75776,
|
||||
"uncached_input_tokens": 32817,
|
||||
"output_tokens": 2774,
|
||||
"completed_mcp_calls": 2,
|
||||
"original_operations": [
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_native_boundaries"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": false,
|
||||
"cited_ids_observed": false,
|
||||
"unreadable_result_texts": 1,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "fe5a74687126fca7aee78b3ba51f03401983ffeae593924b7c738d50d0f04e61",
|
||||
"developer_instructions_sha256": "eaddc85aa5b141538ec280d640c62db799231b394679a17f2e6cc6bc2146a95c",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "64f0769b7ad83f8d86c005a48de7a0cf397043d77378983fabfaa226dcb70f87",
|
||||
"peak_memory_bytes_sampled": 651026432,
|
||||
"peak_tasks_sampled": 110,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 6252846,
|
||||
"user_usec": 4688766,
|
||||
"system_usec": 1564079,
|
||||
"nr_periods": 635,
|
||||
"nr_throttled": 7,
|
||||
"throttled_usec": 11469,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "managed-bridge-3",
|
||||
"task": "managed",
|
||||
"mode": "bridge",
|
||||
"artifact": "sharex.dll",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 40.609,
|
||||
"input_tokens": 125519,
|
||||
"cached_input_tokens": 96000,
|
||||
"uncached_input_tokens": 29519,
|
||||
"output_tokens": 2274,
|
||||
"completed_mcp_calls": 3,
|
||||
"original_operations": [
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_native_boundaries"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": ["classification"],
|
||||
"strict_grade_passed": false,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "fe5a74687126fca7aee78b3ba51f03401983ffeae593924b7c738d50d0f04e61",
|
||||
"developer_instructions_sha256": "247ca3314340f748d1479cc1a0a1f33a234231f17b5768044034bdfe517299c6",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "5fbc3cf0b41224f2ef44ab996b174c8a48fe07a818ac2ad334ef59e5ddf26de6",
|
||||
"peak_memory_bytes_sampled": 1005625344,
|
||||
"peak_tasks_sampled": 113,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 11185145,
|
||||
"user_usec": 9300437,
|
||||
"system_usec": 1884707,
|
||||
"nr_periods": 366,
|
||||
"nr_throttled": 21,
|
||||
"throttled_usec": 24147,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "electron-inventory-guided-bridge-1",
|
||||
"task": "electron-inventory",
|
||||
"mode": "bridge",
|
||||
"artifact": "obsidian.asar",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 85.909,
|
||||
"input_tokens": 101994,
|
||||
"cached_input_tokens": 61440,
|
||||
"uncached_input_tokens": 40554,
|
||||
"output_tokens": 2563,
|
||||
"completed_mcp_calls": 3,
|
||||
"original_operations": [
|
||||
"open_binary",
|
||||
"inspect_artifact",
|
||||
"close_binary"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "f070cdf721805d590c1bd5255ecf1f647ea165885cd5052af35c30ba531da815",
|
||||
"developer_instructions_sha256": "247ca3314340f748d1479cc1a0a1f33a234231f17b5768044034bdfe517299c6",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "566391b8a6db73228fec00dc7cbf0eaf56ed899178a405d5d8f0c8002fb05d89",
|
||||
"peak_memory_bytes_sampled": 1007996928,
|
||||
"peak_tasks_sampled": 109,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 13304542,
|
||||
"user_usec": 10842042,
|
||||
"system_usec": 2462499,
|
||||
"nr_periods": 787,
|
||||
"nr_throttled": 19,
|
||||
"throttled_usec": 27407,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"trial": "electron-inventory-guided-direct-1",
|
||||
"task": "electron-inventory",
|
||||
"mode": "direct",
|
||||
"artifact": "obsidian.asar",
|
||||
"requested_model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"codex_version": "codex-cli 0.161.0",
|
||||
"wall_seconds": 67.54,
|
||||
"input_tokens": 263484,
|
||||
"cached_input_tokens": 221952,
|
||||
"uncached_input_tokens": 41532,
|
||||
"output_tokens": 2499,
|
||||
"completed_mcp_calls": 4,
|
||||
"original_operations": [
|
||||
"open_binary",
|
||||
"inspect_artifact",
|
||||
"inspect_artifact",
|
||||
"close_binary"
|
||||
],
|
||||
"fact_assertions": 7,
|
||||
"fact_mismatches": [],
|
||||
"strict_grade_passed": true,
|
||||
"cited_ids_observed": true,
|
||||
"unreadable_result_texts": 0,
|
||||
"grader_sha256": "707e4eef05634db5075ac64382500b9eea03b38f828358315e54381f9d4a8979",
|
||||
"prompt_sha256": "f070cdf721805d590c1bd5255ecf1f647ea165885cd5052af35c30ba531da815",
|
||||
"developer_instructions_sha256": "eaddc85aa5b141538ec280d640c62db799231b394679a17f2e6cc6bc2146a95c",
|
||||
"provider_settings_sha256": "1db4e19adcb32bb64a3baad58a1b4ef04b52ba648bab29678693f9ab8dbdabb4",
|
||||
"experiment_sources_sha256": {
|
||||
"agent-trial.py": "3c96d0f7b4b3dd02d168258d8aedd79049bb8647f1ebb174b056bd8eaa582502",
|
||||
"bridge.mjs": "c1afb88a3045f233006826ffd466590b33027cc7b95f9c6c551ba1aa93fa4727",
|
||||
"package-lock.json": "6dab2b2dba5101992e9a881ce1820475bd304511dc44362bb1614b08e9b37a87"
|
||||
},
|
||||
"events_sha256": "564711cf28211bb7059a04b919f1b6925a816ded3e2dbeb9678382568f700e58",
|
||||
"peak_memory_bytes_sampled": 728817664,
|
||||
"peak_tasks_sampled": 114,
|
||||
"cpu_stat_last_observed": {
|
||||
"usage_usec": 8708686,
|
||||
"user_usec": 6609914,
|
||||
"system_usec": 2098771,
|
||||
"nr_periods": 597,
|
||||
"nr_throttled": 12,
|
||||
"throttled_usec": 19564,
|
||||
"nr_bursts": 0,
|
||||
"burst_usec": 0
|
||||
},
|
||||
"resource_abort": null,
|
||||
"resource_events_last_observed": {
|
||||
"memory.events": {
|
||||
"low": 0,
|
||||
"high": 0,
|
||||
"max": 0,
|
||||
"oom": 0,
|
||||
"oom_kill": 0,
|
||||
"oom_group_kill": 0
|
||||
},
|
||||
"pids.events": {
|
||||
"max": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
"""Evaluate one real Codex CLI task with an isolated REA MCP registration."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--mode', choices=['direct', 'client-code', 'bridge'], required=True)
|
||||
parser.add_argument('--prompt', type=Path, required=True)
|
||||
parser.add_argument('--artifact-path', help='Replace {{artifact_path}} in a question template')
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
parser.add_argument('--model', default='gpt-5.6-terra')
|
||||
parser.add_argument('--reasoning', default='high')
|
||||
parser.add_argument('--timeout', type=int, default=900)
|
||||
parser.add_argument('--codex', default='/usr/bin/codex')
|
||||
parser.add_argument('--providers', type=Path)
|
||||
args = parser.parse_args()
|
||||
def terminate(signum, _frame):
|
||||
# The outer resource scope terminates descendants together. Ensure the
|
||||
# runner's copied credentials are also removed on a resource abort.
|
||||
raise SystemExit(128 + signum)
|
||||
signal.signal(signal.SIGTERM, terminate)
|
||||
repository = Path(__file__).resolve().parents[2]
|
||||
experiment = Path(__file__).resolve().parent
|
||||
node = shutil.which('node')
|
||||
if subprocess.check_output([node, '--version'], text=True).strip() != 'v24.18.0':
|
||||
raise SystemExit('Use pinned Node 24.18.0 on PATH')
|
||||
args.output.mkdir(parents=True, exist_ok=False, mode=0o700)
|
||||
profile = Path(tempfile.mkdtemp(prefix='profile-', dir=args.output))
|
||||
trial_codex_home = profile / 'codex'
|
||||
account = profile / 'account'
|
||||
workspace = profile / 'workspace'
|
||||
for directory in [trial_codex_home, account, workspace]:
|
||||
directory.mkdir(mode=0o700)
|
||||
source_auth = Path(os.environ.get('CODEX_HOME', str(Path.home() / '.codex'))) / 'auth.json'
|
||||
auth = trial_codex_home / 'auth.json'
|
||||
started = time.monotonic()
|
||||
timeout = False
|
||||
exit_code = None
|
||||
environment = dict(os.environ)
|
||||
environment.update({
|
||||
'CODEX_HOME': str(trial_codex_home), 'HOME': str(account),
|
||||
'XDG_CONFIG_HOME': str(account / '.config'),
|
||||
'XDG_CACHE_HOME': str(account / '.cache'),
|
||||
'XDG_DATA_HOME': str(account / '.local/share'),
|
||||
'NODE_OPTIONS': '--max-old-space-size=512',
|
||||
})
|
||||
provider_env = {'REA_LOG_LEVEL': 'silent', 'NODE_OPTIONS': '--max-old-space-size=512'}
|
||||
if args.providers:
|
||||
# This explicit caller file supplies only selected provider settings.
|
||||
provider_env.update(json.loads(args.providers.read_text()))
|
||||
command_args = [str(repository / 'scripts/rea.mjs'), 'mcp']
|
||||
if args.mode == 'bridge':
|
||||
command_args = [str(experiment / 'bridge.mjs'), node, *command_args]
|
||||
instruction = (
|
||||
'Use the registered REA MCP server for this investigation. Static analysis '
|
||||
'only: do not execute target code, fetch URLs, launch a GUI, or modify targets. '
|
||||
'Preserve observations, derivations, inferences, limitations, and real Evidence IDs. '
|
||||
'An Evidence value has evidence_id, normalized_result and limitations; '
|
||||
'non-Evidence session values have result. '
|
||||
'The analyzed target digest is Evidence.subject.digest.sha256. '
|
||||
'normalized_result.engine.artifact_sha256 identifies the analysis engine '
|
||||
'artifact and must never be substituted for the target digest. '
|
||||
'Prefer summary/focused views where appropriate. Do not invent successful facts '
|
||||
'after a tool error. No other agents. Stop once the requested facts are supported.'
|
||||
)
|
||||
if args.mode == 'bridge':
|
||||
instruction += (
|
||||
' Original REA operations are available through rea_search and rea_execute. '
|
||||
'Discover tool names/schemas on demand. Execute JavaScript bodies using '
|
||||
'tools.<original_name>(original_arguments), json(reply) to parse the original '
|
||||
'MCP result, and return only the needed facts plus their Evidence IDs and '
|
||||
'limitations. Compose known dependent calls in one script when useful. '
|
||||
'Select needed fields from normalized_result inside the script; do not '
|
||||
'return entire Evidence, raw_result or full method/class inventories when '
|
||||
'the question requests a subset. '
|
||||
'Await every call; the upstream session persists across scripts.'
|
||||
)
|
||||
if args.mode == 'client-code':
|
||||
instruction += (
|
||||
' Use the client JavaScript executor to compose registered MCP calls when '
|
||||
'possible and select the requested normalized_result fields before returning '
|
||||
'intermediate output to model context. Keep real Evidence IDs and limitations.'
|
||||
)
|
||||
quote = json.dumps
|
||||
config = '\n'.join([
|
||||
'approval_policy = "never"', 'sandbox_mode = "read-only"',
|
||||
'web_search = "disabled"', 'model = ' + quote(args.model),
|
||||
'model_reasoning_effort = ' + quote(args.reasoning),
|
||||
'developer_instructions = ' + quote(instruction),
|
||||
'[features]', 'multi_agent = false', 'shell_snapshot = false',
|
||||
'shell_tool = false', 'unified_exec = false',
|
||||
'code_mode = ' + ('true' if args.mode == 'client-code' else 'false'),
|
||||
'code_mode_only = ' + ('true' if args.mode == 'client-code' else 'false'),
|
||||
'[history]', 'persistence = "none"',
|
||||
'[mcp_servers.rea]', 'command = ' + quote(node),
|
||||
'args = ' + quote(command_args), 'startup_timeout_sec = 60',
|
||||
'tool_timeout_sec = 600', '[mcp_servers.rea.env]',
|
||||
*[key + ' = ' + quote(value) for key, value in provider_env.items()],
|
||||
'[mcp_servers.rea.tools.rea_execute]', 'approval_mode = "approve"',
|
||||
]) + '\n'
|
||||
# MCP read-only hints cover normal static tools. Explicitly authorize only
|
||||
# the session-opening primitive needed by the requested native tasks.
|
||||
config += '[mcp_servers.rea.tools.open_binary]\napproval_mode = "approve"\n'
|
||||
for tool in ['analyze_javascript_application', 'inspect_analysis_view',
|
||||
'inspect_android_package', 'search_android_classes',
|
||||
'inspect_android_class', 'inspect_android_method',
|
||||
'inspect_firmware_regions', 'inspect_managed_artifact',
|
||||
'inspect_managed_members', 'inspect_managed_native_boundaries',
|
||||
'inspect_artifact', 'inspect_wasm_artifact',
|
||||
'resolve_containing_procedure', 'analyze_function', 'close_binary']:
|
||||
config += '[mcp_servers.rea.tools.' + tool + ']\napproval_mode = "approve"\n'
|
||||
try:
|
||||
if source_auth.exists():
|
||||
shutil.copyfile(source_auth, auth)
|
||||
auth.chmod(0o600)
|
||||
(trial_codex_home / 'config.toml').write_text(config)
|
||||
(trial_codex_home / 'config.toml').chmod(0o600)
|
||||
prompt = args.prompt.read_text()
|
||||
template_digest = hashlib.sha256(prompt.encode()).hexdigest()
|
||||
if '{{artifact_path}}' in prompt:
|
||||
if not args.artifact_path or not os.path.isabs(args.artifact_path):
|
||||
raise ValueError('A question template requires an absolute --artifact-path')
|
||||
prompt = prompt.replace('{{artifact_path}}', args.artifact_path)
|
||||
elif args.artifact_path:
|
||||
raise ValueError('--artifact-path requires the literal template marker')
|
||||
(args.output / 'prompt.txt').write_text(prompt)
|
||||
(args.output / 'trial-settings.json').write_text(json.dumps({
|
||||
'mode': args.mode, 'model': args.model, 'reasoning': args.reasoning,
|
||||
'experiment_commit': subprocess.check_output(
|
||||
['git', 'rev-parse', 'HEAD'], cwd=repository, text=True).strip(),
|
||||
'experiment_sources_sha256': {
|
||||
name: hashlib.sha256((experiment / name).read_bytes()).hexdigest()
|
||||
for name in ['agent-trial.py', 'bridge.mjs', 'package-lock.json']
|
||||
},
|
||||
'developer_instructions': instruction,
|
||||
'developer_instructions_sha256': hashlib.sha256(instruction.encode()).hexdigest(),
|
||||
'prompt_sha256': hashlib.sha256(prompt.encode()).hexdigest(),
|
||||
'question_template_sha256': template_digest,
|
||||
'provider_settings_sha256': hashlib.sha256(
|
||||
json.dumps(provider_env, sort_keys=True).encode()).hexdigest(),
|
||||
'features': {'shell_tool': False, 'unified_exec': False,
|
||||
'multi_agent': False, 'code_mode': args.mode == 'client-code',
|
||||
'code_mode_only': args.mode == 'client-code'},
|
||||
}, indent=2) + '\n')
|
||||
invocation = [args.codex, '--no-daemon', 'exec', '--ephemeral',
|
||||
'--skip-git-repo-check', '--ignore-rules', '--json',
|
||||
'--color', 'never', '-C', str(workspace),
|
||||
'--output-last-message', str(args.output.resolve() / 'answer.txt'), '-']
|
||||
with (args.output / 'events.jsonl').open('w') as events, (args.output / 'stderr.log').open('w') as errors:
|
||||
child = subprocess.Popen(invocation, env=environment, cwd=workspace,
|
||||
stdin=subprocess.PIPE, stdout=events, stderr=errors, text=True)
|
||||
child.stdin.write(prompt)
|
||||
child.stdin.close()
|
||||
try:
|
||||
exit_code = child.wait(timeout=args.timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
timeout = True
|
||||
child.terminate()
|
||||
try:
|
||||
exit_code = child.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
child.kill()
|
||||
exit_code = child.wait()
|
||||
usage = []
|
||||
tools = []
|
||||
event_types = {}
|
||||
for line in (args.output / 'events.jsonl').read_text().splitlines():
|
||||
event = json.loads(line)
|
||||
kind = event.get('type', 'unknown')
|
||||
event_types[kind] = event_types.get(kind, 0) + 1
|
||||
if 'usage' in event:
|
||||
usage.append(event['usage'])
|
||||
item = event.get('item', {})
|
||||
if item.get('type') == 'mcp_tool_call' and kind == 'item.completed':
|
||||
tools.append({k: item.get(k) for k in ['server', 'tool', 'arguments', 'status', 'error']})
|
||||
summary = {
|
||||
'mode': args.mode, 'requested_model': args.model, 'reasoning': args.reasoning,
|
||||
'baseline_sha': 'fb94d9dde0ccda83ae520ac6fb22b1d283562cf3',
|
||||
'codex_version': subprocess.check_output([args.codex, '--version'], text=True).strip(),
|
||||
'exit_code': exit_code, 'timeout': timeout,
|
||||
'wall_seconds': round(time.monotonic() - started, 3),
|
||||
'usage': usage, 'completed_mcp_calls': tools, 'event_types': event_types,
|
||||
'factual_correctness': 'not_assessed',
|
||||
'scope': 'real_cli_trial_requires_transcript_and_fact_review',
|
||||
}
|
||||
(args.output / 'summary.json').write_text(json.dumps(summary, indent=2) + '\n')
|
||||
print(json.dumps(summary))
|
||||
finally:
|
||||
# Always remove copied credentials, config, caches and profile state.
|
||||
shutil.rmtree(profile)
|
||||
raise SystemExit(124 if timeout else exit_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,369 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { resolve, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs } from "node:util";
|
||||
import { getEncoding } from "js-tiktoken";
|
||||
import { CodeModeBridge, connectUpstream, json } from "./bridge.mjs";
|
||||
|
||||
// Replay isolates transport/discovery costs. It does not simulate model reasoning.
|
||||
const workflows = {
|
||||
"sharex.dll": {
|
||||
tools: ["inspect_managed_artifact", "inspect_managed_native_boundaries"],
|
||||
question:
|
||||
"Report assembly/module identity, target framework and declared P/Invoke libraries with their uncertainty.",
|
||||
code: `
|
||||
const artifact = await call("inspect_managed_artifact", {path: input.path});
|
||||
const boundaries = await call("inspect_managed_native_boundaries", {});
|
||||
const n = artifact.normalized_result;
|
||||
return {artifact_sha256: artifact.subject?.digest.sha256,
|
||||
classification: n.classification, assembly: n.assembly, module: n.module,
|
||||
target_frameworks: n.target_frameworks, coverage: n.coverage,
|
||||
native_boundaries: boundaries.normalized_result,
|
||||
evidence_ids: [artifact.evidence_id, boundaries.evidence_id],
|
||||
limitations: [...artifact.limitations, ...boundaries.limitations]};`,
|
||||
},
|
||||
"sqlite.wasm": {
|
||||
tools: ["inspect_wasm_artifact"],
|
||||
question:
|
||||
"Report exact WASM validation, all exported names and runtime/network observation states without executing it.",
|
||||
code: `
|
||||
const wasm = await call("inspect_wasm_artifact", {path: input.path});
|
||||
const n = wasm.normalized_result;
|
||||
return {artifact_sha256: wasm.subject?.digest.sha256,
|
||||
validation: n.validation, sections: n.sections, exports: n.exports,
|
||||
runtime_execution: n.runtime_execution, network_fetch: n.network_fetch,
|
||||
evidence_ids: [wasm.evidence_id], limitations: [...wasm.limitations, ...n.limitations]};`,
|
||||
},
|
||||
"obsidian.asar": {
|
||||
tools: ["analyze_javascript_application", "inspect_analysis_view"],
|
||||
question:
|
||||
"Report application identity, analysis coverage/counts and the first eight module identities.",
|
||||
code: `
|
||||
const summary = await call("analyze_javascript_application", {
|
||||
input_path: input.path, detail: "summary", max_heap_mb: 1024,
|
||||
analysis_timeout_ms: 300000, integrity_policy: "fail"
|
||||
});
|
||||
const parent = summary.normalized_result.parent_evidence_id;
|
||||
const page = await call("inspect_analysis_view", {
|
||||
source: {kind: "retained-evidence", evidence_id: parent},
|
||||
view: {kind: "page", collection: "modules", offset: 0, limit: 8}
|
||||
});
|
||||
return {artifact_sha256: summary.subject?.digest.sha256,
|
||||
summary: summary.normalized_result, modules: page.normalized_result,
|
||||
evidence_ids: [summary.evidence_id, parent, page.evidence_id],
|
||||
limitations: [...summary.limitations, ...page.limitations]};`,
|
||||
},
|
||||
"obsidian-inventory": {
|
||||
artifact: "obsidian.asar",
|
||||
tools: ["open_binary", "inspect_artifact", "close_binary"],
|
||||
question:
|
||||
"Report ASAR identity, complete inventory manifest, entry-kind counts, root package/main/preload occurrences, integrity contradictions and inventory coverage; do not claim AST or runtime analysis.",
|
||||
code: `
|
||||
await call("open_binary", {path: input.path});
|
||||
try {
|
||||
const inspected = await call("inspect_artifact", {integrity_policy: "fail"});
|
||||
const n = inspected.normalized_result;
|
||||
const inventory = n.substeps.find(x => x.operation === "inventory_artifact").evidence;
|
||||
const graph = inventory.normalized_result;
|
||||
const counts = {};
|
||||
for (const row of graph.occurrences)
|
||||
counts[row.entry_kind] = (counts[row.entry_kind] ?? 0) + 1;
|
||||
return {artifact_sha256: inspected.subject.digest.sha256,
|
||||
manifest: graph.manifest, entry_kind_counts: counts,
|
||||
requested_paths: graph.occurrences.filter(x => ["package.json", "main.js", "preload.js"].includes(x.logical_path)),
|
||||
integrity_contradictions: graph.integrity_contradictions,
|
||||
coverage: n.coverage, unexplored_branches: n.unexplored_branches,
|
||||
evidence_ids: [inspected.evidence_id, inventory.evidence_id],
|
||||
limitations: [...inspected.limitations, ...n.limitations, ...inventory.limitations]};
|
||||
} finally { await call("close_binary", {}); }`,
|
||||
},
|
||||
"newpipe.apk": {
|
||||
tools: [
|
||||
"inspect_android_package",
|
||||
"search_android_classes",
|
||||
"inspect_android_class",
|
||||
],
|
||||
question:
|
||||
"Report package/version, exact MainActivity matches and its onCreate method descriptors.",
|
||||
code: `
|
||||
const pkg = await call("inspect_android_package", {path: input.path});
|
||||
const found = await call("search_android_classes", {path: input.path, query: "MainActivity"});
|
||||
if (!found.normalized_result.matches.includes("org.schabi.newpipe.MainActivity"))
|
||||
throw new Error("Requested shipped MainActivity was not found");
|
||||
const cls = await call("inspect_android_class", {
|
||||
path: input.path, class_name: "org.schabi.newpipe.MainActivity"
|
||||
});
|
||||
return {artifact_sha256: pkg.subject?.digest.sha256,
|
||||
package_name: pkg.normalized_result.package_name,
|
||||
version_name: pkg.normalized_result.version_name,
|
||||
version_code: pkg.normalized_result.version_code,
|
||||
matches: found.normalized_result.matches, coverage: found.normalized_result.coverage,
|
||||
onCreate: cls.normalized_result.methods.filter(x => x.name === "onCreate"),
|
||||
evidence_ids: [pkg.evidence_id, found.evidence_id, cls.evidence_id],
|
||||
limitations: [...pkg.limitations, ...found.limitations, ...cls.limitations]};`,
|
||||
},
|
||||
"openwrt.bin": {
|
||||
tools: ["inspect_firmware_regions"],
|
||||
question:
|
||||
"Report firmware identity, every reported region offset/signature and examined coverage.",
|
||||
code: `
|
||||
const inspected = await call("inspect_firmware_regions", {path: input.path});
|
||||
return {artifact_sha256: inspected.subject?.digest.sha256,
|
||||
result: inspected.normalized_result, evidence_ids: [inspected.evidence_id],
|
||||
limitations: inspected.limitations};`,
|
||||
},
|
||||
jq: {
|
||||
tools: [
|
||||
"open_binary",
|
||||
"resolve_containing_procedure",
|
||||
"analyze_function",
|
||||
"close_binary",
|
||||
],
|
||||
question:
|
||||
"Resolve the observed ELF entry address and report the recovered function's first 512 pseudocode units and caller/callee counts.",
|
||||
code: `
|
||||
await call("open_binary", {path: input.path, provider_id: "ghidra"});
|
||||
try {
|
||||
const found = await call("resolve_containing_procedure", {address: input.entry_address});
|
||||
const fn = await call("analyze_function", {procedure: input.entry_address});
|
||||
const n = fn.normalized_result;
|
||||
return {artifact_sha256: fn.subject?.digest.sha256,
|
||||
entry_address: input.entry_address, containing_procedure: found.normalized_result, procedure: n.procedure,
|
||||
pseudocode_prefix: n.pseudocode.slice(0, 512),
|
||||
caller_count: n.callers.length, callee_count: n.callees.length,
|
||||
evidence_ids: [found.evidence_id, fn.evidence_id],
|
||||
limitations: [...found.limitations, ...fn.limitations, ...n.limitations]};
|
||||
} finally { await call("close_binary", {}); }`,
|
||||
},
|
||||
};
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
artifact: { type: "string" },
|
||||
workflow: { type: "string" },
|
||||
mode: { type: "string", default: "direct" },
|
||||
"artifact-root": { type: "string" },
|
||||
providers: { type: "string" },
|
||||
output: { type: "string" },
|
||||
},
|
||||
});
|
||||
if (
|
||||
!workflows[values.workflow ?? values.artifact] ||
|
||||
!["direct", "discovery", "bridge"].includes(values.mode) ||
|
||||
!values["artifact-root"] ||
|
||||
!values.output
|
||||
)
|
||||
throw new Error(
|
||||
"Use --artifact ID [--workflow NAME] --mode direct|discovery|bridge --artifact-root ABS --output NEW_DIR [--providers JSON]",
|
||||
);
|
||||
const experiment = dirname(fileURLToPath(import.meta.url));
|
||||
const repository = resolve(experiment, "../..");
|
||||
const corpus = JSON.parse(
|
||||
await readFile(resolve(experiment, "corpus.json"), "utf8"),
|
||||
);
|
||||
const artifact = corpus.artifacts.find((x) => x.id === values.artifact);
|
||||
const workflowName = values.workflow ?? values.artifact;
|
||||
const workflow = workflows[workflowName];
|
||||
if (!artifact || artifact.id !== (workflow.artifact ?? workflowName))
|
||||
throw new Error(
|
||||
"Selected workflow does not match the frozen corpus artifact",
|
||||
);
|
||||
const artifactPath = resolve(values["artifact-root"], artifact.file);
|
||||
const bytes = await readFile(artifactPath);
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
if (sha256 !== artifact.sha256 || bytes.length !== artifact.bytes)
|
||||
throw new Error("Artifact identity differs from frozen corpus");
|
||||
await mkdir(values.output, { recursive: false, mode: 0o700 });
|
||||
const encoding = getEncoding("cl100k_base");
|
||||
const measure = (value) => {
|
||||
const text = typeof value === "string" ? value : JSON.stringify(value);
|
||||
return {
|
||||
bytes: Buffer.byteLength(text),
|
||||
cl100k_estimated_tokens: encoding.encode(text).length,
|
||||
};
|
||||
};
|
||||
const modelText = (reply) =>
|
||||
reply.content
|
||||
.filter((item) => item.type === "text")
|
||||
.map((item) => item.text)
|
||||
.join("\n");
|
||||
const environment = values.providers
|
||||
? JSON.parse(await readFile(values.providers, "utf8"))
|
||||
: {};
|
||||
const started = performance.now();
|
||||
const upstream = await connectUpstream({
|
||||
command: process.execPath,
|
||||
args: [resolve(repository, "scripts/rea.mjs"), "mcp"],
|
||||
env: environment,
|
||||
});
|
||||
const connected = performance.now();
|
||||
const records = [];
|
||||
const realCall = upstream.call;
|
||||
upstream.call = async (name, args, options) => {
|
||||
const began = performance.now();
|
||||
try {
|
||||
const reply = await realCall(name, args, { timeout: 600_000, ...options });
|
||||
const parsed = json(reply);
|
||||
records.push({
|
||||
name,
|
||||
arguments: args,
|
||||
elapsed_ms: performance.now() - began,
|
||||
is_error: reply.isError === true,
|
||||
input: measure(args),
|
||||
output: measure(reply),
|
||||
text_output: measure(modelText(reply)),
|
||||
evidence_id: parsed.evidence_id,
|
||||
reply,
|
||||
});
|
||||
return reply;
|
||||
} catch (error) {
|
||||
records.push({
|
||||
name,
|
||||
arguments: args,
|
||||
elapsed_ms: performance.now() - began,
|
||||
failure: String(error),
|
||||
input: measure(args),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const bridge = new CodeModeBridge(upstream);
|
||||
const input = { path: artifactPath };
|
||||
if (artifact.id === "jq") {
|
||||
if (bytes.subarray(0, 6).toString("hex") !== "7f454c460201")
|
||||
throw new Error("Expected ELF64 little-endian header for entry seed");
|
||||
input.entry_address = `0x${bytes.readBigUInt64LE(24).toString(16)}`;
|
||||
}
|
||||
const code = `const input = ${JSON.stringify(input)};
|
||||
async function call(name, args) {
|
||||
const reply = await tools[name](args);
|
||||
const parsed = json(reply);
|
||||
if (reply.isError) throw new Error(JSON.stringify(parsed));
|
||||
return parsed;
|
||||
}
|
||||
${workflow.code}`;
|
||||
let discovery;
|
||||
let value;
|
||||
let failure;
|
||||
let bridgeReply;
|
||||
const recovery = [];
|
||||
const operationStarted = performance.now();
|
||||
try {
|
||||
if (values.mode !== "direct")
|
||||
discovery = await bridge.search({ names: workflow.tools });
|
||||
if (values.mode === "bridge") {
|
||||
bridgeReply = await bridge.execute({ code, timeout_ms: 900_000 });
|
||||
value = bridgeReply.value;
|
||||
failure = bridgeReply.bridge_error;
|
||||
} else {
|
||||
const tools = Object.fromEntries(
|
||||
upstream.catalog.map((tool) => [
|
||||
tool.name,
|
||||
(args) => upstream.call(tool.name, args),
|
||||
]),
|
||||
);
|
||||
const AsyncFunction = Object.getPrototypeOf(
|
||||
async function () {},
|
||||
).constructor;
|
||||
value = await new AsyncFunction("tools", "json", code)(tools, json);
|
||||
}
|
||||
} catch (error) {
|
||||
failure = String(error);
|
||||
} finally {
|
||||
const partials = new Set(
|
||||
records.flatMap((record) => {
|
||||
if (!record.reply?.isError) return [];
|
||||
try {
|
||||
const partial = json(record.reply).error?.details?.partial_observation;
|
||||
return partial?.kind === "retained-evidence"
|
||||
? [partial.evidence_id]
|
||||
: [];
|
||||
} catch {
|
||||
return []; // Preserve non-JSON failures in calls.json, without inventing IDs.
|
||||
}
|
||||
}),
|
||||
);
|
||||
for (const evidence_id of partials) {
|
||||
// Capture already retained partial facts before disposing the replay session.
|
||||
// Recovery is not a successful completion of the failed analyst workflow.
|
||||
try {
|
||||
const reply = await realCall(
|
||||
"inspect_analysis_view",
|
||||
{
|
||||
source: { kind: "retained-evidence", evidence_id },
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
recovery.push({ parent_evidence_id: evidence_id, reply });
|
||||
} catch (error) {
|
||||
recovery.push({
|
||||
parent_evidence_id: evidence_id,
|
||||
failure: String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
await upstream.close();
|
||||
}
|
||||
const catalogExposure = values.mode === "direct" ? upstream.catalog : discovery;
|
||||
const summary = {
|
||||
scope: "deterministic_real_provider_replay_not_model_accuracy",
|
||||
baseline_sha: "fb94d9dde0ccda83ae520ac6fb22b1d283562cf3",
|
||||
artifact: artifact.id,
|
||||
workflow: workflowName,
|
||||
artifact_sha256: sha256,
|
||||
mode: values.mode,
|
||||
question: workflow.question,
|
||||
startup_ms: connected - started,
|
||||
operation_ms: performance.now() - operationStarted,
|
||||
original_calls: records.map(({ reply: _reply, ...metadata }) => metadata),
|
||||
exposed_catalog: catalogExposure ? measure(catalogExposure) : null,
|
||||
direct_input_definitions: measure(
|
||||
upstream.catalog.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
})),
|
||||
),
|
||||
execution_code: values.mode === "bridge" ? measure(code) : null,
|
||||
exposed_tool_results:
|
||||
values.mode === "bridge"
|
||||
? [measure(bridgeReply)]
|
||||
: records.filter((x) => x.output).map((x) => x.output),
|
||||
exposed_text_results:
|
||||
values.mode === "bridge"
|
||||
? [measure(bridgeReply)]
|
||||
: records.filter((x) => x.text_output).map((x) => x.text_output),
|
||||
projection: value,
|
||||
failure: failure ?? null,
|
||||
partial_recovery: recovery,
|
||||
correctness: failure
|
||||
? "failed_workflow"
|
||||
: "requires_cross_profile_fact_review",
|
||||
estimator:
|
||||
"cl100k_base_compact_MCP_JSON_including_envelope_not_actual_model_tokens",
|
||||
};
|
||||
await writeFile(
|
||||
resolve(values.output, "summary.json"),
|
||||
JSON.stringify(summary, null, 2),
|
||||
);
|
||||
await writeFile(resolve(values.output, "calls.json"), JSON.stringify(records));
|
||||
await writeFile(resolve(values.output, "code.js"), code);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
artifact: artifact.id,
|
||||
mode: values.mode,
|
||||
calls: records.length,
|
||||
startup_ms: summary.startup_ms,
|
||||
operation_ms: summary.operation_ms,
|
||||
failure: summary.failure
|
||||
? String(
|
||||
typeof summary.failure === "object"
|
||||
? summary.failure.message
|
||||
: summary.failure,
|
||||
).slice(0, 1000)
|
||||
: null,
|
||||
}),
|
||||
);
|
||||
if (failure) process.exitCode = 1;
|
||||
@@ -0,0 +1,426 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
||||
import { compile } from "json-schema-to-typescript";
|
||||
import { newQuickJSWASMModule, Scope } from "quickjs-emscripten";
|
||||
import { z } from "zod";
|
||||
|
||||
// Node maps larger delays to 1 ms. Reject them before creating a timer.
|
||||
const executeInputSchema = z.strictObject({
|
||||
code: z.string(),
|
||||
timeout_ms: z.number().int().positive().max(2_147_483_647).optional(),
|
||||
memory_bytes: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.max(128 * 1024 * 1024)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const digest = (value) =>
|
||||
createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
||||
|
||||
export const json = (reply) => {
|
||||
if (reply.structuredContent !== undefined) return reply.structuredContent;
|
||||
const texts = reply.content.filter((item) => item.type === "text");
|
||||
if (texts.length !== 1) throw new Error("Expected one REA JSON text block");
|
||||
return JSON.parse(texts[0].text);
|
||||
};
|
||||
|
||||
/** One upstream session, original tool names, complete live catalog. */
|
||||
export async function connectUpstream({
|
||||
command,
|
||||
args,
|
||||
env,
|
||||
stderr = "inherit",
|
||||
}) {
|
||||
const transport = new StdioClientTransport({ command, args, env, stderr });
|
||||
const client = new Client({
|
||||
name: "rea-codemode-experiment",
|
||||
version: "0.0.0",
|
||||
});
|
||||
try {
|
||||
await client.connect(transport, { timeout: 60_000 });
|
||||
const catalog = [];
|
||||
let cursor;
|
||||
do {
|
||||
const page = await client.listTools(cursor ? { cursor } : undefined);
|
||||
catalog.push(...page.tools);
|
||||
cursor = page.nextCursor;
|
||||
} while (cursor !== undefined);
|
||||
if (new Set(catalog.map((tool) => tool.name)).size !== catalog.length)
|
||||
throw new Error("Duplicate upstream tool names");
|
||||
return {
|
||||
client,
|
||||
catalog,
|
||||
call: (name, args, options) =>
|
||||
client.callTool({ name, arguments: args }, options),
|
||||
close: () => client.close(),
|
||||
};
|
||||
} catch (error) {
|
||||
await client.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export class CodeModeBridge {
|
||||
constructor(upstream) {
|
||||
this.upstream = upstream;
|
||||
this.byName = new Map(upstream.catalog.map((tool) => [tool.name, tool]));
|
||||
this.catalogDigest = digest(upstream.catalog);
|
||||
this.declarations = new Map();
|
||||
this.queue = Promise.resolve();
|
||||
}
|
||||
|
||||
/** The live catalog is immutable for this connection; derive each type once. */
|
||||
declarationsFor(tool) {
|
||||
if (!this.declarations.has(tool.name)) {
|
||||
const options = { bannerComment: "", format: false, unknownAny: true };
|
||||
const pending = Promise.all([
|
||||
compile(tool.inputSchema, `${tool.name}_Input`, options),
|
||||
tool.outputSchema
|
||||
? compile(
|
||||
{ ...tool.outputSchema, title: `${tool.name}_Output` },
|
||||
`${tool.name}_Output`,
|
||||
options,
|
||||
)
|
||||
: undefined,
|
||||
]);
|
||||
this.declarations.set(tool.name, pending);
|
||||
}
|
||||
return this.declarations.get(tool.name);
|
||||
}
|
||||
|
||||
/** Search exposes original schemas and generated input types on demand. */
|
||||
async search({ query = "", names = [], schemas, output_schemas = false }) {
|
||||
const includeSchemas = schemas ?? (names.length > 0 || output_schemas);
|
||||
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const selected = this.upstream.catalog.filter((tool) =>
|
||||
names.length
|
||||
? names.includes(tool.name)
|
||||
: terms.every((term) =>
|
||||
`${tool.name} ${tool.description ?? ""}`
|
||||
.toLowerCase()
|
||||
.includes(term),
|
||||
),
|
||||
);
|
||||
// Failed all-word searches keep their original semantics. Offer only the
|
||||
// best positive lexical matches as names, without expanding their schemas.
|
||||
const ranked =
|
||||
selected.length || !terms.length || names.length
|
||||
? []
|
||||
: this.upstream.catalog.map((tool) => ({
|
||||
tool,
|
||||
score: terms.filter((term) =>
|
||||
`${tool.name} ${tool.description ?? ""}`
|
||||
.toLowerCase()
|
||||
.includes(term),
|
||||
).length,
|
||||
}));
|
||||
const bestScore = Math.max(0, ...ranked.map((row) => row.score));
|
||||
const suggestions = ranked
|
||||
.filter((row) => row.score === bestScore && bestScore > 0)
|
||||
.map(({ tool }) => ({ name: tool.name, description: tool.description }));
|
||||
const tools = [];
|
||||
for (const tool of selected) {
|
||||
const [inputType, outputType] = includeSchemas
|
||||
? await this.declarationsFor(tool)
|
||||
: [];
|
||||
tools.push(
|
||||
includeSchemas
|
||||
? {
|
||||
...tool,
|
||||
...(!output_schemas ? { outputSchema: undefined } : {}),
|
||||
typescript: inputType,
|
||||
output_typescript: outputType,
|
||||
}
|
||||
: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
annotations: tool.annotations,
|
||||
},
|
||||
);
|
||||
}
|
||||
return {
|
||||
catalog_sha256: this.catalogDigest,
|
||||
total_tools: this.byName.size,
|
||||
matched_tools: tools.length,
|
||||
tools,
|
||||
...(suggestions.length ? { suggestions } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Serialize scripts sharing the active target; no implicit batch races. */
|
||||
execute(input, options = {}) {
|
||||
const parsed = executeInputSchema.parse(input);
|
||||
const work = this.queue.then(() => this.run(parsed, options));
|
||||
this.queue = work.catch(() => undefined);
|
||||
return work;
|
||||
}
|
||||
|
||||
async run(
|
||||
{ code, timeout_ms = 120_000, memory_bytes = 128 * 1024 * 1024 },
|
||||
options,
|
||||
) {
|
||||
const controller = new AbortController();
|
||||
const signal = options.signal
|
||||
? AbortSignal.any([controller.signal, options.signal])
|
||||
: controller.signal;
|
||||
signal.throwIfAborted();
|
||||
const deadline = Date.now() + timeout_ms;
|
||||
const timer = setTimeout(
|
||||
() => controller.abort(new Error("Code Mode deadline exceeded")),
|
||||
timeout_ms,
|
||||
);
|
||||
const QuickJS = await newQuickJSWASMModule();
|
||||
const runtime = QuickJS.newRuntime();
|
||||
// Pending-job execution can create another context in the runtime.
|
||||
// Own contexts at creation, including those allocated by the job pump.
|
||||
const contexts = new Scope();
|
||||
const newContext = runtime.newContext.bind(runtime);
|
||||
runtime.newContext = (options) => contexts.manage(newContext(options));
|
||||
runtime.setMemoryLimit(memory_bytes);
|
||||
runtime.setMaxStackSize(1024 * 1024);
|
||||
runtime.setInterruptHandler(() => signal.aborted || Date.now() > deadline);
|
||||
const vm = runtime.newContext();
|
||||
const pending = new Set();
|
||||
const calls = [];
|
||||
let toolQueue = Promise.resolve();
|
||||
let finished = false;
|
||||
let promise;
|
||||
let value;
|
||||
let failure;
|
||||
const checkedEval = (source) => {
|
||||
const result = vm.evalCode(source);
|
||||
if (result.error) {
|
||||
const error = vm.dump(result.error);
|
||||
result.error.dispose();
|
||||
throw new Error(JSON.stringify(error));
|
||||
}
|
||||
return result.value;
|
||||
};
|
||||
const pump = () => {
|
||||
const result = runtime.executePendingJobs();
|
||||
if (result.error) {
|
||||
failure = vm.dump(result.error);
|
||||
result.error.dispose();
|
||||
finished = true;
|
||||
}
|
||||
};
|
||||
const fromJson = (data) =>
|
||||
checkedEval(`JSON.parse(${JSON.stringify(JSON.stringify(data))})`);
|
||||
const callHandle = vm.newFunction("call", (nameHandle, argumentsHandle) => {
|
||||
const name = vm.getString(nameHandle);
|
||||
const args = JSON.parse(vm.getString(argumentsHandle));
|
||||
if (!this.byName.has(name))
|
||||
throw new Error(`Unknown upstream tool: ${name}`);
|
||||
signal.throwIfAborted();
|
||||
const deferred = vm.newPromise();
|
||||
const audit = { name, arguments_sha256: digest(args) };
|
||||
calls.push(audit);
|
||||
const work = toolQueue.then(async () => {
|
||||
const started = performance.now();
|
||||
try {
|
||||
signal.throwIfAborted();
|
||||
const reply = await this.upstream.call(name, args, {
|
||||
signal,
|
||||
timeout: Math.max(1, deadline - Date.now()),
|
||||
onprogress: options.onprogress,
|
||||
});
|
||||
audit.elapsed_ms = performance.now() - started;
|
||||
audit.result_sha256 = digest(reply);
|
||||
audit.is_error = reply.isError === true;
|
||||
try {
|
||||
const record = json(reply);
|
||||
if (record.evidence_id) audit.evidence_id = record.evidence_id;
|
||||
if (Array.isArray(record.evidence_links))
|
||||
audit.evidence_links = record.evidence_links;
|
||||
const partial = record.error?.details?.partial_observation;
|
||||
if (partial?.kind === "retained-evidence")
|
||||
audit.partial_evidence_id = partial.evidence_id;
|
||||
} catch {
|
||||
/* Non-JSON MCP content is still preserved in reply. */
|
||||
}
|
||||
const handle = fromJson(reply);
|
||||
deferred.resolve(handle);
|
||||
handle.dispose();
|
||||
} catch (error) {
|
||||
audit.transport_error = error.message;
|
||||
const handle = fromJson({ name: error.name, message: error.message });
|
||||
deferred.reject(handle);
|
||||
handle.dispose();
|
||||
} finally {
|
||||
deferred.dispose();
|
||||
pump();
|
||||
}
|
||||
});
|
||||
const settled = work.catch((error) => {
|
||||
failure = { message: error.message };
|
||||
finished = true;
|
||||
});
|
||||
toolQueue = settled;
|
||||
pending.add(settled);
|
||||
void settled.then(() => pending.delete(settled));
|
||||
// The callback transfers its returned handle to QuickJS. Keep the
|
||||
// deferred's original host handle until settlement, with a separate
|
||||
// guest handle; otherwise bulk promise cleanup shares one lifetime.
|
||||
return deferred.handle.dup();
|
||||
});
|
||||
const doneHandle = vm.newFunction("done", (data) => {
|
||||
value = JSON.parse(vm.getString(data));
|
||||
finished = true;
|
||||
});
|
||||
const failHandle = vm.newFunction("fail", (error) => {
|
||||
failure = vm.dump(error);
|
||||
finished = true;
|
||||
});
|
||||
try {
|
||||
vm.setProp(vm.global, "__call", callHandle);
|
||||
vm.setProp(vm.global, "__done", doneHandle);
|
||||
vm.setProp(vm.global, "__fail", failHandle);
|
||||
checkedEval(`
|
||||
const tools = Object.freeze(Object.fromEntries(${JSON.stringify([...this.byName.keys()])}.map(
|
||||
name => [name, args => __call(name, JSON.stringify(args))]
|
||||
)));
|
||||
const json = reply => {
|
||||
if (reply.structuredContent !== undefined) return reply.structuredContent;
|
||||
const texts = reply.content.filter(item => item.type === 'text');
|
||||
if (texts.length !== 1) throw new Error('Expected one REA JSON text block');
|
||||
return JSON.parse(texts[0].text);
|
||||
};
|
||||
`).dispose();
|
||||
promise = checkedEval(`(async () => {\n${code}\n})().then(
|
||||
value => __done(JSON.stringify(value === undefined ? null : value)),
|
||||
error => __fail({name: error?.name, message: error?.message ?? String(error)})
|
||||
)`);
|
||||
pump();
|
||||
while (!finished && !signal.aborted) {
|
||||
if (pending.size) await Promise.race(pending);
|
||||
else
|
||||
throw new Error(
|
||||
"Guest promise cannot progress: no pending tool calls",
|
||||
);
|
||||
pump();
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
if (calls.some((call) => !call.result_sha256 && !call.transport_error)) {
|
||||
controller.abort(
|
||||
new Error("Code returned before awaiting all tool calls"),
|
||||
);
|
||||
await Promise.allSettled(pending);
|
||||
failure = {
|
||||
message:
|
||||
"Code returned before awaiting all tool calls; calls were cancelled",
|
||||
};
|
||||
} else await Promise.allSettled(pending);
|
||||
} catch (error) {
|
||||
failure = { name: error.name, message: error.message };
|
||||
controller.abort(error);
|
||||
await Promise.allSettled(pending);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
promise?.dispose();
|
||||
callHandle.dispose();
|
||||
doneHandle.dispose();
|
||||
failHandle.dispose();
|
||||
contexts.dispose();
|
||||
runtime.dispose();
|
||||
}
|
||||
return {
|
||||
catalog_sha256: this.catalogDigest,
|
||||
calls,
|
||||
...(failure ? { bridge_error: failure } : { value }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createBridgeServer(bridge) {
|
||||
const server = new McpServer({
|
||||
name: "rea-codemode-experiment",
|
||||
version: "0.0.0",
|
||||
});
|
||||
const reply = (result) => ({
|
||||
content: [{ type: "text", text: JSON.stringify(result) }],
|
||||
...(result.bridge_error ? { isError: true } : {}),
|
||||
});
|
||||
server.registerTool(
|
||||
"rea_search",
|
||||
{
|
||||
description:
|
||||
"Discover original upstream REA tools. Start with one domain word, e.g. query=android; query words must all occur. Query/empty discovery defaults to names and descriptions. Then supply all selected exact names together to obtain exact input schemas plus derived input/output TypeScript for code authoring. schemas=true/false overrides this choice. Request output_schemas=true only for original JSON validation contracts. rea_search and rea_execute are already declared bridge tools, not upstream catalog entries. catalog_sha256 hashes the SDK catalog, not REA server_identity.catalog.digests. Code calls tools.<name>(original_arguments); json(reply) parses the original MCP value.",
|
||||
inputSchema: z.strictObject({
|
||||
query: z.string().default(""),
|
||||
names: z.array(z.string()).default([]),
|
||||
schemas: z.boolean().optional(),
|
||||
output_schemas: z.boolean().default(false),
|
||||
}),
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
},
|
||||
async (input) => reply(await bridge.search(input)),
|
||||
);
|
||||
server.registerTool(
|
||||
"rea_execute",
|
||||
{
|
||||
description:
|
||||
"Run a JavaScript async function body with tools.<original_REA_name>(original_arguments), json(reply) and return. Await every call. json(reply) returns the original REA value: Evidence has normalized_result, evidence_id and limitations; session values have result. Select the needed normalized_result fields inside code before returning them with Evidence IDs and limitations. One upstream session persists. Calls run in order, including Promise.all. All original REA effects, validation and retention apply; projections are caller selections, not new Evidence. No host IO or imports. Call audit and Evidence IDs survive script errors. Default deadline 120s and guest memory 128 MiB; use longer deadline for native analysis.",
|
||||
inputSchema: executeInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: true,
|
||||
idempotentHint: false,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (input, context) =>
|
||||
reply(
|
||||
await bridge.execute(input, {
|
||||
signal: context.mcpReq.signal,
|
||||
onprogress:
|
||||
context.mcpReq._meta?.progressToken === undefined
|
||||
? undefined
|
||||
: (progress) =>
|
||||
context.mcpReq.notify({
|
||||
method: "notifications/progress",
|
||||
params: {
|
||||
...progress,
|
||||
progressToken: context.mcpReq._meta.progressToken,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
return server;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
if (!command) throw new Error("Usage: node bridge.mjs COMMAND [ARG...]");
|
||||
const upstream = await connectUpstream({ command, args, env: process.env });
|
||||
const server = createBridgeServer(new CodeModeBridge(upstream));
|
||||
let closing = false;
|
||||
const close = async () => {
|
||||
if (closing) return;
|
||||
closing = true;
|
||||
await server.close();
|
||||
await upstream.close();
|
||||
};
|
||||
process.once("SIGINT", () => void close());
|
||||
process.once("SIGTERM", () => void close());
|
||||
const transport = new StdioServerTransport();
|
||||
transport.onclose = () => void close();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,313 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import Ajv2020 from "ajv/dist/2020.js";
|
||||
import { CodeModeBridge, connectUpstream, json } from "./bridge.mjs";
|
||||
|
||||
const entrypoint = fileURLToPath(
|
||||
new URL("../../scripts/rea.mjs", import.meta.url),
|
||||
);
|
||||
const fixture = fileURLToPath(
|
||||
new URL("../../tests/conformance/readiness/javascript-cli", import.meta.url),
|
||||
);
|
||||
const analysisArgs = {
|
||||
input_path: fixture,
|
||||
detail: "summary",
|
||||
max_heap_mb: 128,
|
||||
};
|
||||
let upstream;
|
||||
let bridge;
|
||||
before(async () => {
|
||||
upstream = await connectUpstream({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: { ...process.env, REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
bridge = new CodeModeBridge(upstream);
|
||||
});
|
||||
after(async () => {
|
||||
await upstream?.close();
|
||||
});
|
||||
|
||||
test("discovery preserves exact current REA schemas and names", async () => {
|
||||
const result = await bridge.search({
|
||||
names: ["binary_session"],
|
||||
output_schemas: true,
|
||||
});
|
||||
assert.equal(result.total_tools, upstream.catalog.length);
|
||||
const original = upstream.catalog.find(
|
||||
(tool) => tool.name === "binary_session",
|
||||
);
|
||||
const { typescript, output_typescript, ...discovered } = result.tools[0];
|
||||
assert.deepEqual(discovered, original);
|
||||
assert.match(typescript, /export interface BinarySession_Input/);
|
||||
assert.match(output_typescript, /export interface BinarySession_Output/);
|
||||
const inputOnly = await bridge.search({ names: ["binary_session"] });
|
||||
assert.deepEqual(inputOnly.tools[0].inputSchema, original.inputSchema);
|
||||
assert.equal(inputOnly.tools[0].outputSchema, undefined);
|
||||
assert.equal(inputOnly.tools[0].output_typescript, output_typescript);
|
||||
const all = await bridge.search({ schemas: false });
|
||||
assert.deepEqual(
|
||||
all.tools.map((tool) => tool.name),
|
||||
upstream.catalog.map((tool) => tool.name),
|
||||
);
|
||||
const query = await bridge.search({ query: "android" });
|
||||
assert.ok(query.tools.length > 0);
|
||||
assert.ok(query.tools.every((tool) => tool.inputSchema === undefined));
|
||||
const querySchemas = await bridge.search({ query: "android", schemas: true });
|
||||
assert.ok(querySchemas.tools.every((tool) => tool.inputSchema !== undefined));
|
||||
const outputRequested = await bridge.search({
|
||||
query: "android",
|
||||
output_schemas: true,
|
||||
});
|
||||
assert.ok(
|
||||
outputRequested.tools.every((tool) => tool.outputSchema !== undefined),
|
||||
);
|
||||
const missed = await bridge.search({
|
||||
query: "android impossible_query_word",
|
||||
});
|
||||
assert.equal(missed.matched_tools, 0);
|
||||
assert.ok(
|
||||
missed.suggestions.some((tool) => tool.name === "inspect_android_package"),
|
||||
);
|
||||
assert.ok(missed.suggestions.every((tool) => tool.inputSchema === undefined));
|
||||
});
|
||||
|
||||
test("raw original result and parsed result survive a real MCP call", async () => {
|
||||
const result = await bridge.execute({
|
||||
code: "const r = await tools.binary_session({}); return {raw:r, parsed:json(r)};",
|
||||
});
|
||||
assert.equal(result.bridge_error, undefined);
|
||||
assert.deepEqual(json(result.value.raw), result.value.parsed);
|
||||
assert.equal(result.calls[0].name, "binary_session");
|
||||
assert.match(result.calls[0].result_sha256, /^[a-f0-9]{64}$/);
|
||||
assert.equal(result.calls[0].is_error, false);
|
||||
});
|
||||
|
||||
test("Promise.all calls settle in order without losing pending work", async () => {
|
||||
const result = await bridge.execute({
|
||||
code: "return await Promise.all([tools.binary_session({}), tools.binary_session({}), tools.binary_session({})]);",
|
||||
});
|
||||
assert.equal(result.bridge_error, undefined);
|
||||
assert.equal(result.value.length, 3);
|
||||
assert.equal(result.calls.length, 3);
|
||||
assert.ok(result.calls.every((call) => call.result_sha256));
|
||||
});
|
||||
|
||||
test("upstream invalid-input result remains an original MCP error envelope", async () => {
|
||||
const args = {
|
||||
input_path: "/nonexistent/rea-codemode-artifact",
|
||||
detail: "summary",
|
||||
};
|
||||
const direct = await upstream.call("analyze_javascript_application", args);
|
||||
const result = await bridge.execute({
|
||||
code: `return await tools.analyze_javascript_application(${JSON.stringify(args)});`,
|
||||
});
|
||||
assert.equal(result.bridge_error, undefined);
|
||||
assert.equal(result.value.isError, true);
|
||||
assert.deepEqual(json(result.value).error, json(direct).error);
|
||||
assert.equal(result.calls[0].is_error, true);
|
||||
});
|
||||
|
||||
test("guest has no Node, filesystem, fetch or module-loading capabilities", async () => {
|
||||
const result = await bridge.execute({
|
||||
code: "return [typeof process,typeof require,typeof fetch,typeof Deno,Function('return typeof process')()];",
|
||||
});
|
||||
assert.deepEqual(result.value, Array(5).fill("undefined"));
|
||||
const imported = await bridge.execute({
|
||||
code: "return await import('node:fs');",
|
||||
});
|
||||
assert.ok(imported.bridge_error);
|
||||
});
|
||||
|
||||
test("CPU deadline interrupts runaway code and the next execution works", async () => {
|
||||
const result = await bridge.execute({
|
||||
code: "while(true) {}",
|
||||
timeout_ms: 20,
|
||||
});
|
||||
assert.ok(result.bridge_error);
|
||||
assert.deepEqual(result.calls, []);
|
||||
assert.equal((await bridge.execute({ code: "return 42;" })).value, 42);
|
||||
});
|
||||
|
||||
test("guest allocation pressure fails within its heap budget and permits reuse", async () => {
|
||||
const result = await bridge.execute({
|
||||
code: "return new Array(1_000_000).fill('x');",
|
||||
memory_bytes: 2 * 1024 * 1024,
|
||||
});
|
||||
assert.match(result.bridge_error.message, /memory/i);
|
||||
assert.deepEqual(result.calls, []);
|
||||
assert.equal((await bridge.execute({ code: "return 42;" })).value, 42);
|
||||
assert.throws(() =>
|
||||
bridge.execute({ code: "return 1;", timeout_ms: 2_147_483_648 }),
|
||||
);
|
||||
});
|
||||
|
||||
test("error after successful call preserves original Evidence reference", async () => {
|
||||
const result = await bridge.execute({
|
||||
code: `await tools.analyze_javascript_application(${JSON.stringify(analysisArgs)}); throw new Error('after-call');`,
|
||||
});
|
||||
assert.match(result.bridge_error.message, /after-call/);
|
||||
assert.ok(result.calls[0].evidence_id);
|
||||
assert.ok(result.calls[0].evidence_links.length > 0);
|
||||
const readback = await upstream.call("get_evidence_bundle", {
|
||||
detail: "summary",
|
||||
});
|
||||
assert.equal(readback.isError, undefined);
|
||||
assert.ok(
|
||||
JSON.stringify(json(readback)).includes(result.calls[0].evidence_id),
|
||||
);
|
||||
for (const evidence_id of result.calls[0].evidence_links) {
|
||||
const view = json(
|
||||
await upstream.call("inspect_analysis_view", {
|
||||
source: { kind: "retained-evidence", evidence_id },
|
||||
view: { kind: "summary" },
|
||||
}),
|
||||
);
|
||||
assert.equal(view.normalized_result.parent_evidence_id, evidence_id);
|
||||
}
|
||||
});
|
||||
|
||||
test("provider timeout retains its actual partial Evidence reference", async () => {
|
||||
const result = await bridge.execute({
|
||||
code: `const reply = await tools.analyze_javascript_application(${JSON.stringify({ ...analysisArgs, analysis_timeout_ms: 1 })}); return json(reply);`,
|
||||
});
|
||||
const partial = result.value.error.details.partial_observation;
|
||||
assert.equal(partial.kind, "retained-evidence");
|
||||
assert.equal(result.calls[0].is_error, true);
|
||||
assert.equal(result.calls[0].partial_evidence_id, partial.evidence_id);
|
||||
const view = json(
|
||||
await upstream.call("inspect_analysis_view", {
|
||||
source: partial,
|
||||
view: { kind: "summary" },
|
||||
}),
|
||||
);
|
||||
assert.equal(view.normalized_result.parent_evidence_id, partial.evidence_id);
|
||||
});
|
||||
|
||||
test("cancelling queued calls releases deferred handles and permits reuse", async () => {
|
||||
let callStarted;
|
||||
const started = new Promise((resolve) => {
|
||||
callStarted = resolve;
|
||||
});
|
||||
const cancelled = new AbortController();
|
||||
const controlled = new CodeModeBridge({
|
||||
catalog: upstream.catalog,
|
||||
async call(name, args, options) {
|
||||
const reply = await upstream.call(name, args, options);
|
||||
callStarted();
|
||||
// A controlled delivery delay after a real result makes the queued
|
||||
// cancellation deterministic, without simulating provider behavior.
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
return reply;
|
||||
},
|
||||
});
|
||||
const execution = controlled.execute(
|
||||
{
|
||||
code: "return await Promise.all([tools.binary_session({}), tools.binary_session({})]);",
|
||||
},
|
||||
{ signal: cancelled.signal },
|
||||
);
|
||||
await started;
|
||||
cancelled.abort(new Error("controlled queued cancellation"));
|
||||
const result = await execution;
|
||||
assert.ok(result.bridge_error);
|
||||
assert.equal(result.calls.length, 2);
|
||||
assert.match(
|
||||
result.calls[1].transport_error,
|
||||
/controlled queued cancellation/,
|
||||
);
|
||||
assert.equal((await controlled.execute({ code: "return 42;" })).value, 42);
|
||||
});
|
||||
|
||||
test("a guest promise without any producer fails without hanging", async () => {
|
||||
const result = await bridge.execute({
|
||||
code: "return await new Promise(() => {});",
|
||||
});
|
||||
assert.match(result.bridge_error.message, /cannot progress/);
|
||||
});
|
||||
|
||||
test("bridge's two tools are usable through actual downstream stdio MCP", async () => {
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [
|
||||
fileURLToPath(new URL("./bridge.mjs", import.meta.url)),
|
||||
process.execPath,
|
||||
entrypoint,
|
||||
"mcp",
|
||||
],
|
||||
env: { ...process.env, REA_LOG_LEVEL: "silent" },
|
||||
stderr: "inherit",
|
||||
});
|
||||
const client = new Client({ name: "codemode-boundary-test", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const catalog = await client.listTools();
|
||||
assert.deepEqual(
|
||||
catalog.tools.map((tool) => tool.name),
|
||||
["rea_search", "rea_execute"],
|
||||
);
|
||||
const ajv = new Ajv2020({ strict: true });
|
||||
const byName = new Map(catalog.tools.map((tool) => [tool.name, tool]));
|
||||
for (const tool of catalog.tools) {
|
||||
assert.equal(
|
||||
tool.inputSchema.$schema ??
|
||||
"https://json-schema.org/draft/2020-12/schema",
|
||||
"https://json-schema.org/draft/2020-12/schema",
|
||||
);
|
||||
assert.equal(
|
||||
ajv.validateSchema(tool.inputSchema),
|
||||
true,
|
||||
ajv.errorsText(),
|
||||
);
|
||||
}
|
||||
const validate = ajv.compile(byName.get("rea_execute").inputSchema);
|
||||
assert.equal(
|
||||
validate({ code: "return 1;", timeout_ms: 2_147_483_647 }),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
validate({ code: "return 1;", timeout_ms: 2_147_483_648 }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
validate({ code: "return 1;", memory_bytes: 128 * 1024 * 1024 + 1 }),
|
||||
false,
|
||||
);
|
||||
const found = json(
|
||||
await client.callTool({
|
||||
name: "rea_search",
|
||||
arguments: { names: ["binary_session"] },
|
||||
}),
|
||||
);
|
||||
assert.equal(found.matched_tools, 1);
|
||||
const result = json(
|
||||
await client.callTool({
|
||||
name: "rea_execute",
|
||||
arguments: {
|
||||
code: "return json(await tools.binary_session({}));",
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.equal(result.value.result.open, false);
|
||||
const invalid = await client.callTool({
|
||||
name: "rea_execute",
|
||||
arguments: { code: 7 },
|
||||
});
|
||||
assert.equal(invalid.isError, true);
|
||||
const oversized = await client.callTool({
|
||||
name: "rea_execute",
|
||||
arguments: { code: "return 1;", timeout_ms: 2_147_483_648 },
|
||||
});
|
||||
assert.equal(oversized.isError, true);
|
||||
const oversizedMemory = await client.callTool({
|
||||
name: "rea_execute",
|
||||
arguments: { code: "return 1;", memory_bytes: 128 * 1024 * 1024 + 1 },
|
||||
});
|
||||
assert.equal(oversizedMemory.isError, true);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"artifacts": [
|
||||
{
|
||||
"id": "sharex.dll",
|
||||
"file": "ShareX.dll",
|
||||
"bytes": 2162688,
|
||||
"sha256": "191a191be964e434daf0f9d76d1e0d8ddce90d7e248cba2a6b5ac9577f288d53",
|
||||
"provenance": {
|
||||
"project": "ShareX",
|
||||
"version": "19.0.2",
|
||||
"upstreamFile": "ShareX-19.0.2-portable.zip",
|
||||
"url": "https://github.com/ShareX/ShareX/releases/download/v19.0.2/ShareX-19.0.2-portable.zip",
|
||||
"transport_sha256": "967b5d838df5ce25fb5fd7cbd16f1bd797427e2df16907b022b74a4bc0fb9d54",
|
||||
"transport_bytes": 168796487,
|
||||
"derivation": "selected regular ZIP member ShareX.dll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sqlite.wasm",
|
||||
"file": "sqlite3.wasm",
|
||||
"bytes": 868907,
|
||||
"sha256": "2ee8f3dab694532afc8840e07703127287662d08b74e6ff50491ce63f00d5752",
|
||||
"provenance": {
|
||||
"project": "SQLite Wasm",
|
||||
"version": "3.53.4-build1",
|
||||
"upstreamFile": "sqlite-wasm-3.53.4-build1.tgz",
|
||||
"url": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.53.4-build1.tgz",
|
||||
"transport_sha256": "24f46db4ce5248fa8f8f24b67e81909ab1a9c152f42a4796a32c6d385a6fb62b",
|
||||
"transport_bytes": 977616,
|
||||
"transport_integrity": "sha512-U8pvL0patYHMgieD8JxxYJTlh9m1LUX8+BDYb7MiKQzBI89f2qbdmdcG/PvnPaLm9wsBbv4VlVG453ixnxBOnA==",
|
||||
"derivation": "selected regular tar member package/dist/sqlite3.wasm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "obsidian.asar",
|
||||
"file": "obsidian-1.14.4.asar",
|
||||
"bytes": 27477966,
|
||||
"sha256": "146ef8470d8cbb4a6db209bdc19bf2bad13350444186c0fd93c31087b142b92f",
|
||||
"provenance": {
|
||||
"project": "Obsidian",
|
||||
"version": "1.14.4",
|
||||
"upstreamFile": "obsidian-1.14.4.asar",
|
||||
"transport": {
|
||||
"file": "obsidian-1.14.4.asar.gz",
|
||||
"url": "https://github.com/obsidianmd/obsidian-releases/releases/download/v1.14.4/obsidian-1.14.4.asar.gz",
|
||||
"sha256": "d1ed428c363968774f0f3906e67d8a53a058ece0b6cdd924b07865977b2ada21",
|
||||
"bytes": 9200670
|
||||
},
|
||||
"derivation": "gzip decompression"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "newpipe.apk",
|
||||
"file": "NewPipe_v0.29.1.apk",
|
||||
"bytes": 11507122,
|
||||
"sha256": "18447bfb1e06d113edc88df93f471827280de06f6f3d4dc42f56f28b9c1bab79",
|
||||
"provenance": {
|
||||
"project": "NewPipe",
|
||||
"version": "0.29.1",
|
||||
"upstreamFile": "NewPipe_v0.29.1.apk",
|
||||
"url": "https://github.com/TeamNewPipe/NewPipe/releases/download/v0.29.1/NewPipe_v0.29.1.apk"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "jq",
|
||||
"file": "jq-linux-amd64",
|
||||
"bytes": 2267912,
|
||||
"sha256": "b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f",
|
||||
"provenance": {
|
||||
"project": "jq",
|
||||
"version": "1.8.2",
|
||||
"upstreamFile": "jq-linux-amd64",
|
||||
"url": "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "openwrt.bin",
|
||||
"file": "openwrt-25.12.5-ath79-generic-tplink_archer-c7-v5-squashfs-sysupgrade.bin",
|
||||
"bytes": 7405862,
|
||||
"sha256": "f15b16b36df7fce8a5cf1302320fdb428c3bb6fbc2949a4858afa257f4f364a2",
|
||||
"provenance": {
|
||||
"project": "OpenWrt Archer C7 v5",
|
||||
"version": "25.12.5",
|
||||
"upstreamFile": "openwrt-25.12.5-ath79-generic-tplink_archer-c7-v5-squashfs-sysupgrade.bin",
|
||||
"url": "https://downloads.openwrt.org/releases/25.12.5/targets/ath79/generic/openwrt-25.12.5-ath79-generic-tplink_archer-c7-v5-squashfs-sysupgrade.bin"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
# Decision from the frozen Code Mode experiment
|
||||
|
||||
Keep REA's existing CLI/MCP contracts as the default. Improve Evidence-aware
|
||||
workflow guidance and retain this two-tool bridge as an optional experiment.
|
||||
The measured Codex profiles do not justify a broad API refactor or a default
|
||||
switch to the external executor.
|
||||
|
||||
## What supports that choice
|
||||
|
||||
The [serial cohort](agent-results.json) contains sixteen real Codex CLI trials,
|
||||
configured with `gpt-5.6-terra` high, across Android, managed PE/CLI, Electron ASAR
|
||||
inventory, firmware and native Ghidra analysis. Twelve pass the strict requested
|
||||
fact/reference checks. Two bridge managed answers omit the classification's
|
||||
container field; two normal-MCP managed transcripts truncate a large producer
|
||||
envelope, preventing complete-envelope verification of one cited ID. Their
|
||||
remaining requested facts match the real-provider oracle. The complete-container
|
||||
assertion is stronger than the question's general classification wording.
|
||||
|
||||
The bridge's Android repetitions take 48.231 and 134.555 seconds, compared with
|
||||
67.329 and 71.624 for normal MCP. The faster bridge trial composes three original
|
||||
operations once; the slower one performs seven original calls across five scripts.
|
||||
The faster trial uses typed discovery; this does not make the model reliably
|
||||
choose that workflow. Uncached input is lower in both bridge repetitions;
|
||||
output and latency vary. This is not a stable overall efficiency win.
|
||||
|
||||
Managed bridge latency also varies, and its two answers omit the same field.
|
||||
Firmware's one-operation question takes 81.944 seconds with the bridge versus
|
||||
30.161 directly. Ghidra takes 334.642 versus 327.150 seconds, with sampled aggregate
|
||||
CPU times of 271.096 and 263.738 seconds. The bridge performs three original
|
||||
operations versus four directly, deriving containing identity from the analyzed
|
||||
procedure. Native analysis cost remains dominant with unchanged default settings.
|
||||
|
||||
Obsidian inventory exposes a useful contract boundary: nested Evidence types leave
|
||||
their normalized payload generic. The model initially returns the whole inventory
|
||||
and repeats analysis. Giving both profiles the concrete manifest/occurrences
|
||||
layout yields a single focused bridge workflow: 101994 total input tokens versus
|
||||
263484 directly, but 40554 versus 41532 uncached tokens and 85.909 versus 67.540
|
||||
seconds. Clear producer/layout guidance matters; total context compression alone
|
||||
does not establish lower cost or latency. Cache pricing is not estimated here.
|
||||
|
||||
Deterministic replays demonstrate large projection savings across real providers.
|
||||
They measure a known script, not the model's ability to discover and write it.
|
||||
SQLite WASM fails the frozen WABT parser on an unpadded DataCount row; Obsidian's
|
||||
full AST analysis times out with a retained partial reference. Inventory does not
|
||||
replace AST analysis. Neither failure is counted as a Code Mode success.
|
||||
|
||||
## Migration scope
|
||||
|
||||
Current users retain original operation names, arguments, provider choices,
|
||||
Evidence and session behavior. A client with equivalent execution support can use
|
||||
the existing registration and focused workflows. The installed Codex transcript
|
||||
does not expose its executor source, so its exact internal composition is unknown.
|
||||
|
||||
For clients that need the bridge, use a separate opt-in profile for an investigation.
|
||||
The bridge owns one original REA connection and looks up live contracts by name.
|
||||
Its session and Evidence ledger are separate from another REA registration; do
|
||||
not mix target lifecycle calls or same-session IDs across those registrations.
|
||||
Derived TypeScript assists code authoring; original JSON schemas remain available.
|
||||
Original producer IDs, links, errors and partial-reference IDs survive auditing;
|
||||
caller projections do not become new Evidence records.
|
||||
|
||||
The outer execution tool combines original effects and client requirements.
|
||||
This prototype has not forwarded roots/elicitation/sampling or preserved a client's
|
||||
per-original-operation approval flow. That capability boundary must be addressed
|
||||
before offering it as a production replacement. The guest's lack of host IO does
|
||||
not remove effects reachable through original REA tools.
|
||||
|
||||
An additive producer-description improvement explaining nested payload layout is
|
||||
a smaller next candidate than changing names or inventing another workflow API.
|
||||
Original inputs and output defaults should remain intact. Provider analysis
|
||||
settings must be evaluated separately from discovery and projection improvements.
|
||||
|
||||
## Verification limits
|
||||
|
||||
These are descriptive samples with matching questions/provider settings per pair,
|
||||
mode-specific instructions and model-selected arguments. They do not provide
|
||||
confidence intervals, independent decompiler truth, Android/.NET runtime behavior,
|
||||
full native-function coverage, complete firmware reconstruction or Windows/IDA
|
||||
provider verification. Citations are checked against observed producer values and
|
||||
links, not resolved individually after session exit.
|
||||
|
||||
Sixteen trials finish without sampled memory/task-limit events or resource aborts.
|
||||
Their maximum sampled memory is 1659744256 bytes (about 1.55 GiB), with at most
|
||||
126 tasks, under one CPU and a 4 GiB hard ceiling/3 GiB early stop. Copied profiles
|
||||
are removed. Twelve actual stdio tests additionally cover cancellation, partial
|
||||
Evidence readback, guest allocation failure/recovery, and advertised schema
|
||||
validation. The earlier 2 GiB build-scope kill remains a documented failed build.
|
||||
@@ -0,0 +1,249 @@
|
||||
"""Check requested JSON facts and real Evidence references against a replay oracle."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def required_field(row, key):
|
||||
return row[key] if key in row else {'missing_field': key}
|
||||
|
||||
|
||||
def same_fact(actual, expected):
|
||||
if isinstance(actual, bool) or isinstance(expected, bool):
|
||||
return type(actual) is type(expected) and actual == expected
|
||||
if isinstance(expected, dict):
|
||||
return isinstance(actual, dict) and actual.keys() == expected.keys() and all(
|
||||
same_fact(actual[key], value) for key, value in expected.items())
|
||||
if isinstance(expected, list):
|
||||
return isinstance(actual, list) and len(actual) == len(expected) and all(
|
||||
same_fact(a, b) for a, b in zip(actual, expected))
|
||||
return actual == expected
|
||||
|
||||
|
||||
def evidence_ids(value):
|
||||
if isinstance(value, str):
|
||||
return {value} if value.startswith('ev_') and len(value) == 67 else set()
|
||||
if isinstance(value, dict):
|
||||
return set().union(*(evidence_ids(v) for v in value.values()))
|
||||
if isinstance(value, list):
|
||||
return set().union(*(evidence_ids(v) for v in value))
|
||||
return set()
|
||||
|
||||
|
||||
def read_answer(path):
|
||||
source = path.read_text().strip()
|
||||
lines = source.splitlines()
|
||||
# Accept a single complete JSON fence, never repair prose or partial JSON.
|
||||
if len(lines) >= 3 and lines[0] == '```json' and lines[-1] == '```':
|
||||
source = '\n'.join(lines[1:-1])
|
||||
answer_format = 'markdown_json'
|
||||
else:
|
||||
answer_format = 'json'
|
||||
answer = json.loads(source)
|
||||
if not isinstance(answer, dict):
|
||||
raise ValueError('Answer is not a JSON object')
|
||||
return answer, answer_format
|
||||
|
||||
|
||||
def requested_facts(value, task):
|
||||
if task == 'native':
|
||||
facts = {field: value.get(field) for field in [
|
||||
'artifact_sha256', 'entry_address', 'containing_procedure',
|
||||
'procedure', 'pseudocode_prefix', 'caller_count', 'callee_count']}
|
||||
containing = facts['containing_procedure']
|
||||
if isinstance(containing, dict) and containing.get('found', True) is True and \
|
||||
containing.get('query_address', facts['entry_address']) == facts['entry_address']:
|
||||
identity = containing.get('procedure', containing)
|
||||
if isinstance(identity, dict):
|
||||
facts['containing_procedure'] = {key: identity.get(key) for key in ['address', 'name']}
|
||||
return facts
|
||||
if task == 'electron-inventory':
|
||||
facts = {field: value.get(field) for field in [
|
||||
'artifact_sha256', 'manifest', 'entry_kind_counts', 'requested_paths',
|
||||
'integrity_contradictions', 'coverage', 'unexplored_branches']}
|
||||
paths = facts['requested_paths']
|
||||
if isinstance(paths, dict) and set(paths) == {'package.json', 'main.js', 'preload.js'}:
|
||||
if all(isinstance(rows, list) and all(isinstance(row, dict) and
|
||||
row.get('logical_path') == name for row in rows)
|
||||
for name, rows in paths.items()):
|
||||
paths = [row for rows in paths.values() for row in rows]
|
||||
if isinstance(paths, list) and all(isinstance(row, dict) and
|
||||
isinstance(row.get('logical_path'), str) for row in paths):
|
||||
paths = sorted(paths, key=lambda row: row['logical_path'])
|
||||
facts['requested_paths'] = paths
|
||||
contradictions = facts['integrity_contradictions']
|
||||
if isinstance(contradictions, dict) and set(contradictions) == {'inspection', 'inventory'}:
|
||||
if all(rows == [] for rows in contradictions.values()):
|
||||
facts['integrity_contradictions'] = []
|
||||
branches = facts['unexplored_branches']
|
||||
if isinstance(branches, list) and all(isinstance(row, dict) for row in branches):
|
||||
# Branch identity includes the source Evidence identity. Compare its
|
||||
# reported meaning here and verify its actual source reference below.
|
||||
facts['unexplored_branches'] = [{key: row.get(key) for key in [
|
||||
'reason', 'detail', 'next_probe']} for row in branches]
|
||||
return facts
|
||||
if task == 'firmware':
|
||||
result = value.get('result')
|
||||
if not isinstance(result, dict):
|
||||
result = {}
|
||||
engine = result.get('engine', result.get('provider'))
|
||||
if isinstance(engine, dict):
|
||||
engine = {'version': engine.get('version'),
|
||||
'executable_sha256': engine.get('executable_sha256',
|
||||
engine.get('engine_artifact_sha256'))}
|
||||
regions = result.get('regions')
|
||||
if isinstance(regions, list) and all(isinstance(row, dict) for row in regions):
|
||||
# Binwalk's UUID is run-local; preserve it in the answer but do not
|
||||
# compare it with the independent replay's different occurrence ID.
|
||||
selected_regions = []
|
||||
for row in regions:
|
||||
selected = {key: row.get(key) for key in [
|
||||
'offset', 'signature', 'description', 'reported_confidence']}
|
||||
if 'reported_size' in row:
|
||||
size = row['reported_size']
|
||||
elif 'reported_extent_bytes' in row:
|
||||
size = row['reported_extent_bytes']
|
||||
else:
|
||||
extent = row.get('reported_extent')
|
||||
size = None
|
||||
if isinstance(extent, dict) and extent.get('offset') == row.get('offset'):
|
||||
length = extent.get('length')
|
||||
offset = row.get('offset')
|
||||
if isinstance(length, int) and isinstance(offset, int) and \
|
||||
extent.get('end_exclusive') == offset + length:
|
||||
size = length
|
||||
selected['reported_size'] = size
|
||||
selected_regions.append(selected)
|
||||
regions = selected_regions
|
||||
return {'artifact_sha256': value.get('artifact_sha256'),
|
||||
'engine': engine, 'input_size': result.get('input_size', result.get('input_size_bytes')),
|
||||
'regions': regions, 'coverage': result.get('coverage', result.get('examined_coverage'))}
|
||||
if task == 'managed':
|
||||
def selected(field, keys):
|
||||
row = value.get(field)
|
||||
return {key: required_field(row, key) for key in keys} if isinstance(row, dict) else row
|
||||
coverage = value.get('coverage')
|
||||
if isinstance(coverage, dict):
|
||||
coverage = {'managed_artifact': coverage.get('managed_artifact'),
|
||||
'native_boundaries': coverage.get('native_boundaries',
|
||||
coverage.get('managed_native_boundaries'))}
|
||||
coverage = {key: {field: row.get(field) for field in ['state', 'issues']}
|
||||
if isinstance(row, dict) else row
|
||||
for key, row in coverage.items() if key in ['managed_artifact', 'native_boundaries']}
|
||||
return {
|
||||
'artifact_sha256': value.get('artifact_sha256'),
|
||||
'assembly': selected('assembly', ['name', 'version']),
|
||||
'module': selected('module', ['name', 'mvid']),
|
||||
'classification': value.get('classification'),
|
||||
'target_frameworks': value.get('target_frameworks'),
|
||||
'native_boundaries': selected('native_boundaries', ['module_refs', 'pinvoke_imports']),
|
||||
'coverage': coverage,
|
||||
}
|
||||
facts = {field: value.get(field) for field in [
|
||||
'artifact_sha256', 'package_name', 'version_name', 'version_code', 'matches', 'onCreate']}
|
||||
methods = facts['onCreate']
|
||||
if isinstance(methods, list) and all(isinstance(row, dict) for row in methods):
|
||||
facts['onCreate'] = [{key: required_field(row, key) for key in [
|
||||
'reported_signature', 'overload_index', 'dex_descriptor']} for row in methods]
|
||||
return facts
|
||||
|
||||
|
||||
def producer_ids(reply):
|
||||
if not isinstance(reply, dict):
|
||||
return set(), 0
|
||||
ids = evidence_ids(reply.get('evidence_id'))
|
||||
ids |= evidence_ids(reply.get('evidence_links'))
|
||||
for call in reply.get('calls', []):
|
||||
ids |= evidence_ids(call.get('evidence_id'))
|
||||
ids |= evidence_ids(call.get('evidence_links'))
|
||||
ids |= evidence_ids(call.get('partial_evidence_id'))
|
||||
unreadable = 0
|
||||
# SDK envelopes and Codex JSONL use distinct declared field spellings.
|
||||
for field in ['structuredContent', 'structured_content']:
|
||||
nested_ids, nested_unreadable = producer_ids(reply.get(field))
|
||||
ids |= nested_ids
|
||||
unreadable += nested_unreadable
|
||||
for content in reply.get('content', []):
|
||||
if content.get('type') != 'text':
|
||||
continue
|
||||
try:
|
||||
value = json.loads(content['text'])
|
||||
except ValueError:
|
||||
unreadable += 1
|
||||
continue
|
||||
nested_ids, nested_unreadable = producer_ids(value)
|
||||
ids |= nested_ids
|
||||
unreadable += nested_unreadable
|
||||
return ids, unreadable
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--trial', type=Path, required=True)
|
||||
parser.add_argument('--oracle', type=Path, required=True)
|
||||
parser.add_argument('--task', choices=[
|
||||
'android', 'managed', 'native', 'firmware', 'electron-inventory'], default='android')
|
||||
args = parser.parse_args()
|
||||
oracle = json.loads(args.oracle.read_text())
|
||||
if oracle.get('failure') or not oracle.get('projection'):
|
||||
raise SystemExit('A failed replay is not a factual oracle')
|
||||
projection = oracle['projection']
|
||||
if args.task == 'managed':
|
||||
projection = {**projection, 'coverage': {
|
||||
'managed_artifact': projection['coverage'],
|
||||
'native_boundaries': projection['native_boundaries']['coverage'],
|
||||
}}
|
||||
expected = requested_facts(projection, args.task)
|
||||
# A task explicitly chooses these assertions; do not grade unrequested fields.
|
||||
if any(value is None for value in expected.values()):
|
||||
raise SystemExit('Replay does not contain the selected task facts')
|
||||
failures = []
|
||||
answer_format = None
|
||||
answer_path = args.trial / 'answer.txt'
|
||||
try:
|
||||
answer, answer_format = read_answer(answer_path)
|
||||
except (FileNotFoundError, ValueError) as error:
|
||||
answer = {}
|
||||
failures.append({'kind': 'answer_parse_failure', 'reason': str(error)})
|
||||
actual_facts = requested_facts(answer, args.task)
|
||||
for field, wanted in expected.items():
|
||||
actual = actual_facts[field]
|
||||
if not same_fact(actual, wanted):
|
||||
failures.append({'kind': 'fact_mismatch', 'field': field,
|
||||
'expected': wanted, 'actual': actual})
|
||||
observed = set()
|
||||
unreadable = 0
|
||||
for line in (args.trial / 'events.jsonl').read_text().splitlines():
|
||||
event = json.loads(line)
|
||||
item = event.get('item', {})
|
||||
if event.get('type') != 'item.completed' or item.get('type') != 'mcp_tool_call':
|
||||
continue
|
||||
# Only producer envelopes and bridge audit IDs establish retention.
|
||||
ids, skipped = producer_ids(item.get('result') or {})
|
||||
observed |= ids
|
||||
unreadable += skipped
|
||||
cited = evidence_ids(answer.get('evidence_ids'))
|
||||
if args.task == 'electron-inventory':
|
||||
for branch in answer.get('unexplored_branches', []):
|
||||
if isinstance(branch, dict):
|
||||
cited |= evidence_ids(branch.get('evidence_id'))
|
||||
if not cited:
|
||||
failures.append({'kind': 'missing_evidence_references'})
|
||||
if cited - observed:
|
||||
failures.append({'kind': 'unobserved_evidence_ids', 'ids': sorted(cited - observed)})
|
||||
result = {'task': args.task,
|
||||
'grader_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||
'scope': 'producer_fact_parity_and_reference_existence_not_runtime_accuracy',
|
||||
'passed': not failures, 'fact_assertions': len(expected),
|
||||
'answer_format': answer_format,
|
||||
'unreadable_result_texts': unreadable,
|
||||
'cited_evidence_ids': sorted(cited), 'failures': failures}
|
||||
(args.trial / 'grade.json').write_text(json.dumps(result, indent=2) + '\n')
|
||||
print(json.dumps(result))
|
||||
raise SystemExit(0 if result['passed'] else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Generated
+518
@@ -0,0 +1,518 @@
|
||||
{
|
||||
"name": "rea-codemode-experiment",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rea-codemode-experiment",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/client": "2.3.1",
|
||||
"@modelcontextprotocol/server": "2.3.1",
|
||||
"js-tiktoken": "1.0.21",
|
||||
"json-schema-to-typescript": "16.0.0",
|
||||
"quickjs-emscripten": "0.32.0",
|
||||
"zod": "4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ajv": "8.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "24.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "11.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz",
|
||||
"integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/philsturgeon"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitl/quickjs-ffi-types": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.32.0.tgz",
|
||||
"integrity": "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jitl/quickjs-wasmfile-debug-asyncify": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.32.0.tgz",
|
||||
"integrity": "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitl/quickjs-wasmfile-debug-sync": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.32.0.tgz",
|
||||
"integrity": "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitl/quickjs-wasmfile-release-asyncify": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.32.0.tgz",
|
||||
"integrity": "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jitl/quickjs-wasmfile-release-sync": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.32.0.tgz",
|
||||
"integrity": "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/client": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.3.1.tgz",
|
||||
"integrity": "sha512-mIGZXpHsjnZ6lD+gD/WCMpR5k8yVQQ8nNFH1N0Srf7AvnwTUMYD6pvTY8ng2+He5FfV7YMZLmrjL7cLa/cc3dQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/core": "2.3.1",
|
||||
"cross-spawn": "^7.0.5",
|
||||
"eventsource": "^3.0.2",
|
||||
"eventsource-parser": "^3.0.8",
|
||||
"jose": "^6.1.3",
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"zod": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/core": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.3.1.tgz",
|
||||
"integrity": "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"zod": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/server": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz",
|
||||
"integrity": "sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/core": "2.3.1",
|
||||
"zod": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.26",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.26.tgz",
|
||||
"integrity": "sha512-48NWfiY6oNcKOh47Z4h+sIvwcQR91h6KPhaYKgeMIrkT2jV82Gy90SErj60vW8jQbeMSOSbhIv3bZ7JoqPPVhA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "^3.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
|
||||
"integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
||||
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.12",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
|
||||
"integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tiktoken": {
|
||||
"version": "1.0.21",
|
||||
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
|
||||
"integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz",
|
||||
"integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-to-typescript": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-16.0.0.tgz",
|
||||
"integrity": "sha512-Ah6kK4q6SjlMzWBH1eNOQkdRh5Qhr5T/RCFwfjQqWZA7UDW+N47A8wfyoiN0L884s4L4bAEI54IjIuN3/u0B/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "^11.9.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/lodash": "^4.17.25",
|
||||
"js-yaml": "^5.2.3",
|
||||
"lodash": "^4.18.1",
|
||||
"minimist": "^1.2.8",
|
||||
"prettier": "^3.9.6",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
"json2ts": "dist/src/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pkce-challenge": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
||||
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
|
||||
"integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/quickjs-emscripten": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/quickjs-emscripten/-/quickjs-emscripten-0.32.0.tgz",
|
||||
"integrity": "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0",
|
||||
"@jitl/quickjs-wasmfile-debug-sync": "0.32.0",
|
||||
"@jitl/quickjs-wasmfile-release-asyncify": "0.32.0",
|
||||
"@jitl/quickjs-wasmfile-release-sync": "0.32.0",
|
||||
"quickjs-emscripten-core": "0.32.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/quickjs-emscripten-core": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/quickjs-emscripten-core/-/quickjs-emscripten-core-0.32.0.tgz",
|
||||
"integrity": "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jitl/quickjs-ffi-types": "0.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "rea-codemode-experiment",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "24.18.0"
|
||||
},
|
||||
"packageManager": "npm@11.16.0",
|
||||
"scripts": {
|
||||
"test": "node --test bridge.test.mjs",
|
||||
"start": "node bridge.mjs",
|
||||
"bench": "node benchmark.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/client": "2.3.1",
|
||||
"@modelcontextprotocol/server": "2.3.1",
|
||||
"js-tiktoken": "1.0.21",
|
||||
"json-schema-to-typescript": "16.0.0",
|
||||
"quickjs-emscripten": "0.32.0",
|
||||
"zod": "4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ajv": "8.20.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Statically investigate the actual shipped APK at {{artifact_path}} using only the registered REA tools. Report the exact artifact SHA-256, package name, version name/code, every class-name match for the case-sensitive substring MainActivity, and all onCreate method descriptors reported for org.schabi.newpipe.MainActivity. Preserve reported signatures and explicit unknowns. For each onCreate entry include reported_signature, overload_index and dex_descriptor exactly as reported, preserving null. Final answer must be JSON with artifact_sha256, package_name, version_name, version_code, matches, onCreate, evidence_ids, limitations. Include real Evidence IDs supporting each result. Do not execute the APK or use shell, host file tools, network or other agents. Stop once these facts are supported.
|
||||
|
||||
When a client JavaScript executor is available, compose the required REA calls in one execution and return only selected facts from normalized_result, real Evidence IDs, target identity from subject.digest.sha256 and limitations. Parse original MCP text content if necessary. Do not return raw_result, whole Evidence or unrelated inventories. All matching classes and onCreate overloads must still be included.
|
||||
@@ -0,0 +1 @@
|
||||
Statically inventory {{artifact_path}} using only the registered REA tools. Open this ASAR as an archive, inspect its artifact inventory, and close the session. Report exact target SHA-256, the complete inventory manifest, counts of every entry_kind across all occurrences, the original occurrence records for root paths package.json, main.js and preload.js if present, all integrity contradictions, inspection coverage, unexplored branches, real Evidence IDs and limitations. This question asks about package bytes and inventory, not AST interpretation or runtime behavior. Do not execute target code, analyze all JavaScript, extract files, launch a GUI, use shell/file tools/network or other agents. When a client JavaScript executor is available, compose original calls in dependency order and return only requested fields from normalized_result; the full inventory is the Evidence nested in the inspect_artifact inventory_artifact substep. Use Evidence.subject.digest.sha256 for the target identity and retain both inspection and inventory Evidence IDs. Final JSON fields: artifact_sha256, manifest, entry_kind_counts, requested_paths, integrity_contradictions, coverage, unexplored_branches, evidence_ids, limitations. Stop when these facts and cleanup are supported. Use the original open_binary, inspect_artifact and close_binary operations, with integrity_policy=fail. The inventory Evidence has normalized_result.manifest and normalized_result.occurrences; each occurrence has entry_kind and logical_path. Count the complete occurrences array and filter those three exact root logical_path values. Keep the complete inventory inside the executor when one is available; return the requested projection.
|
||||
@@ -0,0 +1 @@
|
||||
Statically inventory {{artifact_path}} using only the registered REA tools. Open this ASAR as an archive, inspect its artifact inventory, and close the session. Report exact target SHA-256, the complete inventory manifest, counts of every entry_kind across all occurrences, the original occurrence records for root paths package.json, main.js and preload.js if present, all integrity contradictions, inspection coverage, unexplored branches, real Evidence IDs and limitations. This question asks about package bytes and inventory, not AST interpretation or runtime behavior. Do not execute target code, analyze all JavaScript, extract files, launch a GUI, use shell/file tools/network or other agents. When a client JavaScript executor is available, compose original calls in dependency order and return only requested fields from normalized_result; the full inventory is the Evidence nested in the inspect_artifact inventory_artifact substep. Use Evidence.subject.digest.sha256 for the target identity and retain both inspection and inventory Evidence IDs. Final JSON fields: artifact_sha256, manifest, entry_kind_counts, requested_paths, integrity_contradictions, coverage, unexplored_branches, evidence_ids, limitations. Stop when these facts and cleanup are supported.
|
||||
@@ -0,0 +1 @@
|
||||
Statically inspect {{artifact_path}} using only the registered REA tools. Report exact target SHA-256 and every reported firmware region with offsets, signatures, reported extents, provider provenance and examined coverage. Preserve unknowns and limitations; a signature scan does not prove partition boundaries or boot/runtime validity. Do not execute target code, extract files, launch a GUI, use shell/file tools/network or other agents. When a client JavaScript executor is available, select the requested normalized_result fields, Evidence.subject.digest.sha256 and real Evidence IDs before returning output. Final JSON fields: artifact_sha256, result, evidence_ids, limitations. Stop when the requested facts are supported.
|
||||
@@ -0,0 +1 @@
|
||||
Statically inspect {{artifact_path}} using only the registered REA tools. Report exact target SHA-256, assembly name/version, module name/MVID, managed classification and architecture with its evidence, every reported target framework, and all declared native ModuleRef/PInvoke boundaries with coverage and limitations. Do not load or execute the assembly, launch a GUI, use shell/file tools/network or other agents. When a client JavaScript executor is available, compose the required original MCP calls and return only requested normalized_result fields, target identity from subject.digest.sha256 and real Evidence IDs. Do not return whole Evidence, raw_result or unrelated resources/custom-attribute inventories. Preserve dependency order for operations sharing the selected managed target. Final JSON fields: artifact_sha256, assembly, module, classification, target_frameworks, native_boundaries, coverage, evidence_ids, limitations. Stop when these facts are supported.
|
||||
@@ -0,0 +1 @@
|
||||
Statically inspect {{artifact_path}} using only the registered REA tools and Ghidra. The pinned ELF header was independently observed to report entry address 0x403a50; treat that address as the analyst's seed, not as a function-name assumption. Keep default auto-analysis enabled and allow at least 600 seconds for native analysis (including the script deadline if using a bridge). Open the target with provider_id ghidra, resolve the containing procedure for that address, analyze it, and close the ephemeral session in a finally block or a subsequent cleanup call. Report exact target SHA-256, the entry_address, containing_procedure, procedure metadata, the first 512 UTF-16 units of pseudocode, caller_count, callee_count, real Evidence IDs and limitations. Do not execute target code, launch a GUI, use shell/file tools/network or other agents. When a client JavaScript executor is available, compose the original calls in dependency order and select requested fields before returning output; use Evidence.subject.digest.sha256 for the target identity. Final JSON fields: artifact_sha256, entry_address, containing_procedure, procedure, pseudocode_prefix, caller_count, callee_count, evidence_ids, limitations. The address seed applies only to the pinned jq artifact in corpus.json. Stop when these facts and cleanup are supported.
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Run one owned experiment in a bounded, transient Linux systemd scope."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
def available_bytes():
|
||||
for line in Path('/proc/meminfo').read_text().splitlines():
|
||||
if line.startswith('MemAvailable:'):
|
||||
return int(line.split()[1]) * 1024
|
||||
raise RuntimeError('MemAvailable unavailable')
|
||||
|
||||
|
||||
def scope_has_processes(group):
|
||||
if group is None:
|
||||
return False
|
||||
try:
|
||||
return bool((group / 'cgroup.procs').read_text().strip())
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
def stop_owned_scope(unit, process, group):
|
||||
subprocess.run(['systemctl', '--user', 'stop', '--no-block', unit],
|
||||
check=False, timeout=5)
|
||||
for attempt in range(2):
|
||||
deadline = time.monotonic() + 10
|
||||
while process.poll() is None or scope_has_processes(group):
|
||||
if time.monotonic() >= deadline:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
return
|
||||
if attempt == 1:
|
||||
raise RuntimeError('Cleanup unverified; retain owned scope ' + unit)
|
||||
subprocess.run(['systemctl', '--user', 'kill', '--kill-who=all',
|
||||
'--signal=SIGKILL', unit], check=False, timeout=5)
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if args and args[0] == '--':
|
||||
args = args[1:]
|
||||
if not args:
|
||||
raise SystemExit('Usage: resource-run.py -- COMMAND ARGS...')
|
||||
gib = 1024 ** 3
|
||||
if available_bytes() < 8 * gib:
|
||||
raise SystemExit('Refusing experiment: less than 8 GiB available')
|
||||
unit = 'rea-codemode-' + uuid.uuid4().hex + '.scope'
|
||||
cpu = max(os.sched_getaffinity(0))
|
||||
command = [
|
||||
'systemd-run', '--user', '--scope', '--unit=' + unit,
|
||||
'-p', 'MemoryMax=4G', '-p', 'MemoryHigh=3G', '-p', 'MemorySwapMax=0',
|
||||
'-p', 'CPUQuota=100%', '-p', 'TasksMax=192',
|
||||
'taskset', '-c', str(cpu), *args,
|
||||
]
|
||||
process = subprocess.Popen(command)
|
||||
peak = 0
|
||||
peak_tasks = 0
|
||||
events = {}
|
||||
cpu_stats = {}
|
||||
abort = None
|
||||
started = time.monotonic()
|
||||
try:
|
||||
group = None
|
||||
while process.poll() is None:
|
||||
if group is None:
|
||||
reply = subprocess.run(
|
||||
['systemctl', '--user', 'show', unit, '--value',
|
||||
'--property=ControlGroup'],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
path = reply.stdout.strip()
|
||||
if path:
|
||||
group = Path('/sys/fs/cgroup') / path.lstrip('/')
|
||||
current = group / 'memory.current' if group else None
|
||||
if current and current.exists():
|
||||
try:
|
||||
peak = max(peak, int(current.read_text()))
|
||||
peak_tasks = max(peak_tasks, int((group / 'pids.current').read_text()))
|
||||
cpu_stats = dict((key, int(value)) for key, value in
|
||||
(line.split() for line in (group / 'cpu.stat').read_text().splitlines()))
|
||||
for event_file in ['memory.events', 'pids.events']:
|
||||
events[event_file] = dict(
|
||||
(key, int(value)) for key, value in
|
||||
(line.split() for line in (group / event_file).read_text().splitlines())
|
||||
)
|
||||
except FileNotFoundError:
|
||||
# A completed transient scope can disappear between reads.
|
||||
pass
|
||||
if peak > 3 * gib:
|
||||
abort = 'scope_memory_early_stop'
|
||||
elif available_bytes() < 4 * gib:
|
||||
abort = 'host_available_memory_early_stop'
|
||||
elif events.get('pids.events', {}).get('max', 0):
|
||||
abort = 'scope_task_limit_observed'
|
||||
if abort:
|
||||
stop_owned_scope(unit, process, group)
|
||||
break
|
||||
time.sleep(0.1)
|
||||
code = process.wait()
|
||||
except BaseException:
|
||||
stop_owned_scope(unit, process, group)
|
||||
raise
|
||||
finally:
|
||||
if scope_has_processes(group):
|
||||
stop_owned_scope(unit, process, group)
|
||||
print(json.dumps({
|
||||
'resource_scope': unit, 'peak_cgroup_bytes_sampled': peak,
|
||||
'elapsed_seconds': round(time.monotonic() - started, 3),
|
||||
'abort': abort, 'hard_memory_bytes': 4 * gib,
|
||||
'early_stop_bytes': 3 * gib, 'cpu_quota_percent': 100,
|
||||
'cpu_affinity': [cpu],
|
||||
'task_ceiling': 192, 'peak_tasks_sampled': peak_tasks,
|
||||
'resource_events_last_observed': events,
|
||||
'cpu_stat_last_observed': cpu_stats,
|
||||
}), file=sys.stderr)
|
||||
raise SystemExit(125 if abort else code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,367 @@
|
||||
# Preliminary measurements
|
||||
|
||||
These are real Codex CLI 0.161.0 trials on the frozen checkout, using
|
||||
`gpt-5.6-terra`, high reasoning, one CPU and a guarded memory scope. The task
|
||||
called `binary_session` once and requested open state and server identity. This
|
||||
is a health probe, not an investigation benchmark. API input tokens are cumulative
|
||||
across model steps; cached tokens are a subset, not an additional cost.
|
||||
|
||||
| Profile | Seconds | Input tokens | Cached input | Output tokens | Outcome |
|
||||
| ---------------------------------- | ------: | -----------: | -----------: | ------------: | --------------------------------------------------------------- |
|
||||
| Direct MCP | 47.635 | 103154 | 70656 | 940 | Correct closed state; identity omitted after preview truncation |
|
||||
| Client Code Mode flag | 67.345 | 197315 | 157696 | 1458 | Correct closed state; extra failed resource lookup |
|
||||
| Client Code Mode only flag | 54.276 | 126609 | 84992 | 939 | Correct closed state; identity omitted |
|
||||
| Bridge, original prompt | 300.173 | unavailable | unavailable | unavailable | Timeout before any completed tool call |
|
||||
| Bridge, explicit projection prompt | 84.790 | 209248 | 175616 | 2125 | Correct closed state/version; conflated catalog identities |
|
||||
|
||||
These prompts and flags do not establish a controlled comparison. The projection
|
||||
trial used a more explicit prompt. The client trials expose nested original MCP
|
||||
calls in their transcripts; the flags alone do not establish code composition or
|
||||
intermediate-result filtering. The timeout is retained as a failed trial without
|
||||
an attributed root cause.
|
||||
|
||||
The direct tool delivered the original session result, including package version
|
||||
6.4.0 and REA's catalog digests. In the bridge projection trial the script selected
|
||||
the nonexistent `server_identity.catalog.digest`, which JavaScript omitted. The
|
||||
answer instead used the bridge's SDK catalog hash. That is not the requested REA
|
||||
identity and must fail that factual check. No unsupported digest is used as an
|
||||
oracle for subsequent trials.
|
||||
|
||||
Discovery was unnecessarily returning entire output schemas. For example, the
|
||||
live `binary_session` output schema occupies 18039 compact JSON bytes, compared
|
||||
with 859 for its input schema. `analyze_javascript_application` uses 41662 and
|
||||
2863 bytes respectively. The complete SDK catalog contains 172 tools and 1986857
|
||||
compact JSON bytes. The bridge now defaults to input schemas and makes unchanged
|
||||
output schemas available explicitly. Actual model savings from this change remain
|
||||
unmeasured.
|
||||
|
||||
The upstream/downstream stdio boundary suite passed all nine tests. The repository
|
||||
typecheck and lint passed under a one-CPU resource scope with a sampled peak of
|
||||
2934038528 bytes. A 2 GiB soft limit caused heavy reclaim during native TypeScript
|
||||
checking; the wrapper's soft limit is now aligned with its 3 GiB early-stop
|
||||
threshold. Its 4 GiB hard ceiling and host-available-memory checks remain in force.
|
||||
An earlier cached-build attempt was killed inside its 2 GiB scope; that is a
|
||||
failed build attempt, not evidence of a host OOM or a benchmark result.
|
||||
|
||||
## Real shipped artifacts
|
||||
|
||||
All acquired bytes match `corpus.json`; ShareX's archive also matches the GitHub
|
||||
asset digest and SQLite's npm archive matches its published SHA-512 integrity.
|
||||
No target was executed. These measurements use real REA providers and SDK stdio,
|
||||
with the same analysis arguments in each compared profile.
|
||||
|
||||
NewPipe's direct, discovery-only replay and bridge replay produce equal selected
|
||||
facts, including exact target digest, package/version, two MainActivity matches,
|
||||
reported `onCreate` signature, overload index and unavailable DEX descriptor. The
|
||||
bridge retains all three real producer IDs. Discovery-only here is a replay cost
|
||||
decomposition, not an implemented new MCP client protocol.
|
||||
|
||||
| Replay delivery | Discovery estimate | Intermediate/final result estimate |
|
||||
| -------------------------------------------------- | -----------------: | ---------------------------------: |
|
||||
| Direct: input definitions only, text content once | 107076 | 94945 |
|
||||
| On-demand input definitions, original text content | 877 | 94945 |
|
||||
| On-demand definitions, one composed projection | 877 | 1057 |
|
||||
|
||||
The bridge also sends 290 estimated tokens of code. Estimates use `cl100k_base`;
|
||||
they are not Terra API tokens, latency or model task success. The direct baseline
|
||||
uses input definitions and single text content, avoiding an inflated comparison
|
||||
against duplicated MCP envelopes or unused output schemas. The full SDK envelope
|
||||
figures, where useful for local transport, are recorded separately.
|
||||
|
||||
ShareX's real PE/CLI metadata and native-boundary workflows agree across direct
|
||||
and bridge: assembly ShareX 19.0.2.0, modern .NET, x86_64, framework
|
||||
`.NETCoreApp,Version=v9.0`, declared native boundaries and explicit coverage.
|
||||
Single text-content estimates are 204766 tokens for the two original results and
|
||||
4148 for the bridge projection. No .NET runtime behavior was verified.
|
||||
|
||||
jq's original trial failed because the stripped shipped executable has no
|
||||
`jq_compile` symbol. The corrected direct trial uses the observed ELF entry
|
||||
`0x403a50`, resolves it to Ghidra's `entry` function and analyzes that function.
|
||||
It succeeds with retained Evidence and a closed ephemeral provider session.
|
||||
The corrected bridge returns identical selected facts and limitations. Its final
|
||||
result estimate is 1544 tokens versus 10189 for direct single-text results, plus
|
||||
1610 for discovery and 267 for code. This small entry function
|
||||
does not establish large-function or complete native-analysis coverage.
|
||||
|
||||
OpenWrt's original direct trial returned `unreadable_output` without the expected
|
||||
report. Twelve independent bounded Binwalk invocations produced reports. An
|
||||
observer launcher and a subsequent unchanged real Binwalk bridge trial succeeded,
|
||||
reporting uImage at offset 0 and SquashFS at 2741985 with `signature_scan` coverage.
|
||||
The observer is a diagnostic with a different launcher identity, not a benchmark
|
||||
baseline. The original failure remains unexplained. Subsequent direct and bridge
|
||||
workflows agree on all region facts and coverage; random upstream region IDs vary
|
||||
and are preserved. Single-text estimates are 1387 tokens direct and 827 projected,
|
||||
so this one-operation task provides much less composition benefit than Android.
|
||||
|
||||
SQLite's actual WASM fails the frozen REA WABT output parser with `Unrecognized
|
||||
WABT section row`. WABT 1.0.42 prints the nine-character DataCount label without
|
||||
leading padding; the owning parser currently requires leading whitespace. No
|
||||
downstream shim is used, and no successful WASM provider claim is made.
|
||||
Obsidian's initial real-model analysis ended without Evidence after Codex's host
|
||||
closed stdout. Its standalone SDK replay returns a real provider timeout with a
|
||||
retained partial Evidence ID. This was observed before automatic partial-summary
|
||||
recovery was added, so the transcript retains that reference but does not export
|
||||
its payload. A source-fixture production-boundary test now verifies that provider
|
||||
timeout IDs survive auditing and resolve through the retained-evidence view.
|
||||
Neither failure is a Code Mode speedup.
|
||||
|
||||
## Real Android agents
|
||||
|
||||
After raising the task ceiling, a direct Terra trial completes in 107.478 seconds
|
||||
with 348556 API input tokens (292608 cached), 1815 output tokens and five completed
|
||||
MCP calls. All six strict producer-fact assertions and cited-ID checks pass. Its
|
||||
sampled peak is 1349251072 bytes and 127 tasks, with zero observed limit events.
|
||||
|
||||
A client Code Mode trial passes the same assertions in 183.361 seconds with
|
||||
414869 input tokens (372480 cached), 3618 output tokens. The transcript must still
|
||||
be inspected before attributing native composition or filtering to that flag.
|
||||
|
||||
The bridge trial before explicit projection guidance takes 84.754 seconds,
|
||||
292063 input tokens (244992 cached) and 1661 output tokens. It returns whole
|
||||
Evidence instead of filtering most intermediates. Requested facts and IDs are
|
||||
supported, but its stronger exact-projection check fails because overload index
|
||||
was omitted. A clarified question explicitly requires that index in later trials.
|
||||
|
||||
The next bridge trial composes three calls and filters their results, but selects
|
||||
`normalized_result.engine.artifact_sha256` as the target hash. This is the Jadx
|
||||
bridge JAR identity, not the APK identity. The strict hash check fails. It takes
|
||||
166.411 seconds and 324417 input tokens (279040 cached), 2362 output tokens.
|
||||
This confirms a real provenance risk in model-written projections. Trial
|
||||
instructions now identify `Evidence.subject.digest.sha256` explicitly. This
|
||||
change has not yet established a reliable repeated model efficiency gain.
|
||||
|
||||
The following bridge trial passes all six assertions and cited-ID checks, using
|
||||
one execution script for all three original operations. It takes 137.966 seconds,
|
||||
190313 input tokens (156672 cached) and 1695 output tokens. Four discovery calls
|
||||
precede the script. This is an encouraging token result, but its revised prompt,
|
||||
instructions and concurrent host workload prevent a controlled speed comparison.
|
||||
Instructions and prompt digests are now retained without authentication or an
|
||||
ambient environment snapshot, so future equal-task repetitions can be audited.
|
||||
|
||||
The 6.1 sol diagnostic touched the 96-task limit; it is invalid as a comparison
|
||||
and was interrupted, with its copied profile removed. The wrapper now allows 192
|
||||
tasks, records task/memory limit events and rejects capacity-limited trials. A
|
||||
real thread-creation probe hit exactly 192 tasks, was stopped with exit 125 and
|
||||
retained its limit reason; it used about 12 MiB and caused no memory-limit event.
|
||||
The nine actual stdio boundary tests pass after discovery changes.
|
||||
|
||||
Wall times above include contention on the same pinned CPU during exploratory
|
||||
work and differing prompts/instructions. They cannot support a controlled speed
|
||||
ratio. A reproducible efficiency conclusion requires successful equal-task trials
|
||||
and serial repetitions with the strongest existing-client baseline.
|
||||
|
||||
The stronger client trial uses an explicit instruction to compose and project the
|
||||
existing operations, and passes all six checks: 72.341 seconds, 161370 input
|
||||
tokens (120576 cached), 1664 output tokens and three completed MCP calls. An
|
||||
equivalent question with the bridge passes in 115.962 seconds, 321295 input tokens
|
||||
(283648 cached), 2274 output tokens, five discovery calls and one execution. The
|
||||
native transcript does not expose executor source, so exact internal composition
|
||||
is unverified. Nevertheless, this measured client profile is a materially stronger
|
||||
baseline than the original repeated-call trial. Total input, uncached input,
|
||||
output and wall time must be considered separately; cached-token differences make
|
||||
total input an incomplete cost metric.
|
||||
|
||||
This reverses the preliminary temptation to equate replay compression with the
|
||||
best Codex experience. Existing client support with explicit Evidence-aware
|
||||
recipes may be enough. An optional bridge needs to justify discovery overhead and
|
||||
remains useful to test for clients without equivalent execution support.
|
||||
|
||||
Eleven actual SDK/stdio tests pass, including a controlled delivery delay after a
|
||||
real upstream result to exercise queued cancellation. Both the delayed call and
|
||||
its queued successor release deferred guest handles before runtime disposal.
|
||||
This test does not simulate or establish new provider behavior.
|
||||
|
||||
The bridge remains an experiment and has not changed any published REA contract.
|
||||
|
||||
## Existing-client and managed follow-up
|
||||
|
||||
The same Android v3 question with normal MCP, without the experimental Code Mode
|
||||
feature flags, passes all six fact assertions and reference checks: 75.117 seconds,
|
||||
103471 API input tokens (73728 cached), 1538 output tokens and three original MCP
|
||||
calls. This is the strongest measured Android baseline so far. Its wall time also
|
||||
includes a short boundary-test overlap, so it is not a controlled latency ratio.
|
||||
The installed model catalog requests code-mode-only tool delivery; flag settings
|
||||
and nested MCP events alone cannot establish the internal executor's behavior.
|
||||
|
||||
The first real ShareX managed-agent comparison uses the same question with Terra
|
||||
high and identical provider settings. Normal MCP takes 81.578 seconds, 217573 input
|
||||
tokens (186368 cached), 2647 output tokens and two original calls. The bridge takes
|
||||
96.930 seconds, 218089 input tokens (155904 cached), 3247 output tokens, four
|
||||
searches and two executions. Its first script uses an expression instead of the
|
||||
required async-body `return`, yielding no projection; it repeats both operations.
|
||||
Query discovery now defaults to lightweight names/descriptions, while exact-name
|
||||
discovery supplies input schemas. This run still requested full output schemas
|
||||
individually and therefore does not establish an efficiency gain from that change.
|
||||
|
||||
Both answers match all seven requested managed fact groups, including exact target
|
||||
SHA-256, assembly/module identities, classification evidence, framework, declared
|
||||
ModuleRef/PInvoke arrays and coverage. Those empty arrays concern ShareX.dll only.
|
||||
The bridge passes cited-reference verification. Codex truncated the original
|
||||
managed-artifact response in its normal-MCP JSONL at roughly one MiB, leaving an
|
||||
invalid JSON fragment; the automated grader cannot validate that one reference
|
||||
from a complete producer envelope. Its reference-failure result is retained,
|
||||
despite the matching ID being visible in the transcript prefix and replay. This
|
||||
is a verification limitation, not a demonstrated fabricated ID.
|
||||
|
||||
The runner now handles SIGTERM so copied trial credentials are removed during a
|
||||
resource abort. A controlled non-model launcher received owned-scope SIGTERM,
|
||||
exited 143 and left no trial profile while preserving the dummy source auth file.
|
||||
This verifies credential cleanup only. The eleven actual stdio tests pass after
|
||||
the adaptive discovery change, with a sampled peak of 1365106688 bytes, 42 tasks
|
||||
and zero observed memory/task limit events.
|
||||
|
||||
## Serial comparison and package inventory
|
||||
|
||||
A serial Android v3 pair, without concurrent provider/build work, passes every
|
||||
fact and cited-reference check. Normal MCP uses 56.962 seconds, 105861 input
|
||||
(75776 cached, 30085 uncached), 1106 output tokens and three original calls. The
|
||||
adaptive-discovery bridge uses 106.134 seconds, 200537 input (175872 cached,
|
||||
24665 uncached), 2177 output tokens, four searches and one execution. Its smaller
|
||||
uncached input does not imply lower overall cost: output, cache rates and latency
|
||||
also matter. It still requests original output schemas and mistakenly searches
|
||||
for the already exposed bridge executor. Sampled memory peaks are 1333710848 and
|
||||
1656098816 bytes respectively; neither run touches memory/task limits. One pair
|
||||
is insufficient for a statistical latency or reliability claim.
|
||||
|
||||
A separate Obsidian package-inventory replay succeeds with existing REA tools.
|
||||
Direct and bridge return byte-for-byte equal selected facts: 397 graph nodes and
|
||||
occurrences, 369 files, 28 directories, root main.js/package.json occurrences,
|
||||
no root preload.js, no observed integrity contradiction, complete-within-substeps
|
||||
coverage and an explicit unexplored JavaScript analysis branch. Both original
|
||||
Evidence IDs are preserved. Direct/bridge operations take 6.267/6.618 seconds;
|
||||
original single-text results total 178958 estimated tokens versus a 1404-token
|
||||
bridge result, 1305 for discovery and 322 for code. These are replay estimates,
|
||||
not model latency or success. Inventory does not complete the timed-out AST task.
|
||||
|
||||
An output-type discovery probe compiles seven real SDK schemas successfully.
|
||||
Derived TypeScript estimates are 784/719/751 tokens for Android package/search/
|
||||
class, 1558/1420 for managed artifact/native boundaries, 1264 for artifact inventory
|
||||
and 2289 for function analysis. Their original JSON schemas use 1666/1480/1576,
|
||||
3524/2932, 2812 and 5091 respectively. These declarations describe code values;
|
||||
they do not preserve every JSON Schema validation rule. Exact-name discovery now
|
||||
includes them without replacing explicitly requested original JSON schemas. Real
|
||||
model benefit from this addition remains to be tested. The audit also carries
|
||||
original Evidence links; a real retained-view readback verifies linked IDs rather
|
||||
than trusting references invented by a projection.
|
||||
|
||||
The output-declaration boundary suite first failed its type-name assertion because
|
||||
the compiler preferred the schema's URN identity as a display name. The derived
|
||||
compilation now supplies a descriptive title while returning the original schema
|
||||
unchanged. All eleven tests then pass, including original-link readback, at a
|
||||
sampled peak of 1399996416 bytes and zero observed limit events. Declarations are
|
||||
cached per exact tool name for this immutable live-catalog connection; repeated
|
||||
discovery does not regenerate them. No provider implementation or published
|
||||
REA API has changed.
|
||||
|
||||
## Typed-discovery serial agent checkpoint
|
||||
|
||||
All rows below use real Codex CLI 0.161.0, Terra high, the same rendered question
|
||||
within each pair, and the same provider settings. The bridge/runtime/runner source
|
||||
fingerprints match commit `22326e819`; model-written original arguments can differ.
|
||||
Comparisons include required mode-specific instructions and are not a controlled
|
||||
causal decomposition of inference, caching and provider time.
|
||||
|
||||
| Case | Profile | Wall seconds | Input | Cached | Uncached | Output | MCP calls | Fact/reference check |
|
||||
| ------------------ | ------- | -----------: | -----: | -----: | -------: | -----: | --------: | --------------------------------- |
|
||||
| android | direct | 67.329 | 107007 | 83712 | 23295 | 1670 | 3 | pass |
|
||||
| android | bridge | 48.231 | 113256 | 91648 | 21608 | 1456 | 3 | pass |
|
||||
| managed | bridge | 73.145 | 116974 | 92672 | 24302 | 2065 | 3 | classification field omitted |
|
||||
| managed | direct | 33.15 | 108096 | 75776 | 32320 | 2315 | 2 | facts match; reference unverified |
|
||||
| electron-inventory | direct | 89.494 | 266121 | 238848 | 27273 | 3215 | 3 | pass |
|
||||
| electron-inventory | bridge | 145.577 | 462809 | 407040 | 55769 | 5624 | 8 | pass |
|
||||
| firmware | bridge | 81.944 | 152886 | 126976 | 25910 | 2192 | 3 | pass |
|
||||
| firmware | direct | 30.161 | 103224 | 74752 | 28472 | 1761 | 1 | pass |
|
||||
|
||||
The managed bridge omits the producer classification container field (`pe`) while
|
||||
preserving status/runtime/implementation/architecture and all classification
|
||||
evidence. Its strict complete-classification assertion fails; this is an omission,
|
||||
not an incorrect architecture claim. Normal MCP matches requested facts but its
|
||||
large-result JSONL truncation again prevents automated verification of one ID.
|
||||
|
||||
The initial grader treated grouped path records, extra known coverage counts and
|
||||
an empty per-source contradiction object as mismatches. Task-specific comparison
|
||||
now accepts those lossless report forms. Firmware compares observed offset/length
|
||||
facts, checks any supplied derived end arithmetically, and verifies engine version
|
||||
and executable digest. Run-local UUIDs and differently named display fields are
|
||||
not compared as if they were stable source identity. Original strict grades and
|
||||
pre-equivalence grades remain outside Git, and new grades include a grader digest.
|
||||
These report-format comparisons do not add aliases to any REA tool contract.
|
||||
|
||||
Obsidian bridge discovery takes six searches and two executions. The first returns
|
||||
the entire nested inventory; the second repeats it and computes a focused answer.
|
||||
The live nested Evidence schema describes normalized_result as generic JSON, so
|
||||
derived types alone cannot teach its concrete manifest/occurrences layout. Its
|
||||
recursive record walk happens to match this corpus, but format-specific field
|
||||
selection is the stronger recipe. Both model profiles choose record-and-continue
|
||||
for integrity; deterministic inventory replays used fail. No contradictions were
|
||||
observed, and the exact caller-selected arguments are retained.
|
||||
|
||||
## Completed serial cohort
|
||||
|
||||
The final sixteen-row cohort, raw-event digests, exact runner/bridge/lockfile
|
||||
fingerprints, prompt/instruction/provider digests and sampled resource stats are
|
||||
in [agent-results.json](agent-results.json). The requested model configuration is
|
||||
Terra high. Two managed bridge answers omit the classification container field
|
||||
under the stronger full-object check; two normal-MCP managed logs have a truncated
|
||||
producer envelope. The twelve other rows pass requested fact and observed-reference
|
||||
checks. This is producer parity, not independent runtime truth or a per-fact
|
||||
citation-association proof. Earlier stricter report-form grades remain retained.
|
||||
|
||||
| Trial | Wall seconds | Input | Cached | Uncached | Output | MCP calls | Check |
|
||||
| ---------------------------------- | -----------: | -----: | -----: | -------: | -----: | --------: | --------------------------------- |
|
||||
| android-direct-5 | 67.329 | 107007 | 83712 | 23295 | 1670 | 3 | pass |
|
||||
| android-bridge-7 | 48.231 | 113256 | 91648 | 21608 | 1456 | 3 | pass |
|
||||
| managed-bridge-2 | 73.145 | 116974 | 92672 | 24302 | 2065 | 3 | classification container omitted |
|
||||
| managed-direct-2 | 33.150 | 108096 | 75776 | 32320 | 2315 | 2 | facts match; reference unverified |
|
||||
| electron-inventory-direct-1 | 89.494 | 266121 | 238848 | 27273 | 3215 | 3 | pass |
|
||||
| electron-inventory-bridge-1 | 145.577 | 462809 | 407040 | 55769 | 5624 | 8 | pass |
|
||||
| firmware-bridge-1 | 81.944 | 152886 | 126976 | 25910 | 2192 | 3 | pass |
|
||||
| firmware-direct-1 | 30.161 | 103224 | 74752 | 28472 | 1761 | 1 | pass |
|
||||
| native-direct-1 | 327.150 | 151343 | 122368 | 28975 | 2458 | 4 | pass |
|
||||
| native-bridge-1 | 334.642 | 285961 | 245504 | 40457 | 2129 | 4 | pass |
|
||||
| android-bridge-8 | 134.555 | 159370 | 134400 | 24970 | 3250 | 7 | pass |
|
||||
| android-direct-6 | 71.624 | 153031 | 117504 | 35527 | 1501 | 3 | pass |
|
||||
| managed-direct-3 | 75.229 | 108593 | 75776 | 32817 | 2774 | 2 | facts match; reference unverified |
|
||||
| managed-bridge-3 | 40.609 | 125519 | 96000 | 29519 | 2274 | 3 | classification container omitted |
|
||||
| electron-inventory-guided-bridge-1 | 85.909 | 101994 | 61440 | 40554 | 2563 | 3 | pass |
|
||||
| electron-inventory-guided-direct-1 | 67.540 | 263484 | 221952 | 41532 | 2499 | 4 | pass |
|
||||
|
||||
Ghidra's aggregate sampled CPU time is 263.738 seconds directly and 271.096 with
|
||||
the bridge. The bridge uses three original operations versus four directly,
|
||||
obtaining containing identity from the analyzed procedure. Both close the session
|
||||
successfully. The native comparison uses the same default analysis settings;
|
||||
these data do not establish a meaningful native speedup.
|
||||
|
||||
The second Android bridge repetition completes seven original calls across five
|
||||
scripts, without tool errors, before its final composed answer. Its 134.555-second
|
||||
result reverses the first repetition's latency advantage. The model did not
|
||||
reliably choose one composed execution even with explicit guidance. Uncached
|
||||
input is lower in both bridge repetitions, while output and latency vary.
|
||||
|
||||
The guided Electron question explicitly names the existing operations and the
|
||||
nested inventory's manifest/occurrences layout for both profiles. Both pass, with
|
||||
integrity_policy=fail. The bridge composes open/inspect/close once; the normal-MCP
|
||||
profile repeats inspect_artifact. The total-input reduction from 263484 to 101994
|
||||
is large, but uncached input differs by only 978 tokens and bridge latency is
|
||||
higher. Earlier non-guided profiles are a different question and cannot support
|
||||
a causal prompt-improvement ratio. Unknown nested payload types remain an observed
|
||||
contract-discovery problem; caller-selected values and identities are preserved.
|
||||
|
||||
All sixteen runs stay below memory/task ceilings, with maximum sampled memory
|
||||
1659744256 bytes and 126 tasks; no observed memory/task limit event or resource
|
||||
abort occurs. Copied profile count is zero at completion. Runtime measurements
|
||||
belong to the recorded source fingerprint at 22326e819; subsequent timer-range
|
||||
validation and test-only Ajv installation are not new model measurements.
|
||||
|
||||
Final boundary verification passes twelve actual SDK/stdio tests. A 2 MiB guest
|
||||
budget rejects allocation pressure and the next execution succeeds. This is a
|
||||
guest-budget failure, not an OS OOM or provider behavior claim. A local pinned-Node
|
||||
probe shows a 2147483648-ms timeout becomes 1 ms; the owning named execution
|
||||
contract now rejects that range before allocation. After SDK conversion, both
|
||||
advertised bridge input schemas validate under JSON Schema 2020-12, and the actual
|
||||
stdio client rejects invalid code, oversized memory and timer values. Sampled
|
||||
suite memory peaks at 1387433984 bytes, with zero observed memory/task limit events.
|
||||
|
||||
See [decision.md](decision.md) for the recommendation: keep current REA contracts,
|
||||
improve focused Evidence-aware workflow guidance, and retain the bridge as an
|
||||
opt-in candidate. A broad default switch is not justified by these samples.
|
||||
+88
-104
@@ -1,12 +1,4 @@
|
||||
#!/bin/bash
|
||||
{
|
||||
# Truncation guard: this brace group opens on line 2 and closes on the last
|
||||
# line, so bash must read the entire file before it runs any command. On the
|
||||
# `curl ... | bash` path, a download cut off anywhere before the final `}` is
|
||||
# an unterminated compound command, which bash reports as a syntax error at
|
||||
# EOF without running anything. Without the guard, a cut right after `set`
|
||||
# would run the bare builtin and print every shell variable, and a cut right
|
||||
# after `main` would run the installer without the caller's flags.
|
||||
set -euo pipefail
|
||||
|
||||
PACKAGE="rea-agents"
|
||||
@@ -29,112 +21,104 @@ usage() {
|
||||
cat <<'EOF'
|
||||
Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose]
|
||||
|
||||
Installs the REA CLI, then starts `rea setup` interactively to configure Hopper
|
||||
and agents when a terminal is available. Pass --no-setup (or --no-prompt) to
|
||||
skip setup and run `rea setup` later.
|
||||
Installs only the REA CLI. Run `rea setup` separately to configure Hopper and agents.
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--version)
|
||||
(($# >= 2)) || fail "--version requires a value."
|
||||
version="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=true
|
||||
shift
|
||||
;;
|
||||
--no-setup|--no-prompt)
|
||||
start_setup=false
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
verbose=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
while (($# > 0)); do
|
||||
case "$1" in
|
||||
--version)
|
||||
(($# >= 2)) || fail "--version requires a value."
|
||||
version="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
dry_run=true
|
||||
shift
|
||||
;;
|
||||
--no-setup|--no-prompt)
|
||||
start_setup=false
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
verbose=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*) fail "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$verbose" == true ]]; then
|
||||
set -x
|
||||
fi
|
||||
if [[ "$verbose" == true ]]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
platform="$(uname -s)"
|
||||
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
|
||||
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
|
||||
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
|
||||
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
|
||||
platform="$(uname -s)"
|
||||
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
|
||||
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
|
||||
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
|
||||
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
|
||||
|
||||
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
||||
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
|
||||
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
|
||||
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
|
||||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
|
||||
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
|
||||
fi
|
||||
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
||||
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
|
||||
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
|
||||
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
|
||||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
|
||||
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
|
||||
fi
|
||||
|
||||
if [[ -n "$version" ]]; then
|
||||
[[ "$version" =~ ^${version_pattern}$ ]] || fail "version must be an exact semantic version."
|
||||
else
|
||||
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry."
|
||||
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})' 2>/dev/null)" || fail "the release response was invalid. Retry later or pass --version VERSION."
|
||||
[[ "$tag" =~ ^rea-agents-(${version_pattern})$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION."
|
||||
version="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
if [[ -n "$version" ]]; then
|
||||
[[ "$version" =~ ^${version_pattern}$ ]] || fail "version must be an exact semantic version."
|
||||
else
|
||||
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry."
|
||||
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})' 2>/dev/null)" || fail "the release response was invalid. Retry later or pass --version VERSION."
|
||||
[[ "$tag" =~ ^rea-agents-(${version_pattern})$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION."
|
||||
version="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# npm's exact-version resolver cleans build metadata from the requested version,
|
||||
# so it may install the base release and make the post-install identity check fail.
|
||||
if [[ "$version" =~ \+ ]]; then
|
||||
fail "npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry."
|
||||
fi
|
||||
# npm's exact-version resolver cleans build metadata from the requested version,
|
||||
# so it may install the base release and make the post-install identity check fail.
|
||||
if [[ "$version" =~ \+ ]]; then
|
||||
fail "npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry."
|
||||
fi
|
||||
|
||||
install_args=(install --global)
|
||||
if [[ "$platform" == "Linux" ]]; then
|
||||
install_args+=(--prefix "$HOME/.local")
|
||||
install_bin="$HOME/.local/bin/rea"
|
||||
else
|
||||
npm_prefix="$(npm prefix --global)" || fail "the npm global prefix could not be read. Repair the npm configuration, then retry."
|
||||
install_bin="$npm_prefix/bin/rea"
|
||||
fi
|
||||
install_args+=("$PACKAGE@$version")
|
||||
install_args=(install --global)
|
||||
if [[ "$platform" == "Linux" ]]; then
|
||||
install_args+=(--prefix "$HOME/.local")
|
||||
install_bin="$HOME/.local/bin/rea"
|
||||
else
|
||||
npm_prefix="$(npm prefix --global)" || fail "the npm global prefix could not be read. Repair the npm configuration, then retry."
|
||||
install_bin="$npm_prefix/bin/rea"
|
||||
fi
|
||||
install_args+=("$PACKAGE@$version")
|
||||
|
||||
printf 'REA install plan\n'
|
||||
printf ' Version: %s\n' "$version"
|
||||
printf ' Runtime: Node.js %s\n' "$node_version"
|
||||
printf ' Command:'
|
||||
printf ' %q' npm "${install_args[@]}"
|
||||
printf '\n'
|
||||
printf ' Binary: %s\n' "$install_bin"
|
||||
printf ' Setup: %s\n' "$([[ "$start_setup" == true ]] && printf 'start when a terminal is available' || printf 'skipped')"
|
||||
printf 'REA install plan\n'
|
||||
printf ' Version: %s\n' "$version"
|
||||
printf ' Runtime: Node.js %s\n' "$node_version"
|
||||
printf ' Command:'
|
||||
printf ' %q' npm "${install_args[@]}"
|
||||
printf '\n'
|
||||
printf ' Binary: %s\n' "$install_bin"
|
||||
printf ' Setup: %s\n' "$([[ "$start_setup" == true ]] && printf 'start when a terminal is available' || printf 'skipped')"
|
||||
|
||||
if [[ "$dry_run" == true ]]; then
|
||||
printf 'Dry run complete; no changes made.\n'
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$dry_run" == true ]]; then
|
||||
printf 'Dry run complete; no changes made.\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
|
||||
npm "${install_args[@]}" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
|
||||
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
|
||||
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
|
||||
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
|
||||
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
|
||||
npm "${install_args[@]}" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
|
||||
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
|
||||
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
|
||||
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
|
||||
|
||||
printf 'REA %s is installed.\n' "$version"
|
||||
if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
|
||||
printf 'Starting REA setup...\n'
|
||||
"$install_bin" setup </dev/tty >/dev/tty
|
||||
else
|
||||
printf 'Run %s setup to configure Hopper and agents.\n' "$install_bin"
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
exit $?
|
||||
}
|
||||
printf 'REA %s is installed.\n' "$version"
|
||||
if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
|
||||
printf 'Starting REA setup...\n'
|
||||
"$install_bin" setup </dev/tty >/dev/tty
|
||||
else
|
||||
printf 'Run %s setup to configure Hopper and agents.\n' "$install_bin"
|
||||
fi
|
||||
|
||||
@@ -51,12 +51,10 @@
|
||||
"bridge/process/ProcessRunTokenReader.swift",
|
||||
"bridge/process/ProcessRunTokenReaderMain.swift",
|
||||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"bridge/ghidra/ReaGhidraBundleActivator.java",
|
||||
"bridge/ghidra/ReaGhidraExtensions.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareCom.java",
|
||||
"bridge/ghidra/ReaGhidraApplySeeds.java",
|
||||
"bridge/ghidra/ReaGhidraNoReturnFix.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareRaw.java",
|
||||
"bridge/android/ReaJadxBridge.java",
|
||||
"bridge/mitmproxy/capture.py",
|
||||
"scripts/rea.mjs",
|
||||
@@ -168,7 +166,6 @@
|
||||
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
|
||||
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
|
||||
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
|
||||
"verify:ghidra:raw": "npm run build:cached && node scripts/verify-real-ghidra-raw.mjs",
|
||||
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
|
||||
"verify:interface-builder": "npm run build:cached && node scripts/verify/apple/interface-builder.mjs",
|
||||
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
|
||||
|
||||
@@ -401,22 +401,6 @@ class WebsiteTranslationChecks(unittest.TestCase):
|
||||
server.shutdown()
|
||||
worker.join()
|
||||
|
||||
def test_every_reading_page_offers_light_and_dark_theme_colors(self):
|
||||
site = ROOT / "website/_site"
|
||||
english_pages = reading_pages(ROOT / "website/public")
|
||||
self.assertTrue(english_pages)
|
||||
for language in languages(ROOT):
|
||||
code = language["code"]
|
||||
for original in english_pages:
|
||||
relative = original.relative_to(ROOT / "website/public")
|
||||
for path in {original, site / relative if code == "en" else site / code / relative}:
|
||||
with self.subTest(language=code, page=str(path.relative_to(ROOT))):
|
||||
doc = Document(path.read_text())
|
||||
media = sorted(node.attrs.get("media") for node in doc.nodes if node.tag == "meta" and node.attrs.get("name") == "theme-color")
|
||||
self.assertEqual(media, ["(prefers-color-scheme: dark)", "(prefers-color-scheme: light)"])
|
||||
styles = (ROOT / "website/public/assets/styles.css").read_text()
|
||||
self.assertTrue("@media (prefers-color-scheme: dark)" in styles, "styles.css has no dark color scheme block")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -13,6 +13,10 @@ import { exec } from "./lib/verify-package-core.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { createAnthropicModelFixture } from "./verify/clients/anthropic-model-fixture.mjs";
|
||||
|
||||
assert(
|
||||
process.platform !== "win32",
|
||||
"Native Claude Code verifier requires POSIX; Windows is unverified",
|
||||
);
|
||||
const repo = fileURLToPath(new URL("..", import.meta.url));
|
||||
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
||||
const command = process.env.REA_VERIFY_CLAUDE_COMMAND ?? "claude";
|
||||
@@ -42,13 +46,8 @@ try {
|
||||
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
|
||||
).stdout.trim();
|
||||
} catch (cause) {
|
||||
// npm installs put only a claude.cmd shim on PATH, which Node does not spawn without a shell.
|
||||
const windowsHint =
|
||||
process.platform === "win32"
|
||||
? " On Windows, select claude.exe; npm installs provide it under node_modules/@anthropic-ai/claude-code/bin."
|
||||
: "";
|
||||
throw new Error(
|
||||
`Claude Code preflight failed for ${command}; install Claude Code or set REA_VERIFY_CLAUDE_COMMAND.${windowsHint} ${String(cause)}`,
|
||||
`Claude Code preflight failed for ${command}; install Claude Code or set REA_VERIFY_CLAUDE_COMMAND. ${String(cause)}`,
|
||||
);
|
||||
}
|
||||
const target = join(lab, "fixture α测试");
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
@@ -177,7 +177,6 @@ const scriptCollisions = [
|
||||
"ReaGhidraPrepareCom.java",
|
||||
"ReaGhidraApplySeeds.java",
|
||||
"ReaGhidraNoReturnFix.java",
|
||||
"ReaGhidraPrepareRaw.java",
|
||||
];
|
||||
await mkdir(callerDirectory);
|
||||
for (const name of scriptCollisions) await mkdir(join(callerDirectory, name));
|
||||
@@ -291,23 +290,6 @@ try {
|
||||
await client.connect(transport);
|
||||
await call("open_binary", { path: target });
|
||||
await call("binary_overview", {});
|
||||
const activators = (
|
||||
await readdir(runtimeParent, { recursive: true })
|
||||
).filter((path) => basename(path) === "ReaGhidraBundleActivator.class");
|
||||
assert.equal(activators.length, 1);
|
||||
const [activator] = activators;
|
||||
assert.ok(activator !== undefined);
|
||||
const bundleDirectory = dirname(join(runtimeParent, activator));
|
||||
const manifest = await readFile(
|
||||
join(bundleDirectory, "META-INF", "MANIFEST.MF"),
|
||||
"utf8",
|
||||
);
|
||||
assert.match(manifest, /^Bundle-Activator: ReaGhidraBundleActivator\r?$/mu);
|
||||
await assert.rejects(
|
||||
readFile(join(bundleDirectory, "GeneratedActivator.class")),
|
||||
{ code: "ENOENT" },
|
||||
);
|
||||
report.bundleActivator = "ReaGhidraBundleActivator";
|
||||
assert.equal((await call("list_documents", {})).length, 1);
|
||||
await call("list_names", {});
|
||||
const procedures = await call("list_procedures", {});
|
||||
@@ -412,15 +394,15 @@ try {
|
||||
return evidence.normalized_result;
|
||||
},
|
||||
);
|
||||
const { GHIDRA_MUTATING_OPERATIONS, GHIDRA_SESSION_CAPABILITIES } =
|
||||
const expected = (
|
||||
await import(
|
||||
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
|
||||
);
|
||||
const expected = GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
)
|
||||
).GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
(name) =>
|
||||
name !== "ping" &&
|
||||
name !== "shutdown" &&
|
||||
!GHIDRA_MUTATING_OPERATIONS.has(name),
|
||||
name !== "annotate_native_function",
|
||||
);
|
||||
assert.ok(
|
||||
expected.every((name) => report.mcpOperations.includes(name)),
|
||||
@@ -458,10 +440,7 @@ try {
|
||||
token,
|
||||
"Analysis changed caller token authority.",
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await readdir(callerDirectory)).sort(),
|
||||
[...scriptCollisions].sort(),
|
||||
);
|
||||
assert.deepEqual((await readdir(callerDirectory)).sort(), scriptCollisions);
|
||||
for (const name of scriptCollisions)
|
||||
assert.deepEqual(await readdir(join(callerDirectory, name)), []);
|
||||
report.callerScriptCollisionsPreserved = true;
|
||||
|
||||
@@ -5,16 +5,10 @@ import { promisify } from "node:util";
|
||||
const exec = promisify(execFile);
|
||||
const packedHopperBridge = "package/bridge/hopper_bridge.py";
|
||||
const packedGhidraBridge = "package/bridge/ghidra/ReaGhidraBridge.java";
|
||||
const packedGhidraActivator =
|
||||
"package/bridge/ghidra/ReaGhidraBundleActivator.java";
|
||||
|
||||
/** Check shipped bridge files; real-provider lanes verify runtime behavior. */
|
||||
export async function verifyPackedBridge({ workspace, tarball, packedFiles }) {
|
||||
for (const path of [
|
||||
packedHopperBridge,
|
||||
packedGhidraBridge,
|
||||
packedGhidraActivator,
|
||||
]) {
|
||||
for (const path of [packedHopperBridge, packedGhidraBridge]) {
|
||||
if (!packedFiles.includes(path)) throw new Error(`package omitted ${path}`);
|
||||
}
|
||||
await exec("tar", ["-xf", tarball, "-C", workspace]);
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
if (process.argv.length !== 2)
|
||||
throw new Error("Usage: node scripts/verify-real-ghidra-raw.mjs");
|
||||
if (!["linux", "darwin"].includes(process.platform))
|
||||
throw new Error(
|
||||
"Raw-binary verification requires a Linux or macOS Ghidra host.",
|
||||
);
|
||||
if (process.env.GHIDRA_INSTALL_DIR === undefined)
|
||||
throw new Error(
|
||||
"Set GHIDRA_INSTALL_DIR to a bring-your-own Ghidra installation.",
|
||||
);
|
||||
const installation = inspectGhidraInstallation({
|
||||
environment: process.env,
|
||||
installDir: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { javaHome: process.env.JAVA_HOME }),
|
||||
});
|
||||
assert.equal(installation.status, "available", JSON.stringify(installation));
|
||||
|
||||
const run = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-raw-proof-"));
|
||||
const runtime = join(workspace, "runtime");
|
||||
await mkdir(runtime);
|
||||
const bytes = Buffer.from("1eff2fe1", "hex"); // ARM little-endian bx lr.
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
const path = join(workspace, "image.bin");
|
||||
await writeFile(path, bytes);
|
||||
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
TMPDIR: runtime,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_PROCESS_RUN_ID: run.run_id,
|
||||
GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-real-raw-proof", version: "1" });
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
let report;
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const profiles = [];
|
||||
const sessions = [];
|
||||
for (const entryPoint of ["0x08000000", "0x8000000", "0x09000000"]) {
|
||||
// Omit provider_id to exercise reuse of the current provider route.
|
||||
const opened = await call("open_binary", {
|
||||
path,
|
||||
format: "raw-binary",
|
||||
architecture: "arm",
|
||||
entry_point: entryPoint,
|
||||
});
|
||||
const entry = `0x${BigInt(entryPoint).toString(16)}`;
|
||||
assert.equal(opened.entry_point, entry);
|
||||
const image = await call("inspect_native_load_image");
|
||||
assert.equal(
|
||||
image.normalized_result.status,
|
||||
"verified",
|
||||
JSON.stringify(image),
|
||||
);
|
||||
assert.equal(image.normalized_result.base_address, entry);
|
||||
assert.equal(image.normalized_result.entry.address, entry);
|
||||
assert.equal(
|
||||
image.normalized_result.observations.language_id,
|
||||
"ARM:LE:32:v7",
|
||||
);
|
||||
assert.ok(
|
||||
image.normalized_result.observations.entry_points.includes(entry),
|
||||
);
|
||||
assert.equal(image.analysis_profile.parameters.entry_point, entry);
|
||||
profiles.push(image.analysis_profile.digest);
|
||||
const status = await call("binary_session");
|
||||
assert.equal(status.entry_point, entry);
|
||||
sessions.push(status.analysis_run.run_id);
|
||||
const memory = await call("read_bytes", {
|
||||
address: entry,
|
||||
length: bytes.length,
|
||||
});
|
||||
assert.equal(memory.normalized_result.bytes_hex, bytes.toString("hex"));
|
||||
const procedures = await call("list_procedures");
|
||||
assert.ok(
|
||||
procedures.normalized_result.some(
|
||||
(procedure) => procedure.address === entry,
|
||||
),
|
||||
);
|
||||
const instruction = await call("inspect_native_instruction", {
|
||||
address: entry,
|
||||
});
|
||||
assert.equal(instruction.normalized_result.address, entry);
|
||||
}
|
||||
assert.equal(profiles[0], profiles[1]);
|
||||
assert.notEqual(profiles[1], profiles[2]);
|
||||
assert.equal(sessions[0], sessions[1]);
|
||||
assert.notEqual(sessions[1], sessions[2]);
|
||||
const rejected = await client.callTool({
|
||||
name: "open_binary",
|
||||
arguments: {
|
||||
path,
|
||||
format: "raw-binary",
|
||||
architecture: "arm64",
|
||||
entry_point: "0x8000001",
|
||||
},
|
||||
});
|
||||
assert.equal(rejected.isError, true);
|
||||
assert.match(JSON.stringify(rejected.content), /must be 4-byte aligned/);
|
||||
const unchanged = await call("binary_session");
|
||||
assert.equal(unchanged.analysis_run.run_id, sessions[2]);
|
||||
assert.equal(unchanged.entry_point, "0x9000000");
|
||||
await call("close_binary");
|
||||
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"inspect-native-load-image",
|
||||
path,
|
||||
"--target-format",
|
||||
"raw-binary",
|
||||
"--architecture",
|
||||
"arm",
|
||||
"--entry-point",
|
||||
"0x08000000",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const cli = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(cli.subject.digest.sha256, sha256);
|
||||
assert.equal(cli.normalized_result.status, "verified");
|
||||
assert.equal(cli.normalized_result.base_address, "0x8000000");
|
||||
assert.equal(cli.analysis_profile.digest, profiles[0]);
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
report = {
|
||||
ok: true,
|
||||
mocked: false,
|
||||
lane: "ghidra-raw",
|
||||
host: process.platform,
|
||||
provider: { id: "ghidra", version: installation.providerVersion },
|
||||
target: {
|
||||
format: "raw-binary",
|
||||
architecture: "arm",
|
||||
sha256,
|
||||
bytes: bytes.length,
|
||||
},
|
||||
transport: { cli: true, stdio_mcp: true },
|
||||
canonical_entry_reuses_session: true,
|
||||
changed_entry_replaces_session: true,
|
||||
lifecycle_entry_preserved: true,
|
||||
invalid_entry_preserves_active_session: true,
|
||||
load_images_verified: true,
|
||||
original_unchanged: true,
|
||||
};
|
||||
} catch (error) {
|
||||
if (stderr.trim()) process.stderr.write(stderr);
|
||||
throw error;
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
const verifierRun = await completeVerifierRun(run);
|
||||
assert.equal(verifierRun.process_lineage.status, "verified");
|
||||
assert.deepEqual(verifierRun.process_lineage.descendants, []);
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
verifier_run: verifierRun,
|
||||
...report,
|
||||
cleanup: "complete",
|
||||
}) + "\n",
|
||||
);
|
||||
|
||||
async function call(name, args = {}) {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const projected = requireMcpOperationResult(result, name);
|
||||
if (
|
||||
name === "open_binary" ||
|
||||
name === "close_binary" ||
|
||||
name === "binary_session"
|
||||
)
|
||||
return projected;
|
||||
const evidence = parseEvidence(result.structuredContent);
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.equal(evidence.subject.format, "raw-binary");
|
||||
return evidence;
|
||||
}
|
||||
@@ -21,17 +21,14 @@ import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolic
|
||||
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
|
||||
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
import {
|
||||
GHIDRA_MUTATING_OPERATIONS,
|
||||
GHIDRA_SESSION_CAPABILITIES,
|
||||
} from "../dist/ghidra/GhidraSessionValues.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { verifyGhidraCompactReferences } from "./lib/real-ghidra-compact-references.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
const SESSION_CAPABILITIES = GHIDRA_SESSION_CAPABILITIES.filter(
|
||||
(name) => !GHIDRA_MUTATING_OPERATIONS.has(name),
|
||||
(name) => name !== "annotate_native_function",
|
||||
);
|
||||
|
||||
if (process.platform !== "win32" || process.arch !== "x64")
|
||||
@@ -239,8 +236,10 @@ try {
|
||||
|
||||
const expected = [
|
||||
...GHIDRA_INVENTORY_OPERATIONS,
|
||||
...GHIDRA_FUNCTION_OPERATIONS,
|
||||
].filter((name) => !GHIDRA_MUTATING_OPERATIONS.has(name));
|
||||
...GHIDRA_FUNCTION_OPERATIONS.filter(
|
||||
(name) => name !== "annotate_native_function",
|
||||
),
|
||||
];
|
||||
if (
|
||||
observed.size !== expected.length ||
|
||||
expected.some((operation) => !observed.has(operation))
|
||||
|
||||
@@ -7,16 +7,13 @@ import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { hashAndroidFile } from "./digest.mjs";
|
||||
import { hashAndroidFile } from "../../../dist/android/AndroidTargetSnapshot.js";
|
||||
import { JADX_RELEASE } from "../../../dist/android/JadxRelease.js";
|
||||
import fixture from "../../fixtures/android/apidemos.json" with { type: "json" };
|
||||
import { verifyAndroidCliCancellation } from "./cli-cancellation.mjs";
|
||||
import { verifyAndroidMetadata } from "./metadata.mjs";
|
||||
import { verifyAndroidFailureRecovery } from "./failure-recovery.mjs";
|
||||
import {
|
||||
resolveJadxConfigurationInputs,
|
||||
validateJadxConfiguration,
|
||||
} from "../../../dist/android/JadxConfiguration.js";
|
||||
import { resolveJadxConfiguration } from "../../../dist/android/JadxConfiguration.js";
|
||||
import { verifyWindowsAndroidLifecycle } from "./windows-lifecycle.mjs";
|
||||
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
@@ -38,16 +35,10 @@ assert.equal(
|
||||
JADX_RELEASE.sha256,
|
||||
"Real Android lane requires the audited headless JADX release JAR",
|
||||
);
|
||||
const configuration = await resolveJadxConfigurationInputs(
|
||||
const { java, jvmArguments } = await resolveJadxConfiguration(
|
||||
process.env,
|
||||
"inspect_android_package",
|
||||
);
|
||||
await validateJadxConfiguration(
|
||||
configuration,
|
||||
process.env,
|
||||
"inspect_android_package",
|
||||
);
|
||||
const { java, jvmArguments } = configuration;
|
||||
const execute = promisify(execFile);
|
||||
await execute(java, ["-version"], { timeout: 10_000 });
|
||||
const entrypoint =
|
||||
@@ -61,8 +52,6 @@ await verifyAndroidMetadata({
|
||||
repository,
|
||||
execute,
|
||||
jvmArguments,
|
||||
entrypoint,
|
||||
environment,
|
||||
bridge: resolve(dirname(entrypoint), "../bridge/android/ReaJadxBridge.java"),
|
||||
});
|
||||
const tasks = [
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
|
||||
/** Verify artifact identity without depending on private runtime snapshot helpers. */
|
||||
export const hashAndroidFile = async (path) => {
|
||||
const hash = createHash("sha256");
|
||||
for await (const bytes of createReadStream(path)) hash.update(bytes);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
@@ -1,130 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { hashAndroidFile } from "./digest.mjs";
|
||||
|
||||
/** Verify folded field assignments and remaining static-block calls through both public adapters. */
|
||||
export const verifyAndroidInitializers = async ({
|
||||
apk,
|
||||
repository,
|
||||
entrypoint,
|
||||
environment,
|
||||
execute,
|
||||
}) => {
|
||||
const digest = await hashAndroidFile(apk);
|
||||
const cases = [
|
||||
{
|
||||
class_name: "fixture.InitializationPolicy",
|
||||
witnesses: [
|
||||
"declared-policy",
|
||||
"->singletonList",
|
||||
"->POLICY:Ljava/util/List;",
|
||||
],
|
||||
},
|
||||
{
|
||||
class_name: "fixture.MixedInitialization",
|
||||
witnesses: ["field-value", "->LABEL:Ljava/lang/String;", "->touch()V"],
|
||||
},
|
||||
];
|
||||
const check = (value, scenario) => {
|
||||
const evidence = parseEvidence(value);
|
||||
assert.equal(evidence.subject.digest.sha256, digest);
|
||||
const result = evidence.normalized_result;
|
||||
assert.equal(result.class_name, scenario.class_name);
|
||||
assert.equal(result.method.name, "<clinit>");
|
||||
assert.equal(result.overload_count, 1);
|
||||
assert.equal(result.body_status, "available");
|
||||
assert.equal(result.representation, "smali");
|
||||
assert.equal(result.fell_back, true);
|
||||
assert.ok(result.markers.includes("static_initializer_disassembly"));
|
||||
assert.equal(result.source.status, "complete");
|
||||
for (const witness of scenario.witnesses)
|
||||
assert.ok(
|
||||
result.source.text.includes(witness),
|
||||
`Missing initializer evidence: ${witness}`,
|
||||
);
|
||||
return evidence;
|
||||
};
|
||||
const cli = new Map();
|
||||
for (const scenario of cases) {
|
||||
const response = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"inspect-android-method",
|
||||
apk,
|
||||
scenario.class_name,
|
||||
"<clinit>",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
{
|
||||
cwd: repository,
|
||||
env: environment,
|
||||
timeout: 150_000,
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
cli.set(scenario.class_name, check(JSON.parse(response.stdout), scenario));
|
||||
}
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
cwd: repository,
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
const client = new Client({
|
||||
name: "rea-real-android-initializers",
|
||||
version: "1",
|
||||
});
|
||||
try {
|
||||
await client.connect(transport, { timeout: 30_000 });
|
||||
for (const scenario of cases) {
|
||||
const response = await client.callTool(
|
||||
{
|
||||
name: "inspect_android_method",
|
||||
arguments: {
|
||||
path: apk,
|
||||
class_name: scenario.class_name,
|
||||
method_name: "<clinit>",
|
||||
},
|
||||
},
|
||||
{ timeout: 150_000 },
|
||||
);
|
||||
assert.notEqual(response.isError, true, JSON.stringify(response));
|
||||
const evidence = check(response.structuredContent, scenario);
|
||||
assert.deepEqual(
|
||||
evidence.normalized_result,
|
||||
cli.get(scenario.class_name).normalized_result,
|
||||
);
|
||||
assert.deepEqual(
|
||||
evidence.parameters,
|
||||
cli.get(scenario.class_name).parameters,
|
||||
);
|
||||
assert.deepEqual(
|
||||
evidence.provider,
|
||||
cli.get(scenario.class_name).provider,
|
||||
);
|
||||
}
|
||||
await client.ping();
|
||||
} catch (cause) {
|
||||
process.stderr.write(stderr);
|
||||
throw cause;
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
assert.equal(
|
||||
await hashAndroidFile(apk),
|
||||
digest,
|
||||
"Static inspection changed fixture bytes",
|
||||
);
|
||||
console.log(
|
||||
"PASS real CLI/MCP static initializer evidence, identity, parity and unchanged APK",
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join, resolve } from "node:path";
|
||||
import { verifyAndroidInitializers } from "./initializers.mjs";
|
||||
|
||||
/** Prove cold metadata stays unmaterialized and overload identities survive code generation. */
|
||||
export const verifyAndroidMetadata = async ({
|
||||
@@ -12,8 +11,6 @@ export const verifyAndroidMetadata = async ({
|
||||
bridge,
|
||||
repository,
|
||||
execute,
|
||||
entrypoint,
|
||||
environment,
|
||||
jvmArguments = [],
|
||||
}) => {
|
||||
const javac =
|
||||
@@ -58,8 +55,6 @@ export const verifyAndroidMetadata = async ({
|
||||
`${classes}${delimiter}${jar}`,
|
||||
"ReaJadxMetadataRegression",
|
||||
apk,
|
||||
resolve(repository, "tests/fixtures/android/static-initializers"),
|
||||
classes,
|
||||
],
|
||||
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
@@ -67,23 +62,9 @@ export const verifyAndroidMetadata = async ({
|
||||
result.stdout,
|
||||
/METADATA_REGRESSION_PASS classes=\d+ overloads=3/u,
|
||||
);
|
||||
assert.match(
|
||||
result.stdout,
|
||||
/INITIALIZER_REGRESSION_PASS folded=2 bounded=true/u,
|
||||
);
|
||||
console.log(
|
||||
"PASS real cold metadata, full generic types and stable overloaded method selection",
|
||||
);
|
||||
console.log(
|
||||
"PASS real folded and mixed static initializers, absent bodies and bounded smali",
|
||||
);
|
||||
await verifyAndroidInitializers({
|
||||
apk: resolve(classes, "initializers.apk"),
|
||||
repository,
|
||||
entrypoint,
|
||||
environment,
|
||||
execute,
|
||||
});
|
||||
} finally {
|
||||
await rm(classes, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ import { dirname, join } from "node:path";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import {
|
||||
resolveJadxConfigurationInputs,
|
||||
validateJadxConfiguration,
|
||||
} from "../../../dist/android/JadxConfiguration.js";
|
||||
import { resolveJadxConfiguration } from "../../../dist/android/JadxConfiguration.js";
|
||||
|
||||
/** Verify real Windows cancellation, disconnect and abrupt CLI owner exit. */
|
||||
export async function verifyWindowsAndroidLifecycle(options) {
|
||||
@@ -39,12 +36,7 @@ export async function verifyWindowsAndroidLifecycle(options) {
|
||||
}
|
||||
|
||||
async function verifyJavaSelection(environment) {
|
||||
const selected = await resolveJadxConfigurationInputs(
|
||||
environment,
|
||||
"inspect_android_package",
|
||||
);
|
||||
await validateJadxConfiguration(
|
||||
selected,
|
||||
const selected = await resolveJadxConfiguration(
|
||||
environment,
|
||||
"inspect_android_package",
|
||||
);
|
||||
@@ -59,12 +51,7 @@ async function verifyJavaSelection(environment) {
|
||||
// The owned junction borrows the selected JDK directory without copying it.
|
||||
await symlink(dirname(selected.java), alias, "junction");
|
||||
pathEnvironment.Path = `"${alias}"`;
|
||||
const onPath = await resolveJadxConfigurationInputs(
|
||||
pathEnvironment,
|
||||
"inspect_android_package",
|
||||
);
|
||||
await validateJadxConfiguration(
|
||||
onPath,
|
||||
const onPath = await resolveJadxConfiguration(
|
||||
pathEnvironment,
|
||||
"inspect_android_package",
|
||||
);
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { z } from "zod";
|
||||
import { keyedArchiveResultSchema } from "../../../dist/domain/apple/keyedArchive.js";
|
||||
|
||||
const rowSchema = z.object({
|
||||
container: z.enum(["roots", "objects"]),
|
||||
root: z.string(),
|
||||
objectId: z.number().int().nonnegative().optional(),
|
||||
path: z.array(z.union([z.string(), z.number().int().nonnegative()])),
|
||||
kind: z.enum(["integer", "real"]),
|
||||
literal: z.string(),
|
||||
});
|
||||
const archiveSchema = z.object({
|
||||
filename: z.string(),
|
||||
sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
||||
rows: z.array(rowSchema),
|
||||
});
|
||||
const oracleSchema = z.object({ xml: archiveSchema, binary: archiveSchema });
|
||||
|
||||
const formats = ["xml", "binary"];
|
||||
const requiredRows = [
|
||||
{ root: "unsafeSigned", kind: "integer", literal: "9007199254740993" },
|
||||
{
|
||||
root: "unsafeNegative",
|
||||
kind: "integer",
|
||||
literal: "-9223372036854775808",
|
||||
},
|
||||
{ root: "safeInteger", kind: "integer", literal: "9007199254740991" },
|
||||
{ root: "safeNegative", kind: "integer", literal: "-42" },
|
||||
{ root: "integralReal", kind: "real", literal: "9007199254740992" },
|
||||
{
|
||||
root: "boxedUnsigned",
|
||||
kind: "integer",
|
||||
literal: "9007199254740993",
|
||||
container: "objects",
|
||||
},
|
||||
{
|
||||
root: "boxedReal",
|
||||
kind: "real",
|
||||
literal: "9007199254740992",
|
||||
container: "objects",
|
||||
},
|
||||
];
|
||||
|
||||
const valueAt = (value, path) => {
|
||||
for (const key of path) {
|
||||
if (value === null || typeof value !== "object") return undefined;
|
||||
value = value[key];
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const isUnsafeInteger = (row) =>
|
||||
row.kind === "integer" &&
|
||||
(BigInt(row.literal) > BigInt(Number.MAX_SAFE_INTEGER) ||
|
||||
BigInt(row.literal) < BigInt(Number.MIN_SAFE_INTEGER));
|
||||
|
||||
/** Verify the actual Foundation output and the production CLI/MCP result. */
|
||||
export async function verifyKeyedArchiveIntegerArchives(root) {
|
||||
const oracle = oracleSchema.parse(
|
||||
JSON.parse(await readFile(join(root, "oracle.json"), "utf8")),
|
||||
);
|
||||
const archives = [];
|
||||
|
||||
for (const format of formats) {
|
||||
const archive = oracle[format];
|
||||
for (const required of requiredRows) {
|
||||
assert.ok(
|
||||
archive.rows.some(
|
||||
(row) =>
|
||||
row.root === required.root &&
|
||||
row.kind === required.kind &&
|
||||
row.literal === required.literal &&
|
||||
(required.container === undefined ||
|
||||
row.container === required.container),
|
||||
),
|
||||
`${format} Foundation oracle omitted ${required.root} ${required.literal}`,
|
||||
);
|
||||
}
|
||||
|
||||
const archivePath = join(root, archive.filename);
|
||||
const digest = createHash("sha256")
|
||||
.update(await readFile(archivePath))
|
||||
.digest("hex");
|
||||
assert.equal(digest, archive.sha256, `${format} archive digest`);
|
||||
archives.push({ format, archive, archivePath, digest });
|
||||
}
|
||||
return archives;
|
||||
}
|
||||
|
||||
/** Check every oracle numeric slot, preserving object-table identity lookups. */
|
||||
export function assertKeyedArchiveIntegerResult(
|
||||
result,
|
||||
format,
|
||||
digest,
|
||||
oracle,
|
||||
) {
|
||||
const parsed = keyedArchiveResultSchema.parse(result);
|
||||
assert.equal(parsed.archive_sha256, digest);
|
||||
assert.equal(parsed.target_sha256, digest);
|
||||
assert.equal(
|
||||
parsed.archive_format,
|
||||
format === "xml" ? "xml-plist" : "binary-plist",
|
||||
);
|
||||
|
||||
for (const row of oracle.rows) {
|
||||
const unsafe = isUnsafeInteger(row);
|
||||
const source =
|
||||
row.container === "roots"
|
||||
? parsed.roots
|
||||
: parsed.objects.find((object) => object.id === row.objectId)?.value;
|
||||
const value = valueAt(source, row.path);
|
||||
assert.notEqual(
|
||||
value,
|
||||
undefined,
|
||||
`${row.root} numeric slot remains present`,
|
||||
);
|
||||
|
||||
if (unsafe) {
|
||||
const exact =
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
value.$plist_type === "integer" &&
|
||||
value.decimal === BigInt(row.literal).toString();
|
||||
const disclosed = parsed.limitations.some(
|
||||
(reason) =>
|
||||
/integer|number/iu.test(reason) &&
|
||||
/precision|exact range|rounded/iu.test(reason),
|
||||
);
|
||||
assert.ok(
|
||||
exact || disclosed,
|
||||
`${row.root} ${row.literal} must be exact or disclose numeric loss`,
|
||||
);
|
||||
} else {
|
||||
assert.equal(
|
||||
typeof value,
|
||||
"number",
|
||||
`${row.root} retains its numeric type`,
|
||||
);
|
||||
assert.equal(value, Number(row.literal), `${row.root} numeric value`);
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { execFile, spawnSync } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -11,10 +11,6 @@ import {
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "../../lib/artifact-e2e.mjs";
|
||||
import {
|
||||
assertKeyedArchiveIntegerResult,
|
||||
verifyKeyedArchiveIntegerArchives,
|
||||
} from "./keyed-archive-integers.mjs";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
@@ -174,37 +170,26 @@ try {
|
||||
);
|
||||
});
|
||||
}
|
||||
const integers = join(root, "integers");
|
||||
await mkdir(integers);
|
||||
await promisify(execFile)("/usr/bin/xcrun", [
|
||||
"swift",
|
||||
"-module-cache-path",
|
||||
join(root, "modules"),
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../../../tests/conformance/native/keyed-archive-integers.swift",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
integers,
|
||||
]);
|
||||
for (const fixture of await verifyKeyedArchiveIntegerArchives(integers)) {
|
||||
const cliResult = assertKeyedArchiveIntegerResult(
|
||||
await artifactCli("inspect-keyed-archive", fixture.archivePath),
|
||||
fixture.format,
|
||||
fixture.digest,
|
||||
fixture.archive,
|
||||
);
|
||||
await withArtifactMcp(fixture.archivePath, async (client) => {
|
||||
const mcpResult = assertKeyedArchiveIntegerResult(
|
||||
await artifactMcpResult(client, "inspect_keyed_archive"),
|
||||
fixture.format,
|
||||
fixture.digest,
|
||||
fixture.archive,
|
||||
);
|
||||
assert.deepEqual(mcpResult, cliResult);
|
||||
});
|
||||
}
|
||||
const numericTests = spawnSync(
|
||||
"npm",
|
||||
[
|
||||
"run",
|
||||
"test:focused",
|
||||
"--",
|
||||
"tests/boundary/filesystem/keyedArchiveNativeIntegers.test.ts",
|
||||
],
|
||||
{
|
||||
cwd: fileURLToPath(new URL("../../../", import.meta.url)),
|
||||
stdio: "inherit",
|
||||
timeout: 240_000,
|
||||
},
|
||||
);
|
||||
if (numericTests.error) throw numericTests.error;
|
||||
assert.equal(
|
||||
numericTests.status,
|
||||
0,
|
||||
"Native Foundation integer cases failed",
|
||||
);
|
||||
report = {
|
||||
ok: true,
|
||||
mocked: false,
|
||||
@@ -212,9 +197,6 @@ try {
|
||||
stdio_mcp: true,
|
||||
xml_golden: true,
|
||||
uid_named_root: true,
|
||||
numeric_formats: ["xml", "binary"],
|
||||
numeric_cli: true,
|
||||
numeric_stdio_mcp: true,
|
||||
format: graph.archive_format,
|
||||
objects: graph.total_objects,
|
||||
references: graph.total_references,
|
||||
|
||||
@@ -10,12 +10,12 @@ import type {
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
providerConfigurationRefusal,
|
||||
type AnalysisExecution,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import type { AdbRequest } from "../domain/adb/adbDeviceAnalysis.js";
|
||||
import {
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
@@ -417,8 +417,8 @@ export class AdbProvider {
|
||||
async inspectAvailability(
|
||||
signal?: AbortSignal,
|
||||
): Promise<ProviderAvailability> {
|
||||
const selection = this.selection();
|
||||
try {
|
||||
const selection = this.selection();
|
||||
await inspectAdbClient(selection, signal);
|
||||
return {
|
||||
status: "available",
|
||||
@@ -555,19 +555,21 @@ export class AdbProvider {
|
||||
const signal = options?.signal;
|
||||
if (signal?.aborted === true)
|
||||
return err(new AnalysisCancelledError(request.operation));
|
||||
let selection: AdbBinarySelection;
|
||||
const selection = this.selection();
|
||||
let identity: AdbClientIdentity;
|
||||
try {
|
||||
selection = this.selection();
|
||||
identity = await inspectAdbClient(selection, signal);
|
||||
} catch (cause) {
|
||||
if (cause instanceof AnalysisCancelledError) return err(cause);
|
||||
if (cause instanceof AdbConfigurationFailure)
|
||||
return err(
|
||||
providerConfigurationRefusal(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
ADB_PROVIDER_IDENTITY.id,
|
||||
request.operation,
|
||||
cause,
|
||||
typeof cause.diagnostics.remediation === "string"
|
||||
? `${cause.message} ${cause.diagnostics.remediation}`
|
||||
: cause.message,
|
||||
{ cause },
|
||||
),
|
||||
);
|
||||
return err(
|
||||
|
||||
@@ -22,7 +22,6 @@ import type {
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
providerConfigurationRefusal,
|
||||
type AnalysisExecution,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import type { ApktoolRequest } from "../domain/apktool/apktoolResourceAnalysis.js";
|
||||
@@ -154,13 +153,12 @@ const runApktool = async (
|
||||
invocation = apktoolInvocation(selection, arguments_, hostEnvironment);
|
||||
} catch (cause) {
|
||||
if (!(cause instanceof WindowsBatchCommandError)) throw cause;
|
||||
const reason = `The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`;
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
APKTOOL_PROVIDER_IDENTITY.id,
|
||||
operation,
|
||||
reason,
|
||||
{ cause, userMessage: reason },
|
||||
`The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`,
|
||||
{ cause },
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -347,8 +345,8 @@ export class ApktoolProvider {
|
||||
async #inspectAvailability(
|
||||
signal: AbortSignal,
|
||||
): Promise<ProviderAvailability> {
|
||||
const selection = this.selection();
|
||||
try {
|
||||
const selection = this.selection();
|
||||
await inspectApktoolClient(selection, signal, this.environment);
|
||||
throwIfCancelled("inspect_apktool_client", signal);
|
||||
return {
|
||||
@@ -399,10 +397,9 @@ export class ApktoolProvider {
|
||||
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
||||
if (signal?.aborted === true)
|
||||
return err(new AnalysisCancelledError(request.operation));
|
||||
let selection: ApktoolCommandSelection;
|
||||
const selection = this.selection();
|
||||
let identity: ApktoolClientIdentity;
|
||||
try {
|
||||
selection = this.selection();
|
||||
identity = await inspectApktoolClient(
|
||||
selection,
|
||||
signal,
|
||||
@@ -412,10 +409,13 @@ export class ApktoolProvider {
|
||||
if (cause instanceof AnalysisCancelledError) return err(cause);
|
||||
if (cause instanceof ApktoolConfigurationFailure)
|
||||
return err(
|
||||
providerConfigurationRefusal(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
APKTOOL_PROVIDER_IDENTITY.id,
|
||||
request.operation,
|
||||
cause,
|
||||
typeof cause.diagnostics.remediation === "string"
|
||||
? `${cause.message} ${cause.diagnostics.remediation}`
|
||||
: cause.message,
|
||||
{ cause },
|
||||
),
|
||||
);
|
||||
return err(
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { NativeToolName } from "../contracts/native/nativeToolContracts.js"
|
||||
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
|
||||
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
@@ -211,31 +210,6 @@ export type ProviderAvailability =
|
||||
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
||||
};
|
||||
|
||||
/** A host tool configuration failure observed before any target work. */
|
||||
export interface ProviderConfigurationFailure extends Error {
|
||||
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse an operation because the host tool is misconfigured. The reason,
|
||||
* not the generic target advice, tells the caller what to change.
|
||||
*/
|
||||
export const providerConfigurationRefusal = (
|
||||
providerId: string,
|
||||
operation: string,
|
||||
failure: ProviderConfigurationFailure,
|
||||
): AnalysisCapabilityUnavailableError => {
|
||||
const remediation = failure.diagnostics.remediation;
|
||||
const reason =
|
||||
typeof remediation === "string"
|
||||
? `${failure.message}. ${remediation}.`
|
||||
: `${failure.message}.`;
|
||||
return new AnalysisCapabilityUnavailableError(providerId, operation, reason, {
|
||||
cause: failure,
|
||||
userMessage: reason,
|
||||
});
|
||||
};
|
||||
|
||||
/** Provider-owned target support observation before profile resolution. */
|
||||
export type ProviderTargetSupport =
|
||||
| {
|
||||
|
||||
@@ -26,8 +26,6 @@ import {
|
||||
validateDosComLength,
|
||||
type ExecutableFormatHint,
|
||||
} from "../domain/dosCom.js";
|
||||
import { admitRawBinary } from "../domain/rawBinary.js";
|
||||
import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js";
|
||||
import { mzWindowsHeaderOffset } from "../domain/dosMz.js";
|
||||
import {
|
||||
hasZipSignature,
|
||||
@@ -47,8 +45,6 @@ export interface BinaryTargetResolverOptions {
|
||||
readonly hostArchitecture?: NodeJS.Architecture;
|
||||
readonly targetKind?: BinaryTarget["kind"];
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly entryPoint?: string;
|
||||
readonly signal?: AbortSignal;
|
||||
}
|
||||
|
||||
@@ -65,7 +61,7 @@ export const parseBinaryTarget = async (
|
||||
> => {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const hostArchitecture = options.hostArchitecture ?? process.arch;
|
||||
const { targetKind, formatHint, signal, architecture, entryPoint } = options;
|
||||
const { targetKind, formatHint, signal } = options;
|
||||
const candidate = isAbsolute(input) ? input : resolve(cwd, input);
|
||||
try {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
@@ -87,40 +83,6 @@ export const parseBinaryTarget = async (
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const metadata = await handle.stat();
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (formatHint === "raw-binary") {
|
||||
if (targetKind !== undefined && targetKind !== "executable")
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
path,
|
||||
"raw-binary format requires an executable target kind",
|
||||
),
|
||||
);
|
||||
const admitted = admitRawBinary({
|
||||
architecture,
|
||||
entryPoint,
|
||||
fileBytes: metadata.size,
|
||||
});
|
||||
if (!admitted.ok)
|
||||
return err(new BinaryTargetError(path, admitted.error));
|
||||
return ok({
|
||||
path,
|
||||
sourcePath: canonical,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "executable",
|
||||
format: "raw-binary",
|
||||
architecture: admitted.value.architecture,
|
||||
availableArchitectures: [admitted.value.architecture],
|
||||
entryPoint: admitted.value.entryPoint,
|
||||
entryPointSource: admitted.value.entryPointSource,
|
||||
});
|
||||
}
|
||||
if (architecture !== undefined || entryPoint !== undefined)
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
path,
|
||||
"architecture and entry_point apply only with format=raw-binary",
|
||||
),
|
||||
);
|
||||
if (formatHint === "dos-com") {
|
||||
if (targetKind !== undefined && targetKind !== "executable")
|
||||
return err(
|
||||
@@ -232,13 +194,7 @@ const detectArtifactFormat = async (
|
||||
): Promise<
|
||||
| Exclude<
|
||||
BinaryTarget["format"],
|
||||
| "analysis-database"
|
||||
| "mach-o"
|
||||
| "elf"
|
||||
| "pe"
|
||||
| "dos-mz"
|
||||
| "dos-com"
|
||||
| "raw-binary"
|
||||
"analysis-database" | "mach-o" | "elf" | "pe" | "dos-mz" | "dos-com"
|
||||
>
|
||||
| undefined
|
||||
> => {
|
||||
@@ -288,13 +244,7 @@ const namedArtifactFormat = (
|
||||
const isArchiveFormat = (
|
||||
format: Exclude<
|
||||
BinaryTarget["format"],
|
||||
| "analysis-database"
|
||||
| "mach-o"
|
||||
| "elf"
|
||||
| "pe"
|
||||
| "dos-mz"
|
||||
| "dos-com"
|
||||
| "raw-binary"
|
||||
"analysis-database" | "mach-o" | "elf" | "pe" | "dos-mz" | "dos-com"
|
||||
>,
|
||||
): format is Extract<BinaryTarget, { kind: "archive" }>["format"] =>
|
||||
isZipPackageFormat(format) || ["asar", "dmg", "pkg"].includes(format);
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
clientRequirementsFor,
|
||||
NO_CLIENT_FEATURES,
|
||||
} from "./CapabilityClientRequirements.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
const statusSchema = z.object({
|
||||
open: z.boolean(),
|
||||
@@ -211,7 +210,7 @@ export const buildCapabilityInventory = (
|
||||
reason: availability.reason,
|
||||
remediation: availability.remediation,
|
||||
};
|
||||
}).sort((left, right) => compareUnicodeCodePoints(left.name, right.name));
|
||||
}).sort((left, right) => left.name.localeCompare(right.name));
|
||||
};
|
||||
|
||||
const availabilityFor = (context: AvailabilityContext): Availability => {
|
||||
|
||||
@@ -18,7 +18,6 @@ import { z } from "zod";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
export type ClientConfigurationFormat = NonNullable<SetupClient["format"]>;
|
||||
export type ClientServersKey = "mcp_servers" | "mcpServers" | "mcp" | "servers";
|
||||
@@ -64,7 +63,7 @@ const normalizeConfigurationValue = (value: unknown): unknown => {
|
||||
return value;
|
||||
return Object.fromEntries(
|
||||
Object.entries(value)
|
||||
.sort(([left], [right]) => compareUnicodeCodePoints(left, right))
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, nested]) => [key, normalizeConfigurationValue(nested)]),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type ClientConfigurationDocument,
|
||||
} from "./ClientConfigurationDocument.js";
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Read explicit REA overrides without dropping peers when one value is invalid. */
|
||||
export const clientRegistrationEnvironment = (
|
||||
@@ -48,7 +47,7 @@ export const clientRegistrationEnvironment = (
|
||||
return ok(
|
||||
Object.fromEntries(
|
||||
Object.entries(environment).sort(([left], [right]) =>
|
||||
compareUnicodeCodePoints(left, right),
|
||||
left.localeCompare(right),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
supportedClients,
|
||||
} from "./SupportedClients.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
interface ClientRegistrationStatusBase {
|
||||
readonly client: string;
|
||||
@@ -185,7 +184,7 @@ export const readClientRegistrationStatuses = async (
|
||||
}
|
||||
}
|
||||
return statuses.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.client, right.client),
|
||||
left.client.localeCompare(right.client),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ExecutableFormatHint } from "../domain/dosCom.js";
|
||||
import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
|
||||
import type { BinarySession } from "./binary/BinarySession.js";
|
||||
@@ -92,8 +91,6 @@ export const runDirectAnalysis = async (
|
||||
readonly signal?: AbortSignal;
|
||||
readonly providerId?: AnalysisProviderSelector;
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly entryPoint?: string;
|
||||
} = {},
|
||||
): Promise<JsonValue> =>
|
||||
withProcessCancellation(options.signal, (signal) =>
|
||||
@@ -104,12 +101,6 @@ export const runDirectAnalysis = async (
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
: { formatHint: options.formatHint }),
|
||||
...(options.architecture === undefined
|
||||
? {}
|
||||
: { architecture: options.architecture }),
|
||||
...(options.entryPoint === undefined
|
||||
? {}
|
||||
: { entryPoint: options.entryPoint }),
|
||||
...(options.providerId === undefined
|
||||
? {}
|
||||
: { providerId: options.providerId }),
|
||||
@@ -196,8 +187,6 @@ const runAnalysis = async (
|
||||
readonly signal: AbortSignal;
|
||||
readonly providerId?: AnalysisProviderSelector;
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly entryPoint?: string;
|
||||
},
|
||||
): Promise<JsonValue> => {
|
||||
const { logger, signal, snapshotPath } = options;
|
||||
@@ -214,12 +203,6 @@ const runAnalysis = async (
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
: { formatHint: options.formatHint }),
|
||||
...(options.architecture === undefined
|
||||
? {}
|
||||
: { architecture: options.architecture }),
|
||||
...(options.entryPoint === undefined
|
||||
? {}
|
||||
: { entryPoint: options.entryPoint }),
|
||||
});
|
||||
if (!prepared.ok) return cliError(prepared.error);
|
||||
const { snapshot } = prepared.value;
|
||||
@@ -231,12 +214,6 @@ const runAnalysis = async (
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
: { formatHint: options.formatHint }),
|
||||
...(options.architecture === undefined
|
||||
? {}
|
||||
: { architecture: options.architecture }),
|
||||
...(options.entryPoint === undefined
|
||||
? {}
|
||||
: { entryPoint: options.entryPoint }),
|
||||
...(options.providerId === undefined
|
||||
? {}
|
||||
: { providerId: options.providerId }),
|
||||
@@ -269,12 +246,6 @@ const runAnalysis = async (
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
: { formatHint: options.formatHint }),
|
||||
...(options.architecture === undefined
|
||||
? {}
|
||||
: { architecture: options.architecture }),
|
||||
...(options.entryPoint === undefined
|
||||
? {}
|
||||
: { entryPoint: options.entryPoint }),
|
||||
...(snapshot === undefined ? {} : { snapshot }),
|
||||
...(options.providerId === undefined
|
||||
? {}
|
||||
@@ -469,8 +440,6 @@ const withProcessCancellation = async <Value>(
|
||||
const prepareSnapshot = async (options: {
|
||||
readonly path: string;
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly entryPoint?: string;
|
||||
readonly snapshotPath: string | undefined;
|
||||
readonly signal: AbortSignal;
|
||||
}): Promise<
|
||||
@@ -489,12 +458,6 @@ const prepareSnapshot = async (options: {
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
: { formatHint: options.formatHint }),
|
||||
...(options.architecture === undefined
|
||||
? {}
|
||||
: { architecture: options.architecture }),
|
||||
...(options.entryPoint === undefined
|
||||
? {}
|
||||
: { entryPoint: options.entryPoint }),
|
||||
});
|
||||
if (!target.ok) return target;
|
||||
if (!snapshotMatchesTarget(loaded.value.target, target.value))
|
||||
|
||||
@@ -281,42 +281,6 @@ describe("doctor installation identity", () => {
|
||||
});
|
||||
|
||||
describe("doctor scoped readiness", () => {
|
||||
it("checks explicitly selected skill destinations independently of registrations", async () => {
|
||||
const selected: (readonly string[] | undefined)[] = [];
|
||||
const result = await runDoctor(
|
||||
undefined,
|
||||
host({
|
||||
clientRegistrations: () => Promise.resolve([]),
|
||||
installedSkillIdentity: (clients) => {
|
||||
selected.push(clients);
|
||||
return Promise.resolve({
|
||||
version: "stale",
|
||||
toolCount: 0,
|
||||
canonical: false,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
clients: [],
|
||||
skillClients: ["claude_code", "claude_code"],
|
||||
skill: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(selected).toEqual([["claude_code"]]);
|
||||
expect(result.scope).toMatchObject({
|
||||
mode: "explicit",
|
||||
clients: [],
|
||||
skill_clients: ["claude_code"],
|
||||
skill: true,
|
||||
});
|
||||
expect(result.healthy).toBe(false);
|
||||
expect(result.scope_checks.map(({ name }) => name)).toEqual([
|
||||
"node",
|
||||
"skill:identity",
|
||||
]);
|
||||
});
|
||||
|
||||
it("scopes effective readiness to selected clients without hiding drift", async () => {
|
||||
const registrations: readonly ClientRegistrationStatus[] = [
|
||||
{
|
||||
|
||||
@@ -120,7 +120,6 @@ export interface DoctorReport {
|
||||
/** Caller-selected readiness boundary; omitted values retain audit semantics. */
|
||||
export interface DoctorScope {
|
||||
readonly clients?: readonly string[];
|
||||
readonly skillClients?: readonly string[];
|
||||
readonly providers?: readonly string[];
|
||||
readonly skill?: boolean;
|
||||
}
|
||||
@@ -207,10 +206,9 @@ const collectDoctorIdentity = async (
|
||||
const installationPaths = (await host.installationPaths?.()) ?? [];
|
||||
const registrations = (await host.clientRegistrations?.()) ?? [];
|
||||
const skillClientIds =
|
||||
scope.skill_clients ??
|
||||
(scope.mode === "audit-wide"
|
||||
scope.mode === "audit-wide"
|
||||
? registrations.map(({ client }) => client)
|
||||
: scope.clients);
|
||||
: scope.clients;
|
||||
const installedSkillIdentity =
|
||||
await host.installedSkillIdentity?.(skillClientIds);
|
||||
const skillAligned = skillIdentityAligned(installedSkillIdentity);
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
export interface DoctorScopeReport {
|
||||
readonly mode: "audit-wide" | "explicit";
|
||||
readonly clients: readonly string[];
|
||||
readonly skill_clients?: readonly string[];
|
||||
readonly providers: readonly string[];
|
||||
readonly skill: boolean;
|
||||
readonly target: string | null;
|
||||
@@ -41,9 +40,6 @@ export const normalizeDoctorScope = (
|
||||
): DoctorScopeReport => ({
|
||||
mode: requested === undefined ? "audit-wide" : "explicit",
|
||||
clients: uniqueSorted(requested?.clients ?? []),
|
||||
...(requested?.skillClients === undefined
|
||||
? {}
|
||||
: { skill_clients: uniqueSorted(requested.skillClients) }),
|
||||
providers: uniqueSorted(requested?.providers ?? []),
|
||||
skill: requested?.skill === true,
|
||||
target: target ?? null,
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
semanticObligationPolicy,
|
||||
type ReconstructionObligationCandidatePolicy,
|
||||
} from "./ReconstructionObligationPolicies.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type CandidateFields = Omit<
|
||||
ReconstructionObligation,
|
||||
@@ -55,7 +54,7 @@ export const deriveReconstructionObligationCandidates = (
|
||||
addCandidate(candidates, reviewedCandidate(obligation, records));
|
||||
return {
|
||||
candidates: [...candidates.values()].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.obligation_id, right.obligation_id),
|
||||
left.obligation_id.localeCompare(right.obligation_id),
|
||||
),
|
||||
limitations: [...limitations].sort(),
|
||||
};
|
||||
@@ -431,7 +430,7 @@ const uniqueBy = <Value>(
|
||||
key: (value: Value) => string,
|
||||
): Value[] =>
|
||||
[...new Map(values.map((value) => [key(value), value])).values()].sort(
|
||||
(left, right) => compareUnicodeCodePoints(key(left), key(right)),
|
||||
(left, right) => key(left).localeCompare(key(right)),
|
||||
);
|
||||
|
||||
const obligationId = (key: string): string =>
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
ReconstructionObligationManifest,
|
||||
} from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type Diagnostic = ReconstructionObligation["diagnostics"][number];
|
||||
type Binding = NonNullable<ReconstructionObligation["binding"]>;
|
||||
@@ -51,8 +50,7 @@ export const uniqueObligationDiagnostics = (
|
||||
]),
|
||||
).values(),
|
||||
].sort((left, right) =>
|
||||
compareUnicodeCodePoints(
|
||||
`${left.code}:${left.detail}`,
|
||||
`${left.code}:${left.detail}`.localeCompare(
|
||||
`${right.code}:${right.detail}`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
obligationReports,
|
||||
summarizeObligations,
|
||||
} from "./ReconstructionObligationLedgerProjection.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type Diagnostic = ReconstructionObligation["diagnostics"][number];
|
||||
type Binding = NonNullable<ReconstructionObligation["binding"]>;
|
||||
@@ -61,7 +60,7 @@ export const evaluateReconstructionObligationLedger = ({
|
||||
}): ReconstructionObligationLedger => {
|
||||
const bundle = parseEvidenceBundle(bundleInput);
|
||||
const allCandidates = [...candidatesInput].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.obligation_id, right.obligation_id),
|
||||
left.obligation_id.localeCompare(right.obligation_id),
|
||||
);
|
||||
const orphanManifestIds = findOrphanManifestIds(allCandidates, manifest);
|
||||
const candidates = allCandidates;
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Evidence } from "../domain/evidence.js";
|
||||
import type { ReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
import { evidenceSupportsOriginalAuthority } from "./ReconstructionObligationAuthority.js";
|
||||
import type { ReconstructionObligationCandidate } from "./ReconstructionObligationCandidates.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
type Binding = NonNullable<ReconstructionObligation["binding"]>;
|
||||
type Diagnostic = ReconstructionObligation["diagnostics"][number];
|
||||
@@ -22,8 +21,7 @@ export const originalCasesFor = (
|
||||
),
|
||||
).values(),
|
||||
].sort((left, right) =>
|
||||
compareUnicodeCodePoints(
|
||||
`${left.kind}:${left.evidence_id}:${left.location}`,
|
||||
`${left.kind}:${left.evidence_id}:${left.location}`.localeCompare(
|
||||
`${right.kind}:${right.evidence_id}:${right.location}`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
ReferenceSourceRead,
|
||||
} from "../reference/ReferenceSourceReaderTypes.js";
|
||||
import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.js";
|
||||
import { typescriptEmittedSourceCandidates } from "./TypeScriptEmittedSourceCandidates.js";
|
||||
|
||||
export interface ParsedReferenceSourceEntries {
|
||||
readonly entries: HistoricalSourceGraphInput["entries"];
|
||||
@@ -100,9 +99,22 @@ const resolveInternalSpecifier = (
|
||||
`${normalized}${suffix}`,
|
||||
`${normalized}/index${suffix}`,
|
||||
]);
|
||||
// TypeScript source commonly names the extension emitted for NodeNext.
|
||||
// Retain present runtime files first; substitute only a missing counterpart.
|
||||
// Counterparts cover explicit filenames, not directory/index resolution.
|
||||
candidates.push(...typescriptEmittedSourceCandidates(fromPath, normalized));
|
||||
if (/\.(?:ts|tsx|mts|cts)$/u.test(fromPath)) {
|
||||
const sourceSuffixes = normalized.endsWith(".js")
|
||||
? [".ts", ".tsx", ".d.ts"]
|
||||
: normalized.endsWith(".mjs")
|
||||
? [".mts", ".d.mts"]
|
||||
: normalized.endsWith(".cjs")
|
||||
? [".cts", ".d.cts"]
|
||||
: normalized.endsWith(".jsx")
|
||||
? [".tsx", ".d.ts"]
|
||||
: [];
|
||||
const stem = normalized.slice(0, -posix.extname(normalized).length);
|
||||
candidates.push(...sourceSuffixes.map((suffix) => `${stem}${suffix}`));
|
||||
}
|
||||
const match = lookupPath.resolveCandidates(candidates);
|
||||
return match === undefined
|
||||
? { to: normalized, resolution: "unresolved" }
|
||||
|
||||
@@ -159,7 +159,6 @@ export class FakeSetupHost implements SetupHost {
|
||||
scope === undefined
|
||||
? environmentHealthy
|
||||
: (scope.clients?.length ?? 0) === 0 &&
|
||||
(scope.skillClients?.length ?? 0) === 0 &&
|
||||
(scope.providers?.length ?? 0) === 0 &&
|
||||
scope.skill !== true
|
||||
? true
|
||||
@@ -170,9 +169,6 @@ export class FakeSetupHost implements SetupHost {
|
||||
scope: {
|
||||
mode: scope === undefined ? "audit-wide" : "explicit",
|
||||
clients: scope?.clients ?? [],
|
||||
...(scope?.skillClients === undefined
|
||||
? {}
|
||||
: { skill_clients: scope.skillClients }),
|
||||
providers: scope?.providers ?? [],
|
||||
skill: scope?.skill === true,
|
||||
target: null,
|
||||
|
||||
@@ -232,7 +232,6 @@ describe("setup scoped readiness", () => {
|
||||
clients: ["codex"],
|
||||
providers: [],
|
||||
skill: true,
|
||||
skillClients: ["codex"],
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -125,10 +125,7 @@ const runSetupWorkflow = async (
|
||||
: (options.clientIds ??
|
||||
options.readinessScope?.clients ??
|
||||
discovery.defaultClientIds);
|
||||
let selectedSkillClientIds =
|
||||
options.skillClientIds ??
|
||||
options.readinessScope?.skillClients ??
|
||||
selectedClientIds;
|
||||
let selectedSkillClientIds = options.skillClientIds ?? selectedClientIds;
|
||||
let skillNeedsInstall = await host.skillNeedsInstall(selectedSkillClientIds);
|
||||
let skillSelected =
|
||||
options.installSkill === true ||
|
||||
@@ -223,7 +220,6 @@ const runSetupWorkflow = async (
|
||||
installHopper: planDiscovery.installHopper,
|
||||
installSkill,
|
||||
skillSelected,
|
||||
selectedSkillClientIds,
|
||||
selectedClients: planned.selectedClients,
|
||||
clientsToConfigure: planned.clientsToConfigure,
|
||||
};
|
||||
@@ -523,7 +519,6 @@ const resolvedReadinessScope = (
|
||||
readonly selectedClients: readonly SetupClient[];
|
||||
readonly installHopper: boolean;
|
||||
readonly skillSelected?: boolean;
|
||||
readonly selectedSkillClientIds: readonly string[];
|
||||
},
|
||||
): DoctorScope => ({
|
||||
clients: selection.selectedClients.map(({ name }) => name),
|
||||
@@ -531,9 +526,6 @@ const resolvedReadinessScope = (
|
||||
? ["hopper"]
|
||||
: (requested?.providers ?? []),
|
||||
skill: selection.skillSelected === true || requested?.skill === true,
|
||||
...(selection.skillSelected === true || requested?.skill === true
|
||||
? { skillClients: selection.selectedSkillClientIds }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const summarizeDoctor = (report: DoctorReport): SetupDoctorSummary => ({
|
||||
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
} from "./SetupTypes.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import type { DoctorReport } from "./Doctor.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Read-only setup facts shared by client selection and preflight. */
|
||||
export interface SetupDiscovery {
|
||||
@@ -309,7 +308,7 @@ const clientConfigurationDetail = (
|
||||
installHopper: boolean,
|
||||
): string => {
|
||||
const entries = Object.entries(environment)
|
||||
.sort(([left], [right]) => compareUnicodeCodePoints(left, right))
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([name, value]) => `${name}=${value}`);
|
||||
const environmentDetail =
|
||||
entries.length === 0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { DoctorProviderInspection } from "./Doctor.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Preserve adapter-published settings independently of runtime readiness. */
|
||||
export const providerRegistrationEnvironment = (
|
||||
@@ -10,5 +9,5 @@ export const providerRegistrationEnvironment = (
|
||||
.flatMap(({ registrationEnvironment }) =>
|
||||
Object.entries(registrationEnvironment),
|
||||
)
|
||||
.sort(([left], [right]) => compareUnicodeCodePoints(left, right)),
|
||||
.sort(([left], [right]) => left.localeCompare(right)),
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
HermesClientPathError,
|
||||
resolveHermesClientHome,
|
||||
} from "./HermesClientHome.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** One supported client configuration location. */
|
||||
export interface SetupClient {
|
||||
@@ -643,6 +642,6 @@ export const clientSkillDirectories = (
|
||||
});
|
||||
}
|
||||
return [...destinations.values()].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.directory, right.directory),
|
||||
left.directory.localeCompare(right.directory),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
/** Emitted NodeNext suffixes and the TypeScript source counterparts they name. */
|
||||
const EMITTED_SUFFIXES: ReadonlyArray<
|
||||
readonly [suffix: string, sourceSuffixes: readonly string[]]
|
||||
> = [
|
||||
[".js", [".ts", ".tsx", ".d.ts"]],
|
||||
[".mjs", [".mts", ".d.mts"]],
|
||||
[".cjs", [".cts", ".d.cts"]],
|
||||
[".jsx", [".tsx", ".d.ts"]],
|
||||
];
|
||||
|
||||
/**
|
||||
* Return TypeScript source counterparts for an explicit emitted filename.
|
||||
* Callers retain exact runtime-file precedence and must not use these for
|
||||
* directory/index, URL, HTML, or package target resolution.
|
||||
*/
|
||||
export const typescriptEmittedSourceCandidates = (
|
||||
sourcePath: string,
|
||||
candidate: string,
|
||||
): readonly string[] => {
|
||||
if (!/\.(?:ts|tsx|mts|cts)$/u.test(sourcePath)) return [];
|
||||
const emitted = EMITTED_SUFFIXES.find(([suffix]) =>
|
||||
candidate.endsWith(suffix),
|
||||
);
|
||||
if (emitted === undefined) return [];
|
||||
// Strip the matched suffix by length. posix.extname would miss dotfile
|
||||
// names such as ".js", whose whole basename is the emitted suffix itself.
|
||||
const stem = candidate.slice(0, candidate.length - emitted[0].length);
|
||||
return emitted[1].map((suffix) => `${stem}${suffix}`);
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
import type { EvidenceWriter } from "../investigation/InvestigationRecordPort.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { ok, type Result } from "../../domain/result.js";
|
||||
|
||||
/**
|
||||
* Complete class-inventory pages dominate raw-call provenance for focused
|
||||
* searches: one traversal embeds every examined class name regardless of the
|
||||
* match count. The summary keeps the pagination audit inline (totals, page
|
||||
* offsets and sizes, elided item counts) and links the retained record that
|
||||
* still carries the complete inventory.
|
||||
*/
|
||||
const elideInventoryText = (text: string): string | undefined => {
|
||||
let page: JsonValue;
|
||||
try {
|
||||
page = JSON.parse(text) as JsonValue;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
typeof page !== "object" ||
|
||||
page === null ||
|
||||
Array.isArray(page) ||
|
||||
!Array.isArray(page.items)
|
||||
)
|
||||
return undefined;
|
||||
const { items, ...audited } = page;
|
||||
return JSON.stringify({ ...audited, items_omitted: items.length });
|
||||
};
|
||||
|
||||
/** Replace inventoried page bodies with their pagination audit. */
|
||||
const summarizeInventoryCall = (call: JsonValue): JsonValue => {
|
||||
if (
|
||||
typeof call !== "object" ||
|
||||
call === null ||
|
||||
Array.isArray(call) ||
|
||||
call.operation !== "list_classes"
|
||||
)
|
||||
return call;
|
||||
const response = call.response;
|
||||
if (
|
||||
typeof response !== "object" ||
|
||||
response === null ||
|
||||
Array.isArray(response) ||
|
||||
!Array.isArray(response.content)
|
||||
)
|
||||
return call;
|
||||
let elided = false;
|
||||
const content = response.content.map((entry: JsonValue) => {
|
||||
if (
|
||||
typeof entry !== "object" ||
|
||||
entry === null ||
|
||||
Array.isArray(entry) ||
|
||||
typeof entry.text !== "string"
|
||||
)
|
||||
return entry;
|
||||
const summarized = elideInventoryText(entry.text);
|
||||
if (summarized === undefined) return entry;
|
||||
elided = true;
|
||||
return { ...entry, text: summarized };
|
||||
});
|
||||
return elided
|
||||
? { ...call, inventory_elided: true, response: { ...response, content } }
|
||||
: call;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retain the complete search Evidence — full raw provider calls included —
|
||||
* in the session record, then deliver a bounded summary Evidence inline.
|
||||
* Callers without a retaining session keep the complete inline delivery, so
|
||||
* complete CLI JSON output is unchanged. Coverage metadata stays truthful in
|
||||
* both copies: the examined inventory is never rewritten to matched-only.
|
||||
*/
|
||||
export const summarizeRetainedAndroidSearch = (
|
||||
evidence: Evidence,
|
||||
recordEvidence: EvidenceWriter["recordEvidence"] | undefined,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
if (recordEvidence === undefined) return ok(evidence);
|
||||
const recorded = recordEvidence(evidence);
|
||||
if (!recorded.ok) return recorded;
|
||||
const subject = evidence.subject;
|
||||
const raw = evidence.raw_result;
|
||||
const summarized: JsonValue =
|
||||
raw === null ||
|
||||
typeof raw !== "object" ||
|
||||
Array.isArray(raw) ||
|
||||
!Array.isArray(raw.calls)
|
||||
? raw
|
||||
: {
|
||||
...raw,
|
||||
complete_evidence_id: evidence.evidence_id,
|
||||
calls: raw.calls.map(summarizeInventoryCall),
|
||||
};
|
||||
return ok(
|
||||
createEvidence(
|
||||
subject === null
|
||||
? undefined
|
||||
: {
|
||||
path: subject.local_path,
|
||||
sha256: subject.digest.sha256,
|
||||
format: subject.format,
|
||||
...(subject.architecture === null
|
||||
? {}
|
||||
: { architecture: subject.architecture }),
|
||||
},
|
||||
evidence.provider,
|
||||
{
|
||||
operation: evidence.operation,
|
||||
parameters: evidence.parameters,
|
||||
result: evidence.normalized_result,
|
||||
rawResult: summarized,
|
||||
confidence: evidence.confidence,
|
||||
limitations: evidence.limitations,
|
||||
locations: evidence.locations,
|
||||
evidenceLinks: [evidence.evidence_id],
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
evaluateAnalysisProviderCandidate,
|
||||
type AnalysisProviderCandidateEvaluation,
|
||||
} from "./AnalysisProviderEvaluation.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
|
||||
/** Deterministic discovery truth for one configured deep provider. */
|
||||
export interface AnalysisProviderCandidateStatus {
|
||||
@@ -110,14 +109,12 @@ export class AnalysisProviderRegistry {
|
||||
}
|
||||
this.#providers = Object.freeze(
|
||||
[...providers].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.identity().id, right.identity().id),
|
||||
left.identity().id.localeCompare(right.identity().id),
|
||||
),
|
||||
);
|
||||
this.#byId = byId;
|
||||
this.#declaredOperations = Object.freeze(
|
||||
[...declaredOperations].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left, right),
|
||||
),
|
||||
[...declaredOperations].sort((left, right) => left.localeCompare(right)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -98,30 +98,6 @@ export class BinarySession
|
||||
this.#providerRouter = providerRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast-path liveness gate (#1719): short-circuit only when the retained
|
||||
* client is genuinely alive. The probe reads the child's operation health
|
||||
* rather than the composite's unconditionally-ok `health`, so a dead child
|
||||
* falls through and a real client is started; an aborted caller leaves the
|
||||
* session untouched. Returns the fast-path result, or undefined to fall
|
||||
* through to the slow path.
|
||||
*/
|
||||
async #tryRetainedFastPath(
|
||||
retained: SessionBinding,
|
||||
target: BinaryTarget,
|
||||
options: Pick<BinarySessionOpenOptions, "signal" | "snapshot">,
|
||||
): Promise<Result<BinaryTarget, AnalysisError> | undefined> {
|
||||
const live = await this.#retainedClientLive(retained.client, options);
|
||||
if (isAborted(options.signal))
|
||||
return err(new AnalysisCancelledError("open_binary"));
|
||||
if (!live) return undefined;
|
||||
if (options.snapshot !== undefined) {
|
||||
const imported = this.importAnalysisSnapshot(options.snapshot);
|
||||
if (!imported.ok) return imported;
|
||||
}
|
||||
return ok(target);
|
||||
}
|
||||
|
||||
/** Identify the provider producing evidence for this session. */
|
||||
providerIdentity(operation?: AnalysisOperation): ProviderIdentity {
|
||||
const route = this.#currentRoute();
|
||||
@@ -271,14 +247,12 @@ export class BinarySession
|
||||
if (!pendingClosed.ok) return pendingClosed;
|
||||
if (isAborted(options.signal))
|
||||
return err(new AnalysisCancelledError("open_binary"));
|
||||
const retained = sameTarget && sameProfile ? this.#active : undefined;
|
||||
if (retained !== undefined) {
|
||||
const fastPath = await this.#tryRetainedFastPath(
|
||||
retained,
|
||||
target,
|
||||
options,
|
||||
);
|
||||
if (fastPath !== undefined) return fastPath;
|
||||
if (sameTarget && sameProfile) {
|
||||
if (options.snapshot !== undefined) {
|
||||
const imported = this.importAnalysisSnapshot(options.snapshot);
|
||||
if (!imported.ok) return imported;
|
||||
}
|
||||
return ok(target);
|
||||
}
|
||||
const previous = this.#active;
|
||||
this.#active = undefined;
|
||||
@@ -729,36 +703,6 @@ export class BinarySession
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe whether a retained provider client is still live.
|
||||
*
|
||||
* On production routes the client is a CompositeProvider whose `health`
|
||||
* answers unconditionally ok without touching a child, so `health` alone
|
||||
* cannot see a dead child. `operationHealthSnapshot` reaches the
|
||||
* instantiated children instead. Only the known-live states allow the
|
||||
* fast path: `exited`/`unreachable` are dead, `unknown` is never
|
||||
* evidence of health (it is what a recorded but uncharacterized failure
|
||||
* reports), and `not_started` is a startup that never completed — in the
|
||||
* Hopper timeout case the client holds retained resources that block the
|
||||
* next `start()` until `close()` retries cleanup, so treating it as live
|
||||
* would skip the retire path and hand the wedged client to subsequent
|
||||
* analysis. The gate fails closed: without a snapshot signal, fall back
|
||||
* to the `health` admission probe.
|
||||
*/
|
||||
async #retainedClientLive(
|
||||
client: AnalysisClient,
|
||||
options: Pick<BinarySessionOpenOptions, "signal">,
|
||||
): Promise<boolean> {
|
||||
const snapshot = client.operationHealthSnapshot?.();
|
||||
if (snapshot !== undefined)
|
||||
return snapshot.state === "idle" || snapshot.state === "busy";
|
||||
try {
|
||||
return (await client.execute("health", {}, options)).ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async #restore(
|
||||
previous: SessionBinding | undefined,
|
||||
): Promise<Result<null, AnalysisError>> {
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type AnalysisClient,
|
||||
type AnalysisOperation,
|
||||
type AnalysisProvider,
|
||||
type AnalysisProviderCandidate,
|
||||
type CapabilityDescriptor,
|
||||
type ProviderAvailability,
|
||||
type ProviderIdentity,
|
||||
type ProviderTargetSupport,
|
||||
} from "../AnalysisProvider.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
import type { ProviderOperationHealth } from "../../domain/providerOperationHealth.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
|
||||
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
|
||||
import { BinarySession } from "./BinarySession.js";
|
||||
import { SessionProviderRouter } from "./SessionProviderRouter.js";
|
||||
|
||||
/**
|
||||
* Regression coverage for https://github.com/morluto/rea/issues/1719:
|
||||
* `BinarySession#open` must not short-circuit on the `sameTarget &&
|
||||
* sameProfile` fast path when the retained provider client is dead. It must
|
||||
* start a fresh client instead.
|
||||
*
|
||||
* The fixtures are production-composed: a deep provider plus an auxiliary
|
||||
* provider, so `SessionProviderRouter` wraps them in a real
|
||||
* `CompositeProvider` exactly like `src/composition/binary.ts` does for the
|
||||
* CLI/MCP runtime. The composite's `health` answers unconditionally ok
|
||||
* without touching a child, so a probe that only calls `health` cannot see a
|
||||
* dead child — the liveness signal must reach the actual child through
|
||||
* `operationHealthSnapshot`, the way `HopperClient.operationHealth` reports a
|
||||
* recorded process exit.
|
||||
*/
|
||||
describe("BinarySession open liveness", () => {
|
||||
it("reuses the retained client while the child stays live", async () => {
|
||||
const { session, deep } = openSession();
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
// Instantiate the deep child inside the composite, like a real query would.
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(1);
|
||||
|
||||
const second = await session.open(process.execPath);
|
||||
expect(second.ok).toBe(true);
|
||||
// Still the same composite and child: nothing was retired or recreated.
|
||||
expect(deep.created).toHaveLength(1);
|
||||
expect(deep.created[0]?.closeCalled).toBe(false);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("starts a fresh client when the retained child died", async () => {
|
||||
const { session, deep } = openSession();
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(1);
|
||||
|
||||
// Kill the provider process out from under the session, then re-open
|
||||
// the same target: the session must not short-circuit on the dead
|
||||
// child — it must retire it and start a real one.
|
||||
const dead = deep.created[0];
|
||||
dead?.kill();
|
||||
|
||||
const second = await session.open(process.execPath);
|
||||
expect(second.ok).toBe(true);
|
||||
// The dead binding was retired, not retained.
|
||||
expect(dead?.closeCalled).toBe(true);
|
||||
// The next query runs against a freshly created child, not the dead one.
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("treats an uncharacterized operation failure as dead", async () => {
|
||||
// A recorded failure the provider could not characterize ("unknown",
|
||||
// e.g. a Hopper spawn failure with no observed exit code) is never
|
||||
// evidence of health: the gate fails closed.
|
||||
const { session, deep } = openSession();
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(1);
|
||||
|
||||
const dead = deep.created[0];
|
||||
dead?.kill("unknown");
|
||||
|
||||
const second = await session.open(process.execPath);
|
||||
expect(second.ok).toBe(true);
|
||||
expect(dead?.closeCalled).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("retires a retained client whose startup never completed", async () => {
|
||||
// A Hopper startup timeout leaves the operation-health snapshot in
|
||||
// "not_started" while the retained resources block the next start()
|
||||
// until close() retries cleanup. Classifying that state as live would return success
|
||||
// without entering the retire path, so subsequent analysis still hits
|
||||
// the wedged client.
|
||||
const { session, deep } = openSession();
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(1);
|
||||
|
||||
const wedged = deep.created[0];
|
||||
wedged?.kill("not_started");
|
||||
|
||||
const second = await session.open(process.execPath);
|
||||
expect(second.ok).toBe(true);
|
||||
// The wedged binding was retired (close ran, so cleanup is retried),
|
||||
// not reused.
|
||||
expect(wedged?.closeCalled).toBe(true);
|
||||
// The next query runs against a freshly created child, not the wedged one.
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("falls back to the health probe when the child reports no operation health", async () => {
|
||||
// Without an operation-health hook there is no child-liveness signal,
|
||||
// so the probe keeps the previous `health` admission behavior.
|
||||
const { session, deep } = openSession({ operationHealth: false });
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(1);
|
||||
|
||||
const second = await session.open(process.execPath);
|
||||
expect(second.ok).toBe(true);
|
||||
// The composite's `health` answers ok, so the live client is reused.
|
||||
expect(deep.created).toHaveLength(1);
|
||||
expect(deep.created[0]?.closeCalled).toBe(false);
|
||||
});
|
||||
|
||||
it("reports cancellation without touching the session when aborted mid-probe", async () => {
|
||||
const { session, deep, onProbe } = openSession();
|
||||
expect((await session.open(process.execPath)).ok).toBe(true);
|
||||
expect((await session.execute("address_name", {})).ok).toBe(true);
|
||||
|
||||
const controller = new AbortController();
|
||||
// Fire the abort while the fast-path liveness probe is in flight: the
|
||||
// hook runs synchronously inside the snapshot read, so the abort lands
|
||||
// after the probe started but before the fast-path decision completes.
|
||||
let fired = false;
|
||||
onProbe(() => {
|
||||
if (!fired) {
|
||||
fired = true;
|
||||
controller.abort();
|
||||
}
|
||||
});
|
||||
|
||||
const cancelled = await session.open(process.execPath, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
expect(cancelled).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisCancelledError" },
|
||||
});
|
||||
// The dead-or-live question is moot: the session must be untouched.
|
||||
expect(deep.created).toHaveLength(1);
|
||||
expect(deep.created[0]?.closeCalled).toBe(false);
|
||||
const reopened = await session.open(process.execPath);
|
||||
expect(reopened.ok).toBe(true);
|
||||
expect(deep.created).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
interface KillableChild extends AnalysisClient {
|
||||
kill(state?: "exited" | "unreachable" | "unknown" | "not_started"): void;
|
||||
closeCalled: boolean;
|
||||
}
|
||||
|
||||
const deepIdentity: ProviderIdentity = {
|
||||
id: "liveness-deep",
|
||||
name: "Liveness deep provider",
|
||||
version: "1",
|
||||
};
|
||||
|
||||
const auxIdentity: ProviderIdentity = {
|
||||
id: "liveness-aux",
|
||||
name: "Liveness auxiliary provider",
|
||||
version: "1",
|
||||
};
|
||||
|
||||
const capability = (
|
||||
identity: ProviderIdentity,
|
||||
operation: Exclude<AnalysisOperation, "health">,
|
||||
): CapabilityDescriptor => ({
|
||||
provider: identity,
|
||||
operation,
|
||||
available: true,
|
||||
reason: null,
|
||||
effects: {
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: false,
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem: false,
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
},
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
const openSession = (
|
||||
options: { operationHealth?: boolean } = {},
|
||||
): {
|
||||
session: BinarySession;
|
||||
deep: { created: KillableChild[] };
|
||||
onProbe: (hook: () => void) => void;
|
||||
} => {
|
||||
const { provider, created, onProbe } = killableDeepProvider(options);
|
||||
// Mirror the production composition: a deep provider plus auxiliary
|
||||
// providers, so the route wraps them in a real CompositeProvider (whose
|
||||
// trivially-ok `health` must not mask a dead child).
|
||||
const aux: AnalysisProvider = {
|
||||
identity: () => auxIdentity,
|
||||
capabilities: () => [],
|
||||
createClient: (): AnalysisClient => ({
|
||||
execute: (operation) =>
|
||||
Promise.resolve(
|
||||
ok(createAnalysisExecution(`aux:${operation}`, auxIdentity)),
|
||||
),
|
||||
close: () => Promise.resolve(ok(null)),
|
||||
}),
|
||||
};
|
||||
const session = new BinarySession(
|
||||
SessionProviderRouter.selectable(new AnalysisProviderRegistry([provider]), [
|
||||
aux,
|
||||
]),
|
||||
new InvestigationRecords(),
|
||||
);
|
||||
return { session, deep: { created }, onProbe };
|
||||
};
|
||||
|
||||
const killableDeepProvider = (options: {
|
||||
operationHealth?: boolean;
|
||||
}): {
|
||||
provider: AnalysisProviderCandidate;
|
||||
created: KillableChild[];
|
||||
onProbe: (hook: () => void) => void;
|
||||
} => {
|
||||
const created: KillableChild[] = [];
|
||||
let probeHook: (() => void) | undefined;
|
||||
const withOperationHealth = options.operationHealth !== false;
|
||||
const provider: AnalysisProviderCandidate = {
|
||||
identity: () => deepIdentity,
|
||||
capabilities: () => [capability(deepIdentity, "address_name")],
|
||||
inspectAvailability: (): ProviderAvailability => ({
|
||||
status: "available",
|
||||
code: null,
|
||||
reason: null,
|
||||
diagnostics: {},
|
||||
}),
|
||||
inspectTargetSupport: (): ProviderTargetSupport => ({
|
||||
status: "supported",
|
||||
code: null,
|
||||
reason: null,
|
||||
diagnostics: {},
|
||||
}),
|
||||
resolveAnalysisProfile: () =>
|
||||
Promise.resolve(
|
||||
ok({
|
||||
profile: createAnalysisProfile(
|
||||
{ id: deepIdentity.id, name: deepIdentity.name, version: "1" },
|
||||
{ fixture: "liveness" },
|
||||
),
|
||||
}),
|
||||
),
|
||||
createClient: (): KillableChild => {
|
||||
let alive = true;
|
||||
let closeCalled = false;
|
||||
let deadState: "exited" | "unreachable" | "unknown" | "not_started" =
|
||||
"exited";
|
||||
const client: KillableChild = {
|
||||
execute: (operation) => {
|
||||
return alive
|
||||
? Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution(
|
||||
operation === "health" ? null : `liveness:${operation}`,
|
||||
deepIdentity,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Promise.resolve(
|
||||
err(
|
||||
new ProviderAdapterError(deepIdentity.id, operation, {
|
||||
diagnostics: { reason: "process_killed" },
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
// Genuine child-liveness signal, like HopperClient.operationHealth:
|
||||
// a recorded process exit stays visible after the kill.
|
||||
...(withOperationHealth
|
||||
? {
|
||||
operationHealthSnapshot: (): ProviderOperationHealth => {
|
||||
probeHook?.();
|
||||
return {
|
||||
state: alive ? "idle" : deadState,
|
||||
stage: alive ? null : "connection",
|
||||
retryAction: alive ? null : "restart_provider",
|
||||
exitCode: alive ? null : 1,
|
||||
requests: [],
|
||||
};
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
close: () => {
|
||||
closeCalled = true;
|
||||
return Promise.resolve(ok(null));
|
||||
},
|
||||
get closeCalled() {
|
||||
return closeCalled;
|
||||
},
|
||||
kill: (
|
||||
state:
|
||||
| "exited"
|
||||
| "unreachable"
|
||||
| "unknown"
|
||||
| "not_started" = "exited",
|
||||
) => {
|
||||
alive = false;
|
||||
deadState = state;
|
||||
},
|
||||
};
|
||||
created.push(client);
|
||||
return client;
|
||||
},
|
||||
};
|
||||
return {
|
||||
provider,
|
||||
created,
|
||||
onProbe: (hook: () => void) => {
|
||||
probeHook = hook;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ExecutableFormatHint } from "../../domain/dosCom.js";
|
||||
import type { BinaryArchitecture } from "../../domain/binaryTargetTypes.js";
|
||||
import type { AnalysisProviderSelector } from "../../contracts/providerSelection.js";
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import {
|
||||
@@ -23,8 +22,6 @@ export interface BinarySessionOpenOptions {
|
||||
readonly signal?: AbortSignal;
|
||||
readonly targetKind?: BinaryTarget["kind"];
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly entryPoint?: string;
|
||||
readonly snapshot?: AnalysisSnapshot;
|
||||
readonly providerId?: AnalysisProviderSelector;
|
||||
}
|
||||
@@ -75,12 +72,6 @@ export const resolveSessionOpen = async (
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
: { formatHint: options.formatHint }),
|
||||
...(options.architecture === undefined
|
||||
? {}
|
||||
: { architecture: options.architecture }),
|
||||
...(options.entryPoint === undefined
|
||||
? {}
|
||||
: { entryPoint: options.entryPoint }),
|
||||
...(options.signal === undefined ? {} : { signal: options.signal }),
|
||||
});
|
||||
if (!parsed.ok) return parsed;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { InvestigationRecordPort } from "../investigation/InvestigationRecordPort.js";
|
||||
import type { ExecutableFormatHint } from "../../domain/dosCom.js";
|
||||
import type {
|
||||
BinaryArchitecture,
|
||||
BinaryTarget,
|
||||
} from "../../domain/binaryTargetTypes.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
@@ -47,8 +44,6 @@ export interface BinarySessionPort
|
||||
readonly signal?: AbortSignal;
|
||||
readonly targetKind?: BinaryTarget["kind"];
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly entryPoint?: string;
|
||||
readonly snapshot?: AnalysisSnapshot;
|
||||
readonly providerId?: AnalysisProviderSelector;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { binaryTargetSummary } from "./BinaryTargetSummary.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type { ProviderOperationHealth } from "../../domain/providerOperationHealth.js";
|
||||
import type {
|
||||
@@ -11,7 +10,6 @@ import type {
|
||||
SessionProviderRoute,
|
||||
SessionProviderRouter,
|
||||
} from "./SessionProviderRouter.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
|
||||
interface BinarySessionStatusInput {
|
||||
readonly target: BinaryTarget | undefined;
|
||||
@@ -40,9 +38,7 @@ export const binarySessionStatus = ({
|
||||
}: BinarySessionStatusInput): JsonValue => {
|
||||
const capabilities = [...route.capabilities.values()]
|
||||
.filter(({ operation }) => operation !== "inventory_artifact")
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.operation, right.operation),
|
||||
)
|
||||
.sort((left, right) => left.operation.localeCompare(right.operation))
|
||||
.map((descriptor) => {
|
||||
const unavailable = runtimeUnavailability.get(descriptor.operation);
|
||||
return unavailable === undefined
|
||||
@@ -72,9 +68,7 @@ export const binarySessionStatus = ({
|
||||
capabilities: candidate.capabilities
|
||||
.slice()
|
||||
.filter(({ operation }) => operation !== "inventory_artifact")
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.operation, right.operation),
|
||||
)
|
||||
.sort((left, right) => left.operation.localeCompare(right.operation))
|
||||
.map(capabilityStatus),
|
||||
}));
|
||||
const common = {
|
||||
@@ -98,7 +92,11 @@ export const binarySessionStatus = ({
|
||||
: {
|
||||
open: true,
|
||||
...common,
|
||||
...binaryTargetSummary(target),
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
format: target.format,
|
||||
kind: target.kind,
|
||||
architecture: target.architecture ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -107,9 +105,7 @@ const requestActivityStatus = (
|
||||
) => {
|
||||
const providers = snapshots
|
||||
.slice()
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.provider.id, right.provider.id),
|
||||
)
|
||||
.sort((left, right) => left.provider.id.localeCompare(right.provider.id))
|
||||
.map(({ provider, active, queuedRequests }) => ({
|
||||
provider: providerSummary(provider),
|
||||
active:
|
||||
@@ -157,7 +153,7 @@ const processLineageStatus = (
|
||||
snapshots: snapshots
|
||||
.slice()
|
||||
.sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.provider.id, right.provider.id),
|
||||
left.provider.id.localeCompare(right.provider.id),
|
||||
)
|
||||
.map(({ provider, observation }) => ({
|
||||
provider: providerSummary(provider),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user