Compare commits

..
Author SHA1 Message Date
gensokyo-dev 4453d724f7 test(cli): assert retained managed target selection provenance 2026-10-24 18:18:58 +00:00
962 changed files with 6399 additions and 31985 deletions

No files matched your search

@@ -384,9 +384,8 @@ selected provider. Do not treat those scopes as interchangeable.
For a Ghidra startup timeout on a large native target, follow
[deadline recovery](references/native-and-artifacts.md#ghidra-startup-deadline-recovery)
before abandoning deep analysis or repeating the same cold import. The default
330 seconds can be changed for new startup attempts through
`update_analysis_settings` using `ghidra.startup_timeout_ms`. Changing the
server's launch environment instead requires a new REA process.
330 seconds is configurable; changing the server's environment requires a new
REA process.
## Work summary-first
@@ -28,11 +28,8 @@ stdio. A full JDK, including `jdk.compiler`, is required on every host; Windows
verification coverage. Calls use one worker and provider deadlines; optional
`REA_JADX_HEAP_MIB` selects the JVM heap and `REA_JADX_OPERATION_TIMEOUT_MS`
the per-operation deadline (integer milliseconds from 1 to 2147483647, default
120000). To change that deadline on a running MCP server, set
`jadx.operation_timeout_ms` through `update_analysis_settings`, then retry after
work and cleanup settle. Newly accepted operations use the override; accepted
work keeps its budget. Changing the launch environment requires a new process.
The MCP client's tool timeout must also allow the selected deadline. The APK is never launched,
120000; a change applies on the next process start; the MCP client's tool
timeout must also allow it). The APK is never launched,
and no emulator is required.
Manifest entries and decompiled text are static observations, not execution or
@@ -128,7 +128,7 @@ rea setup --client codex --dry-run --json
Use the current supported client's ID: `claude_code`, `claude_desktop`, `codex`,
`cursor`, `gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`,
`copilot_cli`, `commandcode`, `qwen_code`, `vscode`, `trae`, `grok_build`, `omp`, `pi`,
`hermes`, `qoder`, `droid`, `zcode`, `dsh`, `freebuff`, or `grok_bot`. If unknown, inspect `doctor --json` before choosing a
`hermes`, `qoder`, `droid`, `zcode`, `dsh`, or `grok_bot`. If unknown, inspect `doctor --json` before choosing a
scope. `grok_bot` uses a chat registration step rather than local `mcp.json`.
For unsupported clients, use version-pinned manual stdio registration or CLI.
@@ -175,17 +175,6 @@ writing. Managed policy can constrain client settings. Consult the
for the installed client's supported settings. This is an optional recovery
choice, not a prerequisite for every REA task.
## Repair an editable REA limit
If `details.limit` names an editable setting and its bounds, call
`update_analysis_settings` directly with a `changes` entry containing `setting`,
`action: "set"` and `value`. `inspect_analysis_settings` is optional when those
facts are already present. Retry the same input after work and cleanup settle.
Runtime defaults affect new work only; explicit per-call arguments take
precedence and must be corrected directly when selected. Reset restores launch
values. Updates survive target close/open but disappear on REA restart. They do
not edit host deadlines, response buffers, registrations or fixed limits.
## Continue through an installed CLI
Check the command environment only if the launcher fails:
@@ -24,13 +24,9 @@ not an unfiltered request. `detail: "summary"` reduces delivery
and does not by itself reduce analysis work.
ASAR header/index admission defaults to 16 MiB / 16777216 bytes;
an admission error names `asar.max_header_bytes` and its bounds (at most
256 MiB). Call `update_analysis_settings` with that setting, `action: "set"`
and enough bytes, then retry the same full-archive request. No inspection or
REA restart is required. `REA_ASAR_MAX_HEADER_BYTES` seeds future REA processes.
`source_paths`, `detail`, and `max_heap_mb` do not bypass or raise header admission.
Runtime JavaScript timeout/heap defaults are also editable; explicit per-call
`analysis_timeout_ms` and `max_heap_mb` take precedence.
`REA_ASAR_MAX_HEADER_BYTES` (at most 256 MiB, set where REA starts) changes it.
`source_paths`, `detail`, and `max_heap_mb` do not bypass or raise it. Or
analyze an extracted directory.
Exact `#alias` specifiers resolve through the importer's
enclosing package `imports` map to package-local files; nested `node_modules`
@@ -342,18 +342,14 @@ providers. Top-level session capabilities describe the current route, and
bridge startup, and readiness. The default 330,000 ms is configurable through
`REA_GHIDRA_STARTUP_TIMEOUT_MS`; a smaller query still needs the same cold import.
When the error names `ghidra.startup_timeout_ms`, call
`update_analysis_settings` with `action: "set"` and a larger value within the
reported bounds. Resolve retained cleanup before the next startup attempt.
No REA restart is needed; an already accepted startup keeps its original
budget. A new attempt repeats import. Preserve useful Evidence before closing
and reopening the target when remediation requires that lifecycle step.
For a per-function timeout, update `ghidra.decompile_timeout_seconds` and retry
the same function on the existing Program. Explicit `null` removes that
per-function deadline. Inspection is optional when the error supplies the
setting and bounds. Runtime updates disappear when REA exits; environment
changes only seed future REA processes.
Preserve useful Evidence before closing the session or restarting REA. Check
`close_binary` and resolve reported cleanup failures. Set a larger startup budget
in the environment that launches REA, then restart REA and reconnect the MCP
client before reopening the same target. Closing and reopening a binary in the
existing server keeps its original configuration and starts import again.
Environment changes in an agent's shell do not update an already running MCP
server. If its launch configuration cannot be changed, report that constraint
and the exact retry setting instead of claiming a fixed technical limit.
The client's tool-call deadline must allow startup plus the requested query;
server settings and progress notifications cannot override it. CLI analysis
-7
View File
@@ -36,9 +36,6 @@ unchecked. Maintainers can help resolve open questions.
Use "none" or "not applicable" only when established. -->
- Semantic owner and earliest changed stage:
<!-- Name the producer of parsed facts or resources and its affected consumers.
Summarize superseded paths removed and any distinct policy, lifecycle,
lazy-loading, or compatibility responsibility retained. -->
- Contribution scope fit and linked maintainer decision:
- CLI and MCP/tool-catalog contract:
- Provider, bridge, target-format, or platform compatibility:
@@ -71,10 +68,6 @@ source, link, or rendered-document checks. -->
- User-visible CLI/MCP output (if applicable):
- Remaining proof gaps:
For boundary fixes, identify the applicable [boundary regressions](https://github.com/morluto/rea/blob/main/docs/testing.md#boundary-regressions)
and [lifecycle regressions](https://github.com/morluto/rea/blob/main/docs/testing.md#lifecycle-regressions)
covered by the evidence above, and explain relevant gaps.
For evidence-bearing changes:
- [ ] Observed, derived, and inferred claims remain distinguishable.
-1
View File
@@ -858,7 +858,6 @@ jobs:
src/process/capture/FilesystemSnapshot.test.ts
tests/process-global/filesystemSnapshotFallbackCancellation.test.ts
tests/boundary/filesystem/typescriptCompiler.test.ts
tests/boundary/filesystem/releaseProposal.test.ts
tests/boundary/filesystem/binaryTarget.test.ts
src/contracts/sessionPathInputs.test.ts
tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts
+3 -95
View File
@@ -1,13 +1,7 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn", "oxc", "vitest"],
"plugins": ["typescript", "import", "unicorn", "oxc"],
"rules": {
"rea/no-buffer-deep-equality": "error",
"rea/cancellation-cause": "error",
"no-async-promise-executor": "error",
"no-unreachable": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-escape": ["warn", { "allowRegexCharacters": ["-"] }],
"complexity": ["warn", 20],
"max-depth": ["warn", 4],
"max-lines": [
@@ -29,38 +23,7 @@
"max-params": ["warn", 4],
"no-var": "warn",
"no-var-requires": "error",
"typescript/no-import-type-side-effects": "error",
"vitest/no-standalone-expect": "off",
"vitest/no-conditional-expect": "off",
"vitest/expect-expect": "off",
"vitest/no-disabled-tests": "off",
"vitest/require-mock-type-parameters": "off",
"vitest/require-to-throw-message": "off",
"import/no-duplicates": [
"warn",
{
"preferInline": false
}
],
"vitest/valid-expect": [
"warn",
{
"maxArgs": 2
}
],
"rea/no-evidence-json-preadmission": "error",
"rea/no-positional-tool-contract": "error",
"rea/no-fixture-root-cleanup": "error",
"typescript/no-floating-promises": "error",
"typescript/no-misused-promises": "warn",
"typescript/return-await": ["error", "error-handling-correctness-only"],
"typescript/switch-exhaustiveness-check": [
"error",
{
"considerDefaultExhaustiveForUnions": true
}
],
"rea/no-shared-module-mock": "error"
"typescript/no-import-type-side-effects": "error"
},
"overrides": [
{
@@ -73,12 +36,6 @@
"no-var-requires": "off"
}
},
{
"files": ["src/**/*.ts"],
"rules": {
"typescript/no-misused-promises": "error"
}
},
{
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
"rules": {
@@ -90,8 +47,7 @@
"skipBlankLines": true,
"skipComments": true
}
],
"typescript/no-misused-promises": "warn"
]
}
},
{
@@ -135,45 +91,6 @@
}
]
}
},
{
"files": ["tests/**/*.test.ts", "src/**/*.test.ts"],
"rules": {
"vitest/valid-expect": [
"warn",
{
"maxArgs": 2
}
],
"vitest/valid-expect-in-promise": "error",
"vitest/no-focused-tests": "error"
}
},
{
"files": [
"scripts/lib/http3-capture-assertions.test.mjs",
"scripts/lib/http3-verifier-lifecycle.test.mjs"
],
"rules": {
"typescript/no-floating-promises": [
"error",
{
"allowForKnownSafeCalls": [
{
"from": "package",
"package": "@types/node",
"name": "test"
}
]
}
]
}
},
{
"files": ["scripts/fixtures/javascript-recovery/code-split/entry.mjs"],
"rules": {
"typescript/no-floating-promises": "off"
}
}
],
"env": {
@@ -192,14 +109,5 @@
".devin",
".factory",
"third_party"
],
"options": {
"reportUnusedDisableDirectives": "error"
},
"jsPlugins": [
{
"name": "rea",
"specifier": "./scripts/lint/index.mjs"
}
]
}
+5 -6
View File
@@ -80,7 +80,7 @@ Coordinate expensive checks across agents. If a test unexpectedly consumes heavy
For tool design, follow [docs/tool-design.md](docs/tool-design.md): start from the analyst question, reuse existing contracts, preserve caller choices, and return useful evidence inline. Limits must follow real format, protocol, authority, or resource constraints. Account for representation expansion before allocation; derive completeness from examined coverage.
For test changes or provider claims, use [docs/testing.md](docs/testing.md). Select applicable [boundary regressions](docs/testing.md#boundary-regressions) for parser, inventory, snapshot, and projection fixes. Prefer real public CLI/MCP workflows, then production-boundary integration, then producer goldens. Keep module tests for distinct semantics stronger workflows cannot reliably reproduce. Preserve malformed-input, cancellation, cleanup, permission, capacity, and target-identity coverage when pruning. Report unverified coverage explicitly; simulated providers do not establish real-provider behavior.
For test changes or provider claims, use [docs/testing.md](docs/testing.md). Prefer real public CLI/MCP workflows, then production-boundary integration, then producer goldens. Keep module tests for distinct semantics stronger workflows cannot reliably reproduce. Preserve malformed-input, cancellation, cleanup, permission, capacity, and target-identity coverage when pruning. Report unverified coverage explicitly; simulated providers do not establish real-provider behavior.
Use Conventional Commit subjects and PR titles. Mark breaking changes with `!` or a `BREAKING CHANGE:` footer. Describe behavior changes, relevant validation, and real-provider verification scope.
@@ -108,11 +108,10 @@ distinguish source-derived conclusions, proposed checks, and unavailable coverag
Address actionable automated review findings and explain disagreements.
Leave the template's **Author readiness** declarations unchecked for the human
owner to complete. Never attest to human review on their behalf. Draft status
is optional; preserve the PR's current draft status unless the user asks to change
it. Later pushes do not invalidate the readiness declaration or require another
human sign-off. Keep the PR body and affected validation current as work changes.
The readiness check validates
owner to complete. Never attest to human review on their behalf. Keep the PR in
draft until the human declares the PR initially ready. Later pushes do not
invalidate that declaration or require another human sign-off. Keep the PR body
and affected validation current as work changes. The readiness check validates
the filled declaration; CI and automated review results do not establish that
human review occurred, maintainer scope approval, or merge permission.
+4 -30
View File
@@ -25,18 +25,6 @@ acquiring targets; optional adapter failures must preserve successful peers. Run
`npm run verify:test-discovery` after adding or moving tests. Resolved import
boundaries are checked by `npm run verify:module-boundaries`, included in lint.
Before retiring an alias or facade, trace runtime registration, dynamic imports,
JavaScript verifiers, subprocess scripts, and packaged entrypoints as well as
TypeScript references. Tests may be the last consumers of a superseded internal
path; migrate their distinct assertions to the current production path before
removing it. Preserve validation, lazy loading, caller policy, and resource
acquisition and cleanup supplied by the old boundary. Published APIs, persisted
formats, and independently versioned bridges require an explicit compatibility
decision; an internal representation alone does not establish that obligation.
See [interpreting boundary facts once](docs/tool-design.md#interpret-boundary-facts-once)
for admission ownership and the distinction between repeated validation and
independent checks.
For analysis workflows and agent interfaces, follow the
[performance priorities](AGENTS.md#performance). Explain the selected output
scope, reuse/cache decisions, and relevant before/after measurements when these
@@ -47,9 +35,7 @@ compare equivalent real tasks before claiming an improvement.
When adding or changing an MCP tool, follow the [tool design guide](docs/tool-design.md) and preserve the canonical contracts and generated catalog.
When adding or changing an admission ceiling, budget, or deadline, follow
[analysis settings, limits, and recovery](docs/design/limits-and-recovery.md)
for ownership, operation snapshots, provider integration, and the steps for
adding a runtime setting.
[admission limits and recovery](docs/design/limits-and-recovery.md).
## Reporting issues
@@ -103,11 +89,7 @@ Human ownership and review remain required when CI or an automated reviewer
reports success. Readiness requests maintainer review; it does not grant scope
approval or permission to merge.
Draft status is optional. Draft PRs may request early feedback with open questions
and incomplete checks. Agents preserve the current draft status unless asked to
change it; missing readiness declarations or later updates do not require a PR
to become a draft.
Draft PRs may request early feedback with open questions and incomplete checks.
Leave the readiness declaration unchecked until the human owner has completed
it. The declaration records initial readiness; later pushes do not invalidate
it or require another human sign-off. Keep the PR description and affected
@@ -386,17 +368,9 @@ Real-provider execution is uncached.
### Temporary files and diagnostics
Tests use `createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`
for exact-path, awaited cleanup, including failures and timeouts. It registers
deletion with Vitest's `onTestFinished`; do not also add its result to a roots
array or an `afterEach` removal. `createTestWorkspace` from
`tests/support/workspace/workspaceFixture.ts` leaves cleanup with its caller,
which must await `removeTestWorkspace`. Keep explicit resource teardown before
directory deletion, using the case's `finally` or `afterEach`; Vitest runs
`afterEach` before `onTestFinished` and awaits finish hooks in reverse
registration order.
for exact-path, awaited cleanup, including failures and timeouts.
`npm run verify:test-temp-hygiene` runs the suite under a fresh `TMPDIR` and
rejects remaining REA-owned paths. It cannot detect duplicate deletion owners or
prove resource-close ordering. Never glob-clean shared `/tmp/rea-*` content.
rejects remaining REA-owned paths. Never glob-clean shared `/tmp/rea-*` content.
`REA_LOG_LEVEL` selects `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or
`silent`. Logs go to stderr: MCP defaults to `info`, and one-shot CLI logging
+10 -68
View File
@@ -3,8 +3,6 @@ import com.google.gson.*;
import io.modelcontextprotocol.kotlin.sdk.server.*;
import io.modelcontextprotocol.kotlin.sdk.types.*;
import jadx.api.*;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.attributes.JadxAttrType;
import jadx.api.plugins.input.data.ICodeReader;
import jadx.api.plugins.input.data.IMethodRef;
import jadx.api.plugins.input.insns.InsnIndexType;
@@ -42,8 +40,7 @@ class ReaJadxBridge {
private record MethodSnapshot(String name, String rawName, String shortId, List<String> bodyAbsenceMarkers,
Map<String, Object> summary) {}
private record ClassSnapshot(JavaClass node, List<MethodSnapshot> methods, Map<String, Object> summary,
Map<String, Object> summaryWithValues) {}
private record ClassSnapshot(JavaClass node, List<MethodSnapshot> methods, Map<String, Object> summary) {}
private record ManifestSnapshot(String text, Map<String, Object> summary) {}
private static final class InputFailure extends IllegalArgumentException {
@@ -145,14 +142,6 @@ class ReaJadxBridge {
return value.getAsString();
}
private static boolean flag(JsonObject args, String name) {
JsonElement value = args.get(name);
if (value == null) return false;
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isBoolean())
throw new IllegalArgumentException(name + " must be a boolean");
return value.getAsBoolean();
}
private static int integer(JsonObject args, String name, int fallback) {
JsonElement value = args.get(name);
if (value == null) return fallback;
@@ -274,38 +263,17 @@ class ReaJadxBridge {
"dex_descriptor", descriptor,
"is_constructor", method.isConstructor(), "def_pos", 0));
}).toList();
List<FieldNode> orderedFields = new ArrayList<>(node.getFields());
orderedFields.sort(Comparator.comparing(FieldNode::getAlias));
List<Map<String, Object>> fields = new ArrayList<>();
List<Map<String, Object>> valuedFields = new ArrayList<>();
boolean anyValue = false;
for (FieldNode field : orderedFields) {
Map<String, Object> entry = Map.of("name", field.getAlias(),
"type", field.getType().toString(), "def_pos", 0);
fields.add(entry);
Map<String, Object> value = dexStaticValue(field);
if (value == null) valuedFields.add(entry);
else {
anyValue = true;
Map<String, Object> valued = new LinkedHashMap<>(entry);
valued.put("dex_static_value", value);
valuedFields.add(Collections.unmodifiableMap(valued));
}
}
List<Map<String, Object>> fields = node.getFields().stream()
.sorted(Comparator.comparing(FieldNode::getAlias))
.map(field -> Map.<String, Object>of("name", field.getAlias(),
"type", field.getType().toString(), "def_pos", 0)).toList();
List<String> inner = node.getInnerClasses().stream().map(ClassNode::getFullName).toList();
Map<String, Object> summary = Map.of(
"full_name", cls.getFullName(), "raw_name", cls.getRawName(), "name", cls.getName(),
"method_count", methods.size(), "field_count", fields.size(),
"inner_class_count", inner.size(), "inner_classes", inner,
"methods", methods.stream().map(MethodSnapshot::summary).toList(), "fields", List.copyOf(fields));
// DEX static values are opt-in, so method and reference lookups keep the member-only reply.
Map<String, Object> summaryWithValues = summary;
if (anyValue) {
Map<String, Object> copy = new LinkedHashMap<>(summary);
copy.put("fields", List.copyOf(valuedFields));
summaryWithValues = Collections.unmodifiableMap(copy);
}
ClassSnapshot snapshot = new ClassSnapshot(cls, methods, summary, summaryWithValues);
"methods", methods.stream().map(MethodSnapshot::summary).toList(), "fields", fields);
ClassSnapshot snapshot = new ClassSnapshot(cls, methods, summary);
index.computeIfAbsent(cls.getFullName(), ignored -> new ArrayList<>()).add(snapshot);
// Keep the binary identity supplied by JADX; '$' can also be a literal
// name character, so replacing separators would invent lookup aliases.
@@ -325,28 +293,6 @@ class ReaJadxBridge {
return candidates.get(0);
}
/** A static final field's class-definition static value; a static initializer can still assign it. */
private static Map<String, Object> dexStaticValue(FieldNode field) {
if (!field.getAccessFlags().isStatic() || !field.getAccessFlags().isFinal()) return null;
EncodedValue encoded = field.get(JadxAttrType.CONSTANT_VALUE);
if (encoded == null) return null;
Object value = encoded.getValue();
// JADX also attaches the shared null to static finals without a value, so null is never reported.
// Wide and floating values keep their Java text instead of a lossy JSON number.
return switch (encoded.getType()) {
case ENCODED_BOOLEAN -> Map.of("type", "boolean", "value", value);
case ENCODED_BYTE -> Map.of("type", "byte", "value", value);
case ENCODED_SHORT -> Map.of("type", "short", "value", value);
case ENCODED_CHAR -> Map.of("type", "char", "value", (int) (Character) value);
case ENCODED_INT -> Map.of("type", "int", "value", value);
case ENCODED_LONG -> Map.of("type", "long", "value", value.toString());
case ENCODED_FLOAT -> Map.of("type", "float", "value", value.toString());
case ENCODED_DOUBLE -> Map.of("type", "double", "value", value.toString());
case ENCODED_STRING -> Map.of("type", "string", "value", value);
default -> null;
};
}
private List<MethodSnapshot> methods(ClassSnapshot cls, JsonObject args) {
String name = string(args, "method_name");
List<MethodSnapshot> result = cls.methods().stream().filter(method -> method.name().equals(name)).toList();
@@ -497,8 +443,7 @@ class ReaJadxBridge {
"items", items));
}
ClassSnapshot cls = selected(args);
if (name.equals("get_class_summary"))
return json(flag(args, "dex_static_values") ? cls.summaryWithValues() : cls.summary());
if (name.equals("get_class_summary")) return json(cls.summary());
if (name.equals("get_method_body")) {
List<MethodSnapshot> overloads = methods(cls, args);
int index = integer(args, "overload_index", 0);
@@ -581,7 +526,7 @@ class ReaJadxBridge {
SessionHolder holder = new SessionHolder(defaults);
ServerCapabilities capabilities = new ServerCapabilities(
new ServerCapabilities.Tools(null), null, null, null, null, null, null, Map.of());
Server server = new Server(new Implementation("rea-jadx-bridge", "7", null, null, null),
Server server = new Server(new Implementation("rea-jadx-bridge", "6", null, null, null),
new ServerOptions(capabilities), "", ignored -> Unit.INSTANCE);
SessionToolsKt.registerSessionTools(server, holder);
ClassToolsKt.registerClassTools(server, holder);
@@ -623,12 +568,9 @@ class ReaJadxBridge {
if (name.equals("list_classes")) properties.put("query",
new kotlinx.serialization.json.JsonObject(Map.of(
"type", kotlinx.serialization.json.JsonElementKt.JsonPrimitive("string"))));
if (name.equals("get_class_summary")) properties.put("dex_static_values",
new kotlinx.serialization.json.JsonObject(Map.of(
"type", kotlinx.serialization.json.JsonElementKt.JsonPrimitive("boolean"))));
String description = switch (name) {
case "list_classes" -> "Pages of case-sensitive matching top-level class names; offset and next_offset are inventory positions, limit bounds matching rows. Omitted or empty query inventories all names.";
case "get_class_summary" -> "Parsed member metadata for one exact class name; no source generation. Set dex_static_values to include encoded static final field values.";
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.";
default -> "Complete static using-node inventory for an exact class or selected method overload; one node may contain multiple invoke instructions, and source lines are unresolved.";
};
+4 -17
View File
@@ -404,8 +404,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
writeSuccess(writer, request.id, result);
return;
}
decompileTimeoutSeconds = request.decompileTimeoutSeconds() == null
? descriptor.decompileTimeoutSeconds() : request.decompileTimeoutSeconds();
writeSuccess(writer, request.id, handleRequest(request, descriptor));
}
catch (RequestFailure failure) {
@@ -3383,7 +3381,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
"decompile_timed_out",
"Ghidra decompilation of " + canonicalAddress(function.getEntryPoint()) +
" exceeded the " + decompileTimeoutSeconds +
"-second configured decompilation limit (ghidra.decompile_timeout_seconds; launch seed REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS)"
"-second limit set by REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS"
);
}
if (!results.decompileCompleted()) {
@@ -4773,18 +4771,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
private static Request parseRequest(String line, String expectedToken) {
JsonElement parsed = JsonParser.parseString(line);
Set<String> keys = new HashSet<>(REQUEST_KEYS);
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("decompile_timeout_seconds"))
keys.add("decompile_timeout_seconds");
JsonObject object = requireObject(parsed, keys);
Integer timeout = null;
if (object.has("decompile_timeout_seconds")) {
JsonElement value = object.get("decompile_timeout_seconds");
timeout = value.isJsonNull() ? 0 : requireInteger(object, "decompile_timeout_seconds");
if (!value.isJsonNull() && (timeout < 1 || timeout > MAX_DECOMPILE_TIMEOUT_SECONDS))
throw new IllegalArgumentException("Invalid per-request decompile deadline");
}
JsonObject object = requireObject(JsonParser.parseString(line), REQUEST_KEYS);
int id = requireInteger(object, "id");
if (id <= 0 || !constantTimeEquals(requireString(object, "token"), expectedToken)) {
throw new IllegalArgumentException("Bridge request authentication failed");
@@ -4793,7 +4780,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
if (params == null || !params.isJsonObject()) {
throw new IllegalArgumentException("Bridge request parameters are invalid");
}
return new Request(id, requireString(object, "method"), params.getAsJsonObject(), timeout);
return new Request(id, requireString(object, "method"), params.getAsJsonObject());
}
private static JsonObject requireObject(JsonElement element, Set<String> keys) {
@@ -4987,7 +4974,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
JsonArray analysisExtensions,
int decompileTimeoutSeconds
) {}
private record Request(int id, String method, JsonObject params, Integer decompileTimeoutSeconds) {}
private record Request(int id, String method, JsonObject params) {}
private static final class RequestFailure extends RuntimeException {
private final String code;
+5 -21
View File
@@ -683,12 +683,7 @@ A view that names a field in `sorted_arrays` lists that string array sorted, as
a read-back showed the application does, while the state keeps it in the order
it was written, so a write's own answer echoes it. If a later counted read
contradicts that ordering, the view stays recorded and its earlier reads no
longer confirm served writes. A view that names a field in `nested_fields`
always showed that field's objects with the same keys, in any order, and fewer
than an entity stores, such
as a list showing a parent without the parent's own nested type; a stored
object it never recorded, such as a created entity's parent, is rendered with
those keys. Members a view names in
longer confirm served writes. Members a view names in
`per_response_members`, such as a request id each response mints, keep the
value its recording carried. A list's `counters` and `query.totals` are
top-level members or members one level down in a paging object
@@ -726,7 +721,7 @@ every MCP session and `data/get` then see:
| Effect | Call |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create` | Adds an entity to the collection the path names, built from the observed entity with each ruled field taken from the call or generated, at the plan's `insert` position. A plan tied to its exemplar's collection writes only there. |
| `update` | Sets the sent members on the entity the path names, constants for members accepted only with their recorded value, and the clock, nested clock, increment, slug, stamp, derived, composed, reference, unique and renewed changes the plan models. |
| `update` | Sets the sent members on the entity the path names, constants for members accepted only with their recorded value, and the clock, nested clock, increment, slug, stamp, derived, unique and renewed changes the plan models. |
| `update` (action) | A `POST`, `PUT` or `PATCH` to an item path plus an action segment (`/{type}/{id}/archive`) sets, on the entity before the segment, each scalar the recorded calls showed the action set (`set`), with any sent members; it is served only when a recorded call found those fields already set, so a toggle never is. |
| `delete` | Removes the entity the path names. |
| bulk `update` or `delete` | A `DELETE`, `PUT` or `PATCH` at a collection, or a `POST` to it plus `delete`, `remove` or `destroy`, applies its effect to every entity the body names, as recorded: an array of ids or of objects carrying the id field, at the root or as the body's one array member (`{"IDs": […], "Read": true}`). |
@@ -928,9 +923,6 @@ Known limits of served writes:
updates require `updated_at` to match the entity (a `held-clock`).
- A slug derived from a title can repeat across served creates with the same
title, where the application would make it unique.
- A `composed` string copies sent text as sent: an application that escapes or
renders it (HTML entities, Markdown) differs for text that needs it, as
BookStack's `description_html` would for a description holding `&`.
- A served create's sequence id is the state's next value, not the gaps an
application's own sequence would leave.
- Nested clocks are served in the spelling of the write's own response, which
@@ -1065,11 +1057,9 @@ Numeric arrays retain that containing field's clock context through nested
arrays; an object's own fields establish their own context. Numeric values at
the body root, in root arrays, or under measurement fields remain literal.
Generated values normalize only where this confirmed, served write's entity
response rules identify generation. A UUID identity rule must still compare two
UUIDs, and a `unique` rule two UUIDs or two generated integers (10 to 20
digits) of one JSON kind and length; a `renewed` rule applies only at its
recorded pointers,
Generated strings normalize only where this confirmed, served write's entity
response rules identify generation. A `unique` rule or UUID identity rule must
still compare two UUIDs; a `renewed` rule applies only at its recorded pointers,
with `*` matching actual array items. Token/sequence strings retain the existing
format check: equal length of at least eight, the same character classes, and
a digit. Numeric sequences continue to compare literally unless an ID mapping
@@ -1081,12 +1071,6 @@ Admitted generated values must also preserve aliases and distinct identities
within that response. UUID identity uses the generator's case-insensitive
matching; token and sequence strings retain their literal identity. This
correspondence ends with the answer and is not inferred across calls.
A `composed` response field must spell the value its parts build from that
response's own fields. Its parts then compare by their rendered text or their
source fields' clock and ID rules. A coherent archive URL can therefore differ
only in normalized clock digits without reporting a value difference; an
incoherent URL still differs even when both sides repeat the same stale text.
Normalized compositions count under `generated`; source fields are counted once.
The report counts each kind of normalised
value; the generation rules remain model-derived facts, rather than new
observations of the application.
+5 -26
View File
@@ -194,21 +194,6 @@ queries accept the same `overload_index`; the bridge resolves the selected
method node and the result reports its index and displayed signature. These
indices are tied to the current artifact and engine, not stable across builds.
`inspect_android_class` also reports `dex_static_value` for each `static final`
field whose class definition encodes a value in its DEX static values, so
compile-time constants are readable even when the class has no `<clinit>`.
Each value is typed: `boolean`, `byte`, `short`, `char` (UTF-16 code unit) and
`int` values are JSON booleans or numbers, while `long`, `float` and `double`
values are strings holding Java's text for the value, so 64-bit values stay
exact. The field reports `null` when no value is encoded, the encoded value is
null or of another kind, or the field is not `static final`. The value is read from parsed
metadata without generating source, and a static initializer can still assign
the field; use `inspect_android_method` on `<clinit>` for that evidence.
Only `inspect_android_class` asks the bridge for these values.
`inspect_android_method` and `trace_android_references` read the member-only
class summary, so large encoded strings cannot push their member lookup past
the 8 MiB protocol frame limit.
Smali fallbacks select one complete block using the original method name and DEX
descriptor, before applying the source byte limit. This preserves the selected
overload even when Java code generation renames a method. If the selected Java
@@ -415,17 +400,11 @@ export REA_JADX_OPERATION_TIMEOUT_MS=600000
The value must be a decimal millisecond count from 1 to 2147483647; anything
else is refused before Java starts. REA reads it from the environment captured
at process start. Changing that environment requires a new REA process. On a
running MCP server, set `jadx.operation_timeout_ms` through
`update_analysis_settings` to change the default for newly accepted operations;
queued or running operations retain their accepted budget. The same deadline
bounds each upstream call. Timeout recovery reports the effective milliseconds
and range, with `details.limit` identifying the runtime setting. An invalid
provider-local launch value can also be repaired by a runtime override; reset
exposes that invalid launch value again. See the
[settings design](design/limits-and-recovery.md#invalid-launch-configuration).
A passing JADX configuration check in `rea doctor` reports its launch-derived
`operation_timeout_ms`; it does not inspect another running server's overrides. Explicit
at process start. A change applies on the next process start; an already
running server keeps the deadline it started with. The same deadline bounds
each upstream call. A timeout names `REA_JADX_OPERATION_TIMEOUT_MS`, the
effective milliseconds, and that range. A passing JADX configuration check in
`rea doctor` reports the effective `operation_timeout_ms`. Explicit
cancellation still ends the operation immediately, and partial observations
and owned-process cleanup are unchanged. Raising the deadline does not extend
the bridge's 30-second startup handshake or the upstream **90-second**
+2 -17
View File
@@ -29,9 +29,7 @@ body is parsed only when that string is `"309"`. Other types, including `"288"`
stackshots, return no crash result.
`process`, `exception`, `termination`, `faulting_thread`, `threads`, and
`images` reproduce documented body fields when present, including each image's
`name` and each frame's `symbolLocation` (as `symbol_location`, the byte offset
from the start of `recorded_symbol`). Integers keep their
`images` reproduce documented body fields when present. Integers keep their
decimal literals and a hexadecimal form, including arm64e registers above
2^53. Frame roles follow the recorded frame index: row zero of the resolved
faulting thread is `faulting_instruction`, row zero of other threads is
@@ -40,22 +38,9 @@ address uses that row's own image base and offset. The recorded `inline` flag
does not supply a missing offset or authorize borrowing another row's address.
When `faultingThread` is missing or does not resolve to a recorded thread
object, row-zero roles are `unknown`; the recorded lookup remains available.
A translated process records Rosetta's registers in a nested
`threadState.rosetta` object; each one is reported as `rosetta.<name>` and
counts toward the register budget. A [public ReportCrash `.ips` log](https://github.com/Mortennn/Dozer/issues/189)
shows `rosetta.tmp2`, `tmp1`, and `tmp0` beside ordinary x86 registers. The
test fixture quotes that register excerpt inside an authored report body; it
does not generate a new Rosetta crash during the test.
Observed JSON strings retain `""`; `null` means the field was missing or JSON
`null`. This includes image paths, frame symbols, process text, and the older
string `osVersion` form. Unlisted JSON keys and generic register names also
retain empty spellings. An empty image architecture remains recorded as `""`
while `uuid_unknown` stays true, since that spelling does not establish an
architecture. An empty UUID is malformed, so its exact spelling appears in
`coverage.type_mismatches` and its canonical UUID remains unknown.
`process.os_version` keeps the producer's dictionary (`train`, `build`,
`release_type`, `is_embedded`) or the older string form. Image UUIDs are uppercase 8-4-4-4-12; an all-zero UUID
or a missing or empty `arch` is `uuid_unknown`. Image `source` letters keep their
or a missing `arch` is `uuid_unknown`. Image `source` letters keep their
documented meaning (`P` process, `S` shared cache, `C` shared cache library,
`K` kernel, `U` kernel cache, `T` kernel text exec, `A` absolute). Keys the
documentation does not list remain in `unlisted_keys` or `unlisted_body_keys`.
+2 -2
View File
@@ -23,7 +23,7 @@ graph TD
SESSION["SessionProviderRouter + BinarySession<br/>one immutable deep binding per target<br/>disjoint auxiliary routing"]
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback"]
RECORDS["src/application/investigation/<br/>composed InvestigationRecords + narrow ports<br/>EvidenceLedger and Unknown ownership"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots and session file ownership<br/>record coordination and post-commit notifications"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots<br/>compatibility facade and post-commit notifications"]
JS_RECON["src/application/javascript/ + src/artifacts/javascript/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
end
@@ -75,7 +75,7 @@ graph TD
end
subgraph "Domain Layer"
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>format declarations, Evidence, comparisons, application graphs"]
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>evidence, comparisons, application graphs"]
end
subgraph "Configuration"
-38
View File
@@ -434,17 +434,6 @@ complete Structured Field Item, including parameters such as `report-to`.
These are declarations, not an assessment of the document's effective policy;
absent, malformed or non-token items project as `null`.
Console entries, WebSocket frame summaries, and selected request/response JSON
shape outputs share one 8 MiB retained-observation allowance per capture. Shape
inference admits expanded path bytes before allocating those paths, including
escaped and Unicode property names. Replacing or removing a request's shapes
refunds their retained charges. A refused body shape preserves network metadata
and any captured peer shape with `resource_budget_exhausted` coverage; a refused
WebSocket frame is counted as dropped rather than reclassified as non-JSON text.
The allowance has no runtime override. Protocol messages and JSON input parsing
remain subject to their separate transport admission; this output allowance does
not bound the whole process heap.
REA retains local observation data and removes only URL userinfo credentials, explicitly declared scenario secrets, and values from structured credential/storage fields:
- URL observations preserve the complete local URL, including query values, fragments, duplicate parameters, and their order. Only username/password userinfo is removed; malformed original URL strings remain available with `origin: null`.
@@ -609,22 +598,6 @@ Network and console coverage starts only after REA attaches and enables the rele
Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Each fetch operation uses `source_map_fetch_timeout_ms` (default 30000) and a 64 MiB aggregate streamed-response budget across redirects. The byte budget has no runtime override. A fetch overrun is an item limitation, not a thrown analysis timeout. A deadline or byte-limit failure remains explicitly unavailable and makes bundle completeness partial. Source-map decoding also bounds raw resolved identities to 64 MiB and aggregate inline evidence to 32 MiB. Failed maps preserve their failure context when it fits; if another context cannot fit, collection stops with the full requested count, the returned processed count, and an explicit unknown-results limitation. Retained mappings and original sources are returned inline. See [source-location limits](web-source-location.md) for the structural decoder bounds. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
A fully read response that fails UTF-8, JSON, mapping, or structural validation
retains `artifact: null`, empty original-source, module-edge, and mapping
arrays, and an explicit failure limitation. Invalid content is `invalid`;
decoder or structural capacity refusal is `fetch_failed`. Its optional
`response_identity` records the complete body's SHA-256 digest, byte count,
and `fetch-response-body-bytes` representation without retaining malformed
text or charging that text to the 32 MiB output budget. These are bytes
delivered by Fetch after HTTP content decoding and before UTF-8 decoding, not
HTTP wire bytes. The identity is absent when the body was not fully read, and
older observations may omit it. A response whose text cannot fit the existing
output budget is likewise `fetch_failed` with text omitted and completed-body
identity; JSON parsing and source-map decoding are not attempted. Successful and partial maps keep their
full text artifacts. Their original text, including an initial BOM or XSSI
line, remains distinct from the parsing view that removes that framing.
Collection reads the body once and does not raise its response or output limits.
When fetching is selected, source-map `excluded` facts preserve the capture
ledger's reasons and counts for declarations rejected before download. No
disallowed URL details are retained or fetched. `requested` and `processed`
@@ -632,17 +605,6 @@ count the admitted fetch requests; exclusions are counted separately. Excluded
declarations make the collection unavailable, or partial when maps were retained.
An empty collection with no declarations remains `not_requested`.
If the caller cancels while later source maps are being fetched, the failure's
partial Evidence retains the captured page and every completed map item. Its
`normalized_result.kind` is `web-bundle-acquisition` and
`analysis_status` is `not_performed`: no script-analysis result is claimed.
`source_maps.requested` remains the admitted request count,
`source_maps.processed` counts completed items, and any unprocessed requests
have an explicit unknown-results limitation. Capture exclusions remain in the
same collection. The cancellation error keeps the caller's exact reason;
an already observed response resource or cleanup failure keeps its own error
category and cleanup owner.
Source-map items retain the script's `declared_url` separately from the observed
`effective_url`. Successful maps use the latter as their source-resolution base.
Failed fetches retain the last response URL observed within the approved origins;
+1 -8
View File
@@ -465,14 +465,7 @@ an exact result when the target bytes, operation, parameters, provider and
settings match. Mutations and cursor-dependent calls are excluded from the
cache. Snapshot files are local and use owner-only permissions.
One-shot binary CLI Evidence reports the exact path argument in `caller_input_path`,
including relative spelling and symlink aliases. Its subject identifies the
inspected artifact; `caller_input_path` does not change the Evidence ID or
snapshot query. A replay reports the current invocation's path while leaving
the saved Evidence unchanged. Provider inspections continue to include the
established `parameters.input_path` field.
Importing an Evidence bundle refreshes retained request and subject display metadata.
Importing an Evidence bundle refreshes retained subject display names and paths.
It preserves cached analysis results, including observations for other targets.
Snapshot bindings that use a replaced record retain its updated display path.
-33
View File
@@ -6,17 +6,6 @@ for the reviewed setup transaction and [client IDs](installation.md#supported-ag
The versioned verification notes below describe their stated runs; live service
acceptance is verified only where explicitly recorded.
## Runtime settings and client deadlines
`update_analysis_settings` changes curated defaults inside the running REA
server. It does not edit the client registration, extend the client's tool-call
deadline, or recompute timeout values previously derived by `rea setup`. When
raising a REA startup or operation deadline, separately ensure the client allows
enough time for the full request. Use `inspect_analysis_settings` for the running
server's defaults; `rea doctor` checks launch configuration and cannot observe
another process's runtime overrides. See the
[settings design](design/limits-and-recovery.md) for ownership and lifetime.
## Client tool names and discovery
REA advertises canonical names such as `binary_session`. The MCP host decides
@@ -504,28 +493,6 @@ as stale, and rerunning setup removes the flag to repair it. Native configuratio
uses strict JSON; setup refuses comments, trailing commas and a BOM rather than
writing a registration ZCode cannot load.
## FreeBuff
For FreeBuff, the proposed integration writes `mcpServers.rea` into the user-scope
`~/.agents/mcp.json` shared-agent file. Entries carry `command`, `args`, and
`env`; stdio is the default transport. The proposed personal skill location is
`~/.agents/skills`, so setup installs its skill copy there. Because the same
file doubles as ZCode's user-scope fallback (see above), a `freebuff`
registration is also in the file ZCode uses until ZCode defines its own native server.
Setup and doctor detect FreeBuff from its own profile directory
(`~/.config/freebuff-desktop`), not from the shared `.agents` folder that
REA's skill installs create for every client.
The configuration write and personal skill locations are proposed integration
boundaries. FreeBuff MCP loading and skill discovery have not been verified
with an installed native FreeBuff client/version; filesystem tests cover REA's
editing, discovery, and ownership behavior only. Automatic applicability
requires the FreeBuff profile directory even when the shared MCP file exists.
FreeBuff setup support requires `rea-agents@6.8.0` or later. Older versions
reject `rea setup --client freebuff` as an unsupported client. See the
[installation guide](installation.md#supported-agents) for client availability.
## DeepSeek Harness
DeepSeek Harness registers MCP clients through a Cordis YAML patch sequence
-5
View File
@@ -1,10 +1,5 @@
# Client and agent verification
FreeBuff MCP loading and personal skill discovery are currently unverified with
an installed native client/version. Its temporary-filesystem coverage verifies
REA's configuration and applicability boundaries only; it does not establish
that FreeBuff consumes the written registration or skill.
Use these optional lanes to verify an installed client or evaluate an agent's
REA workflow. [Testing REA](testing.md) provides the normal development commands
and provider lanes. Each record below states its host, client version, model
+37 -290
View File
@@ -1,121 +1,35 @@
# Analysis settings, limits, and recovery
# Admission limits and recovery
This guide explains the runtime settings design for maintainers and developers:
who owns each value, when an update takes effect, how providers enforce it, and
how to extend the system. The public tool contract is in
[MCP contracts](../mcp-contracts.md#editable-analysis-settings); recorded runtime
checks and their limitations are in the
[verification report](../verification/runtime-analysis-settings.md).
## Problem and design boundary
A valid workload can exceed a conservative default. An oversized ASAR header
may prevent analysis of the entire archive; a slow Ghidra function may exceed
its decompile deadline. Requiring a new REA process to change either budget
interrupts the investigation and can discard reusable provider state.
REA exposes a curated set of analysis defaults through MCP so callers can
adjust a reported limit and retry the same workload in the running runtime.
Each setting has a typed value, resource bounds, an enforcement owner, and a
defined application point. The design applies across archive admission,
provider startup, operation deadlines, and worker memory requests.
The setter changes in-memory defaults. It does not edit arbitrary environment
variables, configuration files, MCP registrations, or provider installations.
Those mechanisms have different owners and lifetimes. It also cannot change a
client deadline, extend accepted work, release retained resources, or remove a
fixed representation limit. Exposing a setting requires a working enforcement
path, not just an entry in the tool schema.
Further ceilings that should become settings are tracked in
How to add a REA-owned admission ceiling, budget, or deadline, and what a caller
does when one is hit. This is a design rule, not an inventory. Ceilings that
should become settings are tracked in
[#6176](https://github.com/morluto/rea/issues/6176). Naming honesty for limit
errors is [#5910](https://github.com/morluto/rea/issues/5910).
## Ownership and data flow
| Owner | Responsibility |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Domain definitions](https://github.com/morluto/rea/blob/main/src/domain/analysisSettingDefinitions.ts) and [types](https://github.com/morluto/rea/blob/main/src/domain/analysisSettings.ts) | Exact setting IDs, value types, shared bounds/defaults, units, provider/operation associations, application points, and structured limit metadata. Numeric constants stay with their domain owner. |
| [Launch parser](https://github.com/morluto/rea/blob/main/src/config/analysisSettings.ts) | Parse selected launch values once, preserving an invalid value as a typed configuration failure rather than substituting a default. |
| [AnalysisSettings](https://github.com/morluto/rea/blob/main/src/application/AnalysisSettings.ts) | Own one runtime's launch baseline, immutable override revisions, atomic updates, inspection, and accepted-operation snapshots. It does not own provider lifecycle or cleanup. |
| [Composition](https://github.com/morluto/rea/blob/main/src/main/eagerRuntime.ts) and [typed configuration views](https://github.com/morluto/rea/blob/main/src/config/runtimeAnalysisSettings.ts) | Inject the same settings reader into providers and workflows; translate overrides into the configuration each consumer understands. |
| Provider or resource owner | Resolve the effective budget, enforce it at the applicable boundary, and attach the actual value and known source to failures. |
| [MCP registration boundary](https://github.com/morluto/rea/blob/main/src/server/EvidenceMcpServerCore.ts) | Enter the operation's settings scope before invoking its handler. |
| [Settings contracts](https://github.com/morluto/rea/blob/main/src/contracts/analysisSettingsContracts.ts) and [tool registration](https://github.com/morluto/rea/blob/main/src/server/registerAnalysisSettingsTools.ts) | Validate named requests and deliver inspection/update results without requiring an open target or healthy analysis provider. |
The normal path is: composition creates the store, an MCP handler captures its
current revision, and providers read that snapshot when resolving budgets.
If an owner rejects work, it reports the effective limit through the shared
error projection. A subsequent settings update publishes a new revision for
later handlers. It does not reach into the failed operation or retry it.
### Accepted-operation snapshots
`AnalysisSettings.run()` installs the current immutable snapshot in
`AsyncLocalStorage`. Its `read` function returns that snapshot inside the
operation and the latest revision outside it. Providers depend on the reader
contract rather than on the mutable store or an ambient environment lookup.
Acceptance here is entry into the registered handler's settings scope, not
arrival of bytes at the MCP transport. Work queued or awaited within that scope
keeps its snapshot. Deferred server initialization must still dispatch through
the canonical scoped handler.
| Event | Revision used |
| --------------------------------------------------------- | ----------------------------------- |
| Operation A enters its handler, then waits for a provider | Current revision 0 |
| A settings update publishes revision 1 | Future handlers can read revision 1 |
| Operation A resumes and starts its worker | Its accepted revision 0 |
| Operation B enters its handler | Revision 1 |
This gives each operation stable budget semantics even if another connection
updates settings while it waits. `next_worker` and `next_startup` describe where
an accepted value is applied; they do not cause workers or startups belonging
to an older operation to adopt a newer revision.
Inspection deliberately reads the store's current defaults, rather than the
calling operation's snapshot. It reports `active_values.status: "not_observed"`:
an inspection result cannot establish the budget or remaining time of an
already-running worker.
### Composition and embedding
The MCP runtime creates one store and supplies its reader to provider factories.
`createServer` uses a supplied store or creates one, and wires its default
providers to that store. An embedder supplying a custom binary session or other
provider must also inject the same reader into that provider's composition and
pass the store to `createServer`. Merely exposing the settings tools cannot
make a custom provider consume updates.
Keep default factories, explicitly supplied providers, transport composition,
and deferred handler dispatch covered separately where their wiring differs.
Resolve settings at the operation boundary; copying a runtime value into a
provider constructor would freeze it at launch. For entrypoints outside the
MCP wrapper, an embedder needing the same snapshot guarantee must establish
the operation scope with `AnalysisSettings.run()`.
## Name a default, then a setting when operators need headroom
Put the number in a named constant with a unit. Do not leave it as a magic
number at the call site.
When a workload may need more room, add a setting whose unset value is that
constant. Pure numeric contracts live in `src/domain/analysisResourceLimits.ts`
or their existing domain owner. `analysisSettingDefinitions.ts` binds the
curated runtime IDs to those same bounds, defaults and application points.
Environment parsers and recovery text stay under `src/config/`; register launch
schemas on `environmentSchema` so startup and doctor agree. Add launch settings
to `SELECTED_REGISTRATION_SETTINGS` when setup should forward them. Unset keeps
today's behavior. An invalid launch value remains a configuration failure,
never a silent fallback; its validation boundary determines whether MCP can
start (see [invalid launch configuration](#invalid-launch-configuration)). An
invalid runtime write is `invalid_request` and changes nothing.
constant. One module under
[`src/config/`](https://github.com/morluto/rea/blob/main/src/config) owns the
default, any floor, the maximum, the schema, the parser, and the recovery
text. Register that schema on `environmentSchema` in
[`environment.ts`](https://github.com/morluto/rea/blob/main/src/config/environment.ts).
`parseConfig` calls `parseEnvironment`. Doctor uses that same parser through
`nativeAnalysisConfigurationError`, so startup and `rea doctor` accept the
same values. Add the name to `SELECTED_REGISTRATION_SETTINGS` so `rea setup`
copies it into the client registration. Unset keeps today's behavior. An
invalid value is a `ConfigurationError` and projects as `configuration_invalid`.
Do not fall back to the default.
For example, `regex.matching_deadline_ms` and its launch seed
`REA_REGEX_MATCHING_DEADLINE_MS` share a 5000 ms default, 1 ms floor and
2147483647 ms maximum. Node schedules a longer `setTimeout` delay as 1 ms.
Changing the runtime setting affects the next accepted search; changing the
launch environment affects the next REA process.
[`REA_REGEX_MATCHING_DEADLINE_MS`](https://github.com/morluto/rea/blob/main/src/config/regexMatchingDeadline.ts)
([#6267](https://github.com/morluto/rea/pull/6267)) is the reference. Default
5000 ms, floor 1 ms, maximum 2147483647 ms, because Node schedules a longer
`setTimeout` delay as 1 ms. Setup copies the setting. A hit says:
> The effective REA_REGEX_MATCHING_DEADLINE_MS value is 5000 ms (source default; default 5000 ms, floor 1 ms, maximum 2147483647 ms). Set REA_REGEX_MATCHING_DEADLINE_MS in the environment that starts REA to a positive decimal integer from 1 to 2147483647, then restart REA. The worker old-generation heap does not raise this deadline.
[`REA_MCP_MAX_RESPONSE_BYTES`](https://github.com/morluto/rea/blob/main/src/config/mcpResponseBudget.ts)
is the byte-budget shape: a decimal safe integer with a 10 MiB floor
@@ -125,8 +39,7 @@ raise REA's reserved string limit or Node's string ceiling. The setup
allowlist does not copy this setting. A new headroom setting should, following
the regex deadline.
A per-call budget can have a runtime default without having an environment
setting. JavaScript `max_heap_mb`
A per-call budget is not an environment setting. JavaScript `max_heap_mb`
(CLI `--max-heap-mb`) is an integer from 128 to 16384 MiB, default 1024. It
applies on the next analysis, which starts a new worker, and does not restart
REA. It does not raise the parent result budget, transfer ceilings, or fixed
@@ -138,122 +51,8 @@ does: its recovery says to set `NODE_OPTIONS` (for example
`--max-old-space-size=<MiB>`) on the process that starts REA, then restart
that process. `max_heap_mb` does not raise it.
## Runtime settings through MCP
`inspect_analysis_settings` lists the curated settings, their current and launch
values, source, bounds, application point and relevant operation names. Filters
by exact setting IDs, provider or operation intersect. Active worker values are explicitly `not_observed`; a configured default is
not a measurement of active work. Inspection also separates known server limits from unknown client deadlines and receive buffers.
`update_analysis_settings` accepts an atomic `changes` array. Each entry names
an exact `setting` and either `action: "set"` with a schema-validated `value`, or
`action: "reset"`. Reset restores the launch value. `expected_revision` is an
optional compare-and-set guard; conflicts and duplicate keys change nothing.
An unchanged write does not advance the revision.
The revision tracks override state, not only effective numeric values. Setting
an explicit override equal to the launch value still changes its source to
`runtime` and advances the revision. Reset removes that override, even if the
effective number stays the same. Validation, revision checks, and duplicate-key
checks all happen before publishing the replacement snapshot; a rejected batch
cannot partially apply. Clients coordinating writes can supply
`expected_revision`; without it, valid updates apply to the current state.
| Runtime setting | Application point |
| ------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `asar.max_header_bytes` | Next archive operation, including full JavaScript application analysis |
| `ghidra.startup_timeout_ms`, `hopper.startup_timeout_ms` | Next provider startup attempt |
| `ghidra.decompile_timeout_seconds` | Next operation on the existing Ghidra Program; `null` means unbounded |
| `jadx.operation_timeout_ms` | Next JADX operation |
| `regex.matching_deadline_ms`, `regex.worker_heap_request_mib` | Next search / new regex worker |
| `capture.decoder_old_generation_mib` | Next owned capture decoder |
| `javascript.analysis_timeout_ms`, `javascript.max_heap_mb` | Next source analysis / new JavaScript worker |
| `wasm.command_timeout_ms` | Next WABT operation |
### Precedence and lifetime
Where a setting supports an explicit per-call argument, precedence is:
1. The caller's explicit argument.
2. The accepted operation's runtime override.
3. Valid launch configuration, where that setting has an environment seed.
4. The built-in default when launch configuration is absent.
An invalid launch value does not fall through to the built-in default. Preserve
omitted caller arguments in Evidence: resolving an internal default must not
rewrite the request as though the caller supplied it. `null` is the explicit
unbounded value for `ghidra.decompile_timeout_seconds`; it is not a substitute
for an unknown or omitted value elsewhere.
A write does not extend active work, relaunch a provider, clear retained
cleanup, or invalidate successful Evidence.
Overrides belong to one REA runtime, survive target close/open, and disappear
when REA exits. Connections sharing that runtime share its overrides. No files,
ambient environment, registrations or upstream software are edited. Overrides
are runtime-wide, not scoped to a target or an individual connection.
### Failure and retry
An ASAR admission error supplies `details.limit` with the exact setting,
effective value, units, bounds, scope, application point, restart requirement
and, when known, required bytes and value source. The agent can immediately call:
```json
{
"changes": [
{ "setting": "asar.max_header_bytes", "action": "set", "value": 33554432 }
]
}
```
Then retry the same full-archive request once work and cleanup have settled.
No preliminary inspection or source subset is required. The setter does not
retry automatically. If an explicit per-call value caused the failure, correct
that argument; raising a runtime default cannot override it.
The resource owner constructs `details.limit` from the registry plus the budget
it actually enforced. Metadata includes `agent_editable: true`,
`scope: "rea-runtime"`, and `rea_restart_required: false`. Optional
`required_value`, `source`, and `parameter` fields are supplied only where known
or applicable. Do not infer a source because a value happens to equal a default.
Error copying, attribution, redaction, and delivery must preserve these facts.
Human-readable remediation explains the same failure; clients should not need
to parse that sentence to identify an editable setting.
Cache policy is part of enforcement. Successful reusable analysis can survive
a budget update. Transient Ghidra decompile failures remain uncached so a raised
deadline can recover on the same Program. Admission checks still apply to reused
data: lowering an ASAR limit must reject a cached oversized header. Neither
raising a limit nor clearing a transient analysis failure authorizes skipping
cleanup or changing the identity of the provider session.
### Invalid launch configuration
Launch validation has two relevant boundaries. Settings validated by global
configuration parsing can prevent REA from starting; an MCP setter cannot
repair a server that is not running. Correct the launch environment or
registration, use `rea doctor` for its configuration checks, and start REA again.
Provider-local validation can leave MCP available. For example, an invalid
`REA_JADX_OPERATION_TIMEOUT_MS` is inspectable with `source: "invalid"`, a
`configuration_error`, and omitted `value` and `launch_value`. A valid runtime
override repairs the effective JADX timeout without editing that launch seed.
Inspection then reports the runtime value while retaining the launch error.
Reset exposes the invalid launch state again; it does not repair or replace it
with the built-in default. Provider availability and execution must resolve the
same effective setting so readiness changes along with repair and reset.
## Keep a fixed ceiling honest
Inspection's separate
[constraints](https://github.com/morluto/rea/blob/main/src/server/analysisSettingsConstraints.ts)
describe operator-owned, external, and fixed controls. REA can report its
configured response budget and parent heap limit; client deadlines and receive
buffers remain explicitly unknown. These are not entries accepted by the
runtime setter. Raising a worker heap request cannot raise the parent heap,
and raising a provider deadline cannot make the client wait longer.
If the ceiling must stay fixed, say so in the failure. Name the limit and the
effective value, say this release has no runtime override, and name what does
not raise it. Do not invent a setting.
@@ -269,22 +68,20 @@ The string the caller sees includes:
- the limit or setting name
- the effective value and unit
- the source when known (`default`, `setting`, `runtime`, or `request`)
- the source (`default` or `setting`)
- the default, the maximum, and the floor when the setting has one
- the next action, including where to set it ("in the environment that starts REA")
- whether a restart is required
- what the change does not raise
Distinguish an invalid configuration from a valid budget exhausted by work.
Preserve the configuration failure and its setting identity; apply the
[launch validation boundary](#invalid-launch-configuration) when describing
whether live repair is possible.
Invalid settings never take this path. They are `configuration_invalid`.
Doctor reports `runtime-configuration:<setting>` and says to correct it in the
shell or the MCP registration, then rerun.
## What agents do
Use `details.limit` for a direct runtime update when it names an editable
setting. Follow `remediation.action` for per-call, fixed and external controls.
Inspect settings only when bounds or application details are missing. Restart only when it says the value is
Follow `remediation.action`. Change a request field or a `REA_*` setting only
when that text recommends the change. Restart only when it says the value is
read at startup. A sentence that a control does not raise the limit is not a
request to change it.
@@ -306,72 +103,22 @@ request to change it.
- `resource_constraint` for capacity, when `retryable` is true: wait for the
active admission to finish, then retry within the reported limits. Do not
restart REA to free the slot.
- `configuration_invalid`: when MCP remains available and the failure belongs
to a curated provider-local setting, inspect and set a valid runtime override.
Otherwise correct the named setting in the environment that starts REA, then
start REA again. Run `rea doctor` for the
- `configuration_invalid`: correct the named setting in the environment that
starts REA, then start REA again. Run `rea doctor` for the
`runtime-configuration:` check. Re-run `rea setup` for that client when the
setting is on the registration allowlist, then reconnect so the new process
sees it.
- `invalid_request`: correct the reported fields from the current schema or
CLI help. Do not repeat the unchanged call.
A restart is required to adopt a changed launch environment or registration.
The curated runtime settings above require no REA restart. It is not the first step for a transient
A restart is required for an environment setting and for a client still
running the previous registration. It is not the first step for a transient
MCP drop, a capacity wait, or retained cleanup.
## Adding a runtime setting
## Tests
1. Identify the enforcing owner and the real resource constraint. Decide whether
callers need a per-call argument, a runtime default, a launch-only control,
or a fixed limit. A registry entry must not advertise authority REA lacks.
2. Define the value type and named numeric bounds with the domain owner. Add the
ID and metadata to `analysisSettings.ts` and `analysisSettingDefinitions.ts`.
Reuse those bounds in runtime validation, launch parsing, and error metadata.
Choose the application point and provider/operation associations from actual
consumers; a shared control need not apply to every provider.
3. If there is a launch seed, add its parser and appropriate configuration/doctor
validation. Decide whether an invalid value blocks the runtime or only the
provider. Add it to setup's `SELECTED_REGISTRATION_SETTINGS` when it should
be forwarded; runtime editability does not automatically propagate it.
4. Thread the shared reader through every applicable composition path. Resolve
the effective value within the accepted operation, honor explicit arguments,
and preserve their omission in Evidence. Keep availability checks consistent
with execution. A worker heap request must not be described as a measured
process memory limit.
5. Enforce the value at its resource boundary and construct structured limit
facts there. Preserve those facts through shared error projection and copies.
Check cache reuse, lowered admission, transient failures, and retained cleanup
against the same ownership rules.
6. Update the public contracts and affected guides, then regenerate catalogs
and packaged skill metadata using the repository commands. Keep externally
owned and fixed controls in the separate inspection constraints, not in the
editable registry. Validate both store semantics and a public recovery path.
## Tests and verification
Cover the unset default, accepted overrides, rejected floor/maximum values,
and invalid syntax through the owning parser and configuration boundary.
Lock the unset default, one accepted override, rejected floor, maximum, and
syntax, and the recovery sentence through `parseEnvironment` and `parseConfig`.
[`regexMatchingDeadline.test.ts`](https://github.com/morluto/rea/blob/main/src/config/regexMatchingDeadline.test.ts)
is one example. Assert error codes, setting IDs, values, units, provenance,
application points, and resulting behavior. Check that adapters deliver the
canonical recovery guidance, with independent assertions for its underlying
facts. Avoid freezing prose or deriving all expected values from the same
registry used by the implementation; either can hide the wrong failure.
See [test design](../testing.md#test-design-and-pruning) for the assertion policy.
Runtime checks cover atomic validation, stale writes, reset, runtime isolation,
accepted-operation snapshots, and explicit argument precedence. Public MCP
acceptance must reject an oversized ASAR, raise admission, and successfully
retry the same complete archive on the same connection. Lowering admission must
also refuse previously cached headers. The real Ghidra timeout verifier checks
recovery after a live update without replacing its session. Also cover invalid
provider-local launch configuration, repair/reset availability, default and
custom composition paths, and preservation of structured limits through error
copies. Gates used to overlap work and updates need bounded acknowledgements
and release/join in `finally`.
The [verification report](../verification/runtime-analysis-settings.md) separates
public MCP checks, simulated provider coverage, real Ghidra execution, and
measured task costs. It does not establish real-provider behavior for every
editable setting or active worker telemetry.
is the pattern. Project the failure and assert `configuration_invalid` for a
bad value and the recovery wording for a hit.
+2 -7
View File
@@ -305,13 +305,8 @@ capture provides them.
ASAR observation reads the header through one open file handle and validates its
declared lengths against the archive size. Header/index admission is the same
budget used by static ASAR reads: 16 MiB / 16777216 bytes by default, seeded by
`REA_ASAR_MAX_HEADER_BYTES` (at most 256 MiB). In MCP,
`update_analysis_settings` can override `asar.max_header_bytes` for new
operations without restarting REA. Cached headers are checked against each
operation's admission, including after a decrease. See the
[settings design](design/limits-and-recovery.md#failure-and-retry).
An oversized header is
budget used by static ASAR reads: 16 MiB / 16777216 bytes by default, or
`REA_ASAR_MAX_HEADER_BYTES` (at most 256 MiB) when set. An oversized header is
not parsed; selecting a member path does not bypass admission, and the page
stays unauthorized. The header cache separately retains at most that many bytes
of already-admitted encoded headers across eight archives. Evicting that cache does not raise admission.
+2 -4
View File
@@ -24,8 +24,8 @@ requires an absolute filesystem path and preserves that exact selection:
The inline Evidence result contains the artifact path, SHA-256, and byte length.
`tables` lists each declared note program header or section, including the
section name when a string table is present. Zlib- and Zstandard-compressed
section-name tables are decompressed within the selected file-size budget. An unsupported
section name when a string table is present. Zlib-compressed section-name
tables are decompressed within the selected file-size budget. An unsupported
compression type leaves section names unknown and adds a limitation; note bytes
remain independently examined. Each `entries[]` item reports owner, type,
descriptor SHA-256, and a hex preview for descriptors of 64 bytes or less.
@@ -63,8 +63,6 @@ numbering (`PN_XNUM`, including a section count stored when `e_shnum` is 0) is
unsupported and returns an unsupported-target outcome rather than invalid-input
recovery.
Zstandard section-name tables also use a 128 MiB decoder window limit.
`SHF_COMPRESSED` note sections return `unsupported_target` before their payload
is interpreted or decompressed. Their compression header is storage metadata,
not a note header. The current result requires physical file offsets for each
+1 -8
View File
@@ -187,7 +187,7 @@ profile stages before retrying their retained descriptors.
| Concern | Ghidra contract |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline unless the launch setting `REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS` or runtime override `ghidra.decompile_timeout_seconds` sets one; a decompile that reaches it fails only that request with `remote_code` `decompile_timed_out`, the Program and session stay open, and the next decompile uses a fresh decompiler process. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline unless `REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS` sets one; a decompile that reaches it fails only that request with `remote_code` `decompile_timed_out`, the Program and session stay open, and the next decompile uses a fresh decompiler process. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
| Serialization | A FIFO sends one Program request at a time. Caller cancellation removes queued work promptly and is passed to active socket waits. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
| Assembly and pseudocode | Assembly is complete Ghidra Listing text; pseudocode is Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
@@ -197,13 +197,6 @@ profile stages before retrying their retained descriptors.
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. The dossier is an intra-function graph; `trace_native_values` composes derived parameter/argument and return/output dependencies across resolved calls. It does not resolve memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
Runtime updates apply to newly accepted operations on the existing Program.
They preserve accepted budgets and do not clear cleanup blockers or change the
client deadline. Transient decompile timeout failures remain uncached so the
same function can be retried after raising the limit. See the
[settings design](design/limits-and-recovery.md) for snapshots, precedence,
and provider integration.
Function extents and instruction completeness refer to Ghidra's analyzed body.
An inferred no-return call can terminate that body before subsequent bytes.
Pseudocode, assembly, procedure info, instruction windows, incoming and outgoing
-3
View File
@@ -7,9 +7,6 @@ releases have not been verified. Format 1 is unsupported.
Inspection retains the original header and directory facts, including engine
version, flags, declared file count, source paths, offsets and declared MD5.
For a member declared as `res://scenes/main.gd`, the occurrence's
`source_path` retains that exact declaration while `logical_path` is
`scenes/main.gd`, the relative path used for lookup and extraction.
Observed plaintext bytes receive content-addressed SHA-256 identities. MD5 is a
container integrity claim, separate from the observed SHA-256. A mismatch fails
by default; `--integrity-policy record-and-continue` retains the observed bytes
+4 -29
View File
@@ -241,7 +241,7 @@ record addresses remain distinct candidates, and conflicting file mappings
remain malformed.
Module records establish names through their declared NUL-terminated string pointers. NUL/0xFF padding, spaces, Unicode and suffix-shared storage are supported. A pointer to a longer string does not match just its suffix; a pointer directly into a shared suffix may name it. Exact reversed UTF-8 matching avoids rescanning the file for every name and retains duplicate-name owners until module records resolve them. A contributor investigated a Linux
`GameAssembly.so` from Unity 2023.2.13f1 whose static registration was not located. When no static pair is resolved on x86-64 or arm64, the reader searches executable ranges for bytes consistent with a reference to a module-array candidate: RIP-relative LEA/MOV on x86-64, an adjacent `adrp`/`add` pair on arm64. Exactly one referenced array candidate must remain. `code_gen_modules_field` is then `null`, and Evidence is **inferred**: instruction boundaries, the absence of a static registration elsewhere, and runtime use were not verified. The module records and slots themselves remain file observations. Static-table associations are **derived**. Otherwise the result is `code-registration-not-found`. Cpp2IL's StrippedCodeRegSupport plugin also recovers the module list of an inlined CodeRegistration from the module names; this fallback additionally requires the exact located module set and a single code reference. A Unity 2022.3.62f3 macOS Master build checked by a contributor keeps neither registration symbol; its type table is then inferred as described below.
`GameAssembly.so` from Unity 2023.2.13f1 whose static registration was not located. When no static pair is resolved on x86-64, the reader searches executable ranges for bytes consistent with RIP-relative LEA/MOV references to module-array candidates. Exactly one referenced array candidate must remain. `code_gen_modules_field` is then `null`, and Evidence is **inferred**: instruction boundaries, the absence of a static registration elsewhere, and runtime use were not verified. The module records and slots themselves remain file observations. Static-table associations are **derived**. Otherwise the result is `code-registration-not-found`.
The binary/metadata pair is caller-selected. Name/count/token agreement is structural evidence, not a cryptographic same-build proof. File-offset locations refer to the primary metadata artifact; linked virtual addresses refer to `binary.artifact`. Both SHA-256 identities remain available in compact native pages as `artifact` and `binary_source`.
@@ -299,30 +299,6 @@ The result adds:
`Il2CppMetadataRegistration.typesCount` and its value. That record is found as
the single place whose `fieldOffsetsCount` and `typeDefinitionsSizesCount` both
equal the metadata type-definition count and whose type table is file mapped.
Linking reads only that table. When no record on x86-64 or arm64 has both of
those counts, the reader collects maximal runs of pointers to Il2CppType-shaped
records (an assigned Il2CppTypeEnum kind from 0x01 to 0x1e; class and
value-type data below the type-definition count) that are long enough for every
type index the metadata references. Exactly one such run may exist in the
image, it must start and end inside its mapping (a run touching either end may
continue in a neighbouring mapping), and its start must be loaded by the same
instruction forms as the module-array fallback. `types_field` is then `null`,
`types_count` is that run's length rather than an observed `typesCount`, and
Evidence is **inferred**. The run length is at least every referenced type
index but can differ from `typesCount`: a type-shaped pointer after the table
would lengthen it, and an unreadable slot (such as an unresolved relocation)
would end it early. Only the type indices the metadata references are read. A
second long-enough run anywhere, a run that a type-shaped neighbour extends
before the loaded address, a run touching a mapping end or a slot the loader
binds or leaves unresolved (its stored word is only a placeholder), and a covered
128-byte record whose two counts match but whose other fields are unusable all
leave `metadata-registration-not-found` rather than a guess; the search stops
at the second run. The length requirement only separates the type table from
other pointer runs when the metadata references most of it: on the one Master
build checked, the highest referenced index is 6,031 of 6,576 entries. A
metadata referencing few type indices usually
leaves several short runs and fails closed.
Counts use their declared 32-bit width independently of alignment padding.
The complete 128-byte registration must have unambiguous loader coverage. Its
used fields through byte 112 must be file-backed; a trailing declared zero-fill
@@ -378,8 +354,7 @@ the result has no rows, and `generic_method_pointers_field`,
`generic_method_pointers_count` and `generic_method_table_stride` are `null`.
A table of a few rows can still leave two layouts agreeing, which fails as
ambiguous rather than picking one. Metadata versions 24 and 39 and builds
without a static CodeRegistration (`code_gen_modules_field` `null`) or
MetadataRegistration (`types_field` `null`) fail as
without a static CodeRegistration (`code_gen_modules_field` `null`) fail as
`generic-methods-unsupported`: for version 24 the type-definition generic
container offset and the generic class layout have not been checked against a
real build, so it is refused rather than guessed.
@@ -444,10 +419,10 @@ The builds checked are listed after the module search rules above.
| `code-registration-not-found` | input error | No unique module table or executable-byte array candidate was resolved. |
| `invalid-method-token` | input error | A token is outside its module's method-pointer rows, or a MethodDef row is reused within one image. |
| `invalid-method-pointer` | input error | A slot is unavailable or its nonzero address lacks unambiguous executable file coverage. |
| `metadata-registration-not-found` | input error | No single MetadataRegistration or code-loaded type-table candidate was resolved. |
| `metadata-registration-not-found` | input error | No single MetadataRegistration matches the type-definition count. |
| `invalid-type` | input error | A type index or `Il2CppType` record is out of range, unmapped or malformed. |
| `unsupported-type-kind` | unsupported | A valid type kind has no implemented rendering. |
| `generic-methods-unsupported` | unsupported | Generic instances were requested for metadata version 24 or 39, or a build lacks either static registration. |
| `generic-methods-unsupported` | unsupported | Generic instances were requested for metadata version 24 or 39 or a build without a static CodeRegistration. |
| `generic-method-registration-not-found` | input error | No CodeRegistration layout bounds every genericMethodTable row, or the generic tables are unreadable. |
| `ambiguous-generic-method-registration` | input error | More than one CodeRegistration layout bounds every genericMethodTable row. |
+11 -41
View File
@@ -616,12 +616,9 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
| WorkBuddy | `workbuddy` |
| ZCode | `zcode` |
| DeepSeek Harness | `dsh` |
| FreeBuff | `freebuff` |
`droid`, `dsh`, `kimi`, `qoder`, `trae`, `workbuddy`, and `zcode` require
`rea-agents@6.5.0` or later. The 6.4.0 registry check above does not cover them.
`freebuff` is not in a release yet - `6.7.0` is the latest - so it also needs a
release containing it.
See [client configuration and troubleshooting](client-configuration.md) for
configuration paths, profiles, trust policies, and schema-limit workarounds.
@@ -684,12 +681,6 @@ search permissions (`0700`) on a directory you own before retrying.
On supported Linux distributions, approved setup verifies Hopper's official
`.deb`, `.rpm`, or Arch package before invoking the native package manager.
On apt systems, Hopper's runtime dependencies are installed with
`--no-upgrade` in their own transaction, so setup no longer names an
already-installed dependency as an upgrade target. apt can still upgrade a
package that the Hopper package or the Python library repair requires at a
newer version. Arch's `pacman -S --needed` still updates an outdated
dependency.
The official release feed selects the current public demo without a version
pin. REA verifies the package's detached PGP signature against Hopper's stable
primary fingerprint `7C2D73BAFAF5B9A710A69DA1A26DF815099B1BCD`, then checks
@@ -812,10 +803,7 @@ Hopper startup allows 120,000 ms for launch, initial analysis, and authenticated
bridge readiness. For large targets, set `REA_HOPPER_STARTUP_TIMEOUT_MS` to a
decimal integer from 1 to 2,147,483,647 milliseconds in the environment that
starts REA. CLI and MCP use the same setting; invalid values fail configuration
validation. In MCP, `update_analysis_settings` can set
`hopper.startup_timeout_ms` for the next startup attempt without restarting REA;
accepted work keeps its budget and retained cleanup must still complete. An
MCP client's own request timeout must also allow enough time.
validation. An MCP client's own request timeout must also allow enough time.
`open_binary` validates and selects a target without waiting for native provider
readiness. During Hopper startup, `binary_session.provider_operation_health`
reports `busy` at stage `launch`; `analysis_activity` describes bridge requests
@@ -1185,25 +1173,19 @@ can time out earlier even when Ghidra would complete within its startup deadline
See [Ghidra first-query deadlines and recovery](mcp-contracts.md#ghidra-first-query-deadlines-and-recovery)
for client options and the close/reopen recovery flow.
The 330-second default can be changed on a running MCP server by setting
`ghidra.startup_timeout_ms` through `update_analysis_settings`. The next accepted
operation that starts a provider uses that value; an accepted startup keeps its
original budget. Changing an environment variable in another shell, or closing
and reopening a binary, does not change the launch configuration already read
by that server.
The 330-second default is configurable. Configuration is read when REA starts:
changing an environment variable in another shell, or closing and reopening a
binary in the same MCP server, does not change that server's startup allowance.
For a CLI retry with an example 30-minute allowance:
```sh
REA_GHIDRA_STARTUP_TIMEOUT_MS=1800000 rea analyze /absolute/path/to/app.exe --provider ghidra --json
```
For MCP, update the runtime setting and resolve any retained cleanup before the
next startup attempt. Follow the error's close/reopen guidance where needed;
the settings update does not clear cleanup blockers. To change the launch
environment instead, export useful Evidence, close the binary and resolve any
cleanup failure, update the client's REA server launch configuration, then
restart REA and reconnect. Allow startup plus the query in the client's own
tool-call deadline; runtime updates do not change that deadline. Each new startup attempt uses
For MCP, export useful Evidence before restarting, close the binary and resolve
any cleanup failure, update the environment in the client's REA server launch
configuration, then restart REA and reconnect before reopening the target. Allow
startup plus the query in the client's own tool-call deadline. Every retry uses
a fresh ephemeral import; smaller result requests do not reduce the prerequisite
auto-analysis. A larger time allowance does not raise the JVM heap limit or
guarantee completion. Keep [heap and host CPU controls](#ghidra-heap-and-cpu-controls)
@@ -1230,14 +1212,9 @@ fails with `provider_timeout` and that message; other Ghidra tools stay
available. If the caller also cancels while the timeout settles, the session
still closes and the message says so. Session reuse concerns an active MCP or
library session; a standalone CLI command still closes its own session when it
finishes. In MCP, `ghidra.decompile_timeout_seconds` can override the launch
value for newly accepted operations on the existing Program; explicit `null`
removes the deadline. A transient timeout remains uncached so the same function
can be retried after an update. See the
[settings design](design/limits-and-recovery.md#runtime-settings-through-mcp).
Codex/Kimi registrations derived by `rea setup` add the selected launch value
to the tool deadline as a single-decompile estimate; a runtime update does not
change that registration. A bulk call can need that limit for
finishes. The limit is read when a Ghidra session
starts, and Codex/Kimi registrations derived by `rea setup` add it to the tool
deadline as a single-decompile estimate. A bulk call can need that limit for
every attempted function plus listing and file work; select the client deadline
for the workload, split batches, use `decompile_all` limit/resume, or reduce
`trace_native_values` `max_functions` to select fewer serial decompiles. Ghidra's own interactive default is 30 seconds. Unresolved computed calls
@@ -1485,13 +1462,6 @@ setup runs through `bunx`, and pins it to the exact version that performed
setup. Run current setup to refresh an older registration, then restart the
client.
Package runners key their package cache by the requested spec, so the entry
installed for the `@latest` invocation that ran setup cannot serve the pinned
registration. Setup therefore materializes the pinned entry once, best-effort
with a bounded timeout, before finishing. When that step cannot run, for
example offline, the first client start performs the one-time install inside
the client's MCP startup deadline and may need one more restart.
## Package runners and global installation
A package-runner invocation can use a REA version installed in the current project. To
+8 -40
View File
@@ -140,24 +140,17 @@ returns an unavailable error with the retained inventory, rather than claiming
the selected source does not exist. Observed exact file candidates keep their
identity; unknown traversal cannot establish a unique application root.
ASAR header/index admission defaults to 16 MiB / 16777216 bytes. In MCP, use
`update_analysis_settings` to set `asar.max_header_bytes` to an integer byte
count from 1 to 268435456 (256 MiB), then retry the same full-archive request.
The update applies to new operations without restarting REA; cached headers
must also satisfy the new admission. For CLI or future MCP processes, set
`REA_ASAR_MAX_HEADER_BYTES` in the launch environment. Changing that environment
requires a new REA process. Invalid launch values are configuration errors,
never a silent fallback. See the
[settings design](design/limits-and-recovery.md#failure-and-retry) for recovery
metadata, precedence, and reset behavior.
The archive index is read before any member can be selected, so
ASAR header/index admission defaults to 16 MiB / 16777216 bytes. Set
`REA_ASAR_MAX_HEADER_BYTES` in the environment that starts REA to a decimal
byte count of at most 268435456 (256 MiB) to raise or lower it; restart REA
after changing it. Invalid values are configuration errors, never a silent
fallback. The archive index is read before any member can be selected, so
`source_paths`, `--source-path`, `detail: "summary"`, `max_heap_mb`, and
available host memory do not bypass or raise it. When the header exceeds the
effective allowance, the failure reports the value, its known source, and the
setting's limits. Alternatively, analyze an extracted
effective allowance, the failure names it, whether it came from the default or
the setting, and the setting's limits. Alternatively, analyze an extracted
application directory, which does not admit an ASAR header. Repeating the unchanged
request without changing admission fails the same way.
request fails the same way.
## Read the analysis
@@ -202,27 +195,6 @@ For supplied passive browser or Electron observations, use
[runtime reconciliation](javascript-runtime-reconciliation.md). It compares
existing Evidence without rerunning static analysis or contacting CDP.
Each newly analyzed JavaScript file retains `analysis_coverage` in its graph
observation. `syntax` distinguishes complete parsing, recovered syntax errors,
failed parsing and unexamined source. `semantics` records the number of observed
unknown findings and typed uncertainty, including returned-reference widening
and unavailable type-only compiler options. `examination` reports static semantic
resource limits and Electron wrapper expansion limits independently. A completed
pass does not establish complete runtime semantics. Route examination and result
capacity remain in the separate route inventory.
Electron applicability is `observed` when a static Electron module reference is
retained, and otherwise `unknown`: a source without such a reference does not
prove that the application is inapplicable. Sources excluded by `source_paths`
retain `not-examined` with a reason and null semantic counts. Historical Evidence
without this facet remains unmeasured. The existing `parse_status` remains its
conservative overall summary; finer facts do not relabel earlier partial results.
Summary delivery counts these facts under `summary.coverage.static_sources`.
Read exact per-file measurements with `inspect_analysis_view`, selecting
`kind: "javascript-facts"` and `property_keys: ["analysis_coverage"]`, or inspect
the module item. Both views use retained Evidence and preserve source provenance.
## ASAR integrity
ASAR inventory checks Electron integrity metadata for embedded archive entries
@@ -744,10 +716,6 @@ deadline per source, including startup and fact transfer. Select
`max_heap_mb` and `analysis_timeout_ms` through MCP, or `--max-heap-mb` and
`--analysis-timeout-ms` on the dedicated CLI command. These choices control the
analyzer; the caller's `NODE_OPTIONS` still controls the CLI/MCP process heap.
For defaults across new MCP operations, use `update_analysis_settings` with
`javascript.max_heap_mb` or `javascript.analysis_timeout_ms`. Explicit per-call
arguments take precedence, and accepted work keeps its original settings. See
the [settings design](design/limits-and-recovery.md#precedence-and-lifetime).
V8 old-space limits do not impose a process RSS limit. When REA runs on Bun,
the analyzer ignores the V8 heap flags; resource failures then report
`worker_heap_limit_bytes` as null instead of a requested or V8-reported limit.
+5 -47
View File
@@ -932,25 +932,6 @@ of a later null value. Inspect the slot's `xrefs` and the writer's assembly or
pseudocode, retaining uncertainty about execution order and runtime values.
These operations inspect static evidence and do not execute the target.
## Editable analysis settings
Agents can call `update_analysis_settings` to raise a curated admission,
timeout or worker heap default on a connected server, then retry the same
request. Errors carry `details.limit` with the setting and bounds when the
failing owner supports runtime recovery; `inspect_analysis_settings` is optional
when those facts are supplied. Updates are atomic, apply to new work, survive
target close/open, and vanish when REA exits. Explicit per-call arguments take
precedence. Neither tool changes host deadlines or resolves retained cleanup.
See [runtime settings and recovery](design/limits-and-recovery.md#runtime-settings-through-mcp)
for the full setting list, reset/revision semantics and ASAR retry example.
For implementation ownership, accepted-operation snapshots, provider wiring,
and extension guidance, see the
[maintainer design](design/limits-and-recovery.md#ownership-and-data-flow).
Ghidra and Hopper startup updates apply to the next startup attempt without
restarting REA. Ghidra decompile timeout updates apply on the existing Program.
An operation already accepted retains its original budgets.
## Ghidra first-query deadlines and recovery
A successful MCP initialize handshake establishes the REA connection.
@@ -1052,13 +1033,11 @@ the current client, so it is not a fresh-start recovery. On the same connection:
`provider_id: "ghidra"`, then retry the first query with an appropriate client
deadline. The connection and selected provider do not need to change.
If REA's provider startup allowance also needs raising, set
`ghidra.startup_timeout_ms` through `update_analysis_settings` before the next
startup attempt. This needs no REA restart and does not change accepted work,
clear cleanup blockers, or extend the client's deadline. Alternatively, change
`REA_GHIDRA_STARTUP_TIMEOUT_MS` in REA's launch environment and restart the
server, preserving Evidence and closing as above. Close/reopen on the same
connection cannot apply a changed launch environment. See the
If REA's provider startup allowance also needs raising, preserve Evidence and
close as above, then update `REA_GHIDRA_STARTUP_TIMEOUT_MS` in REA's launch
environment and restart the server. Reconnect the MCP client before reopening
the target. Configuration is read at process startup; close/reopen on the same
connection cannot apply a changed environment. See the
[CLI retry example](installation.md#ghidra).
This close/reopen flow was exercised on one real Linux stdio connection after a
@@ -1253,22 +1232,6 @@ Kimi-K3 backend, not the WorkBuddy MCP client. Set the profile on REA's
registration in the actual MCP host; the host's name alone does not identify
the downstream model or its schema constraints.
The compact profile also protects strict Anthropic-compatible gateways from
known schema-translation failures reported in [#3800](https://github.com/morluto/rea/issues/3800):
primitive `enum` properties without a sibling `type`, and `oneOf`/`allOf`
inside a property listed in `required`. Affected schemas use the same reduced
first-level projection described below, so the gateway receives a plain object
shape while REA continues to validate the complete canonical contract locally.
This is a compatibility projection, not a claim that the gateway reproduces
the full union semantics.
For array items expressed as an untyped `oneOf`/`anyOf`, the compact profile
adds a type when every branch has the same type. Mixed-type item unions use the
reduced projection; selecting one branch's type would exclude valid calls.
This addresses the emitted shape reported through a Gemini-backed gateway in
[#5201](https://github.com/morluto/rea/issues/5201). Gateway conversion and
live provider acceptance have not been verified for this projection.
The compact profile renders the same tool set, output schemas, annotations,
and canonical server-side validation. It changes only the advertised input
JSON Schema:
@@ -1300,11 +1263,6 @@ Boundary tests pin every advertised compact schema to the budget, keep them
valid JSON Schema that still accepts every canonical example. Client verification
also checks provider-facing sizes and canonical examples after conversion by
the actual Kimi Code CLI; local captures do not establish Moonshot acceptance.
The SDK catalog test checks that full-profile schemas retain nested ADB extra,
managed selection, and WASM timeout and detail restrictions. Path-contract tests
check full-profile local-path rules. Compact MCP workflow tests verify canonical
rejection of invalid inputs before provider work, independent of how much detail
the selected advertisement retains.
### Output schemas
+7 -12
View File
@@ -284,22 +284,17 @@ stops matching while leaving the Hopper API available. The deadline is
2147483647 ms. Unset uses 5000 ms. A larger value is refused because Node
schedules a delay past that signed 32-bit bound as 1 ms. Invalid values are
configuration errors and do not fall back to the default. The setting is read
from the environment that starts REA; changing that environment needs a restart.
In MCP, `regex.matching_deadline_ms` overrides the deadline for newly accepted
searches without restarting REA. A deadline failure names the effective
milliseconds, known source, default, and maximum. The worker old-generation heap
does not raise it. IDA and Kuna use these same regex controls; Ghidra's Java
matcher does not. The worker
from the environment that starts REA, so a change needs a restart. A deadline
failure names the effective milliseconds, whether they came from the default
or the setting, the default, and the maximum. The worker old-generation heap
does not raise it. Kuna regex search uses the same deadline. The worker
heap request is `REA_REGEX_WORKER_HEAP_REQUEST_MIB`, a decimal integer from
7 to 16384 MiB. Unset uses 128 MiB. A request below 7 MiB aborts the worker
during startup, and a larger value is refused. Invalid values are
configuration errors and do not fall back to 128. The setting is read from
the environment that starts REA, while MCP `regex.worker_heap_request_mib`
can override it for workers belonging to newly accepted searches. Use
`update_analysis_settings` for either runtime control; accepted work keeps its
budget. See the [settings design](design/limits-and-recovery.md).
A heap failure names the effective request, known source, default, floor, and
maximum. A positive Node.js
the environment that starts REA, so a change needs a restart. A heap failure
names the effective request, whether it came from the default or the setting,
the default, the floor, and the maximum. A positive Node.js
`--max-old-space-size` on the process that starts
REA, including `NODE_OPTIONS`, overrides that request and sets the effective
heap. `--max-old-space-size-percentage` takes precedence over that flag in
+8 -18
View File
@@ -17,7 +17,7 @@ Summary delivery retains the complete Evidence: use `parent_evidence_id` with
Extraction returns a fresh `output_root`; analyze that extracted directory with
`analyze_javascript_application`. Snapshot-only applications have no recovered
main script and receive no JavaScript next probe. Neither does a main script
whose logical path lacks a JavaScript source suffix, such as `main`, because the
whose code path lacks a JavaScript source suffix, such as `main`, because the
analyzer selects extracted files by suffix.
## Recognition and evidence
@@ -35,18 +35,11 @@ inventoried once. Native carrier bytes keep their content hash and native
classification while the root occurrence represents the SEA container.
Root `format_metadata` preserves raw flags, decoded flags, code path, blob
location, exec arguments and fuse observations. Main and asset children retain
their role and exact declared name in `source_path`. Safe, noncolliding names
become logical paths after the shared NFC path normalization. Names that are
unsafe as relative paths, have an exact or file-prefix collision with another
entry, or occupy the reserved `__node_sea__`
namespace receive deterministic logical paths: `__node_sea__/main.cjs` for the
main CommonJS source and `__node_sea__/assets/<ordinal>` for assets, using their
zero-based order in the blob. The original declared name remains
in `source_path`, including an empty asset key. Snapshots and code caches use
`__node_sea__/snapshot` and `__node_sea__/code-cache`. Extraction materializes
the logical paths through the shared path-containment and collision rules and
never overwrites existing files.
location, exec arguments and fuse observations. Children retain their role and
original `source_path`. The main script uses its declared code path; snapshots
and code caches use `__node_sea__/snapshot` and `__node_sea__/code-cache`.
Assets retain their names. Extraction applies the shared path-containment and
collision rules, rejects unsafe names and never overwrites existing files.
The fuse scan independently records `set`, `unset`, `absent`, or `conflicting`.
An unset fuse with a present blob is reported as a limitation. These are static
@@ -63,7 +56,7 @@ including the exec-argument extension byte. Older layouts without that byte,
Carrier byte order is independent of blob byte order.
Malformed magic, truncation, unknown flags/extensions, invalid UTF-8, duplicate
raw asset keys and conflicting blob declarations fail closed with named reasons.
asset names and conflicting blob declarations fail closed with named reasons.
Zero padding after a complete blob is allowed. The blob read ceiling is 256 MiB;
PE carriers also use the shared 512 MiB input, 65,536 directory-entry and 8 MiB
resource-metadata ceilings. The decoder admits at most 100,000 assets whose
@@ -81,10 +74,7 @@ code-cache bytes remain opaque.
Synthetic fixtures cover Mach-O, ELF and PE inventory/extraction, malformed
blobs, collisions and fuse contradictions. CLI and MCP tests exercise the same
artifact provider and advertised schemas. A CLI regression also generates a blob
with the pinned local Node version, an absolute main path and an empty asset
key, then inventories it inside a synthetic ELF carrier. This verifies the real
blob producer, not a real injected executable.
artifact provider and advertised schemas.
The optional real ELF lane uses Docker's official Node v24.10.0 image and pinned
postject to build a fixture, then verifies inventory and extracted script/asset
+2 -6
View File
@@ -25,12 +25,8 @@ metadata retains the original name, table index, type, compression flag,
relative offset and known directory record extent. Logical paths follow the
carrier's path dialect: backslashes separate components for PE carriers, while
they remain literal filename characters for POSIX carriers. The source name
always retains its stored spelling in occurrence `source_path` and
`format_metadata.source_name`. For PYZ rows, that source is the exact module or
data key in the PYZ table, not a reconstructed filesystem name;
`format_metadata.container_name` identifies the containing PYZ image. If the
carrier dialect is unconfirmed, REA preserves backslashes verbatim instead of
inferring POSIX semantics.
always retains its stored spelling. If the carrier dialect is unconfirmed, REA
preserves backslashes verbatim instead of inferring POSIX semantics.
`relative_offset` is relative to the CArchive start for CArchive rows, or to
the decoded PYZ image for PYZ rows. A CArchive directory record starts at
`archive_start + toc_offset + directory_record_offset`; its size is
+13 -3
View File
@@ -325,7 +325,9 @@ REA の利用、バグ報告、機能の要望、ビルドのテスト、修正
REA は、法令に従った研究、教育、相互運用性の確保、ソフトウェアの保存、および所有するソフトウェアや調査を許可されたソフトウェアの解析を目的としています。
違法または無許可の利用に関するプロジェクトの立場は、REA のすべてのバージョン、配布チャネル、およびプラットフォームに適用されます。各コピーには、それぞれに適用されるライセンスが引き続き適用されます。
The project's position on illegal or unauthorized use covers all REA
versions, distribution channels, and platforms; each copy retains its
applicable licenses.
## 貢献する
@@ -337,6 +339,14 @@ REA への貢献を歓迎します![issue を作成](https://github.com/morlut
[MIT](../../LICENSE)
REA が作成したコードは MIT ライセンスの下で提供されます。サードパーティの素材には、それぞれのライセンスと著作権表示が引き続き適用されます。依存関係の一覧、保存されたライセンス本文、および外部ツールの利用条件については、[サードパーティ通知](../../THIRD_PARTY_NOTICES.md)を参照してください。
REA-authored code is licensed under MIT. Third-party material retains its own
licenses and copyright notices; see [Third-party notices](../../THIRD_PARTY_NOTICES.md)
for the dependency inventory, preserved license texts, and external-tool terms.
**IDA Pro / Hex-Rays デコンパイラ、Hopper、JEB を含む商用ツールは、適用されるライセンスに従って合法的に入手し、使用する権限を得ている必要があります。** REA はツールのライセンス、キー、またはアクティベーションの権利を一切付与しません。試用版やデモ版も、ユーザー、機能、自動化、および出力の共有に関する制限を含め、ベンダーの利用条件に従います。クラックされたコピー、盗まれたキー、無許可のキー共有、およびライセンス制限の回避は認められていません。このガイダンスは、MIT ライセンスやサードパーティのライセンスを変更するものではありません。
**You must legally obtain and be authorized to use commercial tools**, including
IDA Pro / Hex-Rays decompilers, Hopper, and JEB, under their applicable licenses.
REA grants no tool license, key, or activation entitlement. Trials and demos
remain subject to vendor terms, including user, feature, automation, and output
sharing restrictions. Cracked copies, stolen keys, unauthorized key sharing,
and bypassing license restrictions are not authorized. This guidance does not
amend MIT or third-party licenses.
+1 -2
View File
@@ -83,8 +83,7 @@ empty dependency set. Retained views describe the captured artifact digest;
they do not assert that the current file or running application is unchanged.
ELF section names are resolved through the section-header string table.
Zlib- and Zstandard-compressed name tables are decoded within the native structural
budget. Zstandard decoding also uses a 128 MiB window limit.
Zlib-compressed name tables are decoded within the native structural budget.
If a name table uses unsupported compression, the result reports that
limitation and leaves `.dep-v0` presence unknown rather than reporting the
section absent.
@@ -1,12 +0,0 @@
# Implementation plan
1. **Defaults.** `WriteBody.defaults` and its schema; a create's echo of a member a call leaves out takes its default.
2. **Merging.** Explain a later create on its own and merge it: agreeing rules, references one call shows alone, joined
bodies and defaults, the plan's rule order.
3. **Sorted echoes.** Learn `sorted` for creates and updates, store sorted strings when serving, and agree plain and
sorted echoes in update and create plans.
4. **References.** A references module that learns and resolves them; created and updated fields resolve them against
the replayed or live state; the members they read are accepted.
5. **Nested view keys.** The gate's view spelling learner, moved to its own module, notes object keys; rendering
applies them.
6. **Schema, tests and docs.**
@@ -1,48 +0,0 @@
# Learn creates from every call, sorted arrays and referenced entities
Issue: https://github.com/morluto/rea/issues/4933
## Outcome
After the derived-values change, the creates prompt-built environments still withheld failed for four reasons the
write model did not cover:
- A create plan was learned from the tool's first call alone, so a later call that sent a member the first did not
(Linkding's `shared`), or left one out, was refused even when it agreed with the plan everywhere else.
- Linkding stores `tag_names` sorted, so a call that sent its tags in another order did not echo them, and its create
was withheld or contradicted.
- Homebox's item create answers its parent and type as whole objects (`parent`, `location`, `entityType`) beside the
`parentId` and `entityTypeId` it sent. Kept as constants, they pinned those members to their recorded values.
- Lists show some objects with fewer keys than an entity holds: Homebox lists an item's parent without the parent's
own type, and Keycloak lists a user's `access` with one key. A created entity's object rendered whole, so the list
check failed.
## Behaviour
- **Every call.** A later create is explained on its own and merged into the plan when both calls show the same fields,
but for a reference whose member a call without it did not send (one an earlier create added stays), every field
both rule they rule alike, and a field neither rules holds one value. Members either call sent are accepted, only
those every call sent are required, and an echoed member a call left out takes the value that call stored
(`body.defaults`). A clock widens to `trimmed` only when no held entity or earlier call showed it at a fixed width. A
later create that disagrees leaves the plan as it was and is judged as before; one that left out a member the plan
fills while showing that field otherwise is `unexplained-change`. Merged rules keep the plan's order.
- **Sorted arrays.** An echo is `sorted` when the application stored the sent strings in code-point order rather than
as sent; serving stores them sorted. A plain echo and a sorted one agree, and the plan keeps the sorted one, since a
call whose array was already in order cannot show it; once a call stored strings out of order as sent, they disagree.
- **References.** A created or updated object field that holds fields of the entity a sent member names, as the state
holds it, is a `reference` (member, type, fields). Several members naming such entities resolve to the one named like
the field. The member is accepted with any value. Replay and serving look the entity up in the state they hold;
serving refuses a call naming one the environment does not hold or holds in more than one collection, and the field
is absent, from the entity and the create's answer, when the call does not send the member. An update that sends the
member naming the entity the field already repeats keeps a reference an earlier call showed.
- **Nested view keys.** A view that always shows a field's objects with the same keys, in any order, and fewer than
an entity stores, names them in `nested_fields` in the first recording's order, and a stored object it never
recorded renders with those keys.
Plans and views that need none of this are unchanged.
## Acceptance
- Linkding's prompt-recorded bookmark create whose read-back lists tags sorted is served, and Homebox's item create is
explained without pinning its parent or type.
- No write tool of the 69 recorded captures goes from served to withheld.
@@ -1,10 +0,0 @@
# Tasks
- [x] Merge later create calls into the plan, with defaults for members a call leaves out.
- [x] Learn and serve sorted echoes.
- [x] Learn, replay and serve references to held entities.
- [x] Learn the keys views show of nested objects and render written objects with them.
- [x] Add view, serving and unit tests.
- [x] Document the rules and view member.
- [x] Measure the recorded captures and served write tasks.
- [x] Run static, formatting, documentation and test gates.
@@ -1,61 +0,0 @@
# Verification
Executed on macOS with Node v24.22.1 at `043053d1` (main `79845063`, the derived-values branch `8ce1127b` and this
branch). Recorded captures and served write tasks were measured with a local, unpushed build of this branch merged with
the scenario-timing branch (#6967), as the derived-values branch was; its baseline is the derived-values branch with the
same merge.
## Gates
- `npm run -s typecheck`, `lint`, `format:check`, `docs:check`, `mcp-catalog:check`, `verify:test-discovery`,
`scan:todos` and `npx knip`: passed.
- `npx vitest run --project domain --project services --project adapters`: 17,258 passed, 25 skipped, 1 failed ("stops
Inspector target authorization after the current stat settles"), which fails the same way on main in this environment.
- `npm run test:focused --` the environment build, export and serve CLI boundaries, the analysis-view CLI and MCP
boundaries, the environment serve, served-writes and bundle MCP boundaries, MCP schema validation and output schema
constraints: 10 files, 82 passed.
## Tests
- View: a later create sending a member the first did not, or leaving one out, widens the plan with the member's default;
a later create storing a member otherwise than sent stays unexplained; sent tags stored sorted give a sorted echo; a
create embedding the shelf its `shelf_id` names gives a reference, and the list that shows the shelf by id and name
learns `nested_fields`.
- Serving: a create leaving the member out stores its default and one sending it stores it; a create embeds the named
shelf as the environment holds it, lists it with the listed keys, and is refused for a shelf it does not hold; sent
tags are stored sorted.
- Unit: references (the member named like the field among several, too few keys, a disagreeing value, an unheld entity)
and merges (a constant that differs, a member one fixes and the other accepts, a reference one call shows alone).
## Write plans over recorded captures
The 69 captures the derived-values branch measured: 50 published, 18 Claude Code recorded from a prompt, and one timed
Grafana recording. After merging main `ed79bf93` (which bounds create learning), the census was run again against main
with the scenario-timing branch: 124 served there, 132 with both branches, no write going from served to withheld; the
columns below are the measurements before that merge, whose totals it reproduces.
| Captures | Write plans | Served before both branches | Derived values | This branch |
| ------------------------- | ----------- | --------------------------- | -------------- | ----------- |
| 50 published | 187 | 68 | 68 | 70 |
| 19 recorded from a prompt | 120 | 56 | 61 | 62 |
- No write goes from served to withheld.
- Gains: Keycloak's published user create and update, whose list shows each user's `access` with one key, so a created
user now lists as recorded and its plan's sorted insert checks; and the round-4 Linkding bookmark create, whose tags
the application stores sorted.
- Explained but still withheld: Homebox's item create now merges with the location create, with references for
`parent`, `location` and `entityType` and `parentId` and `entityTypeId` accepted with any value. Its read-back list
answers items only, sorted by name, from a line with no parameter saying so, which the list model does not cover.
- Linkding's create whose later call sends `shared` merges too, and is withheld as `side-effect`: the recording also
added a bookmark through the UI that no recorded write explains.
## Served write tasks
The same captures rebuilt by the baseline and by this branch, with `fireworks_ai/gpt-oss-120b` and
`fireworks_ai/glm-5p3-flash`. The tasks are new; each checks the result from the environment's state, including that
nothing else changed.
| Capture | Tasks | Baseline (gpt-oss, GLM) | This branch (gpt-oss, GLM) |
| --------------------------- | ------------------------------------------ | ----------------------- | -------------------------- |
| Linkding, recorded round 4 | 2 bookmark creates, one with two tags | 0/2, 0/2 | 2/2, 2/2 |
| Keycloak, published capture | create a user, change another user's email | 0/2, 0/2 | 2/2, 2/2 |
@@ -1,12 +0,0 @@
# Implementation plan
1. **Clock formats.** A `trimmed` ISO format, `fixedWidth` on a clock reading, fitting a format to another value, and
calendar digits, in a clock formats module.
2. **Composed strings.** A module that indexes a create's candidate parts once, segments a string, checks the type's
other entities and an update's before value, and spells the string from written fields.
3. **Explaining.** Create explanation composes unexplained strings after templates, learns trimmed clocks from the
type and generated integers; update explanation composes unsent changes after the existing rules.
4. **Later calls.** A later create or update widens the plan's clocks to a trimmed spelling it shows; a later create's
unique value may be a generated integer.
5. **Serving.** Derived fields are rebuilt after other rules; unique integers are seeded by length and kind.
6. **Schema, tests and docs.** The plan schema, view, serving and unit tests, and the write-plan docs.
@@ -1,59 +0,0 @@
# Explain values an application derives from other fields
Issue: https://github.com/morluto/rea/issues/4933
## Outcome
Environments an agent built from a prompt withheld creates in three of five applications, each for a value the write
model could not explain, though the application derives it deterministically:
- Linkding's `web_archive_snapshot_url` is `https://web.archive.org/web/<creation time as YYYYMMDDhhmmss>/<sent url>`.
The plan kept it as a constant, which pinned `url` to its recorded value, so a later create with another URL was
refused.
- Grafana's folder `url` is `/dashboards/f/<uid>/<slug of the title>`, which pinned `uid` the same way, and its numeric
`id` is a large time-based integer the plan copied from the first create, which the second create's read-back
contradicted.
- Homebox, written in Go, spells nanosecond clocks with trailing fraction zeros dropped (`…34.38812218Z`), so a later
create's clock did not follow the format learned from the first.
## Behaviour
- **Composed strings.** A created field no other rule explains may be a `composed` string: literal text and parts read
from the fields other rules write, each a field's value, its slug, or a clock's calendar digits (`YYYYMMDDhhmmss`,
`YYYYMMDD`, `YYYY-MM-DD`, `YYYY/MM/DD`, in the clock's zone). Parts are found left to right, longest first, delimited
by non-alphanumerics; generated values may be short, other values need eight characters, as a copy does. A create
composes a string only when at least one other entity of the type spells it with the same parts and none spells it
otherwise, and never when one text comes from two sources serving could tell apart (two clocks the write stamps are
one source when they spell the same zone), and never when another entity holds the field as something other than the
parts spell, a non-string value included. An update's unsent change may be composed from members it stored as sent and
fields it left as they were, when the same parts also spell the value before the call and no other entity spells it
otherwise. Serving rebuilds the string from the written fields after every other rule and refuses a call when a part's
field holds nothing to spell, and a composed field no longer pins the members it reads. A composition learned on a
later call is checked against every earlier call, and a later create whose response spells a composed field otherwise
is withheld.
- **Trimmed clocks.** A clock format may be `trimmed`: its values differ only in how many fraction digits they show and
none ends a fraction in a zero. It is learned from the type's held values of the field, from a later create, and from
a later update; other clocks the plan stamps take the spelling too, and no clock takes it once a held entity, the
exemplar or any call the plan accepted showed the field at a fixed width; a later call that then shows it shorter is
withheld. `fraction` is the most digits shown, and serving drops trailing zeros and the point with them.
- **Generated integers.** A created integer of 10 to 20 digits that no entity of the type holds, where every held entity
holds a distinct one of the same length and JSON kind and none is a clock, is `unique`. Serving draws a seeded integer
of that length and kind that no held entity has. One that another field of the created entity also holds, as a value,
a number at any depth or a delimited part of a nested string, is not `unique`, and the create is withheld as
`unexplained-change`, since seeding cannot keep the two equal. A create is also withheld when another string holds its
digits and no `template` or `composed` rule rebuilds that string from it, or when a later create answers the field
with anything but an integer of the same length and kind. Compare treats two such integers of one JSON kind and length
under a `unique` rule as generated values.
Plans that need none of this are unchanged.
Later creates must retain the exemplar's unique-value family (UUID, integer with
the same digit count and JSON kind, or opaque token). Their recorded composed
fields must equal the values the plan rebuilds; simulation cannot replace a
contradictory captured value and then count that replacement as confirmation.
## Acceptance
- The prompt-built Linkding, Homebox and Grafana captures serve the creates and renames they withheld, and write
tasks that use them pass.
- No write tool of the 50 published captures or the earlier prompt-built captures goes from served to withheld.
@@ -1,10 +0,0 @@
# Tasks
- [x] Read and fit trimmed clock formats, and spell calendar digits.
- [x] Compose created strings and unsent update changes from other fields.
- [x] Learn generated integers as unique values and serve them by length and kind.
- [x] Widen plan clocks on later creates and updates, and spread a trimmed spelling to the plan's other clocks.
- [x] Add view, serving and unit tests.
- [x] Document the rules.
- [x] Measure the prompt-built captures, the 50 published captures and the served write tasks.
- [x] Run static, formatting, documentation and test gates.
@@ -1,91 +0,0 @@
# Verification
Executed on macOS with Node v24.22.1 at `a1edeafd` (main `79845063` plus this branch). The prompt-built recordings and
evaluations used a local, unpushed build of this branch merged with the scenario-timing branch (#6967), whose request
times the clock rules need from browser scenarios, on Ubuntu 24.04 VMs with snap Chromium 155. The baseline is the same
merge without this branch.
## Gates
Follow-up on 2026-10-25, after merging main `a5bfd0440`, with Node 24.22.1 and npm 11.16.0 on macOS:
- Environment write/compare source tests: 26 files, 839 passed.
- Analysis-view CLI/MCP, environment served-write MCP, environment-compare CLI, managed bundle read guards, and setup
registration prewarm boundaries: 6 distinct files, 77 passed. The managed low-headroom failure is fixed by main's
Evidence ownership change (#7036); the setup tests share host preparation to satisfy lint without removing cases.
- `check:fast`, `format:check`, and `docs:check`: passed.
- New capture-to-plan regressions reproduce missing composition parts on earlier updates (null and structured sources)
and fixed-width clock evidence from either the selected entity or another held entity. Rule tests exercise several
separator/zone groups in both orders, choosing each group's widest trimmed peer while preserving fixed-width fields.
- Compare uses the domain clock matcher on parsed readings. Tests cover shorter fractions, absent fractions, maximum
width, trailing zeros, separators, zones, and response-entity scope; the compiled CLI accepts a Go-style create whose
live clocks have eight digits and served clocks have none, preserving all input Evidence files.
- The original application's recordings and model evaluations below were not rerun in this follow-up.
Original author validation:
- `npm run -s typecheck`, `lint`, `format:check`, `docs:check`, `mcp-catalog:check`, `verify:test-discovery`,
`scan:todos` and `npx knip`: passed.
- `npx vitest run --project domain --project services --project adapters`: 17,246 passed, 25 skipped, 1 failed ("stops
Inspector target authorization after the current stat settles"), which fails the same way on main in this environment.
- `npm run test:prepare`, then `npm run test:focused --` the environment build, export and serve CLI boundaries, the
analysis-view CLI and MCP boundaries, the environment serve, served-writes and bundle MCP boundaries, MCP schema
validation and output schema constraints: 10 files, 82 passed.
## Tests
- View: Go-spelled tag creates and renames are served with a trimmed clock; bookmark creates are served with a composed
snapshot address and accept any URL; folder creates and renames are served with a composed address and a generated
integer id.
- Serving: a served tag create spells its clocks without trailing zeros; a served bookmark create composes its snapshot
address from the served clock and the sent URL; a served folder create gets a fresh sixteen-digit id and an address
that follows its uid and title, and a rename moves the address.
- Unit: fitting clock formats (trimmed, fixed width, separator and zone), calendar digits, seeded integers, generated
integer detection, composition and its refusals (one call alone, a held entity spelling otherwise, two sources with
one text, a member stored otherwise than sent, a value the parts do not spell before the call), and a wide body
composed within the existing time limit.
## Write plans over recorded captures
Every write plan of 69 captures, built by the baseline and by this branch: the 50 published captures, 18 captures
Claude Code recorded from a prompt for the prompt branch's checks, and one new timed Grafana recording.
| Captures | Write plans | Served on the baseline | Served on this branch |
| ------------------------- | ----------- | ---------------------- | --------------------- |
| 50 published | 187 | 68 | 68 |
| 19 recorded from a prompt | 120 | 56 | 61 |
- No write goes from served to withheld. The five gains are Linkding's bookmark create, Homebox's tag create, and
Grafana's folder create in two recordings and folder rename in the timed one.
- Published plans change only where a value is now composed: Linkding's bookmark create and BookStack's book create and
update (`description_html` as `<p>` around the description) now accept the member they read.
- Unchanged by design: Grafana's published folder, whose uid and title slug are the same text, so its address names two
sources; Ghost's post update, whose `lexical` keeps the `derived` rule that explained it before; BookStack's page HTML
and Jellyfin's playlist path, which other pages and playlists of the capture spell otherwise.
- What still withholds a create the composed value now explains: a later Linkding create that sends a member the first
did not (`shared`), Homebox item creates that differ from the first location create, and a Linkding create whose
read-back lists `tag_names` sorted.
## Served write tasks
The same timed captures, rebuilt by the baseline and by this branch, with the models `fireworks_ai/gpt-oss-120b` and
`fireworks_ai/glm-5p3-flash`. Create tasks are new; recorded write tasks are the applications' existing ones with entity
counts taken relative to each bundle's state.
| Application | Tasks | Baseline (gpt-oss, GLM) | This branch (gpt-oss, GLM) |
| ----------- | ---------------------------------- | ----------------------- | -------------------------- |
| Linkding | 2 bookmark creates | 0/2, 0/2 | 2/2, 2/2 |
| Linkding | 3 recorded (update, notes, delete) | 3/3, 3/3 | 3/3, 3/3 |
| Homebox | 2 tag creates | 0/2, 0/2 | 2/2, 2/2 |
| Homebox | 2 recorded deletes | 0/2, 0/2 | 0/2, 0/2 |
| Grafana | folder create and rename | not served | 1/2, 2/2 |
| Grafana | 2 recorded deletes | | 2/2, 2/2 |
- Smoke and retrieval match on both builds (Linkding 9/9 and 8/9, Homebox 3/3 and 3/3); Grafana's new recording passes
its smoke task and 9/9 retrieval with both models.
- gpt-oss missed the Grafana rename by sending the update without the title it was asked to change, ten times; the
served environment refused each call with the members it accepts.
- Homebox's recorded deletes fail on both builds: this recording's served entity list holds its locations only, so the
models never find the item to delete.
- The created bookmarks' snapshot addresses and the created and renamed folders' addresses are checked from the
environment's state.
+10 -163
View File
@@ -140,72 +140,6 @@ static tasks. Both lint caches include the website, documentation configuration,
and experiment code scanned by the checker; a regression compares the actual
lint inventory with Turbo's resolved inputs.
Static checks locate candidates; they do not establish semantic ownership.
`jscpd` finds token clones, and Knip counts test references as uses. Module
boundaries and architecture guards enforce their declared import and source
rules. Passing them does not prove that two parsers agree, a wrapper is still
needed at runtime, or cleanup has one owner.
The lint gate treats `no-async-promise-executor`, `no-unreachable`, and
`no-unsafe-optional-chaining` as errors. These catch lost async executor failures,
dead code, and unsafe uses of optional-chain results. `no-unsafe-finally` remains
a warning because cleanup paths can deliberately throw aggregated failures;
review whether they preserve the primary failure. Two local rules
cover narrower observed failures: `rea/no-buffer-deep-equality` flags Vitest
deep comparisons of Buffers from recognized constructors and file reads,
including simple `const` aliases; `rea/cancellation-cause` requires an explicit
cause when a directly checked caller `AbortSignal` is converted to an
`AnalysisCancelledError`. The rules do not infer arbitrary Buffer types,
follow indirect cancellation helpers, or prove that an explicit cause equals
the caller's reason. Keep focused boundary and lifecycle tests for those facts.
`rea/no-evidence-json-preadmission` rejects direct generic JSON parses in
`createEvidence` payload expressions; it does not analyze intermediate values.
`rea/no-positional-tool-contract` rejects catalog indices used to select
operation names, schemas or types; display-only selection remains allowed.
`rea/no-fixture-root-cleanup` follows `const` fixture-root aliases into teardown
deletion; mutable bindings and mixed root arrays remain outside its scope.
`rea/no-shared-module-mock` resolves Vitest import bindings before rejecting
module mocks in projects without per-file isolation. The test-project definitions
own isolation for Vitest and lint; the executable gate checks the resolved policy
and runs the same collector. Aliases and namespace imports count; comments,
shadowed locals, ordinary methods and `spyOn` do not. Negative CLI probes use an
external temporary project so concurrent repository checks cannot discover them.
Focused `.only` declarations, broken assertion promise chains, and unused disable
directives are errors. `vitest/valid-expect` warns (`maxArgs: 2`) because valid stored and
collected assertions can be awaited later. `import/no-duplicates` warns with
`preferInline: false`.
`no-useless-escape` keeps intentional `\-` regex escapes because advertised
patterns must also compile in `v` mode; the catalog regression checks every
advertised pattern.
Fast lint stays syntax-based. `npm run lint:typed` runs serially with pinned
`oxlint-tsgolint` 7.0.2005 and records native plus typed diagnostics in
`.cache/static-checks/typed-oxlint.json`. Production `src` treats
`no-misused-promises` as an error; tests/fixtures warn. `no-floating-promises` is
an error throughout the lint inventory. `return-await` is an error in
error-handling scopes so asynchronous completion stays inside its catch or
cleanup boundary. Exhaustiveness is an error when a union member has neither a
case nor a default. Intentional defaults count as handling the remaining union;
new members covered by those defaults therefore require behavioral review.
Two HTTP/3 source-test files allow the runner-owned `node:test` registration API;
ordinary unowned promises in those files still fail. The authored code-split
recovery input is exempt from floating-promise policy because it is analysis
input, rather than application orchestration. Preserve framework-owned callbacks
such as an awaited `Promise.finally`; inspect the actual completion owner before
changing a callback solely to satisfy its declared `void` return type.
`npm run knip:production` writes advisory candidates to
`.cache/static-checks/production-reachability.json`; execution/report errors fail.
Ordinary Knip remains. `check`, `check:pr`, and `check:ci:deep` run reachability
then typed lint sequentially after cached work. Typed lint builds the project
type graph and stays separate from fast feedback. Its default Go settings are
`GOMAXPROCS=2` and `GOMEMLIMIT=1024MiB`; caller-selected settings take precedence.
Routine iterations and rebases need focused regressions and relevant checks.
Before handing off a PR, run `npm run check` and generated-document checks when
applicable; CI owns the full suite and coverage. Use the full local gate for
@@ -235,14 +169,6 @@ with the consumer preloaded as well as alone. For simulated protocol traces,
use an acknowledged event barrier before asserting complete observations;
a zero-duration capture does not guarantee queued events have arrived.
Place faults at the semantic boundary under test: the selected read, EOF, hash
completion, or cleanup attempt. Global call ordinals can shift during setup or
refactoring without reaching that phase. For controlled asynchronous gates, use
`awaitAcknowledgement` from `tests/fixtures/lifecycleAcknowledgement.ts` to race
the acknowledgement against triggering-operation settlement and a bounded
deadline. Register release gates before launch, then release and join launched
work in `finally` before restoring mocks, even when the barrier fails.
Focused contract tests should import the named contract family they exercise;
reserve the full tool catalog for catalog-wide assertions. Resolve contracts by
operation name rather than array position.
@@ -274,10 +200,6 @@ work from fixture generation, schema compilation, subprocess startup and cleanup
Choose elapsed, CPU and process-family memory limits before the run; a worker
count or test timeout alone does not enforce those limits.
Keep capacity fixtures intact when diagnosing a test failure. For byte equality
of large buffers, use `Buffer.equals` or `Buffer.compare`; a generic structural
diff can allocate far more than the production operation.
Remove demonstrated repeated work while preserving distinct assertions and
resource ownership. Share immutable fixture preparation where safe; keep fresh
sessions when lifecycle or concurrency is the behavior under test. Compare the
@@ -386,23 +308,6 @@ Trivial helper assertions need no replacement. Remove abandoned production
scaffolding only after checking runtime and verifier consumers, including
compiled imports.
Assert error codes, structured limit facts, source provenance, and observed
recovery behavior instead of freezing explanatory sentences or banning obsolete
phrases. Keep exact checks for protocol values, setting and parameter names,
caller data, and diagnostic provenance. To verify guidance delivery, compare the
consumer with the owning formatter or constant, alongside independent assertions
for the behavior and facts; shared text alone is not a correctness oracle.
Documentation wording belongs in review, not substring tests of source files.
When replacing prose assertions, verify that a harmless wording change passes
and an incorrect budget or lost fact still fails.
After an owner changes, move fault injection and event spies to the path the
production consumer actually uses, and assert that the intended boundary was
reached. Keep independent expectations from authored input, producer output, or
an external oracle; comparing two paths through the same replacement algorithm
does not preserve the old oracle. A negative probe inside a conditional or loop
must assert that it ran.
Use representative scenarios rather than Cartesian matrices of equivalent
cases. Share setup without hiding inputs or expected evidence. For successful
`Result` values, throw the typed error before asserting the value; a preceding
@@ -430,34 +335,6 @@ that a real loader accepts the artifact. If multiple producer revisions share a
format identifier but differ in behavior, record that ambiguity rather than
using one installed version as an oracle for all of them.
### Boundary regressions
For parser, inventory, snapshot, or projection fixes, select the applicable
cases below. Assert the producing boundary's facts and their meaning in affected
consumers. Add [lifecycle regressions](#lifecycle-regressions) when resource
ownership or finalization changes; a single malformed fixture cannot establish
the whole contract.
| Failure class | Regression that catches it |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Format assumptions | Pair valid padding, offsets, or names containing delimiters with malformed input. Identify the producer revision when behavior varies by version; an unrelated runtime API's range is not an on-disk format limit. |
| Raw identity and normalization | Exercise allowed empty or unusual source names/metadata keys and values that normalize to one lookup or display value. Preserve admitted raw spelling under the existing redactions for transport credentials and explicitly marked sensitive values. Distinguish local paths, file URLs and HTTP paths; assert source identity through consumers. |
| Missing, empty, and unknown values | Distinguish omitted, null, empty, zero, and false where the format admits them. Pair a malformed optional field with usable sibling facts; assert uncertainty without inventing absence or discarding the row. |
| Completed work followed by failure | Complete one input, then fail or cancel a later input. Assert earlier digests, metadata, and parsed facts survive with the later failure reason and accurate processed/unprocessed coverage. For cancellation, assert the exact caller abort reason; where a primary failure was already produced, assert it survives. |
| Independent limits and failures | Trigger a sample/output limit alongside a read or parse failure. Assert both dimensions survive; reaching a limit does not erase sampling failures or prove complete coverage. |
| Representation expansion and capacity | Keep a valid set near the limit and a large malformed input followed by a valid input. Cover JSON escaping, expanded paths or decoded rows as applicable. Assert serialized bounds, retained identities, later valid results, and unknown omissions without charging discarded interpretations as retained output. |
| Snapshots, caches, and comparison | Vary metadata that affects the result while keeping payload bytes equal. Exercise historical records with newly introduced fields omitted, current caller inputs on replay, and reuse across differing selections. Assert correct invalidation or unknown coverage, current request provenance, and unchanged saved Evidence and IDs. |
| Public success and error delivery | Exercise affected public CLI/MCP paths, including partial Evidence on failure. Assert retained facts and named contracts; retrieve partial Evidence when MCP returns a reference. If SDK cancellation suppresses the reply, check server settlement and any retained Evidence. An error-code assertion alone misses lost observations. |
For partial Evidence delivery, verify that same-session readback preserves
identity and provenance without rerunning the provider. Include a no-writer
control that keeps completed Evidence inline and preserves the original failure.
When producer order is unspecified, synchronize fixtures on the observed request
or completion event. A filename, generated key, or fixed delay does not prove
which item completed before cancellation. Preserve real producer ordering and
use a controlled seam for the phase that needs deterministic failure.
Packed bridge validation uses Python's standard-library AST and tar readers
with `-I -S`: caller-local modules, `PYTHONPATH`, and site startup hooks must
not replace those readers or execute during validation. Controlled `ast.py`
@@ -475,19 +352,6 @@ pass that definition through the SDK's `toolDefinition` call option. This keeps
the selected output-schema validation without eagerly compiling unrelated
catalog schemas. Re-read definitions if the server advertises a changed list.
For exact nested input constraints, select the full profile explicitly or use
the full-SDK conformance suite that owns the field. Compact workflow tests
should assert canonical failure categories and provider-call controls where
rejection before provider work matters. Do not make today's omission of a
constraint a compact-profile invariant.
When a file repeatedly validates the same MCP schemas, use
`createMcpSchemaValidation` for file-owned preparation and clear it in
`afterAll`. Its content keys include the complete schema, and the SDK provider
keeps dialect and format validation. Keep sessions and connections scoped to
each test; share immutable schema preparation through the fixture's validator
dependency.
Artifact verification uses `artifactMcpCall` for both lifecycle and analysis
calls. A fresh SDK client has no held tool listing, so a bare `callTool` can
skip output-schema validation. The helper discovers actual advertised
@@ -520,16 +384,15 @@ the affected owners and applicable scenarios below. Include temporary snapshots
and one-shot commands. Use public CLI/MCP workflows where practical and a
controlled production seam for interleavings that cannot be reproduced reliably.
| Scenario | Assertion and control |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Target admitted; startup still pending | Status preserves the readiness distinction and observed startup activity; readiness requires the provider's documented proof. |
| First handshake is late, absent, invalid, or recovered during close | Initial and recovery paths retain the same authenticated identity facts. Foreign run/token controls cannot authorize cleanup; authenticated idle/native work survives the unauthenticated listener deadline. |
| Launch returns an error with a valid process handle | Cover already-exited and still-live owned processes. Verify observed exit separately from creation; failed termination or unavailable identity remains unresolved. |
| Cancellation arrives after workload exit, during result reads or temporary-root removal | Return typed cancellation with available partial observations and actual cleanup status. The non-aborted control succeeds; existing primary/cleanup failures keep their precedence. |
| Children have mixed health or cleanup outcomes | Missing peer health does not erase an observed failure or activity. Idle plus unknown stays unknown. Retry a failed child without closing a successful peer again, including after a close-option change. |
| Cleanup fails after success or primary failure | Public output retains the completed observation or primary diagnostic, remaining resources, and owner recovery action. Restore the denied resource and verify the owning cleanup retry. |
| Cleanup owners recover after an error was returned | Retain the original child and aggregate errors and project them after each owner releases. A successful peer stays released; the aggregate remains incomplete until all owners release. Preserve the first cause even when rejection is `undefined`. |
| Target exits without an acknowledgement, or transport disappears while it stays alive | Positive owned exit proof resolves target lifetime; other resources still need cleanup. Lost transport alone does not prove exit. Verify unrelated/attached targets survive and same-connection recovery works when supported. |
| Scenario | Assertion and control |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Target admitted; startup still pending | Status preserves the readiness distinction and observed startup activity; readiness requires the provider's documented proof. |
| First handshake is late, absent, invalid, or recovered during close | Initial and recovery paths retain the same authenticated identity facts. Foreign run/token controls cannot authorize cleanup; authenticated idle/native work survives the unauthenticated listener deadline. |
| Launch returns an error with a valid process handle | Cover already-exited and still-live owned processes. Verify observed exit separately from creation; failed termination or unavailable identity remains unresolved. |
| Cancellation arrives after workload exit, during result reads or temporary-root removal | Return typed cancellation with available partial observations and actual cleanup status. The non-aborted control succeeds; existing primary/cleanup failures keep their precedence. |
| Children have mixed health or cleanup outcomes | Missing peer health does not erase an observed failure or activity. Idle plus unknown stays unknown. Retry a failed child without closing a successful peer again, including after a close-option change. |
| Cleanup fails after success or primary failure | Public output retains the completed observation or primary diagnostic, remaining resources, and owner recovery action. Restore the denied resource and verify the owning cleanup retry. |
| Target exits without an acknowledgement, or transport disappears while it stays alive | Positive owned exit proof resolves target lifetime; other resources still need cleanup. Lost transport alone does not prove exit. Verify unrelated/attached targets survive and same-connection recovery works when supported. |
Shared process-supervision controls separate leader exit from output closure.
Cover output during held or failed cleanup, bounded retention and drain timeout,
@@ -543,11 +406,6 @@ owned cleanup seams while preserving real cleanup where feasible. Check server
settlement and retained resources even if the MCP SDK rejects the client promise
before the handler finishes. A cancellation during active execution does not
cover cancellation during final cleanup.
For ZIP extraction, replace the pathname after inventory reader close while the
admitted root descriptor remains owned. Verify that identical member bytes in a
different carrier, a directory, or a missing pathname cannot switch the
materialized source. Exercise descriptor close failure and retry before output
publication.
Use explicit abort reasons and assert their identity at admission and after
provider completion, alongside retained observations and cleanup state.
@@ -696,11 +554,7 @@ creation outside the workspace seam and its narrowly documented boundary/package
exceptions.
The shared temporary-directory and workspace helpers preserve caller labels
under an owned `rea-` prefix when needed. Their
[cleanup ownership differs](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#temporary-files-and-diagnostics):
`createTestTempDirectory` registers `onTestFinished` deletion, while
`createTestWorkspace` requires caller cleanup. Close resources before removing
their directory. `npm run verify:test-temp-hygiene`
under an owned `rea-` prefix when needed. `npm run verify:test-temp-hygiene`
uses the same ownership rule to find retained fixtures under a private temporary
root. Third-party caches remain outside that ownership rule.
@@ -2121,13 +1975,6 @@ Codex CLI. The lane runs sequentially; select scenarios with
[agent evaluation reference](client-verification.md#agent-evaluation-and-conformance-records)
contains isolation, authentication cleanup, grading, and conformance boundaries.
The Notes evaluation packages `website/public/examples/notes-electron/`. When
changing those example bytes, run `tests/boundary/mcp/agentKnownAnswerMcp.test.ts`
and check its rubric in `tests/evaluation/model-evals/AgentEvaluationFixtures.ts`.
Preserve exact source facts and the coverage reported by the analysis. If a
source change crosses an unresolved static-analysis boundary, keep that
uncertainty and a negative control rejecting claims of complete coverage.
## Frida instrumentation
`npm run verify:frida` builds REA, spawns a short-lived local Node.js fixture,
+12 -70
View File
@@ -53,11 +53,11 @@ satisfy. A plan without execution does not establish runtime behavior.
- Use strict inputs with explicit target identity and required fields. Preserve
meaningful target, action, capture, and output choices; remove ignored options
and repeated permission declarations.
One-shot binary CLI analysis retains the caller's verbatim target spelling in
Evidence `caller_input_path`, including a result loaded from a snapshot. The
subject keeps the inspected file identity. Provider inspections also retain
the established `parameters.input_path` field for existing consumers. Keep
request provenance separate from provider arguments and snapshot queries.
One-shot CLI provider inspections retain the caller's exact target spelling
in Evidence `parameters.input_path`; the subject keeps the canonical inspected
file identity. Keep request provenance separate from provider arguments.
Snapshot entries bind Evidence parameters to provider queries, so changes to
snapshot-backed request provenance must preserve that cache contract.
- Return the requested facts inline with artifact/provider identity, source
locations, Evidence references, coverage, and relevant confidence and
limitations. Use supported selection, summaries or views when they answer
@@ -85,21 +85,6 @@ satisfy. A plan without execution does not establish runtime behavior.
### Interpret boundary facts once
The format or protocol reader owns interpretation. Pass its parsed facts and
explicit uncertainty to consumers; do not make adapters reparse display text,
reconstruct omitted values, or infer success from missing diagnostics.
Client registration inspections, for example, carry command identity and
activation independently of readiness. An owned registration may be disabled
or stale; unresolved ownership remains unknown. Consumers use those facts
instead of reclassifying the raw command.
A completed read can establish byte identity even when decoding or format
validation fails. State which representation a digest identifies, such as file
bytes, Fetch response-body bytes, or UTF-8 text. Preserve admitted source values
separately from normalized lookup and display values. When the retention
contract keeps only a digest and size, state that the bytes or text are
unavailable; the digest does not establish format validity.
Comparison needs separate proofs for lookup, correspondence, content, and
coverage. A hash cannot supply the proof that two retained values belong to
corresponding objects.
@@ -118,6 +103,10 @@ Complete traversal or inventory does not make unavailable values equal. A
partial aggregate fingerprint difference cannot establish modification when
omissions alone explain it; independently proven differences still survive.
The format or protocol reader owns interpretation. Pass its parsed facts and
explicit uncertainty to consumers; do not make adapters reparse display text,
reconstruct omitted values, or infer success from missing diagnostics.
Keep format recognition separate from provider admission. A recognizable header
can supply useful observations while later structure remains unresolved or a
provider cannot accept the artifact. Report the reason at the affected boundary
@@ -144,12 +133,6 @@ establish each transition. Include temporary files and one-shot commands, as
well as persistent engine sessions. Use the existing provider contracts;
providers need not expose identical state machines.
In multi-stage work, retain each completed, admitted observation before awaiting
the next stage. Later reads, validation, cancellation, or finalization can fail
without erasing those facts. Carry them through typed failure outcomes with
explicit coverage of unperformed work; do not construct a successful result for
stages that never ran.
- Keep target admission, provider readiness, active work, observed process exit,
and resource release separate. A valid process handle, successful launcher,
or selected target establishes only its own fact. Publish available identity
@@ -224,12 +207,6 @@ stages that never ran.
applies rather than duplicate close/finally handling.
- Give descriptor-backed reads an owner before opening the file. A rejected
close must retain the exact handle and preserve any earlier failure.
Carry [`openRegularFile`](https://github.com/morluto/rea/blob/main/src/filesystem/RegularFile.ts)'s
admitted handle and initial metadata rather than restatting only to rebuild
those facts. Keep fresh descriptor and selected-path identity checks after
work, comparing against the original baseline instead of replacing it with
a later stat. Preserve explicit bigint reads when ordinary admission metadata
cannot supply the required size or timestamp precision.
Stable file reads expose completed values separately from cancellation so
consumers retain acquired identity facts after successful close or cleanup retry.
If an abortable caller stops waiting, keep the underlying work in that owner's
@@ -260,38 +237,9 @@ through the producing owner and affected CLI/MCP consumers.
## Implement and evaluate
Put provider wire-protocol parsing in adapters and shared CLI/MCP workflows in
the application layer. Reusable format declarations may live in pure domain
owners: [`peOptionalHeader.ts`](https://github.com/morluto/rea/blob/main/src/domain/peOptionalHeader.ts)
decodes PE structure, while consumers retain machine admission, directory-address
interpretation, and file/mapping bounds. Share Evidence provenance, unknown
projection, and session retention semantics; matching payloads alone do not
establish parity.
Let `createEvidence` own JSON validation and snapshotting of ordinary observation
payloads. `EvidenceObservationInput` accepts semantically parsed parameters,
results, and raw results whose JSON validity still needs admission. Keep format,
selection, and identity validation at their producing boundary. Invalid JSON
issues are rooted at `parameters`, `normalized_result`, or `raw_result` in the
Evidence envelope.
`EvidenceObservation` retains the stronger JSON contract used by producer
helpers and `createImmutableEvidenceSteps`. The cooperative constructor also
requires an authenticated immutable result; preserve that ownership proof.
For objects, proof comes from completed sealing by
[`immutableJson.ts`](https://github.com/morluto/rea/blob/main/src/domain/immutableJson.ts);
`readonly` or `Object.freeze` alone does not establish it. JSON primitives are
directly eligible. Seal owned data and complete cooperative sealing before
reusing its result.
When a failed or cancelled operation retains completed, valid partial Evidence,
use the shared [`ToolResultDelivery`](https://github.com/morluto/rea/blob/main/src/server/ToolResultDelivery.ts)
path with the session's Evidence writer. Keep the original failure and the
named contract's target-content marking; replace the inline partial with a
retained reference only after recording succeeds. An absent writer or a writer
returning a failure `Result` leaves the partial inline, as does a partial that
is not valid Evidence. Unexpected recorder exceptions propagate. An `evidence_id`
member alone does not establish Evidence validity or retention.
Put provider protocols/parsing in adapters and shared CLI/MCP workflows in the
application layer. Share Evidence provenance, unknown projection, and session
retention semantics; matching payloads alone do not establish parity.
Measure the resource before choosing a budget and enforce it before the costly
step. Account for value alternatives, trace-frame products, retained records,
@@ -300,12 +248,6 @@ completeness from examined records and exhaustiveness, rather than diagnostic
prose or an empty failure list. Preserve observations independently of cleanup
success.
Price mutually exclusive success and failure representations separately.
Replace a reserved failure envelope when pricing success, and release discarded
decoding state from retained-output accounting. Retaining additional failed-body
text changes the capacity available to later results; treat that as a contract
change and cover its effect on later valid items.
For example, if ten applicable records are discovered, eight are examined, and
five are returned, the five returned records do not establish complete coverage
of the ten. Keep discovered, applicable, examined, and retained scope distinct.
@@ -1,83 +0,0 @@
# Runtime analysis settings verification
Verified on macOS with Node 24.22.1, Java 21.0.12.1 and Ghidra 12.1.4.
This change adds two MCP tools and 11 runtime defaults without a new dependency.
## Behavioral checks
- Built stdio MCP: a valid ASAR with an index above 16 MiB is refused. Updating
`asar.max_header_bytes` to 32 MiB allows the unchanged full-archive inventory
and JavaScript analysis on the same connection. Both source files are parsed.
Reset refuses the same archive again; target close preserves the revision.
- An Electron header cached under a higher admission remains denied after a
decrease. No bulk extraction or source selection bypasses admission.
- Store and deferred MCP handler tests retain the accepted snapshot across an
overlapping update. Atomic rejection, duplicate keys, revision conflicts,
idempotence, reset, isolation and explicit WABT argument precedence are covered.
- The real Ghidra verifier reaches a natural one-second decompile timeout, raises
the runtime deadline to 60 seconds, and obtains pseudocode for the same slow
function without replacing the Program. Initial and final session run IDs
match; subsequent analysis succeeds and cleanup completes. Its generated
6,000-branch fixture is compiled but never executed.
- The focused suite passed 459 tests across 22 files, with 10 platform-specific
skips. Typecheck/lint, bridge source compilation, runtime build and generated
documentation checks passed.
## Review follow-up
Invalid provider-local launch settings remain typed configuration failures rather
than preventing MCP startup. Inspection reports `source: "invalid"`, omits
`value` and `launch_value`, and includes `configuration_error`; a runtime
replacement supplies a usable value, while reset restores the invalid launch
state. JADX availability and execution resolve the same admitted timeout, so
repair and reset also update capability guidance. The availability regression
uses a fixture JAR and simulated JDK; it does not establish real JADX behavior.
Update history omits an unknown old or new value instead of using null
(the explicit unbounded Ghidra value).
Regex controls apply to IDA, Hopper and Kuna's supervised ECMAScript matcher.
Ghidra uses its own Java matcher and is excluded from these controls. IDA's bound
client and capability descriptions consume the same accepted configuration.
WASM passes its effective deadline separately from caller parameters, preserving
omitted arguments in both successful and partial Evidence. Shared error copying
retains structured limit recovery through attribution and explicit redaction.
## Representative task costs
Measurements below precede the review follow-up; task costs were not remeasured.
One measurement per scenario, using the built stdio server and a valid ASAR with
an index just above 16 MiB and one small JavaScript module. Each scenario ends
with successful full application analysis delivered as a summary. Discovery,
failed calls and recovery are included. Fixture creation, build and tokenizer
work are excluded. Filesystem caches were uncontrolled.
| Recovery workflow | Elapsed | Sampled server-family CPU | Sampled peak server-family RSS |
| ---------------------------------------------------------------------- | ------: | ------------------------: | -----------------------------: |
| Fresh server: fail, runtime update, retry | 3.68 s | 1.81 s | 680 MiB |
| Warm server: fail, runtime update, retry | 0.25 s | 0.49 s | 625 MiB |
| Launch-environment recovery: fail, close, restart with override, retry | 6.02 s | 2.85 s | 674 MiB |
The restart comparison uses the same current build to model the previous
recovery workflow; it is not a benchmark of the previous commit. CPU and RSS
are 100 ms `ps` samples of the server and descendants. Short-lived processes and
unsampled peaks can be missed; CPU estimates exclude the client and sampler.
These observations do not establish a general speedup or performance guarantee.
Tokenized with `cl100k_base` / tiktoken 0.12.0, not billed model usage. Catalog
projections include tool name, description and input schema. The two new tools
add 1,181 tokens; the full 229-tool projection is 59,734 tokens. The runtime
recovery task uses 5,880 tokens for all requests and complete tool replies, plus
1,710 for its selected tools (7,590 total), or 65,614 including full discovery.
Warm recovery reuses discovery. The restart workflow uses 5,492 request/reply
tokens plus two full discoveries (124,960 total). A host that caches discovery
or loads only selected tools has a different context cost. System instructions,
reasoning and client-specific presentation overhead are unmeasured.
## Coverage limits
The archive fixture isolates header admission; it is not the original Loom
archive. Real engine coverage here is Ghidra on macOS. Hopper, JADX, capture
and regex setting integration have source/boundary coverage, not a new real
provider run for every setting. Linux/Windows engines and Bun are unverified
for this change. Client deadlines and receive buffers remain externally owned;
active worker values are explicitly unobserved in the settings inspection.
-19
View File
@@ -71,19 +71,6 @@ selected byte SHA-256 and size, section payload ranges, full header/detail outpu
and its UTF-8 SHA-256. Producer format drift and invalid or truncated modules
are failures, with validation diagnostics and partial Evidence retaining
selected byte identity and tool provenance.
If inspection stops after selected files were read, partial Evidence also keeps
the completed candidate file identities and glue parse observations. A stable
read remains observed when descriptor finalization later fails; cleanup still
requires retry. Partial Evidence for a glue read whose finalization fails
before decoding retains its digest with `not-performed` parse status. Invalid
UTF-8 retains the digest with failed parse status. Neither case invents
references, and a failed file read contributes no invented facts.
These independent observations do not establish that the selected WASM module is
valid: partial results state whether WABT validation is unknown, rejected the
module, or succeeded before a later inspection failure.
CLI errors include this partial Evidence inline. MCP errors can return its
`retained-evidence` reference in `details.partial_observation`; use
`get_evidence_bundle` with `detail: "complete"` to retrieve the matching record.
Core validation remains authoritative when an optional custom section cannot
be interpreted. WABT warning output is preserved in `custom_section_warnings`;
@@ -115,12 +102,6 @@ actual selected deadline and these recovery controls; increasing only the client
deadline does not change the provider budget. No server restart is required to
change this per-call setting. Resolve incomplete cleanup before another attempt.
To change the default across new MCP operations, set `wasm.command_timeout_ms`
through `update_analysis_settings`. An explicit `command_timeout_ms` still wins;
correct that argument if it caused the timeout. Runtime defaults do not insert
omitted arguments into Evidence parameters. See the
[settings design](design/limits-and-recovery.md#precedence-and-lifetime).
For example, `rea inspect-wasm-artifact /artifacts/module.wasm
--command-timeout-ms 60000 --format json` uses a 60-second per-command deadline;
the equivalent MCP input is `{"path":"/artifacts/module.wasm",
+12 -52
View File
@@ -737,45 +737,12 @@ request body must be a usable JSON object (as response bodies are usable), and
the written entity must show in that call's response or in a later read before
the next write. Each field of a created entity gets the first rule that
explains it: generated `identity` (a sequence, UUID or token, by the recorded
value's shape), `echo` of a body member (`sorted` when the application stored
its strings in code-point order rather than as sent), `copy` of a body string of
8 or more characters, a `path` value, a `slug` of a body member, a `clock` value within 10
value's shape), `echo` of a body member, `copy` of a body string of 8 or more
characters, a `path` value, a `slug` of a body member, a `clock` value within 10
minutes of the request in its recorded format, a `sequence` one past the
largest value of its collection or type, a `unique` UUID, opaque token or
integer of 10 or more digits that no entity of the type holds (where every one
holds a distinct integer of the same length, as time-based ids do), a
`nested-clock` object whose clocks the request stamped, a `template` around
a generated value, a `reference` to an entity, or a `composed` string; other
fields keep the observed value. A `reference` is an object holding fields of the
entity a sent member names, as the state holds it (a parent beside `parentId`):
the member is accepted with any value, serving looks the entity up and refuses a
call naming one the environment does not hold or holds in more than one
collection, and the field is absent, from the entity and the create's answer,
when the call does not send the member. An id naming entities of one type in
several collections teaches no reference. Each later create widens its plan
when, explained on its own, it agrees with it: both calls show the same fields
but for a reference whose member a call without it did not send (one an earlier
create added stays), every field both rule they rule alike (an echo one showed
sorted stays sorted, unless a call stored its strings out of order as sent),
and a field neither rules holds one value. Members either call sent are accepted, only those every call sent are
required, and an echoed member a call left out takes the value that call stored
(`body.defaults`). A clock rule widens to `trimmed` only when no held entity or
earlier call showed that clock at a fixed width. A later create that disagrees
leaves the plan as it was. A create whose observed reference differs from the
entity the plan rebuilds, whether or not it sent the reference member, is
`unexplained-change`. So is a create that left out an echoed member yet shows a
value different from the learned default.
A `composed` string is literal text and parts read from the fields other rules
write: a field's value, its slug, or a clock's calendar digits
(`YYYYMMDDhhmmss`, `YYYYMMDD`, `YYYY-MM-DD` or `YYYY/MM/DD`, in the clock's
zone). A create composes a string only when at least one other entity of the
type spells it with the same parts and none spells it otherwise, and never when
one text could come from two sources serving could tell apart. A clock format
is `trimmed` when the field's values differ only in how many fraction digits
they show and none ends a fraction in a zero, as Go spells nanoseconds: its
`fraction` is the most it shows, and serving drops trailing zeros and the point
with them. Other clocks the plan stamps take that spelling too unless their own
value shows a fixed width. A body that wraps one
largest value of its collection or type, a `unique` UUID or opaque token, a
`nested-clock` object whose clocks the request stamped, or a `template` around
a generated value; other fields keep the observed value. A body that wraps one
object in a one-element array is that object, wrapped so on every call
(`body.envelope`). An update's members
echo when stored as sent; a member stored differently is accepted only with its
@@ -783,10 +750,8 @@ recorded value (`body.fixed`) and stores the value the call that showed it
recorded (the `value` of its `constant` rule), and a field it changed without
sending must be a clock, a nested clock, an increment, a slug, a fresh `unique`
value, a `stamp` of the acting user, a value `derived` from the one member it
stores nowhere, a `reference` to the entity a sent member names (kept by a call that names the entity the field already repeats), a `composed` string whose parts read members it stored as sent
or fields it left as they were and spell the value before the call too, or for
an item action a `set` of a scalar to the value it showed, or the plan is
`unexplained-change`; an increment, slug, stamp, derived
stores nowhere, or for an item action a `set` of a scalar to the value it
showed, or the plan is `unexplained-change`; an increment, slug, stamp, derived
value or set must hold for every call of the tool, and an action is served
only once a call found what it sets already set. A write is served only
with the query its first call sent, which serving applies to a call that
@@ -876,11 +841,9 @@ HAR, SAZ and qlog run in an owned Node old-generation heap with one V8 worker.
`REA_CAPTURE_DECODER_OLD_GENERATION_MIB` is a decimal integer of mebibytes from
8 to 16384. Unset uses 192, the previous constant. Invalid values fail
configuration and name the setting; they do not fall back to 192. The setting
seeds the launch value. In MCP, set `capture.decoder_old_generation_mib`
through `update_analysis_settings` for decoders belonging to newly accepted
operations, without restarting REA. Changing the launch environment still
requires a new process. A memory failure names the effective mebibytes, known
source, default, floor, and maximum. The decoder
is read from the environment that starts REA, so a change needs a restart.
A memory failure names the effective mebibytes, whether they came from the
default or the setting, the default, the floor, and the maximum. The decoder
passes that value as `--max-old-space-size` and removes inherited
`NODE_OPTIONS`, so additional host memory and `NODE_OPTIONS` do not raise the
heap. The 8 MiB semi-space stays fixed; a smaller old generation aborts Node
@@ -902,11 +865,8 @@ hosts enforce no decoder heap limit, so raising the setting does not bound them.
Observed decoder memory exhaustion returns `resource_constraint`, with the
selected capture, applicable limits and retained process diagnostics. Raising
host memory or `NODE_OPTIONS` does not raise the old-generation heap. Update
`capture.decoder_old_generation_mib` through MCP, or set
`REA_CAPTURE_DECODER_OLD_GENERATION_MIB` before starting a new REA process.
Retry after owned cleanup completes; the update does not release resources.
See the [settings design](design/limits-and-recovery.md#failure-and-retry).
host memory or `NODE_OPTIONS` does not raise the old-generation heap. Set
`REA_CAPTURE_DECODER_OLD_GENERATION_MIB` and restart REA.
Record selection happens after complete decoding and does not reduce that
workload. Use a smaller capture exported by its producer or another decoder
with sufficient capacity, retaining the original for provenance. Subsets do
-15
View File
@@ -111,10 +111,6 @@ are made.
## Verification
Browser bundle collection uses absolute section offsets for nested inline maps.
If caller cancellation stops browser map fetching after some items complete,
the failed bundle request retains those items with the captured page in partial
Evidence and marks bundle analysis `not_performed`. This is distinct from the
local source-map decoder's failure semantics above.
An enclosing column offset contributes only when the nested section starts on
that enclosing section's first generated line. Parent boundaries still clip
preceding browser mappings when a nested section starts with a gap or has no
@@ -140,17 +136,6 @@ requests are left unprocessed and reported as unknown through the top-level
items.
Malformed UTF-8 map responses are reported as invalid rather than repaired with
replacement characters.
For browser fetching, an `invalid` map or structural or decoder capacity
failure has `artifact: null`, empty decoded arrays, and an explicit limitation.
When its bounded response body was read in full, optional `response_identity`
records that body's SHA-256 digest and byte count after HTTP content decoding
and before UTF-8 decoding. It is not a digest of partial reads or a claim that
the map was valid; historical items may omit it. Malformed text is omitted so
it does not consume the 32 MiB inline budget needed by later valid maps. A
response whose text cannot fit that budget is refused before JSON parsing and
source-map decoding, and retains the same
completed-body identity. Successful and partial maps keep their full original
text, including BOM and XSSI framing; only their parsing view removes it.
Focused tests cover duplicate mappings, indexed offsets/overlap/local indexes,
32-bit VLQ overflow, nullable content, UTF-16/CRLF positions, immutable artifact
-35
View File
@@ -1,35 +0,0 @@
{
"$schema": "./node_modules/knip/schema.json",
"entry": [
"src/main.ts!",
"src/cli.ts!",
"src/cliOutput.ts!",
"src/cliBuiltinArguments.ts!",
"src/cli/cliResultOutput.ts!",
"src/mcpDoctor.ts!",
"src/composition/javascriptAnalysisWorkerProcess.ts!",
"src/composition/webBundleAnalysisWorkerProcess.ts!",
"src/sqlite/SqliteDatabaseWorker.ts!",
"src/evm/EvmoleInterfaceWorker.ts!",
"src/browser/history/NodeCaptureProcess.ts!",
"src/javascript/sourceMaps/SourceMapCodecProcess.ts!",
"scripts/rea.mjs!",
"scripts/rea-android-workspace-guardian.mjs!",
"scripts/electron-active-hook.cjs!",
"scripts/prepack.mjs!",
"scripts/prepare.mjs!",
"scripts/verify-windows-native-artifact.mjs!"
],
"project": ["src/**/*.ts!", "!src/**/*.test.ts", "!src/**/*.fixture.ts"],
"ignoreExportsUsedInFile": true,
"include": [
"files",
"exports",
"nsExports",
"types",
"nsTypes",
"enumMembers",
"namespaceMembers",
"duplicates"
]
}
+1 -36
View File
@@ -2,7 +2,7 @@ REA npm dependency inventory
Prepared from the root package-lock.json; refresh this inventory when locked dependencies change.
License identifiers are upstream lockfile declarations, not a legal assessment.
Runtime includes transitive and optional platform packages; development includes build, test and website tooling.
Locked package entries: 963; runtime: 180.
Locked package entries: 956; runtime: 180.
Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
@algolia/abtesting@1.25.0 | development | MIT
@@ -1100,36 +1100,6 @@ commander@13.1.0 | runtime | MIT
Package: https://www.npmjs.com/package/@oxfmt/binding-win32-x64-msvc/v/0.72.0
Archive: https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.72.0.tgz
Integrity: sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw==
@oxlint-tsgolint/darwin-arm64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/darwin-arm64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/darwin-arm64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2005.tgz
Integrity: sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug==
@oxlint-tsgolint/darwin-x64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/darwin-x64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/darwin-x64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2005.tgz
Integrity: sha512-4kUfLRNYoVpv0Tm3jPYrvxyfOc/LmdvFC3pKZ4tL6K7lA7K/VS81SAtmxRzbYXYSyb3SBWhwsuX+df4neqzxxA==
@oxlint-tsgolint/linux-arm64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/linux-arm64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/linux-arm64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2005.tgz
Integrity: sha512-rhUNcPsBlKvhXtrbhbBUidtRBRdJ8fbEHN9H5Q5N59gby18ruGuDA7KUxl7oRnUzp797uykYQhkBDm+0KpfK/Q==
@oxlint-tsgolint/linux-x64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/linux-x64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/linux-x64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2005.tgz
Integrity: sha512-tKs9yinDQZOHygM0e8EJnjTU3mzJtdfkOZP6Udu82iLMePUSHOhTJ9zzU/mSd1hQxURI4LTRRiFhfzC+PzwdNA==
@oxlint-tsgolint/win32-arm64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/win32-arm64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/win32-arm64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2005.tgz
Integrity: sha512-VYPnu1gjfbSv+ROhuyNfMs382D4zQU2ZRc8Uc3nuEZo1k2kf+1KfTrg4hrlWh73h9GI+dUWyo+DYCQzUBlPwcQ==
@oxlint-tsgolint/win32-x64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/win32-x64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/win32-x64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2005.tgz
Integrity: sha512-ekU89cTGy9fpQzuvxIsFHrnoP8XoFjGIRgPEW6eqgY2NYjGy9pwpjF79o04WXRMJFKRrcaVqkZXUUBdLsqpQDw==
@oxlint/binding-android-arm-eabi@1.87.0 | development, optional | MIT
Lockfile path: node_modules/@oxlint/binding-android-arm-eabi
Package: https://www.npmjs.com/package/@oxlint/binding-android-arm-eabi/v/1.87.0
@@ -3570,11 +3540,6 @@ oxlint@1.87.0 | development | MIT
Package: https://www.npmjs.com/package/oxlint/v/1.87.0
Archive: https://registry.npmjs.org/oxlint/-/oxlint-1.87.0.tgz
Integrity: sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg==
oxlint-tsgolint@7.0.2005 | development | MIT
Lockfile path: node_modules/oxlint-tsgolint
Package: https://www.npmjs.com/package/oxlint-tsgolint/v/7.0.2005
Archive: https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2005.tgz
Integrity: sha512-6ccPGu4u4J9bwYPHXBFPee3RZrX3BboRdU7k14zRDDB1lvN3O7J/vPRFdaEfa1+CWx4xnQQOp4ctoaF66oO0uw==
p-limit@2.3.0 | development | MIT
Lockfile path: node_modules/p-limit
Package: https://www.npmjs.com/package/p-limit/v/2.3.0
-103
View File
@@ -73,7 +73,6 @@
"lint-staged": "16.4.0",
"oxfmt": "0.72.0",
"oxlint": "1.87.0",
"oxlint-tsgolint": "7.0.2005",
"release-please": "17.3.0",
"rolldown": "1.2.12",
"tsrs": "0.18.0-ts7.1.0-dev.20260929",
@@ -3492,90 +3491,6 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxlint-tsgolint/darwin-arm64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2005.tgz",
"integrity": "sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@oxlint-tsgolint/darwin-x64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2005.tgz",
"integrity": "sha512-4kUfLRNYoVpv0Tm3jPYrvxyfOc/LmdvFC3pKZ4tL6K7lA7K/VS81SAtmxRzbYXYSyb3SBWhwsuX+df4neqzxxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@oxlint-tsgolint/linux-arm64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2005.tgz",
"integrity": "sha512-rhUNcPsBlKvhXtrbhbBUidtRBRdJ8fbEHN9H5Q5N59gby18ruGuDA7KUxl7oRnUzp797uykYQhkBDm+0KpfK/Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxlint-tsgolint/linux-x64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2005.tgz",
"integrity": "sha512-tKs9yinDQZOHygM0e8EJnjTU3mzJtdfkOZP6Udu82iLMePUSHOhTJ9zzU/mSd1hQxURI4LTRRiFhfzC+PzwdNA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxlint-tsgolint/win32-arm64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2005.tgz",
"integrity": "sha512-VYPnu1gjfbSv+ROhuyNfMs382D4zQU2ZRc8Uc3nuEZo1k2kf+1KfTrg4hrlWh73h9GI+dUWyo+DYCQzUBlPwcQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@oxlint-tsgolint/win32-x64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2005.tgz",
"integrity": "sha512-ekU89cTGy9fpQzuvxIsFHrnoP8XoFjGIRgPEW6eqgY2NYjGy9pwpjF79o04WXRMJFKRrcaVqkZXUUBdLsqpQDw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@oxlint/binding-android-arm-eabi": {
"version": "1.87.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.87.0.tgz",
@@ -10222,24 +10137,6 @@
}
}
},
"node_modules/oxlint-tsgolint": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2005.tgz",
"integrity": "sha512-6ccPGu4u4J9bwYPHXBFPee3RZrX3BboRdU7k14zRDDB1lvN3O7J/vPRFdaEfa1+CWx4xnQQOp4ctoaF66oO0uw==",
"dev": true,
"license": "MIT",
"bin": {
"tsgolint": "bin/tsgolint.js"
},
"optionalDependencies": {
"@oxlint-tsgolint/darwin-arm64": "7.0.2005",
"@oxlint-tsgolint/darwin-x64": "7.0.2005",
"@oxlint-tsgolint/linux-arm64": "7.0.2005",
"@oxlint-tsgolint/linux-x64": "7.0.2005",
"@oxlint-tsgolint/win32-arm64": "7.0.2005",
"@oxlint-tsgolint/win32-x64": "7.0.2005"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+4 -7
View File
@@ -112,7 +112,7 @@
"prebuild": "npm run deps:check",
"build": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:skills --force",
"precheck": "npm run deps:check",
"check": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards && npm run knip:production && npm run lint:typed",
"check": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards",
"precheck:fast": "npm run deps:check",
"check:fast": "node scripts/run-turbo.mjs run typecheck lint --output-logs=errors-only",
"precheck:changed": "npm run deps:check",
@@ -120,14 +120,14 @@
"precheck:test": "npm run deps:check",
"check:test": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check",
"check:pr": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos && npm run knip:production && npm run lint:typed",
"check:pr": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos",
"precheck:ci": "npm run deps:check",
"precheck:ci:fast": "npm run deps:check",
"precheck:ci:deep": "npm run deps:check",
"check:ci": "npm run check:ci:fast && npm run check:ci:deep",
"check:ci:fast": "node scripts/run-exclusive.mjs artifacts npm run check:ci:fast:tasks",
"check:ci:fast:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check",
"check:ci:deep": "node scripts/run-exclusive.mjs artifacts npm run check:ci:deep:tasks && npm run verify:architecture-guards && npm run verify:test-discovery && npm run test:python && npm run knip:production && npm run lint:typed",
"check:ci:deep": "node scripts/run-exclusive.mjs artifacts npm run check:ci:deep:tasks && npm run verify:architecture-guards && npm run verify:test-discovery && npm run test:python",
"check:ci:deep:tasks": "node scripts/run-turbo.mjs run knip metadata:check jscpd scan:todos",
"check:ci:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check jscpd scan:todos",
"ci:static-context": "node scripts/ci/static-task-context.mjs",
@@ -326,9 +326,7 @@
"docs:prepare": "node scripts/generate-package-metadata.mjs && node scripts/run-turbo.mjs run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema",
"verify:web:qlog": "npm run build:cached && node scripts/verify-qlog-captures.mjs",
"licenses:generate": "node scripts/generate-third-party-inventory.mjs",
"licenses:check": "node scripts/generate-third-party-inventory.mjs --check",
"lint:typed": "node scripts/ci/static-check-report.mjs typed-lint",
"knip:production": "node scripts/ci/production-reachability-report.mjs"
"licenses:check": "node scripts/generate-third-party-inventory.mjs --check"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
@@ -406,7 +404,6 @@
"lint-staged": "16.4.0",
"oxfmt": "0.72.0",
"oxlint": "1.87.0",
"oxlint-tsgolint": "7.0.2005",
"release-please": "17.3.0",
"rolldown": "1.2.12",
"tsrs": "0.18.0-ts7.1.0-dev.20260929",
+1 -1
View File
@@ -108,7 +108,7 @@ function classifyPackage(base, head) {
continue;
}
if (
/^(test(?::|$)|check(?::|$)|lint(?::|$)|format(?::|$)|knip(?::|$)|jscpd$)/.test(
/^(test(?::|$)|check(?::|$)|lint(?::|$)|format(?::|$)|knip$|jscpd$)/.test(
script,
)
)
@@ -1,33 +0,0 @@
export interface ProductionReachabilityFinding {
readonly name: string;
readonly namespace?: string;
readonly kind?: string;
readonly specifier?: string;
readonly line?: number;
readonly col?: number;
readonly pos?: number;
}
export interface ProductionReachabilityIssue {
readonly file: string;
readonly owners?: readonly { readonly name: string }[];
readonly files?: readonly ProductionReachabilityFinding[];
readonly exports?: readonly ProductionReachabilityFinding[];
readonly nsExports?: readonly ProductionReachabilityFinding[];
readonly types?: readonly ProductionReachabilityFinding[];
readonly nsTypes?: readonly ProductionReachabilityFinding[];
readonly enumMembers?: readonly ProductionReachabilityFinding[];
readonly namespaceMembers?: readonly ProductionReachabilityFinding[];
readonly duplicates?: readonly (readonly ProductionReachabilityFinding[])[];
}
export interface ProductionReachabilityReport {
readonly issues: readonly ProductionReachabilityIssue[];
}
export function parseProductionReachabilityReport(
text: string,
): ProductionReachabilityReport;
export function recordProductionReachabilityReport(
root?: string,
): Promise<ProductionReachabilityReport>;
@@ -1,86 +0,0 @@
import { spawnSync } from "node:child_process";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod";
const require = createRequire(import.meta.url);
const finding = z.object({
name: z.string(),
namespace: z.string().optional(),
kind: z.string().optional(),
specifier: z.string().optional(),
line: z.number().optional(),
col: z.number().optional(),
pos: z.number().optional(),
});
const issue = z.strictObject({
file: z.string(),
owners: z.array(z.object({ name: z.string() })).optional(),
files: z.array(finding).optional(),
exports: z.array(finding).optional(),
nsExports: z.array(finding).optional(),
types: z.array(finding).optional(),
nsTypes: z.array(finding).optional(),
enumMembers: z.array(finding).optional(),
namespaceMembers: z.array(finding).optional(),
duplicates: z.array(z.array(finding)).optional(),
});
const reportSchema = z.strictObject({ issues: z.array(issue) });
/** Admit the installed Knip JSON reporter's candidate records. */
export const parseProductionReachabilityReport = (text) =>
reportSchema.parse(JSON.parse(text));
/** Run production reachability without turning candidates into failures. */
export const recordProductionReachabilityReport = async (
root = process.cwd(),
) => {
const reportDirectory = join(root, ".cache", "static-checks");
const reportPath = join(reportDirectory, "production-reachability.json");
// Findings are advisory. Execution, configuration and report-admission failures
// still fail, and must never leave an older successful report at this path.
await mkdir(reportDirectory, { recursive: true });
await rm(reportPath, { force: true });
const result = spawnSync(
process.execPath,
[
join(dirname(require.resolve("knip")), "..", "bin", "knip.js"),
"--config",
"knip.production.json",
"--production",
"--reporter",
"json",
"--no-exit-code",
],
{ cwd: root, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
);
if (result.stderr) process.stderr.write(result.stderr);
if (result.error !== undefined) throw result.error;
if (result.status !== 0) {
if (result.stdout) process.stderr.write(result.stdout);
throw new Error(
result.status === null
? `Production reachability analysis terminated by ${result.signal}`
: `Production reachability analysis failed with status ${result.status}`,
);
}
const report = parseProductionReachabilityReport(result.stdout);
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
console.log(
`Production reachability: ${report.issues.length} files with advisory candidates.`,
);
console.log(`Full report: ${reportPath}`);
console.log(
"Candidates require runtime, verifier and published-contract review before removal; ordinary Knip remains separate.",
);
return report;
};
if (
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
await recordProductionReachabilityReport();
-3
View File
@@ -144,9 +144,6 @@ export const SCOPES = {
"scripts/windows-capabilities.mjs",
"scripts/prepack.mjs",
"scripts/prepare.mjs",
"scripts/lib/release-proposal.mjs",
"patches/release-please+17.3.0.patch",
"tests/boundary/filesystem/releaseProposal.test.ts",
"scripts/rea.mjs",
"scripts/lib/cli-process-environment.mjs",
"install.sh",
+11 -35
View File
@@ -48,12 +48,7 @@ const duplicateReport = z
})
.passthrough();
const run = (
packageName,
entrypoint,
arguments_,
environment = process.env,
) => {
const run = (packageName, entrypoint, arguments_) => {
const executable = join(
dirname(require.resolve(`${packageName}/package.json`)),
entrypoint,
@@ -61,7 +56,6 @@ const run = (
const result = spawnSync(process.execPath, [executable, ...arguments_], {
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
env: environment,
});
if (result.stderr) process.stderr.write(result.stderr);
if (result.error !== undefined) throw result.error;
@@ -72,30 +66,14 @@ const run = (
try {
await mkdir(reportRoot, { recursive: true });
if (mode === "lint" || mode === "typed-lint") {
const typed = mode === "typed-lint";
const reportPath = join(
reportRoot,
typed ? "typed-oxlint.json" : "oxlint.json",
);
if (mode === "lint") {
const reportPath = join(reportRoot, "oxlint.json");
await rm(reportPath, { force: true });
const result = run(
"oxlint",
"bin/oxlint",
[
...(typed ? ["--type-aware", "--threads", "1"] : []),
...extraArguments,
"--format",
"json",
],
typed
? {
...process.env,
GOMAXPROCS: process.env.GOMAXPROCS ?? "2",
GOMEMLIMIT: process.env.GOMEMLIMIT ?? "1024MiB",
}
: process.env,
);
const result = run("oxlint", "bin/oxlint", [
...extraArguments,
"--format",
"json",
]);
let report;
try {
report = lintReport.parse(JSON.parse(result.stdout));
@@ -111,7 +89,7 @@ try {
({ severity }) => severity === "error",
);
console.log(
`${typed ? "Typed lint" : "Lint"}: ${report.number_of_files} files, ${errors.length} errors, ${warnings.length} warnings.`,
`Lint: ${report.number_of_files} files, ${errors.length} errors, ${warnings.length} warnings.`,
);
for (const diagnostic of errors) {
const location = diagnostic.labels[0]?.span;
@@ -128,9 +106,7 @@ try {
)
.slice(0, 8))
console.log(` ${rule}: ${count} warnings`);
console.log(
`Full diagnostics: ${reportPath}${typed ? "" : "; npm run lint:details"}`,
);
console.log(`Full diagnostics: ${reportPath}; npm run lint:details`);
process.exitCode = result.status;
} else if (mode === "duplicates") {
const output = join(reportRoot, "jscpd");
@@ -167,7 +143,7 @@ try {
console.log(`Full diagnostics: ${reportPath}; npm run jscpd:details`);
process.exitCode = result.status;
} else {
throw new Error("Expected lint, typed-lint or duplicates");
throw new Error("Expected lint or duplicates");
}
} catch (cause) {
console.error(cause instanceof Error ? cause.message : String(cause));
+7 -5
View File
@@ -1,5 +1,5 @@
import { createVerifierHttpServer } from "../lib/verifier-http-server.mjs";
import { once } from "node:events";
import { createServer } from "node:http";
import { gzipSync } from "node:zlib";
export const networkFixtureSecret = "私密-network-fixture";
@@ -23,7 +23,7 @@ const html = `<!doctype html><meta charset="utf-8"><body><button id="start">Run<
export async function startBrowserNetworkSite() {
const counts = new Map();
const timers = new Set();
const handleRequest = async (request, response) => {
const server = createServer(async (request, response) => {
const path = new URL(request.url, "http://fixture.test").pathname;
counts.set(path, (counts.get(path) ?? 0) + 1);
if (path === "/same") {
@@ -89,8 +89,7 @@ export async function startBrowserNetworkSite() {
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
response.end(html);
}
};
const { server, close } = createVerifierHttpServer(handleRequest);
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
@@ -101,7 +100,10 @@ export async function startBrowserNetworkSite() {
counts,
async close() {
for (const timer of timers) clearTimeout(timer);
await close();
server.closeAllConnections();
await new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
},
};
}
+9 -5
View File
@@ -1,5 +1,5 @@
import { createVerifierHttpServer } from "../lib/verifier-http-server.mjs";
import { once } from "node:events";
import { createServer } from "node:http";
/** The CSRF cookie and the stored bearer token the page holds; request steps must send both and the capture must not keep them. */
export const requestFixtureCsrf = "csrf-fixture-7c1d2e";
@@ -52,7 +52,7 @@ export async function startBrowserRequestSite() {
};
const writes = [];
const hosts = [];
const handleRequest = async (request, response) => {
const server = createServer(async (request, response) => {
const url = new URL(request.url, "http://fixture.test");
hosts.push(`${request.headers.host} ${url.pathname}`);
const chunks = [];
@@ -108,8 +108,7 @@ export async function startBrowserRequestSite() {
}
const [status, value] = answerItems(store, request.method, id, body);
json(response, status, value);
};
const { server, close } = createVerifierHttpServer(handleRequest);
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
@@ -121,6 +120,11 @@ export async function startBrowserRequestSite() {
/** Host header and path of every request the server received. */
hosts,
items: store.items,
close,
async close() {
server.closeAllConnections();
await new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
},
};
}
@@ -1,18 +0,0 @@
import { tsImport } from "tsx/esm/api";
const { agentEvaluationPassed } = await tsImport(
new URL(
"../../tests/evaluation/model-evals/AgentEvaluationReport.ts",
import.meta.url,
).href,
import.meta.url,
);
/** Fail the runner after publishing a report with any rejected scenario. */
export const assertAgentEvaluationReportPassed = (results) => {
const failed = results.filter((result) => !agentEvaluationPassed(result));
if (failed.length > 0)
throw new Error(
`Agent routing/workflow/answer checks failed: ${failed.map(({ id }) => id).join(", ")}`,
);
};
-1
View File
@@ -123,7 +123,6 @@ export const toolFamilyCatalog = (sources) => {
surface: "application-workflow",
contracts: [
...sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
...sources.analysisSettingsContracts.ANALYSIS_SETTINGS_TOOL_CONTRACTS,
...sources.analysisViewContracts.ANALYSIS_VIEW_TOOL_CONTRACTS,
...sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS,
...sources.javascriptRecoveryContracts
-1
View File
@@ -41,7 +41,6 @@ const SOURCE_PATHS = {
officialContracts: "dist/contracts/officialToolContracts.js",
enhancedContracts: "dist/contracts/enhancedToolContracts.js",
sessionContracts: "dist/contracts/sessionToolContracts.js",
analysisSettingsContracts: "dist/contracts/analysisSettingsContracts.js",
binaryDiagnosticsContracts:
"dist/contracts/native/binaryDiagnosticsToolContracts.js",
cutterContracts: "dist/contracts/cutterToolContracts.js",
+1 -3
View File
@@ -529,11 +529,9 @@ export async function verifyGhidraBoundaries(
regexError.remediation.action,
/literal mode or simplify the regex/u,
);
// The retry advice lives in the remediation; the reason states that the
// session survived the overflow.
assert.match(
regexError.details.reason,
/exhausted its stack.*session and annotations remain available/u,
/exhausted its stack.*Use literal mode/u,
);
rejectedCalls++;
const nestedPattern = "(".repeat(12000) + "a" + ")".repeat(12000);
+2 -8
View File
@@ -1,7 +1,7 @@
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, relative, sep } from "node:path";
import { dirname, relative } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { requireCompleteReleaseHistory } from "./release-notes.mjs";
@@ -24,13 +24,7 @@ export async function applyReleasePleasePatch(
"The release proposal patch requires release-please 17.3.0",
);
}
// Git prefixes patch filenames with a slash-separated path, even on Windows.
// Split only native separators so POSIX backslashes remain filename bytes.
const args = [
"--directory",
relative(project, directory).split(sep).join("/"),
patch,
];
const args = ["--directory", relative(project, directory), patch];
const check = async (reverse) => {
try {
await execute(
-1
View File
@@ -1,7 +1,6 @@
export interface TestProjectDefinition {
readonly name: string;
readonly group: "source" | "built";
readonly isolate?: boolean;
readonly include: readonly string[];
readonly exclude?: readonly string[];
}
-7
View File
@@ -5,13 +5,11 @@ export const SOURCE_TEST_PROJECTS = [
{
name: "domain",
group: "source",
isolate: false,
include: ["src/{contracts,domain}/**/*.test.ts"],
},
{
name: "services",
group: "source",
isolate: false,
include: ["src/application/**/*.test.ts"],
},
{
@@ -25,20 +23,17 @@ export const SOURCE_TEST_PROJECTS = [
{
name: "composition",
group: "source",
isolate: false,
include: ["tests/composition/**/*.test.ts"],
},
{
name: "conformance",
group: "source",
isolate: false,
include: ["tests/conformance/**/*.test.ts"],
exclude: ["tests/conformance/providers/hopper/realHopperSearch.test.ts"],
},
{
name: "evaluation",
group: "source",
isolate: false,
include: ["tests/evaluation/**/*.test.ts"],
},
];
@@ -60,7 +55,6 @@ export const BUILT_TEST_PROJECTS = [
{
name: "cli-boundary",
group: "built",
isolate: false,
include: ["tests/boundary/cli/**/*.test.ts"],
},
{
@@ -71,7 +65,6 @@ export const BUILT_TEST_PROJECTS = [
{
name: "mcp-boundary",
group: "built",
isolate: false,
include: ["tests/boundary/mcp/**/*.test.ts"],
},
{
-12
View File
@@ -1,12 +0,0 @@
import type { IncomingMessage, Server, ServerResponse } from "node:http";
/** Own admitted fixture requests through transport closure and report unexpected failures. */
export function createVerifierHttpServer(
handleRequest: (
request: IncomingMessage,
response: ServerResponse,
) => Promise<void>,
): {
readonly server: Server;
close(): Promise<void>;
};
-39
View File
@@ -1,39 +0,0 @@
import { createServer } from "node:http";
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
/** Own admitted fixture requests through transport closure and report unexpected failures. */
export function createVerifierHttpServer(handleRequest) {
const pending = new Set();
let failure;
const server = createServer((request, response) => {
const work = handleRequest(request, response)
.catch((cause) => {
failure ??= { cause };
response.destroy();
})
.finally(() => {
pending.delete(work);
});
pending.add(work);
});
return {
server,
close() {
return withVerifierCleanup(
() =>
new Promise((resolve, reject) => {
server.close((cause) =>
cause === undefined ? resolve() : reject(cause),
);
server.closeAllConnections();
}),
[
async () => {
await Promise.all(pending);
if (failure !== undefined) throw failure.cause;
},
],
);
},
};
}
-12
View File
@@ -1,12 +0,0 @@
/** Resolve the lexical binding instead of treating a matching name as an API. */
export const findBinding = (sourceCode, identifier) => {
for (
let scope = sourceCode.getScope(identifier);
scope !== null;
scope = scope.upper
) {
const variable = scope.set.get(identifier.name);
if (variable !== undefined) return variable;
}
return undefined;
};
-4
View File
@@ -1,4 +0,0 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const cancellationCauseRule: Parameters<RuleTester["run"]>[1];
export default cancellationCauseRule;
-200
View File
@@ -1,200 +0,0 @@
import { findBinding } from "./bindings.mjs";
/**
* Require an explicit cancellation reason when a directly checked caller signal
* is converted to an AnalysisCancelledError. This is deliberately syntactic:
* internal shutdown and cancellation forwarded through other owners are outside
* the rule.
*/
const coreModule = /(?:^|\/)analysisErrorCore(?:\.js|\.ts)?$/;
const unwrapChain = (node) =>
node?.type === "ChainExpression" ? node.expression : node;
const propertyName = (property) =>
property.computed
? property.property.type === "Literal"
? property.property.value
: undefined
: property.property.name;
const annotationType = (node) => {
const annotation = node?.typeAnnotation?.typeAnnotation;
return annotation?.type === "TSTypeReference" &&
annotation.typeName.type === "Identifier"
? annotation.typeName
: undefined;
};
const soleDefinition = (variable) =>
variable?.defs.length === 1 ? variable.defs[0] : undefined;
const isKnownType = (node, kind, context) => {
const type = annotationType(node);
if (type === undefined) return false;
const definition = soleDefinition(findBinding(context.sourceCode, type));
if (kind === "AbortSignal")
return type.name === "AbortSignal" && definition === undefined;
return (
definition?.type === "ImportBinding" &&
definition.parent?.type === "ImportDeclaration" &&
/(?:^|\/)AnalysisProvider(?:\.js|\.ts)?$/.test(
definition.parent.source.value,
) &&
definition.node.type === "ImportSpecifier" &&
definition.node.imported.name === "ExecutionOptions"
);
};
const unmodified = (variable) =>
variable !== undefined &&
variable.references.every(
(reference) => !reference.isWrite() || reference.init,
);
const isOptionsSignal = (node, context, seen) => {
node = unwrapChain(node);
if (node?.type !== "MemberExpression" || propertyName(node) !== "signal")
return false;
return isKnownBinding(node.object, "ExecutionOptions", context, seen);
};
const isKnownBinding = (node, kind, context, seen) => {
if (node?.type !== "Identifier") return false;
const variable = findBinding(context.sourceCode, node);
if (!unmodified(variable) || seen.has(variable)) return false;
seen.add(variable);
const definition = soleDefinition(variable);
if (definition?.type === "Parameter")
return isKnownType(definition.name, kind, context);
if (definition?.type !== "Variable" || definition.parent?.kind !== "const")
return false;
const declaration = definition.node;
if (isKnownType(declaration.id, kind, context)) return true;
return kind === "AbortSignal"
? isKnownSignal(declaration.init, context, seen)
: false;
};
const isKnownSignal = (node, context, seen = new Set()) =>
isKnownBinding(unwrapChain(node), "AbortSignal", context, seen) ||
isOptionsSignal(node, context, seen);
const isAbortRead = (node, context) => {
node = unwrapChain(node);
return (
node?.type === "MemberExpression" &&
propertyName(node) === "aborted" &&
isKnownSignal(node.object, context)
);
};
const isTrueLiteral = (node) => node?.type === "Literal" && node.value === true;
const positiveAbortGuard = (test, context) => {
if (isAbortRead(test, context)) return true;
if (
test?.type === "BinaryExpression" &&
(test.operator === "===" || test.operator === "==")
)
return (
(isAbortRead(test.left, context) && isTrueLiteral(test.right)) ||
(isTrueLiteral(test.left) && isAbortRead(test.right, context))
);
if (test?.type === "LogicalExpression" && test.operator === "&&")
return (
positiveAbortGuard(test.left, context) ||
positiveAbortGuard(test.right, context)
);
return false;
};
const isCanonicalConstructor = (node, context) => {
if (node?.type !== "Identifier") return false;
const definition = soleDefinition(findBinding(context.sourceCode, node));
return (
definition?.type === "ImportBinding" &&
definition.parent?.type === "ImportDeclaration" &&
coreModule.test(definition.parent.source.value) &&
definition.node.type === "ImportSpecifier" &&
definition.node.imported.name === "AnalysisCancelledError"
);
};
const hasExplicitCause = (node) => {
const options = node.arguments[1];
if (options !== undefined && options.type !== "ObjectExpression") return true; // Forwarded context is owned elsewhere.
if (options === undefined) return false;
return options.properties.some(
(property) =>
property.type === "SpreadElement" ||
(property.type === "Property" &&
(property.computed
? property.key.type !== "Literal" || property.key.value === "cause"
: (property.key.type === "Identifier"
? property.key.name
: property.key.value) === "cause")),
);
};
const inspectExpression = (expression, context) => {
if (expression?.type === "NewExpression") {
if (
isCanonicalConstructor(expression.callee, context) &&
!hasExplicitCause(expression)
)
context.report({
node: expression,
messageId: "missingCause",
});
return;
}
if (expression?.type === "CallExpression") {
// Common Result adapter: return err(new AnalysisCancelledError(...)).
for (const argument of expression.arguments)
inspectExpression(argument, context);
} else if (expression?.type === "AssignmentExpression") {
inspectExpression(expression.right, context);
}
};
const inspectBranch = (branch, context) => {
if (branch.type === "BlockStatement") {
for (const statement of branch.body) inspectBranch(statement, context);
} else if (branch.type === "VariableDeclaration") {
for (const declaration of branch.declarations)
inspectExpression(declaration.init, context);
} else if (
branch.type === "ThrowStatement" ||
branch.type === "ReturnStatement"
) {
inspectExpression(branch.argument, context);
} else if (branch.type === "ExpressionStatement") {
inspectExpression(branch.expression, context);
}
};
const cancellationCauseRule = {
meta: {
type: "problem",
docs: {
description:
"Preserve a directly checked caller AbortSignal reason in AnalysisCancelledError",
},
messages: {
missingCause:
"Pass the checked AbortSignal's reason as an explicit cancellation cause.",
},
},
create(context) {
return {
IfStatement(node) {
if (positiveAbortGuard(node.test, context))
inspectBranch(node.consequent, context);
},
};
},
};
export default cancellationCauseRule;
-19
View File
@@ -1,19 +0,0 @@
import noBufferDeepEquality from "./no-buffer-deep-equality.mjs";
import cancellationCause from "./cancellation-cause.mjs";
import noEvidenceJsonPreadmission from "./rules/no-evidence-json-preadmission.mjs";
import noFixtureRootCleanup from "./rules/no-fixture-root-cleanup.mjs";
import noPositionalToolContract from "./rules/no-positional-tool-contract.mjs";
import noSharedModuleMock from "./rules/no-shared-module-mock.mjs";
export default {
meta: { name: "rea" },
rules: {
"no-buffer-deep-equality": noBufferDeepEquality,
"cancellation-cause": cancellationCause,
"no-evidence-json-preadmission": noEvidenceJsonPreadmission,
"no-fixture-root-cleanup": noFixtureRootCleanup,
"no-positional-tool-contract": noPositionalToolContract,
"no-shared-module-mock": noSharedModuleMock,
},
};
@@ -1,4 +0,0 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const noBufferDeepEquality: Parameters<RuleTester["run"]>[1];
export default noBufferDeepEquality;
-166
View File
@@ -1,166 +0,0 @@
import { findBinding } from "./bindings.mjs";
const memberName = (node) =>
node?.type === "MemberExpression" &&
!node.computed &&
node.property.type === "Identifier"
? node.property.name
: null;
const unwrap = (node) => {
while (
node?.type === "TSAsExpression" ||
node?.type === "TSSatisfiesExpression" ||
node?.type === "TSNonNullExpression" ||
node?.type === "TSTypeAssertion" ||
node?.type === "ParenthesizedExpression"
)
node = node.expression;
return node;
};
const importedKind = (variable) => {
const definition = variable?.defs[0];
if (definition?.type !== "ImportBinding") return null;
const source = definition.parent?.source?.value;
const node = definition.node;
if (node?.type === "ImportNamespaceSpecifier") {
if (source === "node:fs/promises") return "fsPromises";
if (source === "node:fs") return "fs";
return null;
}
if (node?.type !== "ImportSpecifier") return null;
const imported = node.imported?.name ?? node.imported?.value;
if (source === "vitest" && imported === "expect") return "expect";
if (source === "node:buffer" && imported === "Buffer") return "Buffer";
if (source === "node:fs/promises" && imported === "readFile")
return "readFile";
if (source === "node:fs" && imported === "readFileSync")
return "readFileSync";
return null;
};
const constInitializer = (variable) => {
if (variable?.defs.length !== 1) return null;
const definition = variable.defs[0];
if (
definition?.type !== "Variable" ||
definition.node?.type !== "VariableDeclarator" ||
definition.parent?.kind !== "const" ||
variable.references.some(
(reference) => reference.isWrite() && !reference.init,
)
)
return null;
return definition.node.init;
};
/** Flag byte comparisons that make Vitest enumerate a Node Buffer. */
const noBufferDeepEquality = {
meta: {
type: "suggestion",
messages: {
nativeBytes:
"Compare Node Buffers with Buffer.equals or Buffer.compare; deep equality can enumerate every byte and exhaust memory.",
},
},
create(context) {
const { sourceCode } = context;
const symbolKind = (expression, seen = new Set()) => {
const node = unwrap(expression);
if (node?.type === "Identifier") {
const variable = findBinding(sourceCode, node);
if (
(variable === undefined || variable.defs.length === 0) &&
sourceCode.isGlobalReference(node)
)
return node.name === "Buffer" ? "Buffer" : null;
const imported = importedKind(variable);
if (imported !== null) return imported;
if (variable === undefined || seen.has(variable)) return null;
const initializer = constInitializer(variable);
if (initializer === null) return null;
seen.add(variable);
return symbolKind(initializer, seen);
}
const property = memberName(node);
if (property === null) return null;
const owner = symbolKind(node.object, seen);
if (owner === "fsPromises" && property === "readFile") return "readFile";
if (owner === "fs" && property === "readFileSync") return "readFileSync";
if (
owner === "Buffer" &&
["from", "alloc", "allocUnsafe", "allocUnsafeSlow", "concat"].includes(
property,
)
)
return "bufferFactory";
return null;
};
const isBuffer = (expression, seen = new Set(), awaited = false) => {
const node = unwrap(expression);
if (node?.type === "AwaitExpression")
return isBuffer(node.argument, seen, true);
if (node?.type === "Identifier") {
const variable = findBinding(sourceCode, node);
if (variable === undefined || seen.has(variable)) return false;
const initializer = constInitializer(variable);
if (initializer === null) return false;
seen.add(variable);
return isBuffer(initializer, seen, awaited);
}
if (node?.type !== "CallExpression" || node.optional) return false;
const kind = symbolKind(node.callee);
return (
kind === "bufferFactory" ||
((kind === "readFileSync" || (kind === "readFile" && awaited)) &&
node.arguments.length === 1 &&
node.arguments[0].type !== "SpreadElement")
);
};
const expectedCall = (object, settlement = null) => {
const node = unwrap(object);
const property = memberName(node);
if (property === "not") return expectedCall(node.object, settlement);
if (property === "resolves" || property === "rejects")
return expectedCall(node.object, property);
return node?.type === "CallExpression" &&
!node.optional &&
symbolKind(node.callee) === "expect"
? { call: node, settlement }
: null;
};
return {
CallExpression(node) {
if (node.optional) return;
const matcher = memberName(node.callee);
if (matcher !== "toEqual" && matcher !== "toStrictEqual") return;
const assertion = expectedCall(node.callee.object);
if (
assertion === null ||
![1, 2].includes(assertion.call.arguments.length) ||
assertion.call.arguments.some(
(argument) => argument.type === "SpreadElement",
) ||
node.arguments.length !== 1
)
return;
if (
(assertion.settlement !== "rejects" &&
isBuffer(
assertion.call.arguments[0],
new Set(),
assertion.settlement === "resolves",
)) ||
isBuffer(node.arguments[0])
)
context.report({
node: node.callee.property,
messageId: "nativeBytes",
});
},
};
},
};
export default noBufferDeepEquality;
-4
View File
@@ -1,4 +0,0 @@
export declare const resolveImportSource: (
filename: string,
specifier: unknown,
) => string | undefined;
-20
View File
@@ -1,20 +0,0 @@
import { dirname, isAbsolute, resolve } from "node:path";
import { fileURLToPath } from "node:url";
/** Resolve local import spellings without assigning local meaning to bare or HTTP imports. */
export const resolveImportSource = (filename, specifier) => {
if (typeof specifier !== "string") return undefined;
if (isAbsolute(specifier)) return resolve(specifier);
if (specifier.startsWith("file:")) {
try {
const url = new URL(specifier);
if (url.search !== "" || url.hash !== "") return undefined;
return fileURLToPath(url);
} catch {
return undefined;
}
}
if (specifier.startsWith("./") || specifier.startsWith("../"))
return resolve(dirname(filename), specifier);
return undefined;
};
@@ -1,4 +0,0 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const rule: Parameters<RuleTester["run"]>[1];
export default rule;
@@ -1,137 +0,0 @@
import { findBinding } from "../bindings.mjs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveImportSource } from "./import-source.mjs";
const domain = fileURLToPath(new URL("../../../src/domain/", import.meta.url));
const payloadFields = new Set(["parameters", "result", "rawResult"]);
const propertyName = (node) => {
if (!node.computed && node.property?.type === "Identifier")
return node.property.name;
if (
node.property?.type === "Literal" &&
typeof node.property.value === "string"
)
return node.property.value;
return undefined;
};
const fieldName = (node) => {
if (node.type !== "Property") return undefined;
if (!node.computed && node.key.type === "Identifier") return node.key.name;
if (node.key.type === "Literal" && typeof node.key.value === "string")
return node.key.value;
return undefined;
};
const importedBinding = (sourceCode, node) => {
if (node.type !== "Identifier") return undefined;
const variable = findBinding(sourceCode, node);
if (!variable || variable.defs.length !== 1) return undefined;
const definition = variable.defs[0];
return definition.type === "ImportBinding" ? definition : undefined;
};
const isImported = (context, node, module, names) => {
const member = node.type === "MemberExpression" && !node.optional;
const binding = importedBinding(
context.sourceCode,
member ? node.object : node,
);
if (!binding || binding.parent?.type !== "ImportDeclaration") return false;
const declaration = binding.parent;
if (declaration.importKind === "type" || binding.node.importKind === "type")
return false;
const specifier = declaration.source.value;
const target = resolveImportSource(
context.physicalFilename ?? context.filename,
specifier,
);
if (
target !== resolve(domain, `${module}.js`) &&
target !== resolve(domain, `${module}.ts`)
)
return false;
if (member)
return (
binding.node.type === "ImportNamespaceSpecifier" &&
names.has(propertyName(node))
);
if (binding.node.type !== "ImportSpecifier") return false;
return names.has(binding.node.imported.name ?? binding.node.imported.value);
};
const evidenceCreator = new Set(["createEvidence"]);
const genericJsonSchemas = new Set(["jsonValueSchema", "jsonObjectSchema"]);
export default {
meta: {
type: "suggestion",
docs: {
description:
"Leave ordinary Evidence JSON payload admission with createEvidence.",
},
schema: [],
messages: {
redundantJsonAdmission:
"Pass the {{field}} payload directly to createEvidence; it owns generic JSON admission. Keep semantic validation at its own boundary.",
},
},
create(context) {
return {
CallExpression(node) {
if (
node.optional ||
!isImported(context, node.callee, "evidence", evidenceCreator)
)
return;
const observation = node.arguments[2];
if (observation?.type !== "ObjectExpression") return;
for (const [index, property] of observation.properties.entries()) {
const field = fieldName(property);
if (
!payloadFields.has(field) ||
property.kind !== "init" ||
property.method
)
continue;
// A later spread or unknown key may replace this field before admission.
if (
observation.properties
.slice(index + 1)
.some(
(later) =>
later.type !== "Property" ||
fieldName(later) === undefined ||
fieldName(later) === field,
)
)
continue;
const parsed = property.value;
if (
parsed.type !== "CallExpression" ||
parsed.optional ||
parsed.arguments.length !== 1
)
continue;
const parse = parsed.callee;
if (
parse.type !== "MemberExpression" ||
parse.optional ||
propertyName(parse) !== "parse"
)
continue;
if (
isImported(context, parse.object, "jsonValue", genericJsonSchemas)
)
context.report({
node: parsed,
messageId: "redundantJsonAdmission",
data: { field },
});
}
},
};
},
};
@@ -1,3 +0,0 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const rule: Parameters<RuleTester["run"]>[1];
export default rule;
@@ -1,149 +0,0 @@
import { findBinding } from "../bindings.mjs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveImportSource } from "./import-source.mjs";
const root = fileURLToPath(new URL("../../../", import.meta.url));
const fixtureModule = resolve(root, "tests/fixtures/temporaryDirectory");
const workspaceModule = resolve(
root,
"tests/support/workspace/workspaceFixture",
);
const hooks = new Set(["afterEach", "afterAll", "onTestFinished"]);
const unwrap = (node) => {
while (
node &&
[
"AwaitExpression",
"TSAsExpression",
"TSNonNullExpression",
"ChainExpression",
].includes(node.type)
)
node = node.argument ?? node.expression;
return node;
};
const isFunction = (node) =>
node &&
[
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
].includes(node.type);
export default {
meta: {
type: "problem",
docs: {
description: "Let createTestTempDirectory own removal of its root.",
},
schema: [],
messages: {
duplicate:
"createTestTempDirectory already owns root cleanup; remove the duplicate teardown deletion, retaining cleanup of other resources.",
},
},
create(context) {
const sourceCode = context.sourceCode;
const filename = context.physicalFilename ?? context.filename;
const calls = [];
const variable = (node) => findBinding(sourceCode, node);
const imported = (node) => {
const member = node?.type === "MemberExpression" ? node : undefined;
const binding = member ? member.object : node;
if (binding?.type !== "Identifier") return undefined;
const definition = variable(binding)?.defs[0];
if (
definition?.type !== "ImportBinding" ||
(member
? definition.node.type !== "ImportNamespaceSpecifier"
: definition.node.type !== "ImportSpecifier")
)
return undefined;
const declaration = definition.parent ?? definition.node.parent;
const specifier = declaration.source?.value;
if (typeof specifier !== "string") return undefined;
return {
name: member
? member.computed
? member.property.value
: member.property.name
: (definition.node.imported.name ?? definition.node.imported.value),
module:
resolveImportSource(filename, specifier)?.replace(
/\.(?:[cm]?[jt]s)$/u,
"",
) ?? specifier,
};
};
const fixtureRoot = (raw, seen = new Set()) => {
const node = unwrap(raw);
if (node?.type === "CallExpression") {
const identity = imported(node.callee);
return (
identity?.module === fixtureModule &&
identity.name === "createTestTempDirectory"
);
}
if (node?.type !== "Identifier") return false;
const binding = variable(node);
if (!binding || seen.has(binding)) return false;
seen.add(binding);
const definition = binding.defs[0];
// Mutable ownership and mixed root registries require flow analysis, not
// a guess based on the variable name or one assignment.
return (
definition?.type === "Variable" &&
definition.parent.kind === "const" &&
definition.node.id.type === "Identifier" &&
fixtureRoot(definition.node.init, seen)
);
};
const callback = (raw) => {
const node = unwrap(raw);
if (isFunction(node)) return node;
if (node?.type !== "Identifier") return undefined;
const definition = variable(node)?.defs[0];
if (definition?.type === "FunctionName") return definition.node;
if (
definition?.type === "Variable" &&
definition.parent.kind === "const"
) {
const value = unwrap(definition.node.init);
if (isFunction(value)) return value;
}
return undefined;
};
return {
CallExpression(node) {
calls.push(node);
},
"Program:exit"() {
const teardowns = new Set();
for (const call of calls) {
const identity = imported(call.callee);
if (identity?.module === "vitest" && hooks.has(identity.name)) {
const body = callback(call.arguments[0]);
if (body) teardowns.add(body);
}
}
for (const call of calls) {
const identity = imported(call.callee);
if (!identity) continue;
const removesRoot =
(identity.module === "node:fs/promises" &&
["rm", "rmdir"].includes(identity.name)) ||
(identity.module === "node:fs" &&
["rm", "rmSync", "rmdir", "rmdirSync"].includes(identity.name)) ||
(identity.module === workspaceModule &&
identity.name === "removeTestWorkspace");
if (!removesRoot || !fixtureRoot(call.arguments[0])) continue;
let parent = call.parent;
while (parent && !isFunction(parent)) parent = parent.parent;
if (teardowns.has(parent))
context.report({ node: call, messageId: "duplicate" });
}
},
};
},
};
@@ -1,3 +0,0 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const rule: Parameters<RuleTester["run"]>[1];
export default rule;
@@ -1,218 +0,0 @@
import { findBinding } from "../bindings.mjs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { collectToolCatalogSources } from "./tool-catalog-sources.mjs";
import { resolveImportSource } from "./import-source.mjs";
const root = fileURLToPath(new URL("../../../", import.meta.url));
const catalogs = collectToolCatalogSources(
resolve(root, "src/contracts/toolContracts.ts"),
);
const unwrap = (node) => {
while (
node &&
[
"TSAsExpression",
"TSSatisfiesExpression",
"TSNonNullExpression",
"ChainExpression",
].includes(node.type)
)
node = node.expression;
return node;
};
const numeric = (node) =>
(node?.type === "Literal" &&
(typeof node.value === "number" ||
(typeof node.value === "string" &&
/^(?:0|[1-9][0-9]*)$/u.test(node.value)))) ||
(node?.type === "UnaryExpression" &&
["+", "-"].includes(node.operator) &&
node.argument.type === "Literal" &&
typeof node.argument.value === "number");
export default {
meta: {
type: "problem",
docs: {
description:
"Select canonical tool contracts by name, never catalog position.",
},
schema: [],
messages: {
positional:
"Select a named tool contract; catalog ordering must not select an operation or schema.",
},
},
create(context) {
const sourceCode = context.sourceCode;
const filename = context.physicalFilename ?? context.filename;
const variable = (node) => findBinding(sourceCode, node);
const imported = (node) => {
if (node?.type !== "Identifier") return undefined;
const definition = variable(node)?.defs[0];
if (definition?.type !== "ImportBinding") return undefined;
const declaration = definition.parent ?? definition.node.parent;
const specifier = declaration.source?.value;
const path = resolveImportSource(filename, specifier)?.replace(
/\.(?:[cm]?[jt]s)$/u,
"",
);
const expected = catalogs.get(path);
if (expected === undefined) return undefined;
return { definition, expected };
};
const isCatalog = (raw, seen = new Set()) => {
const node = unwrap(raw);
if (!node) return false;
if (node.type === "Identifier") {
const binding = variable(node);
if (!binding || seen.has(binding)) return false;
seen.add(binding);
const identity = imported(node);
if (identity) {
const specifier = identity.definition.node;
return (
specifier.type === "ImportSpecifier" &&
identity.expected.has(
specifier.imported.name ?? specifier.imported.value,
)
);
}
const definition = binding.defs[0];
return (
definition?.type === "Variable" &&
definition.parent.kind === "const" &&
definition.node.id.type === "Identifier" &&
isCatalog(definition.node.init, seen)
);
}
if (node.type === "MemberExpression" || node.type === "TSQualifiedName") {
const object =
node.type === "MemberExpression" ? node.object : node.left;
const identity = imported(object);
if (identity?.definition.node.type !== "ImportNamespaceSpecifier")
return false;
const property =
node.type === "MemberExpression" ? node.property : node.right;
const name = node.computed ? property.value : property.name;
return identity.expected.has(name);
}
return false;
};
const selectedContract = (raw, seen = new Set()) => {
const node = unwrap(raw);
if (!node) return undefined;
if (
node.type === "MemberExpression" &&
node.computed &&
numeric(node.property) &&
isCatalog(node.object)
)
return node;
if (node.type === "CallExpression") {
const callee = unwrap(node.callee);
if (
callee?.type === "MemberExpression" &&
(callee.computed ? callee.property.value : callee.property.name) ===
"at" &&
numeric(node.arguments[0]) &&
isCatalog(callee.object)
)
return node;
}
if (node.type !== "Identifier") return undefined;
const binding = variable(node);
if (!binding || seen.has(binding)) return undefined;
seen.add(binding);
const definition = binding.defs[0];
if (definition?.type !== "Variable" || definition.parent.kind !== "const")
return undefined;
if (definition.node.id.type === "Identifier")
return selectedContract(definition.node.init, seen);
if (
definition.node.id.type === "ArrayPattern" &&
definition.node.id.elements.some(
(element) =>
element?.type === "Identifier" && element.name === node.name,
) &&
isCatalog(definition.node.init)
)
return definition.node;
return undefined;
};
const bindingFields = new Set(["name", "inputSchema", "outputSchema"]);
const isRegistrationOptions = (node) => {
if (node?.type !== "Identifier") return false;
const definition = variable(node)?.defs[0];
if (
definition?.type !== "ImportBinding" ||
definition.node.type !== "ImportSpecifier"
)
return false;
const declaration = definition.parent ?? definition.node.parent;
const specifier = declaration.source?.value;
const path = resolveImportSource(filename, specifier)?.replace(
/\.(?:[cm]?[jt]s)$/u,
"",
);
const name =
definition.node.imported.name ?? definition.node.imported.value;
return (
path === resolve(root, "src/server/toolRegistrationOptions") &&
[
"toolRegistrationOptions",
"applicationToolRegistrationOptions",
].includes(name)
);
};
const reported = new Set();
const reportSelection = (raw) => {
const node = selectedContract(raw);
if (node && !reported.has(node)) {
reported.add(node);
context.report({ node, messageId: "positional" });
}
};
return {
MemberExpression(node) {
const name = node.computed ? node.property.value : node.property.name;
const parent = node.parent;
const writesProperty =
(parent?.type === "AssignmentExpression" && parent.left === node) ||
parent?.type === "UpdateExpression" ||
(parent?.type === "UnaryExpression" && parent.operator === "delete");
if (bindingFields.has(name) && !writesProperty)
reportSelection(node.object);
},
CallExpression(node) {
if (isRegistrationOptions(unwrap(node.callee)))
reportSelection(node.arguments[0]);
},
VariableDeclarator(node) {
if (
node.id.type === "ObjectPattern" &&
node.id.properties.some(
(property) =>
property.type === "Property" &&
bindingFields.has(
property.computed ? property.key.value : property.key.name,
),
)
)
reportSelection(node.init);
},
TSIndexedAccessType(node) {
if (
node.objectType.type === "TSTypeQuery" &&
node.indexType.type === "TSLiteralType" &&
numeric(node.indexType.literal) &&
isCatalog(node.objectType.exprName)
)
context.report({ node, messageId: "positional" });
},
};
},
};
@@ -1,4 +0,0 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const rule: Parameters<RuleTester["run"]>[1];
export default rule;
@@ -1,116 +0,0 @@
import { relative, isAbsolute } from "node:path";
import { fileURLToPath } from "node:url";
import { findBinding } from "../bindings.mjs";
import { projectForTest } from "../../lib/test-projects.mjs";
const repository = fileURLToPath(new URL("../../../", import.meta.url));
const mockMethods = new Set(["mock", "doMock"]);
const unwrap = (node) =>
node?.type === "ChainExpression" ? node.expression : node;
const propertyName = (member) => {
if (!member?.computed && member?.property?.type === "Identifier")
return member.property.name;
if (member?.computed && member.property?.type === "Literal")
return member.property.value;
return undefined;
};
const importedFromVitest = (sourceCode, identifier) => {
const binding = findBinding(sourceCode, identifier);
const definition = binding?.defs[0];
if (definition?.type !== "ImportBinding") return undefined;
const declaration = definition.parent ?? definition.node.parent;
if (declaration?.source?.value !== "vitest") return undefined;
return definition.node;
};
const isViImport = (sourceCode, identifier) => {
const specifier = importedFromVitest(sourceCode, identifier);
return (
specifier?.type === "ImportSpecifier" &&
(specifier.imported.name ?? specifier.imported.value) === "vi"
);
};
const isVitestNamespace = (sourceCode, identifier) =>
importedFromVitest(sourceCode, identifier)?.type ===
"ImportNamespaceSpecifier";
const isVitestViMember = (sourceCode, node) => {
const member = unwrap(node);
if (
member?.type !== "MemberExpression" ||
propertyName(member) !== "vi" ||
member.object?.type !== "Identifier"
)
return false;
return isVitestNamespace(sourceCode, member.object);
};
const isModuleMockCall = (sourceCode, callee) => {
const method = unwrap(callee);
if (
method?.type !== "MemberExpression" ||
!mockMethods.has(propertyName(method))
)
return false;
const receiver = unwrap(method.object);
return (
(receiver?.type === "Identifier" && isViImport(sourceCode, receiver)) ||
isVitestViMember(sourceCode, receiver)
);
};
const isolationForFile = (filename, cwd) => {
if (typeof filename !== "string" || !filename.endsWith(".test.ts"))
return undefined;
const testPath = relative(cwd, filename).split("\\").join("/");
if (
testPath === "" ||
isAbsolute(testPath) ||
testPath === ".." ||
testPath.startsWith("../")
)
return undefined;
try {
return projectForTest(testPath).isolate !== false;
} catch (error) {
if (
error instanceof Error &&
error.message.startsWith("No test project owns ")
)
return undefined;
throw error;
}
};
export default {
meta: {
type: "problem",
docs: {
description:
"Keep Vitest module mocks in projects with per-file module isolation.",
},
schema: [],
messages: {
shared:
"Vitest module mocks need an isolated test project; move the test to an isolated project or replace the module mock with a scoped spy or injected dependency.",
},
},
create(context) {
const filename = context.physicalFilename ?? context.filename;
const sourceCode = context.sourceCode;
const cwd =
context.cwd ?? context.getCwd?.() ?? sourceCode.getCwd?.() ?? repository;
if (isolationForFile(filename, cwd) !== false) return {};
return {
CallExpression(node) {
if (isModuleMockCall(sourceCode, node.callee))
context.report({ node, messageId: "shared" });
},
};
},
};
@@ -1,4 +0,0 @@
export declare const collectToolCatalogSources: (
entryPath: string,
readSource?: (path: string) => string,
) => ReadonlyMap<string, ReadonlySet<string>>;
-118
View File
@@ -1,118 +0,0 @@
import { parse } from "@babel/parser";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
const withoutExtension = (path) => path.replace(/\.(?:[cm]?[jt]s)$/u, "");
const arrayExpression = (raw) => {
let node = raw;
while (
node &&
["TSAsExpression", "TSSatisfiesExpression"].includes(node.type)
)
node = node.expression;
return node;
};
/** Read only source composition, never execute the runtime contract catalog. */
export const collectToolCatalogSources = (
entryPath,
readSource = (path) => readFileSync(path, "utf8"),
) => {
const catalogs = new Map();
const modules = new Map();
const active = new Set();
const complete = new Set();
const moduleFacts = (path) => {
let facts = modules.get(path);
if (facts) return facts;
// Read/parse errors intentionally escape: a broken inventory cannot disable
// the rule by presenting an empty set of catalog bindings.
const ast = parse(readSource(path), {
sourceType: "module",
plugins: ["typescript"],
});
const imports = new Map();
const exports = new Map();
const locals = new Map();
for (const statement of ast.program.body) {
if (statement.type === "ImportDeclaration") {
for (const specifier of statement.specifiers) {
if (specifier.type === "ImportSpecifier")
imports.set(specifier.local.name, {
source: statement.source.value,
name: specifier.imported.name ?? specifier.imported.value,
});
}
}
const declarations =
statement.type === "ExportNamedDeclaration"
? statement.declaration
: statement;
if (declarations?.type === "VariableDeclaration")
for (const declaration of declarations.declarations)
if (declaration.id.type === "Identifier") {
locals.set(declaration.id.name, declaration.init);
if (statement.type === "ExportNamedDeclaration")
exports.set(declaration.id.name, declaration.init);
}
}
facts = { imports, exports, locals };
modules.set(path, facts);
return facts;
};
const visit = (path, name) => {
const key = JSON.stringify([path, name]);
if (complete.has(key)) return;
if (active.has(key))
throw new Error(`Cyclic tool catalog composition: ${path}#${name}`);
active.add(key);
const facts = moduleFacts(path);
const array = arrayExpression(facts.exports.get(name));
if (array?.type !== "ArrayExpression")
throw new Error(`Expected exported tool catalog array: ${path}#${name}`);
const source = withoutExtension(path);
const names = catalogs.get(source) ?? new Set();
names.add(name);
catalogs.set(source, names);
const composition = (raw, localPath = new Set()) => {
const expression = arrayExpression(raw);
if (expression?.type === "ArrayExpression") {
for (const element of expression.elements)
if (element?.type === "SpreadElement")
composition(element.argument, localPath);
return;
}
// Calls such as HOPPER_MEMORY_TOOL_DEFINITIONS.map(official) construct
// entries locally; they are not imported catalog arrays. Do not execute
// or infer catalog ownership from arbitrary computed expressions.
if (expression?.type !== "Identifier") return;
const binding = facts.imports.get(expression.name);
if (binding) {
if (!binding.source.startsWith("."))
throw new Error(`Expected local catalog module: ${path}#${name}`);
visit(
`${withoutExtension(resolve(dirname(path), binding.source))}.ts`,
binding.name,
);
return;
}
if (localPath.has(expression.name))
throw new Error(
`Cyclic local catalog composition: ${path}#${expression.name}`,
);
if (!facts.locals.has(expression.name))
throw new Error(
`Unresolved local catalog spread: ${path}#${expression.name}`,
);
composition(
facts.locals.get(expression.name),
new Set([...localPath, expression.name]),
);
};
composition(array);
active.delete(key);
complete.add(key);
};
visit(entryPath, "TOOL_CONTRACTS");
return catalogs;
};
+6 -3
View File
@@ -15,7 +15,6 @@ import { tsImport } from "tsx/esm/api";
import { PRODUCT_IDENTITY } from "../dist/identity.js";
import { createAgentEvaluationCommands } from "./lib/agent-evaluation-commands.mjs";
import { assertAgentEvaluationReportPassed } from "./lib/agent-evaluation-report-gate.mjs";
import {
closeVerifierResources,
withVerifierCleanup,
@@ -45,7 +44,7 @@ const {
).href,
import.meta.url,
);
const { summarizeFactualCorrectness } = await tsImport(
const { agentEvaluationPassed, summarizeFactualCorrectness } = await tsImport(
new URL(
"../tests/evaluation/model-evals/AgentEvaluationReport.ts",
import.meta.url,
@@ -414,7 +413,11 @@ await withVerifierCleanup(
await writeFile(resolve(reportPath), encoded);
}
assertAgentEvaluationReportPassed(results);
const failed = results.filter((result) => !agentEvaluationPassed(result));
if (failed.length > 0)
throw new Error(
`Agent routing/workflow/answer checks failed: ${failed.map(({ id }) => id).join(", ")}`,
);
},
// Even a deliberately retained fixture directory must never retain auth.
[
+5 -1
View File
@@ -245,7 +245,11 @@ export function dossierSummary(dossier) {
};
}
export function assertSession(session, expectedDigest, expectedTargetSha256) {
export async function assertSession(
session,
expectedDigest,
expectedTargetSha256,
) {
if (
session.provider.id !== "ghidra" ||
session.provider.version !== SUPPORTED_GHIDRA_VERSION ||
@@ -1,5 +1,3 @@
import { AnalysisSettings } from "../dist/application/AnalysisSettings.js";
import { runtimeAnalysisConfig } from "../dist/config/runtimeAnalysisSettings.js";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
@@ -69,7 +67,6 @@ const environment = {
...process.env,
REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS: String(LIMIT_SECONDS),
};
const settings = new AnalysisSettings(environment);
const config = parseConfig(environment);
if (!config.ok) throw config.error;
assert.equal(config.value.ghidraDecompileTimeoutSeconds, LIMIT_SECONDS);
@@ -137,7 +134,7 @@ try {
// One real provider session over the read-only authenticated TCP bridge.
let bridgeClient;
const provider = new GhidraProvider(
runtimeAnalysisConfig(config.value, settings.read),
config.value,
silentLogger,
environment,
undefined,
@@ -180,7 +177,7 @@ try {
if (!initialSession.ok) throw initialSession.error;
assert.equal(initialSession.value.target.sha256, targetDigest);
observed.initial_session_run_id = initialSession.value.run_id;
const limitText = `exceeded the ${String(LIMIT_SECONDS)}-second configured decompilation limit (ghidra.decompile_timeout_seconds; launch seed REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS)`;
const limitText = `exceeded the ${String(LIMIT_SECONDS)}-second limit set by REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS`;
let started = performance.now();
const slow = await client.execute("analyze_function", {
procedure: slowEntry,
@@ -228,26 +225,6 @@ try {
message: projected.message,
};
const changed = settings.update({
changes: [
{
setting: "ghidra.decompile_timeout_seconds",
action: "set",
value: 60,
},
],
});
assert.equal(changed.ok, true);
const recovered = await settings.run(() =>
client.execute("procedure_pseudo_code", { procedure: slowEntry }),
);
if (!recovered.ok) throw recovered.error;
assert.ok(String(recovered.value.result).includes("rea_state"));
observed.runtime_recovery = {
timeout_seconds: 60,
pseudocode_bytes: Buffer.byteLength(String(recovered.value.result)),
};
// The same session decompiles the next function with a fresh decompiler.
const small = await client.execute("procedure_pseudo_code", {
procedure: smallEntry,
@@ -290,7 +267,7 @@ try {
...observed,
verifier_run: verifierRun,
scope:
"Real Ghidra ELF or Mach-O analysis of a generated slow function through the read-only TCP provider: the decompile limit stops it, analyze_function degrades, procedure_pseudo_code names the setting, a runtime update recovers the same slow function on the same Program, and the same session decompiles the next function. No target execution; Windows is unverified.",
"Real Ghidra ELF or Mach-O analysis of a generated slow function through the read-only TCP provider: the decompile limit stops it, analyze_function degrades, procedure_pseudo_code names the setting, and the same session decompiles the next function. No target execution; Windows is unverified.",
};
} finally {
if (cleaned) await rm(workspace, { recursive: true, force: true });
-4
View File
@@ -93,10 +93,6 @@ export const verifyAndroidMetadata = async ({
result.stdout,
/MERGED_METHOD_REGRESSION_PASS exact=true overloaded=true handlers=true/u,
);
assert.match(
result.stdout,
/CONSTANT_FIELD_REGRESSION_PASS encoded=9 absent=4 opt_in=true/u,
);
assert.match(
result.stdout,
/DIRECT_INVOKE_REGRESSION_PASS nodes=1 offsets=0,3 cross_dex=true/u,
+17 -7
View File
@@ -209,15 +209,20 @@ it.each([true, false])(
if (result.ok) throw new Error("Expected a capture allowance refusal");
const projected = projectAnalysisError(result.error);
expect(projected.retryable).toBe(volatileOutput);
expect(projected).toMatchObject({
code: "resource_constraint",
details: { reported_limits: { max_output_bytes: 1024 } },
});
expect(projected.details?.limit).toBeUndefined();
expect(projected.remediation.action).toContain(
"The adb output capture allowance",
);
expect(projected.remediation.action).toContain(
volatileOutput
? "live output may change between requests"
: "Repeating the unchanged request fails the same way.",
);
expect(projected.remediation.action).toContain("1024 bytes");
expect(projected.remediation.action).toContain("no runtime override");
},
);
it("reports the effective fixed adb deadline", async () => {
it("names the effective adb deadline and says it has no runtime override", async () => {
vi.mocked(execFileBytes).mockRejectedValueOnce(
Object.assign(new Error("deadline"), {
code: "ETIMEDOUT",
@@ -242,7 +247,12 @@ it("reports the effective fixed adb deadline", async () => {
code: "provider_timeout",
details: { timeout_ms: 600_000 },
});
expect(projected.details?.limit).toBeUndefined();
expect(projected.message).toContain("600000 ms");
expect(projected.message).toContain("no runtime override");
expect(projected.remediation.action).toContain("no runtime override");
expect(projected.remediation.action).not.toContain(
"REA_JADX_OPERATION_TIMEOUT_MS",
);
});
it("keeps a physical failure even when the later abort reason is that same error", async () => {
+7 -7
View File
@@ -61,13 +61,12 @@ export const snapshotAdbInput = async (
let primaryCause: unknown;
try {
const { handle, initial } = await (
dependencies.openSource ?? openRegularFile
)(sourcePath, {
symlinks: "follow",
signal,
});
source = new OwnedFileHandle(handle);
source = new OwnedFileHandle(
await (dependencies.openSource ?? openRegularFile)(sourcePath, {
symlinks: "follow",
signal,
}),
);
const destinationPath = join(root.path, basename(sourcePath));
destination = new OwnedFileHandle(
await (dependencies.openDestination ?? open)(
@@ -76,6 +75,7 @@ export const snapshotAdbInput = async (
0o600,
),
);
const initial = await source.handle.stat();
if (!Number.isSafeInteger(initial.size) || initial.size < 0)
throw new RangeError(
`Regular file size cannot be represented safely: ${sourcePath}`,
+5 -3
View File
@@ -18,13 +18,15 @@ import {
} from "node:fs/promises";
import { basename, dirname, extname, join, resolve } from "node:path";
import type {
ExecutionOptions,
ProviderAvailability,
} from "../application/AnalysisProvider.js";
import { providerConfigurationRefusal } from "../domain/providerConfigurationRefusal.js";
import {
type ExecutionOptions,
type ProviderAvailability,
createAnalysisExecution,
type AnalysisExecution,
} from "../application/AnalysisProvider.js";
import { providerConfigurationRefusal } from "../domain/providerConfigurationRefusal.js";
import type {
AdbDisplayNumericIssues,
AdbDisplayPartialObservation,
+8 -12
View File
@@ -15,7 +15,7 @@ const selection = {
pathSource: "environment",
} as const;
it("reports the fixed adb version-probe deadline", async () => {
it("names the adb version-probe deadline and says it has no runtime override", async () => {
vi.mocked(execFileOutput).mockRejectedValueOnce(
Object.assign(new Error("deadline"), {
code: "ETIMEDOUT",
@@ -34,10 +34,12 @@ it("reports the fixed adb version-probe deadline", async () => {
code: "provider_timeout",
details: { timeout_ms: 10_000 },
});
expect(projected.details?.limit).toBeUndefined();
expect(projected.message).toContain("10000 ms");
expect(projected.message).toContain("no runtime override");
expect(projected.remediation.action).toContain("no runtime override");
});
it("reports the fixed adb version-output cap", async () => {
it("names the adb version-output cap and says it has no runtime override", async () => {
vi.mocked(execFileOutput).mockRejectedValueOnce(
Object.assign(new Error("too much"), {
code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
@@ -54,15 +56,9 @@ it("reports the fixed adb version-output cap", async () => {
const projected = projectAnalysisError(
failure as Parameters<typeof projectAnalysisError>[0],
);
expect(projected).toMatchObject({
code: "resource_constraint",
retryable: false,
details: {
resource: "memory",
reported_limits: { max_output_bytes: 65536 },
},
});
expect(projected.details?.limit).toBeUndefined();
expect(projected.message).toContain("65536-byte");
expect(projected.remediation.action).toContain("65536 bytes");
expect(projected.remediation.action).toContain("no runtime override");
});
it("distinguishes execute denial from missing or unreadable version output", async () => {
+4 -7
View File
@@ -19,7 +19,6 @@ import {
RegularFileAdmissionFailure,
assertRegularFileActive,
sameRegularFileState,
type StableRegularFileDescriptor,
} from "../filesystem/RegularFile.js";
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
@@ -174,12 +173,9 @@ const copyAndroidSnapshot = async (
// Close each descriptor through its owner. A rejected close keeps the
// copy outcome and stays retained until that owner is retried; the
// partial file is removed only after those close attempts.
const { handle, initial } = await openSnapshotSource(
sourcePath,
signal,
retain(sourcePath),
const source = adoptSnapshotHandle(
await openSnapshotSource(sourcePath, signal, retain(sourcePath)),
);
const source = adoptSnapshotHandle(handle);
return await withOwnedFileHandle(
sourcePath,
source.owner,
@@ -188,6 +184,7 @@ const copyAndroidSnapshot = async (
...retain(sourcePath),
},
async () => {
const initial = await source.handle.stat();
assertRegularFileActive(signal);
let copiedBytes = 0;
const hash = createHash("sha256");
@@ -289,7 +286,7 @@ const openSnapshotSource = async (
retention: {
readonly retainFailedClose?: (owner: OwnedFileHandle) => void;
},
): Promise<StableRegularFileDescriptor> => {
): Promise<FileHandle> => {
try {
return await openRegularFile(path, { symlinks: "follow", signal });
} catch (cause: unknown) {
-6
View File
@@ -71,13 +71,8 @@ const classSummary = async (
{ operation: "inspect_android_package" | "search_android_classes" }
>,
): Promise<ClassSummary> => {
// Only class inspection reports DEX static values; method and reference
// lookups keep the member-only summary so large constants cannot block them.
const raw = await tools.json("get_class_summary", {
class_name: request.input.class_name,
...(request.operation === "inspect_android_class"
? { dex_static_values: true }
: {}),
});
if (
raw !== null &&
@@ -298,7 +293,6 @@ export const analyzeJadxRequest = async (
fields: summary.fields.map((field) => ({
name: field.name,
reported_type: field.type,
dex_static_value: field.dex_static_value ?? null,
})),
inner_classes: summary.inner_classes,
});
+8 -19
View File
@@ -1,8 +1,3 @@
import { DEFAULT_JADX_OPERATION_TIMEOUT_MS } from "../domain/analysisResourceLimits.js";
import {
jadxOperationTimeoutGuidance,
JADX_OPERATION_TIMEOUT_REMEDIATION,
} from "./JadxOperationTimeout.js";
import { inspectJadxJava, type JadxJavaProbeOptions } from "./JadxJavaProbe.js";
import {
JadxInterruption,
@@ -37,10 +32,13 @@ import { analysisClientFailureForOperation } from "../providers/AnalysisClientFa
import { ZipArtifactReader } from "../artifacts/ZipArtifactReader.js";
import type { DoctorProviderCheck } from "../application/Doctor.js";
import {
DEFAULT_JADX_OPERATION_TIMEOUT_MS,
jadxOperationTimeoutGuidance,
JADX_JAR_CONFIGURATION_REMEDIATION,
JADX_JAVA_PROBE_REMEDIATION,
JADX_JAVA_RUNTIME_REMEDIATION,
JADX_JVM_OPTIONS_REMEDIATION,
JADX_OPERATION_TIMEOUT_REMEDIATION,
JADX_UNSUPPORTED_HOST_REMEDIATION,
JADX_WINDOWS_NATIVE_REMEDIATION,
} from "./JadxRelease.js";
@@ -180,16 +178,10 @@ class JadxConfigurationFailure extends Error {
/** Inspect the same JAR and Java prerequisites that gate actual execution. */
export const inspectJadxAvailability = async (
environment: Readonly<Record<string, string | undefined>>,
options: JadxJavaProbeOptions & {
/** Admitted runtime override; omission retains launch configuration. */
readonly operationTimeoutMs?: number | undefined;
},
options: JadxJavaProbeOptions,
): Promise<ProviderAvailability> => {
try {
const operationTimeoutMs = readOperationTimeout(
environment,
options.operationTimeoutMs,
);
const operationTimeoutMs = readOperationTimeout(environment);
const configuration = await readJadxConfigurationInputs(
environment,
options.signal,
@@ -235,10 +227,9 @@ export const inspectJadxAvailability = async (
export const resolveJadxOperationTimeout = (
environment: Readonly<Record<string, string | undefined>>,
operation: AndroidOperation,
runtimeTimeoutMs?: number,
): Result<number, AnalysisCapabilityUnavailableError> => {
try {
return ok(readOperationTimeout(environment, runtimeTimeoutMs));
return ok(readOperationTimeout(environment));
} catch (cause) {
if (!(cause instanceof JadxConfigurationFailure)) throw cause;
return err(projectConfigurationFailure(operation, cause));
@@ -350,7 +341,7 @@ const requireEngineArchive = async (
),
},
options.signal,
async ({ handle, initial }) => {
async (handle) => {
try {
// Metadata enumeration borrows this descriptor. The file workflow
// alone owns admission, final close, cancellation and failed retry.
@@ -358,7 +349,7 @@ const requireEngineArchive = async (
jar,
"zip",
JAR_METADATA_READ_MAX_BYTES,
{ handle, initial },
{ handle, initial: await handle.stat() },
);
const missing = new Set([
"com/atxx/jhmcp/JadxSession.class",
@@ -711,9 +702,7 @@ const resolveJvmArguments = (
const readOperationTimeout = (
environment: Readonly<Record<string, string | undefined>>,
runtimeTimeoutMs?: number,
): number => {
if (runtimeTimeoutMs !== undefined) return runtimeTimeoutMs;
const value = environment.REA_JADX_OPERATION_TIMEOUT_MS;
if (value === undefined) return DEFAULT_JADX_OPERATION_TIMEOUT_MS;
const parsed = providerStartupTimeoutSchema.safeParse(value);
+1 -1
View File
@@ -1,4 +1,4 @@
import { jadxOperationTimeoutGuidance } from "./JadxOperationTimeout.js";
import { jadxOperationTimeoutGuidance } from "./JadxRelease.js";
import { JadxInterruption } from "./JadxInterruption.js";
import { z } from "zod";
import { AnalysisError } from "../domain/analysisErrorBase.js";
+1 -1
View File
@@ -22,7 +22,7 @@ import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
import { retainOwnedCommandCleanup } from "../providers/OwnedCommandCleanup.js";
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
import { jadxOperationTimeoutGuidance } from "./JadxOperationTimeout.js";
import { jadxOperationTimeoutGuidance } from "./JadxRelease.js";
const JAVA_MODULE_LISTING_TIMEOUT_MS = 10_000;
const JAVA_MODULE_LISTING_MAX_OUTPUT_BYTES = 1024 * 1024;
+8 -14
View File
@@ -2,7 +2,7 @@ import { expect, it } from "vitest";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { captureJadxFailure } from "./JadxFailure.js";
import { jadxOperationTimeoutGuidance } from "./JadxOperationTimeout.js";
import { jadxOperationTimeoutGuidance } from "./JadxRelease.js";
it("names REA_JADX_OPERATION_TIMEOUT_MS when the operation deadline elapses", () => {
const timeout = AbortSignal.abort(new Error("deadline"));
@@ -21,19 +21,13 @@ it("names REA_JADX_OPERATION_TIMEOUT_MS when the operation deadline elapses", ()
const guidance = jadxOperationTimeoutGuidance(1500);
expect(projected.message).toBe(guidance.userMessage);
expect(projected.remediation.action).toBe(guidance.remediationAction);
expect(projected.details).toMatchObject({
limit: {
setting: "jadx.operation_timeout_ms",
effective_value: 1500,
unit: "ms",
minimum: 1,
maximum: 2_147_483_647,
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
},
});
expect(projected.message).toContain("1500 ms");
expect(projected.remediation.action).toContain(
"REA_JADX_OPERATION_TIMEOUT_MS",
);
expect(projected.remediation.action).toContain("1 to 2147483647");
expect(projected.remediation.action).toContain("120000");
expect(projected.remediation.action).toContain("next process start");
expect(projected).toMatchObject({
code: "provider_timeout",
details: { timeout_ms: 1500 },
Loaded 100 of 962 files, more files were not shown because too many files have changed in this diff. Show more