mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 17:32:36 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4a7c20e04 | ||
|
|
27edc5c83f | ||
|
|
c52a9b9d79 |
No files matched your search
@@ -130,6 +130,19 @@ status checks alongside its existing checks. Deploy the workflow to each
|
||||
protected target branch before requiring it there. The workflow reads PR
|
||||
metadata using trusted default-branch code and writes only a commit status.
|
||||
|
||||
When an open PR may overlap another open change or a recently landed fix, note
|
||||
the related PR, issue, or commit in the description or a comment as soon as the
|
||||
overlap is known. Prefer one clear pointer over repeated status updates.
|
||||
|
||||
If an overlapping change merges while a PR awaits review, link that commit on
|
||||
the open PR and state what remains useful. Rebase or otherwise align with the
|
||||
current base when the remaining work is still wanted, narrow the PR to the
|
||||
uncovered portion, or close it when the change is fully superseded. Keep the
|
||||
description and validation current after that update. Maintainers may also note
|
||||
the expected disposition (rebase, narrow, or close) when they land or recognize
|
||||
an overlapping fix. Readiness still does not grant merge permission or a review
|
||||
date.
|
||||
|
||||
Maintainers may return a PR to the author or close it when the problem is
|
||||
unsupported, validation is missing, or the owner cannot explain the change.
|
||||
Contributors are responsible for completing this work before requesting review.
|
||||
|
||||
@@ -119,9 +119,9 @@ import ghidra.program.util.VarnodeContext;
|
||||
public final class ReaGhidraBridge extends HeadlessScript {
|
||||
// At most 24 KiB after worst-case JSON escaping (six bytes per UTF-16 unit).
|
||||
private static final int PDB_ATTRIBUTE_UTF16_BUDGET = 4096;
|
||||
private static final int DEFAULT_HIGH_PCODE_OPS = 3000;
|
||||
private static final int DEFAULT_HIGH_PCODE_INPUTS_PER_OP = 64;
|
||||
private static final int DEFAULT_HIGH_PCODE_DEF_USE_EDGES = 12000;
|
||||
private static final int MAX_HIGH_PCODE_OPS = 3000;
|
||||
private static final int MAX_HIGH_PCODE_INPUTS_PER_OP = 64;
|
||||
private static final int MAX_HIGH_PCODE_DEF_USE_EDGES = 12000;
|
||||
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
|
||||
private final Map<FileBytes, Boolean> targetSourceIdentity = new IdentityHashMap<>();
|
||||
private String admittedTargetSha256;
|
||||
@@ -184,10 +184,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
private String identityAdmissionFailure;
|
||||
/** Per-function native decompiler limit from the session; 0 leaves it unbounded. */
|
||||
private int decompileTimeoutSeconds;
|
||||
private Integer decompileMaxPayloadMiB;
|
||||
// DecompileProcess converts MiB to bytes using a signed-int left shift.
|
||||
private static final int MAX_DECOMPILE_PAYLOAD_MIB = Integer.MAX_VALUE >> 20;
|
||||
private ValueFlowLimits valueFlowLimits = defaultValueFlowLimits();
|
||||
// DecompileProcess converts seconds to an int millisecond timer delay.
|
||||
private static final int MAX_DECOMPILE_TIMEOUT_SECONDS = Integer.MAX_VALUE / 1000;
|
||||
private final ReaGhidraExtensions analysisExtensions = new ReaGhidraExtensions();
|
||||
@@ -430,8 +426,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
decompileTimeoutSeconds = request.decompileTimeoutSeconds() == null
|
||||
? descriptor.decompileTimeoutSeconds() : request.decompileTimeoutSeconds();
|
||||
valueFlowLimits = request.valueFlowLimits();
|
||||
applyDecompilePayloadLimit(request.decompileMaxPayloadMiB());
|
||||
writeSuccess(writer, request.id, handleRequest(request, descriptor));
|
||||
}
|
||||
catch (RequestFailure failure) {
|
||||
@@ -1044,26 +1038,11 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private DecompileOptions decompileOptions(Integer payloadMiB) {
|
||||
private void initializeDecompiler() {
|
||||
DecompileOptions options = new DecompileOptions();
|
||||
options.setMaxInstructions(Integer.MAX_VALUE);
|
||||
if (payloadMiB != null) options.setMaxPayloadMBytes(payloadMiB);
|
||||
return options;
|
||||
}
|
||||
|
||||
private void applyDecompilePayloadLimit(Integer payloadMiB) {
|
||||
if (java.util.Objects.equals(decompileMaxPayloadMiB, payloadMiB)) return;
|
||||
// DecompInterface supports live setOptions: it updates the existing
|
||||
// DecompileProcess result-size bound before sending native options.
|
||||
if (!decompiler.setOptions(decompileOptions(payloadMiB)))
|
||||
throw new RequestFailure("decompile_options_failed", "Ghidra could not apply the selected native decompiler payload limit");
|
||||
decompiler.flushCache();
|
||||
decompileMaxPayloadMiB = payloadMiB;
|
||||
}
|
||||
|
||||
private void initializeDecompiler() {
|
||||
decompiler = new DecompInterface();
|
||||
decompiler.setOptions(decompileOptions(decompileMaxPayloadMiB));
|
||||
decompiler.setOptions(options);
|
||||
decompiler.toggleCCode(true);
|
||||
decompiler.toggleSyntaxTree(true);
|
||||
decompiler.toggleJumpLoads(true);
|
||||
@@ -1877,7 +1856,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
while (operations.hasNext()) {
|
||||
monitor.checkCancelled();
|
||||
PcodeOpAST operation = operations.next();
|
||||
if (selected.size() >= valueFlowLimits.maxOperations()) {
|
||||
if (selected.size() >= MAX_HIGH_PCODE_OPS) {
|
||||
omittedOperationsLowerBound = 1;
|
||||
break;
|
||||
}
|
||||
@@ -1905,7 +1884,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonArray effects = new JsonArray();
|
||||
int omittedInputs = 0;
|
||||
int omittedEdges = 0;
|
||||
boolean edgeLimitReached = false;
|
||||
for (PcodeOpAST operation : selected) {
|
||||
monitor.checkCancelled();
|
||||
String operationId = operationIds.get(operation);
|
||||
@@ -1921,16 +1899,14 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
value.addProperty("block_membership", operation.getParent() == null ? "detached" : "member");
|
||||
JsonArray inputs = new JsonArray();
|
||||
int inputCount = operation.getNumInputs();
|
||||
int capturedInputs = Math.min(inputCount, valueFlowLimits.maxInputsPerOperation());
|
||||
int capturedInputs = Math.min(inputCount, MAX_HIGH_PCODE_INPUTS_PER_OP);
|
||||
for (int inputIndex = 0; inputIndex < capturedInputs; inputIndex += 1) {
|
||||
Varnode input = operation.getInput(inputIndex);
|
||||
inputs.add(varnodeValue(input));
|
||||
if (input instanceof VarnodeAST astInput) {
|
||||
if (astInput.getHigh() instanceof HighParam parameter) {
|
||||
if (parameterUses.size() >= valueFlowLimits.maxEdges()) {
|
||||
edgeLimitReached = true;
|
||||
omittedEdges++;
|
||||
} else {
|
||||
if (parameterUses.size() >= MAX_HIGH_PCODE_DEF_USE_EDGES) omittedEdges++;
|
||||
else {
|
||||
JsonObject use = new JsonObject();
|
||||
use.addProperty("ordinal", parameter.getSlot());
|
||||
use.addProperty("use", operationId);
|
||||
@@ -1941,8 +1917,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
PcodeOp definition = astInput.getDef();
|
||||
if (definition instanceof PcodeOpAST astDefinition) {
|
||||
String definitionId = operationIds.get(astDefinition);
|
||||
if (definitionId == null || defUse.size() >= valueFlowLimits.maxEdges()) {
|
||||
if (defUse.size() >= valueFlowLimits.maxEdges()) edgeLimitReached = true;
|
||||
if (definitionId == null || defUse.size() >= MAX_HIGH_PCODE_DEF_USE_EDGES) {
|
||||
omittedEdges += 1;
|
||||
} else {
|
||||
JsonObject edge = new JsonObject();
|
||||
@@ -1965,16 +1940,6 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("available", true);
|
||||
result.addProperty("provenance", "ghidra-high-pcode");
|
||||
JsonObject limits = new JsonObject();
|
||||
limits.addProperty("max_operations", valueFlowLimits.maxOperations());
|
||||
limits.addProperty("max_inputs_per_operation", valueFlowLimits.maxInputsPerOperation());
|
||||
limits.addProperty("max_edges", valueFlowLimits.maxEdges());
|
||||
result.add("limits", limits);
|
||||
JsonArray limitsReached = new JsonArray();
|
||||
if (omittedOperationsLowerBound > 0) limitsReached.add("max_operations");
|
||||
if (omittedInputs > 0) limitsReached.add("max_inputs_per_operation");
|
||||
if (edgeLimitReached) limitsReached.add("max_edges");
|
||||
result.add("limits_reached", limitsReached);
|
||||
result.add("operations", operationValues);
|
||||
result.add("def_use", defUse);
|
||||
result.add("parameters", parameterValues);
|
||||
@@ -2003,35 +1968,15 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
"Memory read/write effects include stack and temporary memory operations; they do not identify persistent state or business meaning."
|
||||
);
|
||||
limitations.add(
|
||||
"Operation, input, and def-use limits can truncate large functions; inspect the effective limits, limits_reached and omitted counts before drawing conclusions."
|
||||
"Operation, input, and def-use limits can truncate large functions; inspect truncation, omitted lower bounds, and known omitted counts before drawing conclusions."
|
||||
);
|
||||
limitations.add(
|
||||
"Known omitted-edge counts cover links omitted from retained operations; omitted operations may contain additional uncounted links."
|
||||
);
|
||||
if (omittedOperationsLowerBound > 0)
|
||||
limitations.add(valueFlowLimitRecovery("max_operations", valueFlowLimits.maxOperations()));
|
||||
if (omittedInputs > 0)
|
||||
limitations.add(valueFlowLimitRecovery("max_inputs_per_operation", valueFlowLimits.maxInputsPerOperation()));
|
||||
if (edgeLimitReached)
|
||||
limitations.add(valueFlowLimitRecovery("max_edges", valueFlowLimits.maxEdges()));
|
||||
result.add("limitations", limitations);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String valueFlowLimitRecovery(String field, int effective) {
|
||||
String setting = "ghidra.value_flow_" + field;
|
||||
String environment = "REA_GHIDRA_VALUE_FLOW_" + field.toUpperCase(Locale.ROOT);
|
||||
String adjustment = effective < Integer.MAX_VALUE
|
||||
? "In MCP, use update_analysis_settings with setting " + setting +
|
||||
", action set and a larger integer value from 1 to " + Integer.MAX_VALUE +
|
||||
", then repeat the selected function analysis on this Program without restarting REA. " +
|
||||
"For a one-shot CLI retry, set " + environment +
|
||||
" to a larger count in the environment that starts the next rea invocation, then repeat the selected analysis; the previous invocation's Program is no longer active."
|
||||
: "The setting is at its Java integer maximum; select a smaller function for this projection.";
|
||||
return setting + " reached its effective value of " + effective + ". " + adjustment +
|
||||
" These controls do not raise the decompile deadline, bridge transport or MCP client limits.";
|
||||
}
|
||||
|
||||
private String pcodeOperationId(PcodeOpAST operation) {
|
||||
return canonicalAddress(operation.getSeqnum().getTarget()) + "#" +
|
||||
operation.getSeqnum().getTime();
|
||||
@@ -3754,26 +3699,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
if (!results.decompileCompleted()) {
|
||||
throw new RequestFailure(
|
||||
"decompile_failed",
|
||||
"Ghidra decompilation failed: " + diagnosticMessage(results.getErrorMessage()) +
|
||||
". The effective native decompiler payload allowance is " +
|
||||
decompiler.getOptions().getMaxPayloadMBytes() +
|
||||
" MiB. " + decompilePayloadRecovery()
|
||||
"Ghidra decompilation failed: " + diagnosticMessage(results.getErrorMessage())
|
||||
);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private String decompilePayloadRecovery() {
|
||||
int effective = decompiler.getOptions().getMaxPayloadMBytes();
|
||||
if (effective >= MAX_DECOMPILE_PAYLOAD_MIB)
|
||||
return "If the diagnostic reports payload exhaustion, the native payload allowance is already at its supported maximum; no higher value is accepted. Use analyze_function for available listing facts or explicitly select a smaller function for decompilation.";
|
||||
return "If the diagnostic reports payload exhaustion, in MCP raise ghidra.decompile_max_payload_mib with update_analysis_settings " +
|
||||
"(integer MiB from 1 to " + MAX_DECOMPILE_PAYLOAD_MIB +
|
||||
") and repeat the selected operation on this Program without restarting REA. " +
|
||||
"For a one-shot CLI retry, set REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB to a larger MiB value in the environment that starts the next rea invocation, then repeat the selected operation; the previous invocation's Program is no longer active. " +
|
||||
"Runtime null or an omitted launch seed restores the installed Ghidra default. This does not raise the decompile deadline, high-pcode projection budgets or MCP client limits.";
|
||||
}
|
||||
|
||||
private static String decompiledText(DecompileResults results) {
|
||||
if (results == null) {
|
||||
return null;
|
||||
@@ -5225,28 +5156,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
Set<String> keys = new HashSet<>(REQUEST_KEYS);
|
||||
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("decompile_timeout_seconds"))
|
||||
keys.add("decompile_timeout_seconds");
|
||||
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("value_flow_limits"))
|
||||
keys.add("value_flow_limits");
|
||||
if (parsed.isJsonObject() && parsed.getAsJsonObject().has("decompile_max_payload_mib"))
|
||||
keys.add("decompile_max_payload_mib");
|
||||
JsonObject object = requireObject(parsed, keys);
|
||||
Integer payloadMiB = null;
|
||||
if (object.has("decompile_max_payload_mib") && !object.get("decompile_max_payload_mib").isJsonNull()) {
|
||||
payloadMiB = requireInteger(object, "decompile_max_payload_mib");
|
||||
if (payloadMiB < 1 || payloadMiB > MAX_DECOMPILE_PAYLOAD_MIB)
|
||||
throw new IllegalArgumentException("Invalid per-request native decompiler payload limit");
|
||||
}
|
||||
ValueFlowLimits flowLimits = defaultValueFlowLimits();
|
||||
if (object.has("value_flow_limits")) {
|
||||
JsonObject limits = requireObject(object.get("value_flow_limits"),
|
||||
Set.of("max_operations", "max_inputs_per_operation", "max_edges"));
|
||||
int operations = requireInteger(limits, "max_operations");
|
||||
int inputs = requireInteger(limits, "max_inputs_per_operation");
|
||||
int edges = requireInteger(limits, "max_edges");
|
||||
if (operations < 1 || inputs < 1 || edges < 1)
|
||||
throw new IllegalArgumentException("Invalid per-request value-flow limits");
|
||||
flowLimits = new ValueFlowLimits(operations, inputs, edges);
|
||||
}
|
||||
Integer timeout = null;
|
||||
if (object.has("decompile_timeout_seconds")) {
|
||||
JsonElement value = object.get("decompile_timeout_seconds");
|
||||
@@ -5262,7 +5172,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, payloadMiB, flowLimits);
|
||||
return new Request(id, requireString(object, "method"), params.getAsJsonObject(), timeout);
|
||||
}
|
||||
|
||||
private static JsonObject requireObject(JsonElement element, Set<String> keys) {
|
||||
@@ -5470,11 +5380,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
int decompileTimeoutSeconds,
|
||||
boolean existingProject
|
||||
) {}
|
||||
private static ValueFlowLimits defaultValueFlowLimits() {
|
||||
return new ValueFlowLimits(DEFAULT_HIGH_PCODE_OPS, DEFAULT_HIGH_PCODE_INPUTS_PER_OP, DEFAULT_HIGH_PCODE_DEF_USE_EDGES);
|
||||
}
|
||||
private record ValueFlowLimits(int maxOperations, int maxInputsPerOperation, int maxEdges) {}
|
||||
private record Request(int id, String method, JsonObject params, Integer decompileTimeoutSeconds, Integer decompileMaxPayloadMiB, ValueFlowLimits valueFlowLimits) {}
|
||||
private record Request(int id, String method, JsonObject params, Integer decompileTimeoutSeconds) {}
|
||||
|
||||
private static final class RequestFailure extends RuntimeException {
|
||||
private final String code;
|
||||
|
||||
@@ -18,6 +18,7 @@ from mitmproxy import ctx, version
|
||||
from mitmproxy.io import tnetstring
|
||||
|
||||
PROFILE = "12.2.3"
|
||||
OUTPUT_BYTES = 96 * 1024 * 1024
|
||||
CREDENTIALS = {b"authorization", b"proxy-authorization", b"cookie", b"set-cookie"}
|
||||
|
||||
|
||||
@@ -203,7 +204,7 @@ def without_userinfo(value, base_scheme=None):
|
||||
raise CaptureFailure("decoder", "Normalized URL authority could not be bound to its source.")
|
||||
|
||||
|
||||
def project_record(state, secrets, max_depth):
|
||||
def project_record(state, secrets):
|
||||
binaries, numbers, redactions = [], [], []
|
||||
byte_secrets = []
|
||||
for secret in secrets:
|
||||
@@ -250,8 +251,8 @@ def project_record(state, secrets, max_depth):
|
||||
del redactions[before[2]:]
|
||||
|
||||
def visit(value, path, depth=0, credential=False, transport_url=False, base_scheme=None):
|
||||
if depth > max_depth:
|
||||
raise CaptureFailure("input-limit", f"Native capture exceeds max_nesting_depth ({max_depth} levels).", path)
|
||||
if depth > 64:
|
||||
raise CaptureFailure("input-limit", "Native capture exceeds the 64-level evidence nesting budget.", path)
|
||||
if credential:
|
||||
validate_omitted(value, path, depth)
|
||||
redactions.append({"pointer": path, "reason": "transport-credential"})
|
||||
@@ -336,12 +337,12 @@ def decode(request):
|
||||
except CaptureFailure:
|
||||
raise
|
||||
except RecursionError:
|
||||
raise CaptureFailure("unsupported", "Native parser reached Python's recursion limit at byte offset " + str(start) + ". max_nesting_depth does not raise that upstream limit.") from None
|
||||
raise CaptureFailure("input-limit", "Native parser exceeded its recursion budget at byte offset " + str(start) + ".") from None
|
||||
except (ValueError, IndexError, UnicodeError, EOFError):
|
||||
raise CaptureFailure("format", "Malformed native record at byte offset " + str(start) + ".") from None
|
||||
if not isinstance(state, dict):
|
||||
raise CaptureFailure("format", "Native flow record must be a dictionary at byte offset " + str(start) + ".")
|
||||
records.append({"ordinal": len(records), "location": {"kind": "byte-range", "offset": start, "bytes": handle.tell() - start}, **project_record(state, request["sensitive_values"], request["budgets"]["depth"]), "limitations": ["Fields are original native states without FlowReader migration. Binary fields contain exact producer bytes; reported UTF-8 strings for those fields are derived display views. Unknown flow/version extensions are retained without interpretation."]})
|
||||
records.append({"ordinal": len(records), "location": {"kind": "byte-range", "offset": start, "bytes": handle.tell() - start}, **project_record(state, request["sensitive_values"]), "limitations": ["Fields are original native states without FlowReader migration. Binary fields contain exact producer bytes; reported UTF-8 strings for those fields are derived display views. Unknown flow/version extensions are retained without interpretation."]})
|
||||
return {"decoder": {"id": "mitmproxy-native-tnetstring", "name": "REA offline native mitmproxy adapter", "version": PROFILE}, "container": {"reported": None, "numeric_literals": [], "redactions": [], "records_pointer": None}, "total_records": len(records), "records": records}
|
||||
|
||||
|
||||
@@ -349,8 +350,6 @@ def failure_reply(error):
|
||||
"""Keep producer constraints, resource exhaustion and adapter defects distinct."""
|
||||
if isinstance(error, CaptureFailure):
|
||||
return {"ok": False, "reason": error.reason, "message": str(error), "pointer": error.pointer}
|
||||
if isinstance(error, RecursionError):
|
||||
return {"ok": False, "reason": "unsupported", "message": "Native projection reached Python's recursion limit; max_nesting_depth does not raise that upstream limit.", "pointer": ""}
|
||||
if isinstance(error, MemoryError):
|
||||
return {"ok": False, "reason": "resource-limit", "message": "Native decoder exhausted its 768 MiB address-space budget.", "pointer": ""}
|
||||
return {"ok": False, "reason": "decoder", "message": "Offline native decoder failed unexpectedly: " + type(error).__name__ + ".", "pointer": ""}
|
||||
@@ -363,13 +362,9 @@ class OfflineCapture:
|
||||
def running(self):
|
||||
resource.setrlimit(resource.RLIMIT_AS, (768 * 1024 * 1024, 768 * 1024 * 1024))
|
||||
resource.setrlimit(resource.RLIMIT_CPU, (30, 30))
|
||||
resource.setrlimit(resource.RLIMIT_FSIZE, (OUTPUT_BYTES, OUTPUT_BYTES))
|
||||
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
|
||||
request = json.loads(Path(ctx.options.rea_request_path).read_text(encoding="utf-8"))
|
||||
budgets = request["budgets"]
|
||||
if any(type(budgets[key]) is not int or budgets[key] < 1 for key in ("inputBytes", "outputBytes", "depth")):
|
||||
raise ValueError("Invalid admitted capture budgets")
|
||||
output_bytes = budgets["outputBytes"]
|
||||
resource.setrlimit(resource.RLIMIT_FSIZE, (output_bytes, output_bytes))
|
||||
try:
|
||||
response = {"ok": True, "value": decode(request)}
|
||||
except Exception as error:
|
||||
@@ -379,7 +374,7 @@ class OfflineCapture:
|
||||
output = json.dumps(response, ensure_ascii=True, allow_nan=False, separators=(",", ":")).encode("ascii")
|
||||
except Exception as error:
|
||||
output = json.dumps(failure_reply(error), separators=(",", ":")).encode("ascii")
|
||||
if len(output) > output_bytes:
|
||||
if len(output) > OUTPUT_BYTES:
|
||||
output = b'{"ok":false,"reason":"limit","message":"Native reply exceeds the complete-evidence output budget.","pointer":""}'
|
||||
Path(request["reply_path"]).write_bytes(output)
|
||||
finally:
|
||||
|
||||
@@ -36,7 +36,7 @@ def location(value, scheme=None):
|
||||
state = {"response": {"headers": [[b"Location", value]]}}
|
||||
if scheme is not None:
|
||||
state["request"] = {"scheme": scheme}
|
||||
record = capture.project_record(state, [], 64)
|
||||
record = capture.project_record(state, [])
|
||||
sidecar = next(
|
||||
field for field in record["binary_fields"] if field["pointer"] == "/response/headers/0/1"
|
||||
)
|
||||
@@ -75,11 +75,11 @@ class TransportUserinfoTests(unittest.TestCase):
|
||||
def test_native_request_paths_preserve_origin_form_and_redact_absolute_form(self):
|
||||
for value in [b"/\\invoice@customer", b"//invoice@customer", b"\\\\invoice@customer"]:
|
||||
with self.subTest(value=value):
|
||||
record = capture.project_record({"request": {"path": value}}, [], 64)
|
||||
record = capture.project_record({"request": {"path": value}}, [])
|
||||
self.assertEqual(record["reported"]["request"]["path"], value.decode("ascii"))
|
||||
self.assertEqual(record["binary_fields"][0]["state"], "retained")
|
||||
self.assertEqual(record["redactions"], [])
|
||||
record = capture.project_record({"request": {"path": b"https://user:pass@host/x"}}, [], 64)
|
||||
record = capture.project_record({"request": {"path": b"https://user:pass@host/x"}}, [])
|
||||
self.assertEqual(record["reported"]["request"]["path"], "https://host/x")
|
||||
self.assertEqual(record["binary_fields"][0]["state"], "redacted")
|
||||
|
||||
@@ -110,7 +110,7 @@ class TransportUserinfoTests(unittest.TestCase):
|
||||
"request": {"scheme": b"https"},
|
||||
"response": {"headers": [[b"Location", b"https:invoice@customer"]]},
|
||||
},
|
||||
}, [], 64)
|
||||
}, [])
|
||||
self.assertEqual(record["reported"]["backup"]["response"]["headers"][0][1], "https:invoice@customer")
|
||||
self.assertEqual(record["redactions"], [])
|
||||
|
||||
|
||||
@@ -25,17 +25,24 @@ ARGUMENT_REGISTERS = {
|
||||
"x86_64": ["rdi", "rsi", "rdx", "rcx", "r8", "r9"],
|
||||
"x86_64h": ["rdi", "rsi", "rdx", "rcx", "r8", "r9"],
|
||||
}
|
||||
MAX_SELECTOR_BYTES = 1024
|
||||
MAX_OTHER_STOPS = 256
|
||||
MAX_TRACE_BYTES = 8 * 1024 * 1024
|
||||
MAX_TRACE_FRAMES = 65_536
|
||||
MAX_OBSERVATION_JOURNAL_BYTES = MAX_TRACE_BYTES
|
||||
MAX_BREAKPOINT_LOCATION_BYTES = 8 * 1024 * 1024
|
||||
MAX_TARGET_OUTPUT_BYTES = 1024 * 1024
|
||||
OUTPUT_DRAIN_BYTES_PER_PASS = 1024 * 1024
|
||||
OUTPUT_EOF_WAIT_SECONDS = 1.0
|
||||
OUTPUT_READER_JOIN_SECONDS = 0.5
|
||||
LISTENER_NAME = "rea-native-call-observation"
|
||||
|
||||
|
||||
def _can_retain_trace(current_bytes, current_frames, event_bytes, event_frames, limits):
|
||||
def _can_retain_trace(current_bytes, current_frames, event_bytes, event_frames):
|
||||
"""Whether a complete event fits both aggregate trace budgets."""
|
||||
return (
|
||||
current_bytes + event_bytes <= limits["trace_bytes"]
|
||||
and current_frames + event_frames <= limits["trace_frames"]
|
||||
current_bytes + event_bytes <= MAX_TRACE_BYTES
|
||||
and current_frames + event_frames <= MAX_TRACE_FRAMES
|
||||
)
|
||||
|
||||
|
||||
@@ -64,7 +71,7 @@ def _fits_trace_budget(value, remaining_bytes):
|
||||
class _ObservationJournal:
|
||||
"""Flush a bounded JSONL prefix so observations survive tracer failure."""
|
||||
|
||||
def __init__(self, handle, max_bytes):
|
||||
def __init__(self, handle, max_bytes=MAX_OBSERVATION_JOURNAL_BYTES):
|
||||
self.handle = handle
|
||||
self.max_bytes = max_bytes
|
||||
self.bytes = 0
|
||||
@@ -364,7 +371,7 @@ def _record(context, thread, frame, index, sequence, location, remaining_bytes):
|
||||
error = lldb.SBError()
|
||||
text = process.ReadCStringFromMemory(
|
||||
values[names[1]],
|
||||
config["capture_limits"]["selector_bytes"],
|
||||
MAX_SELECTOR_BYTES,
|
||||
error,
|
||||
)
|
||||
selector = text if error.Success() and text else None
|
||||
@@ -397,16 +404,10 @@ def _record(context, thread, frame, index, sequence, location, remaining_bytes):
|
||||
_TRACE_CONTEXT = None
|
||||
|
||||
|
||||
def _resource_limit(context, limit):
|
||||
context["resource_limit"] = limit
|
||||
return "resource-limit"
|
||||
|
||||
|
||||
def _handle_breakpoint(context, frame, index):
|
||||
"""Read one hit while LLDB's breakpoint callback owns the stopped frame."""
|
||||
if not frame.IsValid() or frame.GetPC() == lldb.LLDB_INVALID_ADDRESS:
|
||||
raise RuntimeError("LLDB breakpoint callback did not supply a readable frame")
|
||||
limits = context["config"]["capture_limits"]
|
||||
thread = frame.GetThread()
|
||||
events = context["events"]
|
||||
location = _location(frame.GetPCAddress(), context["target"])
|
||||
@@ -423,50 +424,49 @@ def _handle_breakpoint(context, frame, index):
|
||||
context["retained_frames"],
|
||||
0,
|
||||
event_frame_count,
|
||||
limits,
|
||||
):
|
||||
return _resource_limit(context, "trace_frames")
|
||||
return "resource-limit"
|
||||
try:
|
||||
remaining_bytes = limits["trace_bytes"] - context["retained_trace_bytes"]
|
||||
remaining_bytes = MAX_TRACE_BYTES - context["retained_trace_bytes"]
|
||||
record = _record(
|
||||
context, thread, frame, index, len(events), location, remaining_bytes
|
||||
)
|
||||
except ValueError:
|
||||
return _resource_limit(context, "trace_bytes")
|
||||
return "resource-limit"
|
||||
if record is not None:
|
||||
if not _fits_trace_budget(record, remaining_bytes):
|
||||
return _resource_limit(context, "trace_bytes")
|
||||
return "resource-limit"
|
||||
record_bytes = len(json.dumps(record).encode("utf-8"))
|
||||
if context["retained_trace_bytes"] + record_bytes > limits["trace_bytes"]:
|
||||
return _resource_limit(context, "trace_bytes")
|
||||
if context["retained_trace_bytes"] + record_bytes > MAX_TRACE_BYTES:
|
||||
return "resource-limit"
|
||||
for depth in range(1, frame_count + 1):
|
||||
try:
|
||||
location = _location(
|
||||
thread.GetFrameAtIndex(depth).GetPCAddress(), context["target"]
|
||||
)
|
||||
except ValueError:
|
||||
return _resource_limit(context, "trace_bytes")
|
||||
return "resource-limit"
|
||||
if (
|
||||
_estimated_json_size(location) + record_bytes + 1
|
||||
> limits["trace_bytes"] - context["retained_trace_bytes"]
|
||||
> MAX_TRACE_BYTES - context["retained_trace_bytes"]
|
||||
):
|
||||
return _resource_limit(context, "trace_bytes")
|
||||
return "resource-limit"
|
||||
location_bytes = len(json.dumps(location).encode("utf-8"))
|
||||
addition = location_bytes + (1 if record["backtrace"] else 0)
|
||||
if (
|
||||
context["retained_trace_bytes"]
|
||||
+ record_bytes
|
||||
+ addition
|
||||
> limits["trace_bytes"]
|
||||
> MAX_TRACE_BYTES
|
||||
):
|
||||
return _resource_limit(context, "trace_bytes")
|
||||
return "resource-limit"
|
||||
record["backtrace"].append(location)
|
||||
record_bytes += addition
|
||||
journal = context.get("observation_journal")
|
||||
if journal is not None and not journal.append(
|
||||
{"kind": "event", "event": record}
|
||||
):
|
||||
return _resource_limit(context, "trace_bytes")
|
||||
return "resource-limit"
|
||||
events.append(record)
|
||||
context["retained_frames"] += event_frame_count
|
||||
context["retained_trace_bytes"] += record_bytes
|
||||
@@ -498,9 +498,9 @@ def _handle_stop(context, other_stops):
|
||||
if journal is not None and not journal.append(
|
||||
{"kind": "other-stop", "reason": stop_description}
|
||||
):
|
||||
return _resource_limit(context, "trace_bytes")
|
||||
return "resource-limit"
|
||||
other_stops.append(stop_description)
|
||||
if len(other_stops) >= context["config"]["capture_limits"]["other_stops"]:
|
||||
if len(other_stops) >= MAX_OTHER_STOPS:
|
||||
return "stop-limit"
|
||||
return None
|
||||
|
||||
@@ -520,7 +520,7 @@ def _breakpoint_report(target, created, config):
|
||||
continue
|
||||
count += 1
|
||||
remaining_bytes = (
|
||||
config["capture_limits"]["breakpoint_location_bytes"] - retained_location_bytes
|
||||
MAX_BREAKPOINT_LOCATION_BYTES - retained_location_bytes
|
||||
)
|
||||
if not _fits_trace_budget(location, remaining_bytes):
|
||||
locations_truncated = True
|
||||
@@ -651,7 +651,7 @@ def trace(debugger, config):
|
||||
for breakpoint in breakpoints:
|
||||
breakpoint_index[breakpoint.GetID()] = index
|
||||
listener = lldb.SBListener(LISTENER_NAME)
|
||||
output_budget = config["capture_limits"]["output_bytes"]
|
||||
output_budget = config.get("max_output_bytes", MAX_TARGET_OUTPUT_BYTES)
|
||||
stdout = _OutputPipe(
|
||||
config["stdout_path"], config["stdout_capture_path"], output_budget
|
||||
)
|
||||
@@ -671,8 +671,7 @@ def trace(debugger, config):
|
||||
observation_path = config.get("observation_path")
|
||||
if observation_path:
|
||||
observation_journal = _ObservationJournal(
|
||||
open(observation_path, "wb"),
|
||||
config["capture_limits"]["trace_bytes"],
|
||||
open(observation_path, "wb")
|
||||
)
|
||||
process = target.Launch(_launch_info(target, config, listener), error)
|
||||
process_created = process.IsValid()
|
||||
@@ -863,7 +862,6 @@ def trace(debugger, config):
|
||||
"breakpoint_locations_truncated": breakpoint_locations_truncated,
|
||||
"events": events,
|
||||
"resource_limit_reached": outcome == "resource-limit",
|
||||
"resource_limit": context.get("resource_limit"),
|
||||
"target_output": {
|
||||
"stdout_bytes": stdout.bytes,
|
||||
"stderr_bytes": stderr.bytes,
|
||||
|
||||
@@ -121,7 +121,6 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
result = tracer.trace(Debugger(), {
|
||||
"executable": "fixture", "architecture": "arm64", "expected_sha256": "expected",
|
||||
"breakpoints": [], "working_directory": "fixture/absent",
|
||||
"capture_limits": {"output_bytes": 1024},
|
||||
"stdout_path": "stdout", "stdout_capture_path": "stdout-capture",
|
||||
"stderr_path": "stderr", "stderr_capture_path": "stderr-capture",
|
||||
})
|
||||
@@ -321,44 +320,37 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
return super().flush()
|
||||
|
||||
stream = CountingStream()
|
||||
journal = tracer._ObservationJournal(stream, max_bytes=4096)
|
||||
journal = tracer._ObservationJournal(stream)
|
||||
journal.append({"kind": "event", "event": {"sequence": 0}})
|
||||
journal.append({"kind": "other-stop", "reason": "signal"})
|
||||
self.assertEqual((stream.writes, stream.flushes), (2, 2))
|
||||
self.assertEqual(journal.bytes, len(stream.getvalue()))
|
||||
|
||||
def test_aggregate_trace_budget_uses_the_selected_admission(self):
|
||||
limits = {"trace_bytes": 100, "trace_frames": 10}
|
||||
self.assertTrue(tracer._can_retain_trace(90, 8, 10, 2, limits))
|
||||
self.assertFalse(tracer._can_retain_trace(90, 8, 11, 2, limits))
|
||||
self.assertFalse(tracer._can_retain_trace(90, 8, 10, 3, limits))
|
||||
raised = {"trace_bytes": 101, "trace_frames": 11}
|
||||
self.assertTrue(tracer._can_retain_trace(90, 8, 11, 3, raised))
|
||||
|
||||
def test_breakpoint_reports_exact_exhausted_budget_and_accepts_more_room(self):
|
||||
frame = types.SimpleNamespace(
|
||||
IsValid=lambda: True, GetPC=lambda: 1,
|
||||
GetThread=lambda: types.SimpleNamespace(GetNumFrames=lambda: 1),
|
||||
GetPCAddress=lambda: object(),
|
||||
def test_aggregate_trace_budget_admits_exact_boundary_and_rejects_next_record(self):
|
||||
self.assertTrue(
|
||||
tracer._can_retain_trace(
|
||||
tracer.MAX_TRACE_BYTES - 10,
|
||||
tracer.MAX_TRACE_FRAMES - 2,
|
||||
10,
|
||||
2,
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
tracer._can_retain_trace(
|
||||
tracer.MAX_TRACE_BYTES - 10,
|
||||
tracer.MAX_TRACE_FRAMES - 2,
|
||||
11,
|
||||
2,
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
tracer._can_retain_trace(
|
||||
tracer.MAX_TRACE_BYTES - 10,
|
||||
tracer.MAX_TRACE_FRAMES - 2,
|
||||
10,
|
||||
3,
|
||||
)
|
||||
)
|
||||
context = {
|
||||
"target": object(), "events": [], "retained_frames": 1,
|
||||
"retained_trace_bytes": 0,
|
||||
"config": {"breakpoints": [{}], "backtrace_frames": 0, "max_events": 10,
|
||||
"capture_limits": {"trace_bytes": 4096, "trace_frames": 1}},
|
||||
}
|
||||
with patch.object(tracer, "_location", return_value={"symbol": "main"}), patch.object(
|
||||
tracer, "_wanted", return_value=True
|
||||
), patch.object(tracer, "_record", return_value={"backtrace": []}):
|
||||
self.assertEqual(tracer._handle_breakpoint(context, frame, 0), "resource-limit")
|
||||
self.assertEqual(context["resource_limit"], "trace_frames")
|
||||
context["config"]["capture_limits"] = {"trace_bytes": 8, "trace_frames": 2}
|
||||
self.assertEqual(tracer._handle_breakpoint(context, frame, 0), "resource-limit")
|
||||
self.assertEqual(context["resource_limit"], "trace_bytes")
|
||||
self.assertEqual(context["events"], [])
|
||||
context["config"]["capture_limits"]["trace_bytes"] = 4096
|
||||
self.assertIsNone(tracer._handle_breakpoint(context, frame, 0))
|
||||
self.assertEqual(context["events"], [{"backtrace": []}])
|
||||
|
||||
def test_oversized_producer_string_is_rejected_before_serialization(self):
|
||||
self.assertTrue(tracer._fits_trace_budget("method", 1024))
|
||||
@@ -467,7 +459,6 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
object(),
|
||||
[[Breakpoint()]],
|
||||
{
|
||||
"capture_limits": {"breakpoint_location_bytes": 4096},
|
||||
"breakpoints": [
|
||||
{
|
||||
"kind": "objc-method",
|
||||
@@ -502,7 +493,7 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
side_effect=lambda address, _target: {"address": address, "symbol": None},
|
||||
), patch.object(tracer, "_wanted", return_value=True):
|
||||
report, locations_truncated = tracer._breakpoint_report(
|
||||
object(), [[Breakpoint()]], {"breakpoints": [{}], "capture_limits": {"breakpoint_location_bytes": 4096}}
|
||||
object(), [[Breakpoint()]], {"breakpoints": [{}]}
|
||||
)
|
||||
|
||||
self.assertEqual(report[0]["location_count"], 65)
|
||||
@@ -583,10 +574,9 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
def GetLocationAtIndex(self, index):
|
||||
return Location()
|
||||
|
||||
breakpoint_count = 30
|
||||
breakpoint_count = 1000
|
||||
created = [[Breakpoint()] for _ in range(breakpoint_count)]
|
||||
config = {"breakpoints": [{"kind": "objc-method"}] * breakpoint_count,
|
||||
"capture_limits": {"breakpoint_location_bytes": 512 * 1024}}
|
||||
config = {"breakpoints": [{"kind": "objc-method"}] * breakpoint_count}
|
||||
for source_path in (None, "/tmp/" + "源🚀" * 3000 + "/calls.pas"):
|
||||
with self.subTest(source_path_present=source_path is not None):
|
||||
report, locations_truncated = tracer._breakpoint_report(
|
||||
@@ -600,13 +590,13 @@ class NativeTraceBudgetTests(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue(locations_truncated)
|
||||
self.assertEqual(
|
||||
sum(row["location_count"] for row in report), 1920
|
||||
sum(row["location_count"] for row in report), 64_000
|
||||
)
|
||||
self.assertLess(
|
||||
sum(len(row["locations"]) for row in report), 1920
|
||||
sum(len(row["locations"]) for row in report), 64_000
|
||||
)
|
||||
self.assertLessEqual(
|
||||
retained_bytes, 512 * 1024
|
||||
retained_bytes, tracer.MAX_BREAKPOINT_LOCATION_BYTES
|
||||
)
|
||||
if source_path is not None:
|
||||
self.assertTrue(
|
||||
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
import resource
|
||||
import gdb
|
||||
|
||||
OUTPUT_BYTES = 64 * 1024 * 1024
|
||||
|
||||
|
||||
class ContextFailure(Exception):
|
||||
@@ -78,7 +79,6 @@ def failure_exit(request_path, status, marker):
|
||||
def main():
|
||||
request_path = os.environ["REA_CORE_CONTEXT_REQUEST"]
|
||||
request = json.loads(Path(request_path).read_text(encoding="utf-8"))
|
||||
output_bytes = request["producer_limits"]["output_bytes"]
|
||||
reserve = bytearray(1024 * 1024)
|
||||
try:
|
||||
reply = {"ok": True, "value": inspect(request)}
|
||||
@@ -94,8 +94,8 @@ def main():
|
||||
reply = {"ok": False, "reason": "decoder", "message": type(error).__name__ + ": " + str(error)}
|
||||
try:
|
||||
encoded = json.dumps(reply, ensure_ascii=True, allow_nan=False).encode("utf-8")
|
||||
if len(encoded) > output_bytes:
|
||||
encoded = json.dumps({"ok": False, "reason": "output-limit", "message": "Complete debugger context exceeds producer_limits.output_bytes=" + str(output_bytes) + "."}).encode("utf-8")
|
||||
if len(encoded) > OUTPUT_BYTES:
|
||||
encoded = json.dumps({"ok": False, "reason": "output-limit", "message": "Complete debugger context exceeds the 64 MiB reply budget."}).encode("utf-8")
|
||||
descriptor = os.open(request["reply_path"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(descriptor, "wb") as handle:
|
||||
handle.write(encoded)
|
||||
|
||||
@@ -14,7 +14,7 @@ BRIDGE = Path(__file__).with_name("core_context.py")
|
||||
|
||||
|
||||
class CoreContextTests(unittest.TestCase):
|
||||
def inspect(self, failure=None, connection="core", symbols=None, architecture="x86_64-little-endian", objfile="recorded-executable", output_bytes=16384):
|
||||
def inspect(self, failure=None, connection="core", symbols=None, architecture="x86_64-little-endian", objfile="recorded-executable"):
|
||||
commands, reads, queries = [], [], []
|
||||
state = {"executable": None, "symbols": None, "connection": None}
|
||||
gdb = types.ModuleType("gdb")
|
||||
@@ -63,7 +63,7 @@ class CoreContextTests(unittest.TestCase):
|
||||
plugin.write_text("")
|
||||
request = stage / "request.json"
|
||||
reply = stage / "reply.json"
|
||||
request.write_text(json.dumps({"gdbinit": str(plugin), "architecture": architecture, "snapshot_path": str(root / "object.snapshot"), "reply_path": str(reply), "producer_limits": {"output_bytes": output_bytes}}))
|
||||
request.write_text(json.dumps({"gdbinit": str(plugin), "architecture": architecture, "snapshot_path": str(root / "object.snapshot"), "reply_path": str(reply)}))
|
||||
with patch.dict(sys.modules, modules), patch.dict(os.environ, REA_CORE_CONTEXT_REQUEST=str(request)):
|
||||
runpy.run_path(str(BRIDGE))
|
||||
result = json.loads(reply.read_text())
|
||||
@@ -104,15 +104,14 @@ class CoreContextTests(unittest.TestCase):
|
||||
self.assertEqual(reads, [])
|
||||
self.assertEqual(queries, [])
|
||||
|
||||
def test_selected_output_limit_reports_recovery_and_accepts_more_headroom(self):
|
||||
result, _, reads, queries = self.inspect(objfile="m" * 2048, output_bytes=1024)
|
||||
def test_output_limit_message_separates_the_words_and_names_the_limit(self):
|
||||
result, _, reads, queries = self.inspect(objfile="m" * (64 * 1024 * 1024))
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertEqual(result["reason"], "output-limit")
|
||||
self.assertIn("producer_limits.output_bytes=1024", result["message"])
|
||||
self.assertNotIn("exceeds64", result["message"])
|
||||
self.assertIn("64 MiB", result["message"])
|
||||
self.assertEqual(reads, [])
|
||||
self.assertEqual(queries, ["maps"])
|
||||
recovered, _, _, _ = self.inspect(objfile="m" * 2048, output_bytes=4096)
|
||||
self.assertTrue(recovered["ok"], recovered)
|
||||
|
||||
def test_wrong_connection_or_remaining_symbols_never_queries_mappings(self):
|
||||
for options in [{"connection": "native"}, {"symbols": "unexpected-symbols"}]:
|
||||
|
||||
@@ -5,12 +5,14 @@ from pathlib import Path
|
||||
import resource
|
||||
import sys
|
||||
|
||||
def lower_resource_limits(policy):
|
||||
OUTPUT_BYTES = 64 * 1024 * 1024
|
||||
|
||||
def lower_resource_limits():
|
||||
limits = {}
|
||||
for name, kind, maximum in (
|
||||
("address_space_bytes", resource.RLIMIT_AS, policy["address_space_bytes"]),
|
||||
("cpu_seconds", resource.RLIMIT_CPU, policy["cpu_seconds"]),
|
||||
("file_size_bytes", resource.RLIMIT_FSIZE, policy["file_bytes"]),
|
||||
("address_space_bytes", resource.RLIMIT_AS, 3 * 1024**3),
|
||||
("cpu_seconds", resource.RLIMIT_CPU, 30),
|
||||
("file_size_bytes", resource.RLIMIT_FSIZE, OUTPUT_BYTES),
|
||||
):
|
||||
soft, hard = resource.getrlimit(kind)
|
||||
finite = [maximum] + [value for value in (soft, hard) if value != resource.RLIM_INFINITY]
|
||||
@@ -23,11 +25,6 @@ def lower_resource_limits(policy):
|
||||
|
||||
def main(request_path, inspect, failure_type, profile):
|
||||
request = json.loads(Path(request_path).read_text(encoding="utf-8"))
|
||||
policy = request["producer_limits"]
|
||||
for key in ("input_bytes", "output_bytes", "diagnostic_bytes", "address_space_bytes", "file_bytes", "cpu_seconds"):
|
||||
value = policy[key]
|
||||
if type(value) is not int or not 1 <= value <= 9007199254740991:
|
||||
raise ValueError("producer_limits." + key + " must be a positive safe integer")
|
||||
os.environ["PWNLIB_NOTERM"] = "1"
|
||||
os.environ["PWNLIB_CACHE_DIR"] = str(Path(request_path).parent / "cache")
|
||||
limits = None
|
||||
@@ -35,7 +32,7 @@ def main(request_path, inspect, failure_type, profile):
|
||||
# The outer exit-status contract also covers allocation failure during serialization.
|
||||
memory_reserve = bytearray(1024 * 1024)
|
||||
try:
|
||||
limits = lower_resource_limits(policy)
|
||||
limits = lower_resource_limits()
|
||||
descriptor = os.open(Path(request_path).parent / "limits.json", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
||||
json.dump(limits, handle)
|
||||
@@ -54,8 +51,8 @@ def main(request_path, inspect, failure_type, profile):
|
||||
else:
|
||||
reply = {"ok": False, "reason": "decoder", "message": type(error).__name__ + ": " + str(error)}
|
||||
encoded = json.dumps(reply, ensure_ascii=True, allow_nan=False).encode("utf-8")
|
||||
if len(encoded) > policy["output_bytes"]:
|
||||
encoded = json.dumps({"ok": False, "reason": "output-limit", "message": "Complete artifact result exceeds producer_limits.output_bytes=" + str(policy["output_bytes"]) + "."}).encode("utf-8")
|
||||
if len(encoded) > OUTPUT_BYTES:
|
||||
encoded = json.dumps({"ok": False, "reason": "output-limit", "message": "Complete artifact result exceeds the 64 MiB reply budget."}).encode("utf-8")
|
||||
descriptor = os.open(request["reply_path"], os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(descriptor, "wb") as handle:
|
||||
handle.write(encoded)
|
||||
|
||||
@@ -111,10 +111,6 @@ product pages. The VitePress documentation source is this `docs/` tree.
|
||||
## Development and verification
|
||||
|
||||
- [Settings and recovery](design/limits-and-recovery.md)
|
||||
- [Flutter budget calibration](verification/flutter-budget-calibration.md)
|
||||
- [Source-map budget calibration](verification/source-map-budget-calibration.md)
|
||||
- [Source-location delivery qualification](verification/source-location-delivery.md)
|
||||
- [JavaScript precision calibration](verification/javascript-precision-calibration.md)
|
||||
- [MCP tool design](development/tool-design.md)
|
||||
- [Testing REA](development/testing.md)
|
||||
- [Client and agent verification](development/client-verification.md)
|
||||
|
||||
+3
-18
@@ -24,7 +24,7 @@ entry-point status, sizes and SHA-256. Sourcemaps, bytecode and module-info regi
|
||||
are separate children. Source names such as `/$bunfs/root/main.js` remain in
|
||||
metadata; extraction uses a relative path. Unsafe or non-UTF-8 names receive an
|
||||
indexed path and retain their source spelling or bytes. Virtual names deeper
|
||||
than the selected `path_parts` allowance (default 256) also use an indexed path with an explicit limitation,
|
||||
than 256 logical path parts also use an indexed path with an explicit limitation,
|
||||
preventing unbounded path-tree work while retaining the source fact. Duplicate names remain
|
||||
separate occurrences; ordinary extraction collision rules apply.
|
||||
|
||||
@@ -43,27 +43,12 @@ compatible source files; bytecode is retained as evidence, not decoded.
|
||||
Module parsing reads exact metadata ranges rather than loading stored asset
|
||||
bodies or whole pages around small pointers. A cache retains at most 256 KiB
|
||||
and 1,024 ranges for one reader preparation. Cumulative uncached metadata reads
|
||||
default to 256 MiB, checked before allocation or I/O. The module table,
|
||||
are limited to 256 MiB, checked before allocation or I/O. The module table,
|
||||
decoded strings and combined logical path tree have separate admission checks;
|
||||
by default at most 250,000 distinct path prefixes are admitted before inventory construction.
|
||||
at most 250,000 distinct path prefixes are admitted before inventory construction.
|
||||
Stored bodies are range-streamed for hashing and extraction. Header/suffix observations are reused
|
||||
only for the same admitted file state, with selected-path checks before publication.
|
||||
|
||||
Both artifact operations accept `artifact_limits.bun`. Its independent controls
|
||||
are `payload_bytes`, `module_count`, `representation_bytes`, `metadata_read_bytes`,
|
||||
`recognition_table_bytes`, `recognition_load_command_bytes`, `path_parts` and
|
||||
`path_nodes`. Failed recognition remains explicitly limited instead of proving
|
||||
absence. ELF section names are examined at their actual offsets, including names
|
||||
beyond a large section-name prefix. Select `artifact_limits.expanded_bytes` to
|
||||
change the aggregate referenced-plaintext work allowance.
|
||||
|
||||
For a reported metadata-read refusal, keep the same carrier and retry with
|
||||
`--artifact-limits '{"bun":{"metadata_read_bytes":536870912}}'` or the equivalent
|
||||
MCP `artifact_limits` object. Omitted fields keep defaults. The response names
|
||||
the effective limit and exact repair; accepted overrides are reported separately
|
||||
from caller selections. Changes apply on the next call without restarting REA.
|
||||
Summary and selected extraction do not change the producer's admission.
|
||||
|
||||
See [limits and recovery](design/limits-and-recovery.md) for admission ceilings.
|
||||
Synthetic tests cover the carriers and malformed declarations. Run
|
||||
`node scripts/verify-bun-standalone.mjs` after `npm run build:cached` for local Bun
|
||||
|
||||
@@ -172,20 +172,17 @@ 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 |
|
||||
| `asar.max_listing_bytes` | Next expanded ASAR listing, independently of packed header bytes |
|
||||
| `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 |
|
||||
| `ghidra.decompile_max_payload_mib` | Next native decompile; `null` preserves the selected provider's payload default |
|
||||
| `ghidra.value_flow_max_operations`, `ghidra.value_flow_max_inputs_per_operation`, `ghidra.value_flow_max_edges` | Next function value-flow projection on the existing Program |
|
||||
| `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 |
|
||||
| `native.matching_representation_bytes` | Next `match_native_functions` request |
|
||||
| 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 |
|
||||
| `native.matching_representation_bytes` | Next `match_native_functions` request |
|
||||
|
||||
### Precedence and lifetime
|
||||
|
||||
@@ -205,48 +202,6 @@ for an unknown or omitted value elsewhere.
|
||||
A write does not extend active work, relaunch a provider, clear retained
|
||||
cleanup, or invalidate successful Evidence.
|
||||
|
||||
Expanded ASAR listing admission defaults to 64 MiB and has its own launch seed,
|
||||
`REA_ASAR_MAX_LISTING_BYTES`. Raising `asar.max_header_bytes` alone cannot admit
|
||||
expanded paths and entries that exceed this separate budget. The listing bound
|
||||
uses exact safe-integer byte accounting; the host heap and transport constraints
|
||||
remain independent. A lower runtime value is enforced on the next listing even
|
||||
when the archive header was already examined.
|
||||
|
||||
Ghidra value-flow controls default to 3,000 operations, 64 inputs per operation,
|
||||
and 12,000 edges. They accept positive Java integer counts and use the respective
|
||||
`REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS`,
|
||||
`REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION`, and
|
||||
`REA_GHIDRA_VALUE_FLOW_MAX_EDGES` launch seeds. Truncation names the exhausted
|
||||
control. Completed Evidence remains accessible, but a new function dossier is
|
||||
observed with the accepted controls instead of replaying an earlier projection.
|
||||
|
||||
The native decompiler payload control is separate from those projections and
|
||||
from decompile time. Its launch seed is
|
||||
`REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB`. Accepted explicit values are 1..2047 MiB:
|
||||
the supported Ghidra implementation shifts that value into a signed integer byte
|
||||
capacity. Omission or runtime `null` keeps the provider's own default; REA does
|
||||
not guess that default from a different provider version. Changing payload
|
||||
options takes effect before the next native decompile.
|
||||
|
||||
### Per-call resource policies and selection
|
||||
|
||||
An agent can also use controls declared directly by an operation. These do not
|
||||
require a runtime setter or restart. Keep omitted fields omitted in Evidence;
|
||||
report the resolved policy separately where needed. See the owning guides for
|
||||
[Apktool string pages](../guides/android/apktool-resource-analysis.md),
|
||||
[source maps](../guides/web/web-source-location.md),
|
||||
[historical captures](../guides/web/web-network-captures.md),
|
||||
[JavaScript precision](../guides/javascript/javascript-application-graph.md),
|
||||
[managed inspection](../guides/managed/managed-code-analysis.md), and
|
||||
[Frida capture](../guides/runtime/frida-instrumentation.md).
|
||||
|
||||
The same accepted policy must reach acquisition, parsing, worker requests,
|
||||
serialization, cached-result admission and generated consumers where those
|
||||
stages exist. Desktop state views and emitted AgentEnv Python share one policy;
|
||||
delivery pagination never substitutes for producer selection. Capture limits
|
||||
must be selected before an experiment: another experiment cannot recover bytes
|
||||
that were dropped from a previous live observation.
|
||||
|
||||
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
|
||||
@@ -464,55 +419,17 @@ editable setting or active worker telemetry.
|
||||
|
||||
## Bun standalone module graphs
|
||||
|
||||
`inspect_artifact` and `extract_artifact` accept per-call `artifact_limits`.
|
||||
The `bun` group defaults to 100,000 modules, independent 256 MiB payload,
|
||||
retained-representation and physical metadata-read allowances, 256 path parts
|
||||
and 250,000 logical path nodes. Recognition separately admits a 1 MiB ELF
|
||||
section table and 16 MiB Mach-O load commands. Exhausted recognition remains
|
||||
unknown rather than proving the carrier has no Bun graph. Inventory preserves
|
||||
native-root facts or another confirmed pack and reports the exact unexamined
|
||||
alternative inline; it does not assert Bun presence. ELF section names
|
||||
are examined with exact bounded reads, including names beyond the former
|
||||
prefix window.
|
||||
|
||||
For example, keep the same complete carrier and retry a reported recognition
|
||||
limitation with `{"artifact_limits":{"bun":{"recognition_table_bytes":2097152}}}`.
|
||||
The CLI spelling is `--artifact-limits '{"bun":{"recognition_table_bytes":2097152}}'`.
|
||||
No REA restart is needed. A smaller later selection is re-admitted; previously
|
||||
parsed facts cannot bypass it. The 256 KiB / 1,024-entry exact-range cache
|
||||
avoids full-page amplification and remains an internal optimization, not an agent
|
||||
resource setting. Shared logical path prefixes count once before nodes are added.
|
||||
|
||||
## Artifact reader admission
|
||||
|
||||
The same request provides `dotnet_bundle`, `node_sea` and `godot` groups. Their
|
||||
controls apply when recognition or inventory encounters that format; supplying
|
||||
a policy does not assert the format is present. Unsupported keys are rejected.
|
||||
Each group follows one domain-owned default policy. See [.NET bundles](../dotnet-bundles.md),
|
||||
[Node SEA](../node-sea.md), [Godot PCK](../godot-pck.md) and
|
||||
[Bun standalone](../bun-standalone.md) for format semantics.
|
||||
|
||||
`artifact_limits.expanded_bytes` selects aggregate plaintext work for ZIP, Bun,
|
||||
SEA and PCK readers, including repeated readable logical occurrences. Omission
|
||||
retains each archive's encoded-size-derived default. The .NET bundle group's
|
||||
own `expanded_bytes` controls its independently enforced bundle expansion.
|
||||
|
||||
Failures preserve `details.reader_limit` with the exact parameter, effective
|
||||
value, units, supported range, required value when known, next-call scope and
|
||||
`can_increase`. When `can_increase` is true, raise that field and retry the same
|
||||
complete artifact. At the supported maximum, or when the required representation
|
||||
exceeds it, the failure is not retryable by increasing that budget; inspect the
|
||||
declared dimensions and input validity, or explicitly choose a suitable reader.
|
||||
Summary, pagination and
|
||||
selected extraction do not bypass producing admission. Intrinsic format,
|
||||
safe-integer, path validity and runtime representation constraints remain fixed.
|
||||
|
||||
Evidence parameters retain the exact selected object, including omission or
|
||||
`{}`. When an object was selected, inventory and extraction report the resolved
|
||||
`effective_limits`; summaries preserve those facts. These are accepted policies,
|
||||
not process-memory telemetry or a claim that every format reader ran. Complete
|
||||
source identities and graph manifests remain content-addressed independently
|
||||
of delivery selection. No partial extraction is claimed on admission failure.
|
||||
`MAX_BUN_MODULE_COUNT` admits at most 100,000 module records.
|
||||
`MAX_BUN_RETAINED_BYTES` admits at most 256 MiB of serialized graph payload and
|
||||
separately bounds retained entry metadata. Physical metadata read work is also
|
||||
limited to 256 MiB per preparation, including repeated uncached ranges. A
|
||||
256 KiB / 1,024-entry exact-range cache avoids full-page amplification. The
|
||||
combined logical path tree admits at most 250,000 distinct prefixes; this is
|
||||
checked before adding nodes, with shared prefixes counted once. The shared
|
||||
artifact expansion budget also bounds the sum of referenced member lengths. Refusals name the boundary.
|
||||
There is no runtime override; additional host memory or `NODE_OPTIONS` does not
|
||||
raise these ceilings. Use a smaller producer fixture or a separately reviewed
|
||||
reader for larger graphs. No partial extraction is claimed on admission failure.
|
||||
|
||||
## Deno compile sections
|
||||
|
||||
|
||||
@@ -290,8 +290,8 @@ found nowhere are listed in
|
||||
not an ELF image (a launcher script, for example), `ldconfig`, `dpkg-query` or
|
||||
`apt-cache` is missing, `dpkg-query -S` could not tell which package owns a
|
||||
library (it exits 2 rather than 1, which only says no package owns the paths
|
||||
it did not name), a library could not be read or exceeds the selected dependency
|
||||
inventory budget, or the walk reached that budget. A failed owner query does
|
||||
it did not name), a library could not be read or names more than 512
|
||||
libraries, or the walk stopped after 512 libraries. A failed owner query does
|
||||
not list the libraries it covered in `unpackaged_libraries`, since a lookup
|
||||
that could not finish is not proof that no package owns them. Completed batches
|
||||
retain their package and unowned-library facts. Paths containing package-query
|
||||
@@ -428,18 +428,3 @@ evidence for attribution, not proof.
|
||||
|
||||
Clicks and keystrokes are not recorded. Window titles are sampled every half
|
||||
second, so short-lived titles can be missed.
|
||||
|
||||
Dependency inventory headroom is selected for each capture with
|
||||
`--library-policy '{"libraries":1024,"string_bytes":8192}'`. Unset fields retain
|
||||
their defaults: `libraries: 512` bounds both the closure walk and distinct
|
||||
DT_NEEDED offsets in each ELF image; `search_path_entries: 16` bounds distinct
|
||||
DT_RUNPATH and DT_RPATH offsets per kind; `string_bytes: 4096` includes the NUL
|
||||
terminator of each decoded string; `table_bytes: 1048576` bounds each program
|
||||
header or dynamic table. Values must be positive safe integers. A budget refusal
|
||||
names the field that needs headroom; partial package facts and unresolved
|
||||
coverage remain visible. The policy applies during this capture's admission,
|
||||
without restarting REA, and explicit selections are retained in Evidence. Raising
|
||||
content retention or a worker heap does not raise these inventory budgets.
|
||||
Choosing a new capture still launches the selected application; changing these
|
||||
read budgets does not add execution authority or recover an earlier capture's
|
||||
omitted inventory.
|
||||
@@ -560,21 +560,7 @@ or whose retained value nests more than 500 levels deep, gets an `error` instead
|
||||
REA. Duplicate keys keep their last value before the depth check. Text that is not one JSON
|
||||
document, such as JSON Lines, stays text. Format detection follows REA's
|
||||
rules. Files larger than 64 MiB get an `error` instead of a view
|
||||
and are not read. The selected `file_bytes` also applies to scrubbed seed task
|
||||
views before admission, independently of how much seed data the export can
|
||||
copy. SQLite table views have a shared `sqlite_view_bytes` allowance, default
|
||||
16 MiB of compact UTF-8 JSON per table, checked before retaining each row.
|
||||
An oversized table keeps its error while other tables remain readable. Raising
|
||||
this view allowance does not raise the independent 16 MiB native SQLite
|
||||
value/record limit or REA worker reply limit; worker facts include additional
|
||||
schema and typed-cell metadata and may be refused earlier. The TypeScript
|
||||
and exported Python readers count their respective native compact JSON encodings;
|
||||
float and exponent spelling can make their exact byte thresholds differ even
|
||||
when they report equal numeric facts. The Python byte counter quotes large
|
||||
strings in bounded chunks before retention.
|
||||
Exported SQLite readers require Python 3.11 or later for the native connection limits; the
|
||||
AgentEnv compatibility workflow uses Python 3.12.
|
||||
XML is read with Python's `expat` twice: with namespace
|
||||
and are not read. XML is read with Python's `expat` twice: with namespace
|
||||
processing, which refuses what REA's parser refuses, such as an undeclared
|
||||
prefix, then without it, so element and attribute names keep the prefixes the
|
||||
document spells and namespace declarations stay attributes, as in REA's view;
|
||||
|
||||
@@ -11,65 +11,6 @@ again.
|
||||
rea inspect-desktop-application-state ./mousepad-capture --format json
|
||||
```
|
||||
|
||||
Views use bounded defaults. When a view reports truncation or a byte-budget
|
||||
refusal, retry the same capture with `--view-policy` JSON overrides:
|
||||
|
||||
```bash
|
||||
rea inspect-desktop-application-state ./mousepad-capture \
|
||||
--state-path /home/ada/Documents/notes.txt \
|
||||
--view-policy '{"text_lines":10000,"file_bytes":134217728}' --format json
|
||||
```
|
||||
|
||||
`--state-path` selects an exact original captured path and can be repeated.
|
||||
Omitting it examines every captured path. Selection preserves the captured
|
||||
checkpoint and digest; it does not substitute the file's current contents.
|
||||
A path absent from the capture is an input error.
|
||||
|
||||
`--view-policy` accepts positive safe-integer overrides for `file_bytes`,
|
||||
`sqlite_tables`, `sqlite_rows`, `sqlite_view_bytes`, `text_lines`, `xml_nodes`, `xml_depth`,
|
||||
`parsed_bytes`, `json_depth`, `zip_entries`, `zip_entry_bytes`, `zip_bytes`,
|
||||
and `zip_ratio`. Unspecified controls keep the defaults described below.
|
||||
`xml_depth` admits at most 494 and `json_depth` at most 991: these bounds
|
||||
follow the fixed Evidence JSON nesting limit, including the captured-version
|
||||
and ZIP-member wrappers and XML's element/children representation. A request
|
||||
above either bound is rejected before reading the capture.
|
||||
Evidence parameters retain only the supplied `view_policy` overrides; omission
|
||||
and an explicitly empty selection remain distinct. State inspection and task
|
||||
drafts report the resolved policy separately as
|
||||
`normalized_result.effective_view_policy`. Changes apply on this call, without
|
||||
a restart. File and ZIP byte counts refer to retained file bytes or uncompressed
|
||||
ZIP member bytes, not transport size. Gzip document views share `zip_entry_bytes`
|
||||
and `zip_ratio` with ZIP entry views; `zip_bytes` bounds a ZIP archive, not a
|
||||
gzip stream. These selections also apply to the decompressed document's format
|
||||
view. `sqlite_view_bytes` instead bounds each
|
||||
projected table's compact UTF-8 JSON, including column names, primary keys,
|
||||
rows and truncation metadata; it defaults to 16 MiB. Rows are charged before
|
||||
retention. A table that exceeds this allowance keeps an actionable `error`,
|
||||
while other tables remain readable. Increase this control and retry the same
|
||||
capture for a projected-view refusal. It does not raise the SQLite worker's
|
||||
separate fixed value/record or complete-reply limits, which may refuse earlier.
|
||||
Each reader counts its native compact JSON encoding. Float spelling can differ:
|
||||
Python emits `16.0` where JavaScript emits `16`, and exponent spelling also
|
||||
varies. Identical numeric facts can therefore cross this byte threshold at
|
||||
different settings. This is not a common transport-byte guarantee.
|
||||
|
||||
The same option is available on `describe-desktop-application-task` and
|
||||
`export-desktop-application`. Export uses the accepted policy for task
|
||||
validation and emits it into the Python readers used by the environment's
|
||||
`data/get`; re-export with another policy to change those reader budgets.
|
||||
Task checks apply `file_bytes` to the actual scrubbed seed bytes, including any
|
||||
growth from replacing the capture home with `/home/user`. A seed over that
|
||||
view allowance may still be exported under the independent acquisition limit,
|
||||
but its unreadable view cannot establish a known task failure.
|
||||
The TypeScript and exported Python readers share the policy's defaults.
|
||||
These desktop capture workflows remain CLI-only.
|
||||
|
||||
Raising a view budget cannot recover content the capture never retained,
|
||||
change numeric exactness, enable encrypted or unsupported archive formats,
|
||||
or raise SQLite worker, Evidence nesting, process-memory or transport limits.
|
||||
Keep increases specific to the exhausted budget. Truncation and incomplete
|
||||
comparison coverage remain visible until the requested facts were examined.
|
||||
|
||||
The result is Evidence with operation `inspect_desktop_application_state` and
|
||||
predicate type `rea.desktop-application-state`. Its confidence is `inferred`,
|
||||
because roles come from heuristics; the views themselves are read from the
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
A [desktop application export](desktop-application-export.md) can carry one
|
||||
task: a prompt for a computer-use agent and checks that grade what the agent
|
||||
left in the application's files. An agent, not REA, writes the prompt and the
|
||||
checks from the change the capture recorded. For file checks, REA validates
|
||||
the task against the capture and keeps it only when its checks fail on the
|
||||
seed and all pass on the captured end state. The checks are declarative;
|
||||
optional [readers](#readers) run commands to interpret files, and
|
||||
[screen judges](#screen-judges) use a model when no file shows the change.
|
||||
The exported verifier uses the same checks and reader runner as REA.
|
||||
checks from the change the capture recorded. REA validates the task against
|
||||
the capture and keeps it only when its checks fail on the seed and all pass on
|
||||
the captured end state. REA runs no model and no code the agent wrote: the
|
||||
checks are declarative, and the exported verifier evaluates them the same way
|
||||
REA does.
|
||||
|
||||
```sh
|
||||
rea describe-desktop-application-task ./mousepad-capture --format json > draft.json
|
||||
@@ -519,7 +518,7 @@ and `CIRCLE`, and only `CIRCLE` going from 5 to 9 is the user's change.
|
||||
and hyphens); `prompt` is what the user asks for, naming the app, since a
|
||||
solving agent's own shell cannot see the computer's files and the app's name
|
||||
sends it to the screen; `checks` holds 1 to 32
|
||||
checks. Each file check has a `kind`, a `file` given as a path under the home such as
|
||||
checks. Each check has a `kind`, a `file` given as a path under the home such as
|
||||
`Documents/todo.txt`, and an optional `id` (`check-1`, `check-2` and so on by
|
||||
position otherwise). A check reads the typed view `data/get` returns for that
|
||||
file, in the shapes of the state view.
|
||||
@@ -590,82 +589,6 @@ value, or one of only whitespace with `normalize_whitespace`, an empty
|
||||
content, such as a
|
||||
counter the app bumps on every save, is for the author to leave out.
|
||||
|
||||
## Readers
|
||||
|
||||
For a file REA cannot interpret itself, such as an app's binary document,
|
||||
the task can define `readers`: 1 to 8 commands using the app's own reading
|
||||
capability. Each reader has a unique `id` of lowercase letters, digits and
|
||||
hyphens, a home-relative `file`, an `argv` array and `output`, either `json`
|
||||
or `lines`. Its `argv`, or optional `stdin` text, must contain `{file}`;
|
||||
the runner replaces it with the path of a private copy of that file.
|
||||
The command must be installed where the export runs and in the exported
|
||||
computer where the task will be graded.
|
||||
|
||||
The command prints its answer in lines starting with `REA `; other stdout
|
||||
is ignored. With `output: "json"`, those marked lines join into one JSON
|
||||
value for `value_equals`, `value_absent` and `member_count`. With
|
||||
`output: "lines"`, they supply the lines for `lines_present`, `lines_absent`
|
||||
and `line_count`. A check selects a reader with `"reader": "READER-ID"`
|
||||
and must give that reader's file as its `file`. Every reader must be used by
|
||||
at least one check.
|
||||
|
||||
Readers execute commands, including any code in `argv` or `stdin`; review
|
||||
them before exporting or running a task. The runner uses a private copy, a
|
||||
fresh home, no session bus or computer control token, and no display unless
|
||||
the reader requests `display: true`. This is execution isolation from the
|
||||
running app's normal profile, not a filesystem sandbox. `timeout_s` defaults
|
||||
to 60 seconds and accepts 1 to 300. A failed command, timeout, malformed
|
||||
answer or missing marked output reports a reader error; it does not establish
|
||||
that the document lacks the expected value. Correct the command or its output and retry; increase
|
||||
`timeout_s` within its accepted range when the reader needs longer.
|
||||
|
||||
Export runs readers on both retained ends, using the seed bytes as the bundle
|
||||
actually ships them. The witness and exported verifier run them in the
|
||||
computer after the app's files settle. Cleanup failures retain the reader's
|
||||
reported resources: retry the check or close the build computer to retry
|
||||
cleanup; when the command's outcome is unknown, confirm it stopped before
|
||||
removing its folder.
|
||||
|
||||
## Screen judges
|
||||
|
||||
A task whose result exists only on screen can use `screen_judge` checks:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "show-list",
|
||||
"prompt": "In Notes, show the shopping list.",
|
||||
"checks": [
|
||||
{
|
||||
"id": "shown",
|
||||
"kind": "screen_judge",
|
||||
"question": "Does the window show the shopping list?"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each question must be a nonempty yes-or-no question of at most 2,000
|
||||
characters, false on the seed and true when the task is done. A screen task
|
||||
contains only screen checks and takes no readers. Export refuses it when an
|
||||
owned file's readable change could be checked instead, or when the build
|
||||
computer's seed or end screenshot is missing or exceeds 2 MiB. Capture those
|
||||
checkpoints in a build computer before exporting.
|
||||
|
||||
Export ships the two screenshots as labelled failing and passing examples;
|
||||
it does not call the judge or establish that the questions distinguish them.
|
||||
The [build witness](desktop-build-witness.md) runs the exported judge on a
|
||||
fresh screenshot at each stage. The verifier takes a screenshot after the
|
||||
solving agent stops. For each question, the judge sees both examples and
|
||||
votes three times; a majority of yes answers passes the check.
|
||||
|
||||
The judge uses `[model.roles] judge` in the runner's
|
||||
`.agentenv/config.toml`, then `[model] default`, then
|
||||
`anthropic/claude-sonnet-5-5`. Configure its endpoint with
|
||||
`LITELLM_BASE_URL` and `LITELLM_API_KEY`, or `[model] base_url` and `api_key`.
|
||||
`agent-env run --judge-model` does not reach this verifier. Without an
|
||||
endpoint, grading raises and the run fails rather than scoring the agent;
|
||||
configure the endpoint and rerun the witness or task.
|
||||
|
||||
## Pointers and selectors
|
||||
|
||||
An app that rewrites a document moves things: FreeCAD 1.0 records more
|
||||
@@ -873,7 +796,7 @@ that may hold only part of the array.
|
||||
## Validation and export
|
||||
|
||||
`rea export-desktop-application <capture> <directory> --task <task.json>`
|
||||
evaluates file checks on the capture's state view: on each file as it was at
|
||||
evaluates the checks on the capture's state view: on each file as it was at
|
||||
the seed checkpoint, which is the seed, as the bundle ships it with the
|
||||
capture host's name and user removed, and at the end checkpoint, the
|
||||
captured end state. Those are the capture's first and last checkpoints unless
|
||||
|
||||
+1
-14
@@ -50,8 +50,7 @@ explicitly with the managed tools to establish PE/CLI facts.
|
||||
|
||||
## Admission and containment
|
||||
|
||||
Metadata is range-read with a cache of at most 64 KiB, bounded by the remaining
|
||||
selected metadata allowance. Default policy budgets are 16 MiB of
|
||||
Metadata is range-read with a 64 KiB cache. Policy budgets are 16 MiB of
|
||||
manifest metadata, 100,000 files, 64 KiB per UTF-8 string, 512 MiB of stored or
|
||||
expanded bytes per file, 256 path parts per member, 250,000 distinct in-memory
|
||||
path-tree nodes, and 2 GiB of aggregate expanded bytes. The node budget counts
|
||||
@@ -61,18 +60,6 @@ streams 64 KiB chunks and reuses completed probe facts during extraction;
|
||||
ordinary native files without a marker incur one extra
|
||||
read pass before host hashing.
|
||||
|
||||
Both artifact tools accept `artifact_limits.dotnet_bundle` to select
|
||||
`metadata_bytes`, `entry_count`, `string_bytes`, `member_bytes`,
|
||||
`expanded_bytes`, `path_parts`, and `path_nodes` for the next operation. The
|
||||
string allowance applies to every manifest string, including the bundle ID.
|
||||
For example, retry a complete bundle whose manifest exceeds the default with
|
||||
`{"artifact_limits":{"dotnet_bundle":{"metadata_bytes":33554432}}}`.
|
||||
The CLI accepts the same selection through `--artifact-limits` JSON. Omitted
|
||||
fields preserve defaults; errors name the exact selected control, effective
|
||||
value and required minimum when known. No REA restart is needed. Summary
|
||||
delivery and paging do not change producer admission, and larger reader
|
||||
budgets do not raise MCP response or Node representation limits.
|
||||
|
||||
Extraction hashes the whole host again through the same descriptor used to read
|
||||
members and compares it with the inventory's root digest before creating an
|
||||
output directory. This adds a full-host read pass to extraction, including when
|
||||
|
||||
+1
-26
@@ -144,34 +144,9 @@ keys, decode Godot resources, decompile GDScript bytecode, analyze signals,
|
||||
apply patch/removal entries or execute targets.
|
||||
|
||||
Readable logical member bytes, including repeated physical ranges, share the ZIP
|
||||
archive expansion policy by default: at most the larger of 256 MiB or 500 times the PCK's
|
||||
archive expansion policy: at most the larger of 256 MiB or 500 times the PCK's
|
||||
encoded byte span. Embedded carrier bytes do not enlarge that budget. The full
|
||||
directory is checked before any member is yielded, hashed or extracted. Encrypted
|
||||
members are not read and do not consume this plaintext-work allowance. Small
|
||||
bounded aliases remain valid. This is a processing/output resource bound, separate
|
||||
from the directory metadata retention budget and format validity.
|
||||
|
||||
Directory retention defaults to 64 MiB of encoded metadata. Select
|
||||
`artifact_limits.godot.directory_bytes` to change it; path/source facts reserve
|
||||
worst-case JSON expansion before declared path bytes are read, and the reader
|
||||
then charges actual retained metadata. A preallocation `required_value` is the
|
||||
allowance needed by that admission check, not a measurement of final output size.
|
||||
Select `artifact_limits.expanded_bytes` independently to change aggregate
|
||||
plaintext work, including repeated readable logical occurrences. Both controls
|
||||
apply to inspection and extraction, before any member is yielded.
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
rea inspect-artifact ./game.pck --artifact-limits '{"godot":{"directory_bytes":134217728},"expanded_bytes":1073741824}' --json
|
||||
```
|
||||
|
||||
MCP uses the same object under `artifact_limits` on `inspect_artifact` or
|
||||
`extract_artifact`. A refusal reports the exact `reader_limit` parameter,
|
||||
effective value, unit, supported range and required value when known. Raise the
|
||||
named admission and retry the same complete PCK on the next call; no REA restart
|
||||
is needed. Summary or page size does not change producer admission. Omitted
|
||||
policy selections stay omitted in Evidence parameters; a selected object records
|
||||
the accepted selection and `effective_limits`, conditional on which readers are
|
||||
encountered. Raising these policies does not admit malformed offsets, unsupported
|
||||
flags, unsafe extraction paths or unavailable encrypted contents.
|
||||
@@ -66,8 +66,6 @@ rea decode-android-resources /targets/Example.apk --locales de --locales pt-BR
|
||||
rea decode-android-resources /targets/Example.apk --locales de --locales pt-BR --string-tables-only
|
||||
rea decode-android-resources /targets/Example.apk --no-strings
|
||||
rea decode-android-resources /targets/Example.apk --decode-timeout-ms 900000
|
||||
rea decode-android-resources /targets/Example.apk --string-offset 2000 --string-limit 100 --string-tables-only
|
||||
rea decode-android-resources /targets/Example.apk --max-string-table-bytes 16777216 --max-total-string-table-bytes 67108864
|
||||
```
|
||||
|
||||
Equivalent MCP methods are `inspect_apktool_client` and
|
||||
@@ -89,31 +87,6 @@ responses and charges the aggregate output budget for one copy per table.
|
||||
Omitting the option preserves existing responses. `include_strings: false`
|
||||
still suppresses all string projection, including in this mode.
|
||||
|
||||
Use `string_offset` and `string_limit` (CLI `--string-offset` and
|
||||
`--string-limit`) to select a page of valid parsed entries from each requested
|
||||
table, in source order. Defaults are offset 0 and limit 2000. Explicit page or
|
||||
budget controls include `string_tables` even for a single primary table; its
|
||||
`page` gives the requested offset and limit, returned count, and `next_offset`.
|
||||
Retry that offset with the same locale and confirm `target.sha256` matches.
|
||||
Each call decodes a fresh owned snapshot; it does not retain a workspace or
|
||||
promise identity across changed APKs. A null continuation means the page reaches
|
||||
the end, while table coverage remains partial if earlier entries were omitted.
|
||||
An offset at or beyond the end returns an empty, present table with known counts.
|
||||
If the continuation equals the requested offset, no output entry fit: increase
|
||||
`max_total_strings` or use `string_tables_only` before retrying.
|
||||
|
||||
`max_total_strings` (CLI `--max-total-strings`, default 10000) bounds returned
|
||||
string copies across all tables. Raise it, request fewer locales, or use
|
||||
`string_tables_only` when aggregate output is exhausted. The legacy
|
||||
multi-table shape charges both copies of primary strings. `max_string_table_bytes`
|
||||
(CLI `--max-string-table-bytes`, default 8388608) bounds one XML source read;
|
||||
`max_total_string_table_bytes` (CLI `--max-total-string-table-bytes`, default 33554432) bounds XML reads across distinct physical tables. Locale aliases
|
||||
share the physical read and parsed page. Raise the relevant read budget to
|
||||
retry an oversized table; a larger string limit or timeout does not raise it.
|
||||
These controls take nonnegative safe-integer offsets and positive safe-integer
|
||||
budgets, apply on the next decode, and require no REA restart. Process memory
|
||||
and response transport capacity still apply.
|
||||
|
||||
## Interpret the result
|
||||
|
||||
- **An owned APK snapshot is digested and decoded.** `target.sha256` and
|
||||
@@ -144,8 +117,8 @@ and response transport capacity still apply.
|
||||
Malformed XML and a wrong document root are whole-document projection
|
||||
failures: the result is partial, the failure reason is disclosed, and entry
|
||||
counts remain unknown. They are not counted as one unsupported string.
|
||||
The default page returns up to 2000 entries with partial coverage when more
|
||||
exist; its limitation names the next `string_offset`. `locales` lists observed regular `strings.xml` tables whose exact
|
||||
The default table is bounded to 2000 entries with partial coverage when more
|
||||
exist. `locales` lists observed regular `strings.xml` tables whose exact
|
||||
`values-<locale>` directory is selectable by the request's locale grammar.
|
||||
Locale suffixes allow up to 23 ASCII characters, derived from Apktool's
|
||||
language, script, region, and variant fields; for example,
|
||||
@@ -160,10 +133,9 @@ and response transport capacity still apply.
|
||||
When multiple tables are requested with `locales`, `string_tables` records
|
||||
each projected table with its locale, status (`present`, `missing`,
|
||||
`oversized`, `malformed`, or `skipped`), entry counts, and coverage,
|
||||
with default aggregate budgets of 10,000 strings and 32 MiB XML. When `locales`
|
||||
is omitted, `string_tables_only` is not true and no string page or budget
|
||||
control is selected, legacy single-table payloads are preserved without adding
|
||||
`string_tables`.
|
||||
bounded at an aggregate budget of 10,000 strings and 32 MiB XML. When `locales`
|
||||
is omitted and `string_tables_only` is not true, legacy single-table payloads are preserved
|
||||
without adding `string_tables`.
|
||||
- **Smali is skipped by design** (`apktool d --no-src`): Java-source
|
||||
recovery belongs to the JADX family. Resource smali edits and rebuild are
|
||||
a follow-up lane with mutation authority.
|
||||
@@ -184,8 +156,8 @@ and response transport capacity still apply.
|
||||
- The version probe waits 20000 ms and retains 65536 bytes. This release has
|
||||
no runtime override for that probe. `decode_timeout_ms` does not raise it.
|
||||
- Manifest reads are bounded to 512 KiB before decoding UTF-8. Strings XML
|
||||
reads default to 8 MiB per table; larger documents are skipped with partial
|
||||
coverage and an explicit read-budget recovery, rather than reported as missing.
|
||||
reads are bounded to 8 MiB; larger documents are skipped with partial
|
||||
coverage and an explicit limit, rather than reported as missing.
|
||||
- `decoded_file_count` is capped at 100,000 files; the cap is reported in
|
||||
the limitations when hit.
|
||||
- An ordinary nonzero decode exit surfaces as `unsupported_target` naming
|
||||
|
||||
@@ -217,47 +217,3 @@ The Gallery workflow and all 147 focused Flutter tests also pass on macOS Sonoma
|
||||
including the conflicting-load and note-range-limit CLI/MCP regressions. Windows
|
||||
verification runs under a non-administrator account. Verifiers use a shared
|
||||
temporary parent so each CLI/MCP process can own and release its own snapshots.
|
||||
|
||||
## Selecting and recovering string-pool observations
|
||||
|
||||
`inspect_dart_aot` accepts `pool_policy`; the CLI uses `--pool-policy` JSON:
|
||||
|
||||
```sh
|
||||
rea inspect-dart-aot app.apk --pool-policy '{"contains":"package:my_app/"}'
|
||||
rea inspect-dart-aot app.apk --pool-policy '{"uri_limit":2000,"token_limit":4000,"run_bytes":4096,"token_bytes":256}'
|
||||
```
|
||||
|
||||
A literal, case-sensitive `contains` filter runs before distinct collection,
|
||||
so an omitted URI or identifier can be selected without returning every other
|
||||
string. URI/token counts describe matching entries; `printable_run_count` counts
|
||||
complete printable runs before filtering. Unset budgets preserve the defaults:
|
||||
500 returned entries per URI category, 2000 identifier-like tokens, 1024 bytes
|
||||
per printable run, and 128 bytes per identifier. Budget values must be positive
|
||||
safe integers. Collection retains at most twice the selected category limit;
|
||||
counts remain lower bounds when that capacity is reached. A run exceeding
|
||||
`run_bytes` is omitted whole, rather than reported as a fabricated prefix.
|
||||
The result names the budget reached, and `string_pool.policy` records the
|
||||
selected filter and effective values. A larger budget or a narrower selection
|
||||
applies to the next call without a REA restart; explicit choices stay in Evidence.
|
||||
|
||||
For a larger package-URI inventory, select the category and its relevant budget:
|
||||
|
||||
```sh
|
||||
rea inspect-dart-aot app.apk --pool-policy '{"contains":"package:","uri_limit":2048}'
|
||||
```
|
||||
|
||||
The equivalent MCP input is `pool_policy: {contains: "package:", uri_limit: 2048}`.
|
||||
This recovers all bounded package-URI observations in the measured Gallery and
|
||||
LocalSend APKs. Check the result's limits and coverage for other targets; long
|
||||
runs and unsupported snapshot layouts remain separate limitations. The
|
||||
[calibration report](../../verification/flutter-budget-calibration.md)
|
||||
keeps the default at 500 because increasing every complete overview also delivers
|
||||
more strings when they are unnecessary. Use `detail: "summary"` for the MCP
|
||||
identity/count task described above.
|
||||
|
||||
These controls do not raise APK, ELF, library or response budgets, deserialize
|
||||
Dart objects, or establish typed class/function semantics. Retained
|
||||
`inspect_analysis_view` pages cover the returned selection; exhausting those
|
||||
pages does not recover facts that the producing pool scan omitted. Re-run the
|
||||
producer with the reported selection or budget change to obtain a new
|
||||
observation, checking its APK and library digests before comparing it.
|
||||
@@ -117,8 +117,8 @@ are never passed to TShark.
|
||||
Installed libraries, global configuration and plugins remain trusted, are not
|
||||
fully fingerprinted, and can influence dissection. This is not an OS sandbox.
|
||||
|
||||
Default limits are 32 MiB input before reading, 30 seconds per command, and
|
||||
4 MiB combined stdout/stderr during collection. Output overflow fails instead
|
||||
Input is limited to 32 MiB before reading, each command to 30 seconds, and
|
||||
combined stdout/stderr to 4 MiB during collection. Output overflow fails instead
|
||||
of returning truncated success. Before execution, `prlimit` enforces **1 GiB
|
||||
address space per process**, **64 MiB per file**, **30 CPU seconds** and no core
|
||||
dumps. Stricter inherited soft/hard limits are preserved; Evidence records the effective
|
||||
@@ -128,23 +128,6 @@ These are not aggregate process-memory or filesystem quotas. The selected
|
||||
limiter is trusted; its path, digest and arguments are retained in Evidence.
|
||||
No arbitrary process flags are accepted.
|
||||
|
||||
Each capture/recording operation accepts `producer_limits` with positive safe
|
||||
integer overrides: `input_bytes`, `output_bytes`, `command_timeout_ms`,
|
||||
`address_space_bytes`, `file_bytes`, and `cpu_seconds`. The wall-clock timeout
|
||||
is at most 2,147,483,647 milliseconds, matching Node timers. Omitted fields keep
|
||||
the defaults above. Discovery accepts the same controls except `input_bytes`,
|
||||
since it does not read a capture or recording. Packet field discovery defaults
|
||||
to 32 MiB output rather than 4 MiB.
|
||||
|
||||
CLI supplies this object as JSON with `--producer-limits`, for example
|
||||
`--producer-limits '{"input_bytes":67108864,"output_bytes":8388608,"cpu_seconds":60}'`.
|
||||
Values apply to the next call without restarting REA. Effective native ceilings
|
||||
still preserve any tighter inherited soft and hard limit. Errors identify the
|
||||
exhausted producer control and retain partial output and cleanup status; follow
|
||||
that repair after cleanup settles. Raising producer budgets does not raise
|
||||
separate parser representations, transport buffers or external client deadlines.
|
||||
Never truncate the selected artifact to fit an input budget.
|
||||
|
||||
## Verification
|
||||
|
||||
```sh
|
||||
@@ -199,9 +182,8 @@ proof that the field appears in a selected recording.
|
||||
|
||||
The adapter uses the documented `tshark -G fields` grammar under its existing
|
||||
private empty profile and Linux process limits. TShark 4.6.9's full glossary was
|
||||
measured at 22,303,197 bytes; discovery defaults to **32 MiB** combined command
|
||||
output (capture inspection defaults to **4 MiB**); `producer_limits.output_bytes`
|
||||
can change either request budget. Parsing walks lines once and
|
||||
measured at 22,303,197 bytes; discovery admits at most **32 MiB** combined command
|
||||
output (capture inspection remains at **4 MiB**). Parsing walks lines once and
|
||||
constructs objects only for the selected page. Returned strings are copied so
|
||||
retained pages do not retain the complete glossary string. Before constructing a selected
|
||||
record it charges six times the source UTF-8 bytes plus 512 bytes against an
|
||||
|
||||
@@ -118,25 +118,16 @@ CLI keeps its existing complete result.
|
||||
|
||||
## Resource and lifecycle limits
|
||||
|
||||
Per-call `producer_limits` selects `input_bytes`, `output_bytes`,
|
||||
`diagnostic_bytes`, `address_space_bytes`, `file_bytes` and `cpu_seconds`.
|
||||
Omission keeps 4 MiB input, 16 MiB reply/file, 1 MiB diagnostics, 3 GiB address
|
||||
space and 30 CPU seconds. Omitted `file_bytes` follows the selected
|
||||
`output_bytes`, so raising the reply allowance does not silently meet the old
|
||||
file-write allowance. Explicit file limits remain independent. These fields
|
||||
accept positive safe integers; changes apply to the next call without a REA
|
||||
restart. The CLI accepts the same exact policy as JSON, for example:
|
||||
|
||||
```sh
|
||||
rea inspect-evm-interface ./runtime.hex hex --producer-limits '{"input_bytes":8388608,"output_bytes":33554432}'
|
||||
```
|
||||
|
||||
The owned worker's 256 MiB JavaScript heap remains separate. The worker command
|
||||
deadline is `timeout_ms` (default 30000 ms, integer 1 through 2147483647), or
|
||||
`--timeout-ms` on the CLI. Raising it does not raise producer, heap or client
|
||||
limits. An oversized carrier reports non-retryable `resource_constraint` with
|
||||
`reported_limits.maximum_input_bytes` and recovery naming
|
||||
`producer_limits.input_bytes`.
|
||||
Inputs are limited to 4 MiB, complete replies to 16 MiB and combined diagnostic
|
||||
output to 1 MiB. An owned worker has a 256 MiB JavaScript heap; its profile
|
||||
maxima are 3 GiB virtual address space and 30 CPU seconds, with tighter inherited
|
||||
limits retained. The worker command deadline is
|
||||
`timeout_ms` (default 30000 ms, integer 1 through 2147483647) on the next call,
|
||||
or `--timeout-ms` on `rea inspect-evm-interface`. Omission keeps 30000 ms.
|
||||
Those byte, address-space, and CPU ceilings have no runtime override, and
|
||||
`timeout_ms` does not raise them. An oversized selected carrier returns a
|
||||
non-retryable `resource_constraint` with resource `file-size` and its fixed
|
||||
input allowance in `reported_limits.maximum_input_bytes`.
|
||||
Complete-reply allowance refusals use the same non-retryable resource family,
|
||||
with `reported_limits.maximum_reply_bytes`; malformed JSON remains unreadable
|
||||
output. This refusal leaves no complete report to retrieve through another
|
||||
@@ -144,7 +135,7 @@ delivery channel. Virtual address space differs from resident memory. The worker
|
||||
soft/hard limits, reports its configured soft limits inline and leaves hard limits
|
||||
unchanged. WASM trap reservations are disabled
|
||||
so the engine can run within that virtual limit; actual memory is not equated to
|
||||
the selected address-space allowance. Limits fail without partial success. Stable snapshots,
|
||||
its 3 GiB allowance. Limits fail without partial success. Stable snapshots,
|
||||
private files and owned processes are cleaned independently of caller cancellation.
|
||||
Invalid-input, unsupported, cancellation, timeout and malformed-reply failures
|
||||
retain bounded stdout/stderr under `details.captured_output`, including whether
|
||||
|
||||
@@ -763,7 +763,7 @@ These safeguards are not caller-selectable output budgets. Source-map parsing
|
||||
reports truncation when its format or parser safety boundary is reached. The
|
||||
application graph has no aggregate node, edge, root, or observation prefix cap.
|
||||
|
||||
By default, semantic evaluation retains at most 256 distinct primitive candidates per
|
||||
Semantic evaluation retains at most 256 distinct primitive candidates per
|
||||
expression and at most 1 MiB of worst-case JSON string bytes across a normalized
|
||||
primitive value's string candidates. The byte budget estimates six JSON bytes
|
||||
per UTF-16 code unit, the expansion bound for escaped strings, and bounds both
|
||||
@@ -783,48 +783,13 @@ available for endpoint queries, tracing and comparison. Each analysis records en
|
||||
later projections: a selected healthy sibling can remain exact while whole-file
|
||||
coverage reports the encountered bound. Projection preserves limiting values
|
||||
from the shared property-slot owner. Expression
|
||||
evaluation and provenance walks default to 256 nested levels. Union products
|
||||
evaluation and provenance walks stop after 256 nested levels. Union products
|
||||
and string lengths are checked before allocating combinations or concatenated
|
||||
strings. When a boundary is reached, the value stays unknown, semantic coverage
|
||||
becomes partial, and graph evidence retains the expression location and typed
|
||||
limiting reason. The number of unresolved alternatives is unknown; it is not
|
||||
reported as an exact omission count.
|
||||
|
||||
`semantic_precision` selects per-source evaluator budgets independently of
|
||||
retained graph capacity, worker memory and deadlines. Both
|
||||
`analyze_javascript_application` and `analyze_web_bundle` accept a partial object:
|
||||
|
||||
| Field | Default | Unit |
|
||||
| ---------------------- | ------: | ------------------------------------------- |
|
||||
| `primitive_candidates` | 256 | alternatives per normalized primitive value |
|
||||
| `primitive_bytes` | 1048576 | estimated JSON bytes per primitive value |
|
||||
| `expression_depth` | 256 | nested expression/provenance levels |
|
||||
| `getter_expansions` | 512 | scheduled states per precise getter query |
|
||||
| `alias_selections` | 512 | additional precise selector states |
|
||||
| `wrapper_visits` | 1000000 | wrapper visits/substitutions per source |
|
||||
| `wrapper_depth` | 128 | active symbolic wrapper levels |
|
||||
|
||||
`primitive_bytes` charges six estimated JSON bytes per UTF-16 code unit plus
|
||||
quotes before creating concatenated strings. Ordinary ASCII can exhaust that
|
||||
conservative estimate before its actual JSON encoding reaches the selected
|
||||
byte count. The [calibration record](../../verification/javascript-precision-calibration.md)
|
||||
keeps existing defaults for its measured literal-value tasks; it does not
|
||||
establish optimal candidate, getter or alias budgets for larger applications.
|
||||
|
||||
All fields accept positive safe integers; worker memory, execution and JSON
|
||||
representation bounds remain independently enforced. Omission keeps the defaults;
|
||||
invalid values fail validation. For example, retry a finite candidate-limit
|
||||
observation with `semantic_precision: {"primitive_candidates": 512}`. The CLI
|
||||
accepts `--semantic-precision '{"primitive_candidates":512}'` or a JSON file.
|
||||
An override applies to that next analysis without restarting REA, remains in
|
||||
Evidence parameters and effective coverage, and cannot change already retained
|
||||
facts. Increasing these budgets does not raise graph retention, worker heap,
|
||||
transfer admission, or parent result memory. Cyclic or unsupported evaluations
|
||||
remain unknown. Wrapper limitations name visits and depth separately so a
|
||||
caller can change the resource actually exhausted. Finite inline literal object
|
||||
spreads use iterative syntax traversal and no longer inherit a duplicate
|
||||
semantic-depth cut.
|
||||
|
||||
Byte, entry, path, and graph-shape bounds are hard limits. The parse deadline is
|
||||
checked before and between bounded parsing and traversal phases; the synchronous
|
||||
Babel and JSON parser calls cannot be preempted mid-call, so their input byte
|
||||
|
||||
@@ -264,30 +264,12 @@ is retained in the classification evidence list.
|
||||
|
||||
### Resource admission
|
||||
|
||||
The static provider defaults to a 128 MiB eagerly retained PE snapshot and a
|
||||
separate conservative 64 MiB budget for metadata, CIL decoding and projected
|
||||
inspection records. All three static inspection tools accept
|
||||
`max_snapshot_bytes` and `max_metadata_representation_bytes` as positive safe
|
||||
integers in bytes. CLI inspection and path comparison expose
|
||||
`--max-snapshot-bytes` and `--max-metadata-representation-bytes`. An omitted
|
||||
value keeps its default; explicit values apply to that call without a restart.
|
||||
|
||||
Snapshot refusal names its selected byte budget and tells the caller to raise
|
||||
it for the same complete file. Representation exhaustion retains partial or
|
||||
unavailable coverage and names the selected budget in its `resource-limit`
|
||||
issue; raise that budget on the next inspection, or select fewer `type_names`
|
||||
for members. For example:
|
||||
|
||||
```bash
|
||||
rea inspect-managed-members /absolute/path/Assembly.dll --max-snapshot-bytes 268435456 --max-metadata-representation-bytes 134217728
|
||||
```
|
||||
|
||||
Reads remain bounded if a file grows after admission. A cached snapshot is
|
||||
rechecked against the current call's snapshot allowance and on-disk digest.
|
||||
Shared heap strings are cached, but repeated output occurrences still consume
|
||||
the representation budget. These are per-call resource policies, not PE format
|
||||
limits or a bound on total V8 heap use. Raising one does not raise the other,
|
||||
transport limits or available host memory.
|
||||
The static provider admits regular input files up to 128 MiB before retaining
|
||||
their bytes. Reads remain bounded if a file grows after admission. Metadata,
|
||||
CIL decoding, and projected inspection records have a separate conservative
|
||||
64 MiB representation budget; shared heap strings are cached, but repeated
|
||||
output occurrences still consume that budget. These are inspection resource
|
||||
policies, not PE format limits or a hard bound on total V8 heap use.
|
||||
|
||||
Path-based member comparison uses the same per-input snapshot policy and
|
||||
inspects its two selected paths sequentially, retaining each inspection rather
|
||||
@@ -344,7 +326,7 @@ original `resource-limit` coordinate. It does not return partially admitted
|
||||
member collections: empty fields, methods, references and edges are unexamined,
|
||||
not proof of absence; exported types, user strings and signature-resolution
|
||||
attestation remain null. Summary and retained pages keep this partial coverage.
|
||||
The selected capacity applies to the metadata and member projection together. If metadata or the type inventory itself cannot
|
||||
The capacity limit is unchanged. If metadata or the type inventory itself cannot
|
||||
be admitted, the result remains unavailable.
|
||||
|
||||
Use the returned `types[].full_name` values to select declaring types before
|
||||
|
||||
@@ -120,13 +120,7 @@ CLI output are unchanged.
|
||||
|
||||
Complete results have a 32 MiB input, 64 MiB reply, 1 MiB combined diagnostics
|
||||
and 30-second owned command deadline. `REA_PWNTOOLS_MAX_INPUT_BYTES` raises the
|
||||
launch input default alone, up to 1 GiB. Per-call `producer_limits` selects
|
||||
`input_bytes`, `output_bytes`, `diagnostic_bytes`, `address_space_bytes`,
|
||||
`file_bytes`, and `cpu_seconds`; positive safe integers apply on the next call
|
||||
without restart. `input_bytes` overrides the launch default. File admission
|
||||
follows selected `output_bytes` unless `file_bytes` is explicit. The separate
|
||||
`pwntools_timeout_ms` controls each command wall deadline. On CLI pass
|
||||
`--producer-limits '{"cpu_seconds":60,"output_bytes":134217728}'`. The
|
||||
input bound alone, up to 1 GiB; the reply and deadline bounds are unchanged. The
|
||||
reply budget applies to the decoder record; the CLI/MCP Evidence envelope adds
|
||||
copies and encoding overhead. The
|
||||
pinned MCP SDK defaults to a 10 MiB receive buffer. Large complete results need
|
||||
@@ -134,8 +128,8 @@ a caller-configured `StdioClientTransport({ maxBufferSize: ... })` and a matchin
|
||||
server `REA_MCP_MAX_RESPONSE_BYTES`. See [MCP delivery budgets](../../reference/mcp-contracts.md#tool-results).
|
||||
These settings affect delivery, not decoder coverage or limits. The large
|
||||
extended-index verification uses 256 MiB and an explicit five-minute request
|
||||
timeout in its separate opt-in MCP lane. REA does not silently truncate the result. Python lowers resource soft limits to the selected policy: defaults are 3 GiB
|
||||
virtual address space, 30 CPU seconds and 64 MiB file output. Inherited tighter soft/hard limits are
|
||||
timeout in its separate opt-in MCP lane. REA does not silently truncate the result. Python lowers resource soft limits to at most 3 GiB virtual address space,
|
||||
30 CPU seconds and 64 MiB file output. Inherited tighter soft/hard limits are
|
||||
retained, and the effective values are returned in the result limitations. Virtual address space is not RSS;
|
||||
Unicorn needs a large virtual map. Limits fail with no partial success, and
|
||||
owned process/root cleanup completes independently of cancellation.
|
||||
|
||||
@@ -116,22 +116,18 @@ The result's limitations state this explicitly. A module-mode build with a main
|
||||
module record and no dependency records has an observed empty dependency list.
|
||||
|
||||
When `include_string_literals: true` (CLI `--include-string-literals`, optional
|
||||
`max_string_literals` / `--max-string-literals`, default `2000`)
|
||||
`max_string_literals` / `--max-string-literals`, default `2000`, maximum `10000`)
|
||||
is selected, `string_literals` reports `evidence_kind: "inferred"` alongside
|
||||
deduplicated printable UTF-8 literals referenced by word-aligned two-word
|
||||
`(ptr, len)` string headers in non-executable initialized data. Because Go packs
|
||||
read-only string data contiguously without NUL terminators, slicing by `(ptr, len)`
|
||||
recovers exact literal boundaries instead of concatenated C-string runs. Each
|
||||
target slice (default `1`–`4096` bytes) must stay within a single read-only mapped
|
||||
target slice (`1`–`4096` bytes) must stay within a single read-only mapped
|
||||
section or segment, decode as printable UTF-8 (`\t`, `\n`, `\r`, joiners
|
||||
`U+200C` / `U+200D` and printable Unicode scalar values, excluding control and
|
||||
other format characters), and fit within
|
||||
a default 64 MiB header-scan budget and 512 KiB aggregate string budget
|
||||
(`truncated: true` when capped). Select `max_string_literal_bytes`,
|
||||
`max_string_bytes`, `max_string_header_scan_bytes`, or `max_string_candidates`
|
||||
to change these independent budgets. Their CLI flags use hyphens, for example
|
||||
`--max-string-literal-bytes 16384`. `limits_reached` names exhausted controls;
|
||||
increase those controls for a new inspection of the same complete artifact. Compiler-inlined or
|
||||
a 64 MiB header-scan budget and 512 KiB aggregate string budget
|
||||
(`truncated: true` when capped). Compiler-inlined or
|
||||
stack-constructed strings without static headers, unapplied relocation addends
|
||||
and Mach-O `LC_DYLD_CHAINED_FIXUPS` pointer encodings are noted in `limitations`.
|
||||
|
||||
@@ -205,39 +201,23 @@ host permission denial, changed artifacts and cancellation retain distinct
|
||||
errors. The selected file is never modified; this inspection creates no
|
||||
temporary files, starts no subprocesses and accesses no network endpoint.
|
||||
|
||||
The existing default resource budgets are now explicit per-call controls:
|
||||
An input above 256 MiB returns `resource_constraint` with resource `file-size`
|
||||
and `reported_limits.maximum_input_bytes`. Select a suitable input within that
|
||||
fixed allowance; changing an output-write or transport budget does not admit it.
|
||||
|
||||
| Parameter | Default | What it admits |
|
||||
| ------------------------------ | ----------- | ------------------------------------------------------------- |
|
||||
| `max_input_bytes` | 256 MiB | Complete stable input file |
|
||||
| `max_report_bytes` | 8 MiB | Complete normalized metadata representation |
|
||||
| `max_build_info_bytes` | 1 MiB | Aggregate embedded string contents, excluding length prefixes |
|
||||
| `max_header_search_bytes` | 256 MiB | Cumulative build-info search work |
|
||||
| `max_string_literals` | 2,000 | Returned inferred string rows |
|
||||
| `max_string_literal_bytes` | 4,096 bytes | Each inferred string candidate |
|
||||
| `max_string_bytes` | 512 KiB | Aggregate returned inferred text |
|
||||
| `max_string_header_scan_bytes` | 64 MiB | String-header and candidate-byte examination |
|
||||
| `max_string_candidates` | 100,000 | Distinct inferred candidates retained before projection |
|
||||
|
||||
Every value is a positive safe integer. Input and string byte maxima follow the
|
||||
runtime Buffer/string representation, with base64 expansion accounted for in
|
||||
build-info admission; schemas advertise their exact maxima. CLI options use the same names with
|
||||
hyphens, such as `--max-build-info-bytes 2097152`. Inferred-string controls
|
||||
require `include_string_literals: true`. Omitted controls keep the defaults
|
||||
and stay omitted in request Evidence. Error recovery names the exhausted
|
||||
parameter and effective bytes; increase that parameter on the next call,
|
||||
without restarting REA. The original complete artifact and its identity are
|
||||
preserved. An unchanged retry cannot increase a deterministic allowance.
|
||||
|
||||
The shared native container reader separately bounds structural table decoding
|
||||
to 16 MiB. None of these Go controls changes that boundary. Inline strings must
|
||||
also fit their actual mapped source range. Identical search spans with the same
|
||||
virtual alignment are searched once; partially overlapping spans consume the
|
||||
cumulative search budget. Oversized complete reports fail without partial
|
||||
success and without allocating the full JSON serialization. MCP's separate
|
||||
[response transport budget](../../reference/mcp-contracts.md) can still require
|
||||
export or paging of successfully retained Evidence. These controls do not
|
||||
establish operating-system memory or CPU isolation.
|
||||
Resource guards bound the selected file and cumulative header searches to 256 MiB
|
||||
each, aggregate embedded string contents to 1 MiB (varint length prefixes are
|
||||
framing and do not count toward this allowance), structural table decoding to
|
||||
16 MiB and the complete normalized metadata report to 8 MiB. Inline strings must
|
||||
also fit within their actual mapped source range. Identical search spans with
|
||||
the same virtual alignment are searched once; partially overlapping spans
|
||||
consume the cumulative search budget
|
||||
and fail with a CPU resource constraint when it is exhausted. Oversized results
|
||||
fail without partial success. MCP also applies
|
||||
its [response transport budget](../../reference/mcp-contracts.md), which counts both text and
|
||||
structured content; a report within the metadata limit can still require export
|
||||
from retained Evidence. These limits are not operating-system memory or CPU
|
||||
isolation.
|
||||
|
||||
Embedded metadata is evidence of file content. It does not authenticate a
|
||||
compiler, dependency, commit or supply chain, and it does not establish runtime
|
||||
|
||||
@@ -1247,7 +1247,7 @@ resource, REA releases that runtime and admits the next observation.
|
||||
Cancellation observed while output and Evidence are finalized returns partial
|
||||
cancellation Evidence, even if the target has already exited.
|
||||
|
||||
- the outcome, exit status and captured stdout/stderr (1 MiB by default kept per stream
|
||||
- the outcome, exit status and captured stdout/stderr (1 MiB kept per stream
|
||||
while excess bytes are drained and counted without growing capture files).
|
||||
Each stream reports `complete`; when draining could not finish, its byte
|
||||
count is an observed lower bound and truncation is explicit;
|
||||
@@ -1260,32 +1260,16 @@ cancellation Evidence, even if the target has already exited.
|
||||
code makes coverage partial;
|
||||
- limitations.
|
||||
|
||||
The bridge defaults to 8 MiB retained event JSON and 65,536 caller frames
|
||||
across the run. Select `capture_limits` before launch to adjust `trace_bytes`,
|
||||
`trace_frames`, `breakpoint_location_bytes`, `other_stops`, `selector_bytes`,
|
||||
and `output_bytes`. Defaults are respectively 8388608, 65536, 8388608, 256,
|
||||
1024, and 1048576. Byte budgets accept positive safe integers up to the smaller of the running
|
||||
Node Buffer and single-string representation ceilings; counts are positive safe
|
||||
integers. These representability ceilings do not establish available memory or
|
||||
raise complete Evidence/transport admission. Omitted fields keep their defaults. The journal shares
|
||||
`trace_bytes`; completed and partial capture readers use the selected budgets.
|
||||
For example, `"capture_limits":{"trace_bytes":16777216,"output_bytes":2097152}`
|
||||
selects 16 MiB trace/journal and 2 MiB retained output per stream. These choices
|
||||
do not extend `duration_ms`, `max_events`, the outer client deadline or MCP
|
||||
receive allowance, and require no REA restart. A larger budget applies to a new
|
||||
authorized observation; it cannot recover facts omitted from an earlier run. It checks admission before retaining another complete event;
|
||||
The bridge limits retained event JSON to 8 MiB and caller frames to 65,536
|
||||
across the run. It checks admission before retaining another complete event;
|
||||
`resource-limit` reports which observations could not be completed without
|
||||
silently changing the requested event or frame settings.
|
||||
Resolved breakpoint locations have a separate aggregate metadata budget,
|
||||
default 8 MiB (`capture_limits.breakpoint_location_bytes`).
|
||||
Resolved breakpoint locations have a separate aggregate 8 MiB metadata budget.
|
||||
If their details exceed it, `location_count` retains the full observed match
|
||||
count, coverage is partial, and `breakpoint_locations_truncated` is true. This
|
||||
post-run metadata limit does not imply that the target was killed.
|
||||
|
||||
Accepted entries and signal stops are also flushed to the bounded trace journal.
|
||||
When event admission stops, `coverage.capture_limit` identifies `trace_bytes`
|
||||
or `trace_frames` when the bridge supplied that fact; historical reports that
|
||||
lack it retain the combined limitation without guessing the exhausted resource.
|
||||
Accepted entries and signal stops are also flushed to a bounded 8 MiB journal.
|
||||
If cancellation, timeout, capture failure, or a later bridge error prevents a
|
||||
full result, the error retains this evidence in `details.partial_observation`
|
||||
with partial coverage. Available output prefixes remain inline; their byte
|
||||
|
||||
@@ -105,14 +105,8 @@ Initial Linux x64 limits: 32 MiB input, 64 MiB decoder reply, 1 MiB diagnostics
|
||||
stage, 30 seconds owned wall deadline per stage, 3 GiB address-space soft ceiling
|
||||
and 30 seconds CPU soft ceiling. `REA_PWNTOOLS_MAX_INPUT_BYTES` raises the input
|
||||
bound alone, up to 1 GiB, and returns `provider_unavailable` when it is not a
|
||||
decimal integer in range. Per-call `producer_limits` selects input/output/diagnostic,
|
||||
file/address-space bytes and CPU seconds; positive safe integers apply on the
|
||||
next call without restart. `input_bytes` overrides the launch default, and the
|
||||
file allowance follows selected output unless `file_bytes` is explicit. Both
|
||||
Python decoder and optional core-only debugger consume the same accepted policy.
|
||||
Use CLI `--producer-limits '{"cpu_seconds":60,"output_bytes":134217728}'`.
|
||||
`pwntools_timeout_ms` is the independent per-stage wall deadline. A large core
|
||||
that still exceeds the selected complete-reply allowance fails explicitly. Tighter
|
||||
decimal integer in range; the reply, diagnostic and deadline bounds stay fixed, so a
|
||||
large core that outruns them still fails explicitly instead of partially. Tighter
|
||||
inherited soft/hard limits are retained; hard limits are not raised. Complete
|
||||
oversized results fail explicitly. These process limits do not constitute a sandbox
|
||||
for caller-supplied engine code.
|
||||
|
||||
@@ -26,6 +26,6 @@ Use `get_evidence_bundle` with `detail="summary"` and `filters={"evidence_id":"P
|
||||
|
||||
Failed startup cleanup retains acquired Frida handles, including a paused spawn that has not attached, for later cleanup retries. Its remote connection stays leased until session detach and target resume complete. CLI commands await provider cleanup before returning; MCP retains these resources for cleanup when its server closes. Server shutdown stops admitting new Frida operations and waits for admitted startup, discovery, and queued work before releasing their resources. A failed cleanup remains reported and can be retried by closing the server again, rather than treating an unregistered session as fully released.
|
||||
|
||||
Frida scripts execute inside the selected target with that process's memory, filesystem, and network authority. Local device lookup waits 1000 ms. This release has no runtime override for that lookup; another REA timeout does not raise it. Remote endpoints do not use that lookup. Message capture defaults to 16 MiB per session, 4 KiB per type, 32 KiB per error text field, 250,000 JSON nodes and depth 64. Select `capture_limits` on `start_frida_session` or `instrument_with_frida` before observing: `session_bytes`, `message_type_bytes`, `message_text_bytes`, `json_nodes`, and `json_depth`. Omitted fields keep these defaults. The session allowance includes JSON/base64 expansion, the envelope and metadata; a budget too small for one complete event retains no event. Depth is bounded to 992 levels, reserving eight enclosing message/status/Evidence containers within REA's shared JSON representation limit. These are retention budgets, independent of target authority, observation duration and MCP/client delivery limits. The selected policy is fixed for that session and returned as `capture_limits` in observations; no REA restart is required. The CLI exposes `--capture-session-bytes`, `--capture-message-type-bytes`, `--capture-message-text-bytes`, `--capture-json-nodes`, and `--capture-json-depth`. Evidence names the effective control when that observation dropped data. Changing a budget requires choosing a new session or experiment and cannot recover already discarded live observations. Script source is not stored in Evidence; its selected path and SHA-256 digest are. For a file, `source_sha256` identifies the original selected bytes. Invalid UTF-8 continues to use replacement decoding; Evidence then records that limitation and `raw_result.source_representation` with the encoding, decoding mode, and `loaded_source_sha256` of the UTF-8 string passed to Frida. Valid UTF-8 files and inline scripts do not need this additional representation fact. Evidence retains non-authentication target selection such as device or endpoint, mode, program or PID, and spawn arguments. Authentication values remain excluded. Results report the observed session and messages, while unsupported platform metadata remains unknown. A request cancelled before a Frida operation starts has no provider effects. If cancellation arrives during a native Frida call, REA waits for that call to return, then closes any session it created and returns the observed session as a partial observation. A cancelled script load on an existing session releases only its new script before queued operations can run; the session and previously loaded scripts remain available. If cancellation is observed when the native call returns, partial Evidence retains the source identity and captured messages even if that call rejects, including messages emitted during awaited cleanup. The caller's cancellation reason remains primary and a native rejection is retained as a sanitized diagnostic. A failure already observed before cleanup stays primary if cancellation arrives during cleanup. The cancellation cutoff is REA's observation of native completion, rather than a claim about unobservable native event order. `script_id` is REA's load-attempt identity. When creation rejects without returning a handle, `script_cleanup: "no_handle"` means REA acquired no handle to unload; it does not prove that the engine performed no work. Once the provider commits a successful load, later request cancellation does not unload that script; use `unload_frida_script` to release it. If script cleanup fails, the script remains owned and a paused target cannot resume until the pending instrumentation is released. Queued script loads, resumes, and unloads check cancellation before dispatching native effects. Cancellation Evidence separately reports whether a target resume was observed, whether instrumentation cleanup completed, and that final target liveness remains unknown. REA does not kill the target. Remote discovery retains its observations and reports a separate cleanup error if the temporary connection cannot be removed.
|
||||
Frida scripts execute inside the selected target with that process's memory, filesystem, and network authority. Local device lookup waits 1000 ms. This release has no runtime override for that lookup; another REA timeout does not raise it. Remote endpoints do not use that lookup. Message capture is bounded to 16 MiB and reports truncation. Metadata, type, and text field caps, and the JSON node cap, are also fixed. Evidence names a cap when that observation actually dropped data. This release has no runtime override for those capture caps. Script source is not stored in Evidence; its selected path and SHA-256 digest are. For a file, `source_sha256` identifies the original selected bytes. Invalid UTF-8 continues to use replacement decoding; Evidence then records that limitation and `raw_result.source_representation` with the encoding, decoding mode, and `loaded_source_sha256` of the UTF-8 string passed to Frida. Valid UTF-8 files and inline scripts do not need this additional representation fact. Evidence retains non-authentication target selection such as device or endpoint, mode, program or PID, and spawn arguments. Authentication values remain excluded. Results report the observed session and messages, while unsupported platform metadata remains unknown. A request cancelled before a Frida operation starts has no provider effects. If cancellation arrives during a native Frida call, REA waits for that call to return, then closes any session it created and returns the observed session as a partial observation. A cancelled script load on an existing session releases only its new script before queued operations can run; the session and previously loaded scripts remain available. If cancellation is observed when the native call returns, partial Evidence retains the source identity and captured messages even if that call rejects, including messages emitted during awaited cleanup. The caller's cancellation reason remains primary and a native rejection is retained as a sanitized diagnostic. A failure already observed before cleanup stays primary if cancellation arrives during cleanup. The cancellation cutoff is REA's observation of native completion, rather than a claim about unobservable native event order. `script_id` is REA's load-attempt identity. When creation rejects without returning a handle, `script_cleanup: "no_handle"` means REA acquired no handle to unload; it does not prove that the engine performed no work. Once the provider commits a successful load, later request cancellation does not unload that script; use `unload_frida_script` to release it. If script cleanup fails, the script remains owned and a paused target cannot resume until the pending instrumentation is released. Queued script loads, resumes, and unloads check cancellation before dispatching native effects. Cancellation Evidence separately reports whether a target resume was observed, whether instrumentation cleanup completed, and that final target liveness remains unknown. REA does not kill the target. Remote discovery retains its observations and reports a separate cleanup error if the temporary connection cannot be removed.
|
||||
|
||||
The [Frida Core API](https://frida.re/docs/frida-core-api/) documents device management, sessions, and script loading; the [JavaScript API](https://frida.re/docs/javascript-api/) documents target-side instrumentation. [Luma](https://luma.frida.re/) is Frida's standalone GUI; REA does not automate or control it. `npm run verify:frida` requires a Linux host with the optional Frida binding and uses a short-lived local Node.js fixture created by the verifier. Add `-- --address HOST:PORT --pid PID` to run the separate remote attach lane against a caller-selected target. Mock tests do not establish real device/server compatibility.
|
||||
@@ -103,10 +103,10 @@ failure; bounded post-run integrity checks finish before honoring a late abort.
|
||||
never passed to sigrok. Caller-supplied tools and their libraries/decoder modules
|
||||
are trusted, and hashes identify bytes rather than publisher authenticity.
|
||||
|
||||
By default, the input copy is bounded at 32 MiB before reading, each command has
|
||||
a 30-second deadline, and combined stdout/stderr is bounded at 4 MiB during collection.
|
||||
The input copy is bounded at 32 MiB before reading, each command has a 30-second
|
||||
deadline, and combined stdout/stderr is bounded at 4 MiB during collection.
|
||||
Oversized output fails rather than becoming truncated success. These limits do
|
||||
not form an OS sandbox. Before execution, `prlimit` defaults to **1 GiB address
|
||||
not form an OS sandbox. Before execution, `prlimit` enforces **1 GiB address
|
||||
space per process**, **64 MiB per file**, **30 CPU seconds** and no core dumps.
|
||||
Stricter inherited soft/hard limits are preserved; Evidence records the effective
|
||||
ceilings. Both limits are set before execution, so decoder allocations and compressed
|
||||
@@ -114,23 +114,6 @@ recording expansion cannot exceed the per-process address-space cap. These are
|
||||
not aggregate process-memory or filesystem quotas. The selected limiter is
|
||||
trusted and its path, digest and arguments are retained in Evidence.
|
||||
|
||||
Each capture/recording operation accepts `producer_limits` with positive safe
|
||||
integer overrides: `input_bytes`, `output_bytes`, `command_timeout_ms`,
|
||||
`address_space_bytes`, `file_bytes`, and `cpu_seconds`. The wall-clock timeout
|
||||
is at most 2,147,483,647 milliseconds, matching Node timers. Omitted fields keep
|
||||
the defaults above. Discovery accepts the same controls except `input_bytes`,
|
||||
since it does not read a capture or recording. Packet field discovery defaults
|
||||
to 32 MiB output rather than 4 MiB.
|
||||
|
||||
CLI supplies this object as JSON with `--producer-limits`, for example
|
||||
`--producer-limits '{"input_bytes":67108864,"output_bytes":8388608,"cpu_seconds":60}'`.
|
||||
Values apply to the next call without restarting REA. Effective native ceilings
|
||||
still preserve any tighter inherited soft and hard limit. Errors identify the
|
||||
exhausted producer control and retain partial output and cleanup status; follow
|
||||
that repair after cleanup settles. Raising producer budgets does not raise
|
||||
separate parser representations, transport buffers or external client deadlines.
|
||||
Never truncate the selected artifact to fit an input budget.
|
||||
|
||||
## Verification
|
||||
|
||||
```sh
|
||||
|
||||
@@ -64,11 +64,10 @@ Equivalent MCP input:
|
||||
```
|
||||
|
||||
Each selected regular file is read through the existing stable artifact reader;
|
||||
symlinks, changing files and files over the selected `max_input_bytes` (default
|
||||
8 MiB) fail closed. An oversized input returns `resource_constraint` with
|
||||
resource `file-size` and the effective allowance in
|
||||
`reported_limits.maximum_input_bytes`. Its recovery names `max_input_bytes`;
|
||||
raise that per-call value before retrying the same complete file. WASM bytes are copied
|
||||
symlinks, changing files and files over 8 MiB fail closed. An oversized input
|
||||
returns `resource_constraint` with resource `file-size` and its fixed byte
|
||||
allowance in `reported_limits.maximum_input_bytes`; retrying the unchanged file
|
||||
does not raise that allowance. WASM bytes are copied
|
||||
into a private workspace as read-only `module.wasm`; its digest is rechecked
|
||||
before and after each tool. WABT validates with
|
||||
`wasm-validate --enable-all --ignore-custom-section-errors`, reports sections
|
||||
@@ -138,30 +137,12 @@ the equivalent MCP input is `{"path":"/artifacts/module.wasm",
|
||||
"command_timeout_ms":60000,"detail":"summary"}`. Summary delivery retains the
|
||||
complete Evidence but does not reduce WABT analysis work.
|
||||
|
||||
The default input, executable, and combined command-output allowances remain
|
||||
8 MiB, 64 MiB, and 32 MiB respectively. Select `max_input_bytes`,
|
||||
`max_tool_bytes`, or `max_output_bytes` (CLI `--max-input-bytes`,
|
||||
`--max-tool-bytes`, `--max-output-bytes`) to change one allowance for the next
|
||||
inspection. Values are positive integer bytes; the advertised schema maximum
|
||||
follows the runtime's single-string or Buffer representation ceiling. Raising a
|
||||
budget does not reserve host memory or raise another budget, the command
|
||||
deadline, or the client's receive buffer. No REA restart is required. Unset
|
||||
values preserve the defaults, and explicit values remain in Evidence parameters.
|
||||
The same effective input/tool limit is used for initial and integrity rereads.
|
||||
Before cloning and committing complete Evidence, REA accounts for the actual
|
||||
escaped JSON representation and authentication copies. A large command budget
|
||||
does not guarantee that the complete report fits the runtime's single-string
|
||||
ceiling or available heap. Those refusals name the distinct representation or
|
||||
heap constraint; increasing `max_output_bytes` cannot repair them. WABT reports
|
||||
use no redundant raw-result mirror, but exact import/export forms and derived
|
||||
interface facts can still expand beyond their source WAT size.
|
||||
|
||||
A limit failure names its effective byte value and exact per-call control.
|
||||
Raise that control, resolve incomplete cleanup, then retry the same complete
|
||||
artifact. Do not truncate required module bytes. If already at the runtime
|
||||
maximum, report the limit or explicitly select another suitable complete input.
|
||||
Output past the budget fails with `resource_constraint`, including a `--version`
|
||||
probe; no truncated output is claimed as a successful inspection. Both streams share the
|
||||
The 8 MiB input allowance and 32 MiB combined command-output budget remain fixed;
|
||||
increasing `command_timeout_ms`, host memory or the client deadline does not raise
|
||||
them. Do not truncate required module bytes to fit. Select another suitable
|
||||
complete artifact only if it answers the question, otherwise report the limit.
|
||||
Inspection output past the budget fails with `resource_constraint`; an oversized
|
||||
`--version` probe still reports `unreadable_output`. Both streams share the
|
||||
budget, so a stream that starts after the other has filled it retains nothing.
|
||||
The captured output keeps up to the first 4,096 bytes that each stream retained,
|
||||
each stream's retained byte count, and the exit code and signal. Retained text
|
||||
@@ -257,25 +238,3 @@ Glue parsing accepts Node-supported WASM source-phase imports, including
|
||||
`await import.source("./module.wasm")`. Their string literals retain the same
|
||||
coordinates and local candidate associations as ordinary import literals; these
|
||||
are static source observations, without module instantiation or execution.
|
||||
|
||||
Typed interface decoding separately charges `max_interface_bytes` (default
|
||||
32 MiB; CLI `--max-interface-bytes`). The selected allowance applies to both
|
||||
nested WAT parsing and retained typed entries, so increasing it is not blocked
|
||||
by an independent fixed nested-parser cap. A partial interface preserves the
|
||||
original WAT, admitted entries, exact exhausted allowance and retry guidance.
|
||||
Inspect the same complete module again with a larger value to recover omitted
|
||||
typed facts.
|
||||
|
||||
`compare_wasm_interfaces` accepts its own `max_interface_bytes` in the JSON
|
||||
input, also default 32 MiB. It admits selected entries and group metadata before
|
||||
building the comparison inventory. A larger comparison budget cannot upgrade
|
||||
an already partial inspection: first recover the producer Evidence, then compare
|
||||
those exact replacement records. Both allowances retain independent runtime
|
||||
serialization and heap boundaries.
|
||||
|
||||
Auxiliary `candidate_paths` and `glue_paths` each retain a default admission of
|
||||
16 paths. Set `max_artifact_paths` (CLI `--max-artifact-paths`) to admit a larger
|
||||
explicit selection. Admission happens before any provider acquisition or command,
|
||||
and errors report the selected list, requested count and required control.
|
||||
Per-file byte budgets still apply to each admitted path; increasing the path
|
||||
count does not raise those limits or reserve host memory.
|
||||
@@ -635,7 +635,7 @@ rejects it as unknown; other command rejections retain their reported failure.
|
||||
|
||||
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Scripts are different: scripts the page parsed before REA attached are still reported under `scripts`. A page that loaded before attach can therefore list its scripts while `network.requests` lacks the requests that fetched them. If CDP reuses a request ID without supplying redirect metadata, REA retains the predecessor observation and marks network coverage incomplete.
|
||||
|
||||
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 default 64 MiB aggregate streamed-response budget across redirects. Select `source_map_limits.fetch_bytes` to change byte admission for the next call. 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 defaults to 64 MiB for raw resolved identities and 32 MiB for aggregate inline evidence; `source_map_limits.fetch_bytes` and `source_map_limits.output_bytes` own those independent admissions. 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`.
|
||||
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
|
||||
|
||||
@@ -1114,21 +1114,44 @@ for the hint per reason.
|
||||
|
||||
## Resource and lifecycle boundaries
|
||||
|
||||
Complete evidence defaults to a 32 MiB input, 96 MiB decoder reply and
|
||||
64-level nesting allowance. Set `max_input_bytes`, `max_reply_bytes` or
|
||||
`max_nesting_depth` on the next `inspect_web_network_capture` call to change
|
||||
that operation's policy; CLI equivalents are `--max-input-bytes`,
|
||||
`--max-reply-bytes` and `--max-nesting-depth`. No REA restart is required.
|
||||
Byte allowances are positive safe integers; nesting is bounded by REA's
|
||||
advertised maximum of 996 levels, derived by reserving four enclosing
|
||||
reply/Evidence levels within the 1,000-level JSON representation. Input admission also covers expanded SAZ
|
||||
members and protobuf descriptor/body reads. Reply admission covers the complete
|
||||
child reply and protobuf interpretation work. These choices do not raise the
|
||||
separately owned worker heap, CPU limits or client deadline/receive buffer.
|
||||
Record selection takes place after complete decoding, so it cannot bypass
|
||||
whole-capture admission. A refusal names the exhausted control; increase that
|
||||
control and retry the same unchanged capture after cleanup. No completed capture
|
||||
Evidence exists for a refused decode.
|
||||
Complete evidence has a 32 MiB input, 96 MiB decoder reply and 64-level nesting
|
||||
budget. Those budgets have no runtime override. `capture_timeout_ms` does not
|
||||
raise them. Exceeding a budget returns no partial success.
|
||||
Oversized selected captures return an input error identifying `capture_path`
|
||||
and the byte budget; owned reply/output failures remain output or provider errors.
|
||||
An explicit complete-reply refusal names the 100663296-byte allowance and its
|
||||
lack of a runtime override. Record selection, paging, delivery mode and worker
|
||||
memory do not increase that allowance; a smaller producer export is a narrower
|
||||
observation, and the refusal leaves no completed capture Evidence to retrieve.
|
||||
Cancellation during reply-file finalization retains validated completed capture
|
||||
facts as partial observations. Malformed or unvalidated replies are not promoted.
|
||||
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
|
||||
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
|
||||
during startup instead of enforcing a limit. Mitmproxy decoding applies
|
||||
Linux limits of 768 MiB address space, 30 seconds CPU and 96 MiB file output;
|
||||
both commands use `capture_timeout_ms` (default 30000) and independently supervised
|
||||
process cleanup. Owned decoder diagnostics retain at most 1 MiB across stdout
|
||||
and stderr; the collector drops overflow as it arrives, reports the limit
|
||||
failure and verifies cleanup. Heap limits alone do not establish aggregate RSS limits.
|
||||
SAZ and qlog refuse decoding when neither a Node heap limit nor a Windows Job Object bound can be enforced. Existing HAR availability is preserved.
|
||||
When REA runs on Bun, the HAR/SAZ/qlog decoder runs on Bun, which ignores these V8 heap
|
||||
flags; its limitations and resource failures then omit the V8 heap sizes. On
|
||||
Windows, the decoder's owned Job Object instead limits its committed memory to
|
||||
the effective `REA_CAPTURE_DECODER_OLD_GENERATION_MIB` value plus a 512 MiB
|
||||
reserve for Bun itself. The default 192 MiB setting therefore yields 704 MiB.
|
||||
The limitations state that limit, and reaching it returns `resource_constraint`
|
||||
with `process_memory_limit_bytes`, whatever exit status Bun reports. Other Bun
|
||||
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
|
||||
|
||||
@@ -82,13 +82,13 @@ implement every current source-map extension.
|
||||
One owned Node subprocess decodes the map. Its private request directory is
|
||||
removed before success is returned. Only this child's inherited `NODE_OPTIONS`
|
||||
is removed; explicit V8 flags set a 192 MiB old-generation and 8 MiB semi-space
|
||||
budget, one V8 worker and `decode_timeout_ms` (default 20000). These are the defaults: select `source_map_limits.codec_heap_mib` for a different heap request. Reported actual V8 heap limits
|
||||
budget, one V8 worker and `decode_timeout_ms` (default 20000). The codec heap, map budget, and reply budget have no runtime override. Reported actual V8 heap limits
|
||||
are checked. These are heap limits, not an aggregate process RSS limit. When
|
||||
REA runs on Bun, the subprocess is Bun, which ignores these V8 flags: `runtime`
|
||||
records `id: "bun"`, the Bun version, its Node.js compatibility version and a
|
||||
null `v8_heap_limit_bytes`. On Windows, the codec's owned Job Object then
|
||||
limits its committed memory to the selected codec heap request plus a 512 MiB
|
||||
reserve for Bun itself (704 MiB with the default request); `process_memory_limit_bytes` records that limit, the
|
||||
limits its committed memory to 704 MiB, the 192 MiB budget plus a 512 MiB
|
||||
reserve for Bun itself; `process_memory_limit_bytes` records that limit, the
|
||||
limitations state it, and reaching it returns `resource_constraint` with
|
||||
`codec_process_memory_limit_bytes`. Elsewhere Bun has no enforced codec limit:
|
||||
`process_memory_limit_bytes` is null and the limitations state that no codec
|
||||
@@ -102,143 +102,16 @@ run token while process release is unconfirmed, and the directory until removal
|
||||
succeeds. A later successful retry settles every error bound to that command;
|
||||
historical failure diagnostics remain available.
|
||||
|
||||
Complete evidence defaults to an 8 MiB map, 32 MiB codec reply and a shared
|
||||
Complete evidence is bounded by a 4 MiB map, 32 MiB codec reply and a shared
|
||||
262144-record decoder budget. Generated rows, mapping segments, leaf maps,
|
||||
source and name declarations, `sourcesContent` entries and indexed sections all
|
||||
consume this budget. Generated rows and mapping segments also each have a
|
||||
262144 limit; indexed maps default to 64 nested section levels. Select per-call
|
||||
`source_map_limits` to change these admissions. Exceeding a budget returns an explicit
|
||||
262144 limit; indexed maps allow at most 64 nested section levels. Those budgets and the
|
||||
192 MiB codec heap have no runtime override. Exceeding a budget returns an explicit
|
||||
failure, without partial results. The existing 32 MiB manifest and 16 MiB selected
|
||||
script boundaries also apply. No global toolchain or browser configuration changes
|
||||
are made.
|
||||
|
||||
## Compact point delivery and retained source text
|
||||
|
||||
Summary delivery is useful for large point answers. Complete output can answer
|
||||
a small-map task with fewer calls. The
|
||||
[delivery qualification](../../verification/source-location-delivery.md)
|
||||
records measured task costs and first-use discovery tradeoffs.
|
||||
|
||||
Complete delivery remains the default and includes raw map text and embedded source
|
||||
content. Select `detail: "summary"` in MCP, or `--detail summary` with
|
||||
`--evidence_output /analysis/source-point.json` in CLI, to retain the same complete
|
||||
Evidence and return body-free point metadata. Summary includes source/map/manifest
|
||||
identity, requested and matched generated positions, original source positions,
|
||||
content digests and sizes, match count, runtime qualifications and limitations.
|
||||
MCP returns the summary Evidence directly; its `normalized_result.parent_evidence_id`
|
||||
identifies the complete parent. CLI returns `{ summary: <Evidence>, evidence_bundle:
|
||||
{ path, bytes } }`; read `summary.normalized_result.parent_evidence_id` and use the
|
||||
published bundle path for later views. The receipt byte count describes the written
|
||||
complete Evidence bundle.
|
||||
Delivery selection does not enter that parent's analysis parameters or change its ID.
|
||||
|
||||
Summary returns the first metadata page (up to 64 matches and 1 MiB), with explicit
|
||||
`coverage.total`, `coverage.next_offset` and `coverage.exhausted`. Every equal-position
|
||||
alternative remains in the parent, including duplicate reported names, distinct
|
||||
indexed-section/source indexes and generated-only segments. Page exhaustion describes
|
||||
the retained lookup alternatives; execution and deployment authenticity remain unknown.
|
||||
For another page, call `inspect_analysis_view` with the same retained parent:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "<parent_evidence_id>"
|
||||
},
|
||||
"view": {
|
||||
"kind": "source-location",
|
||||
"collection": "matches",
|
||||
"offset": 64,
|
||||
"limit": 64
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Source text is explicit. Select `collection: "source-text"`, an original
|
||||
`match_index` from the metadata page, and UTF-16 `offset`/`limit` (default 0/1024).
|
||||
For a snippet around the mapped point, use its `original_position.offset` when
|
||||
known. The result preserves content digest/size and total code units, returns only
|
||||
the selected text, and reports text-page coverage. Unretained or generated-only
|
||||
content returns `item: null` and unavailable coverage. An offset splitting a
|
||||
surrogate pair is rejected; a page never splits a complete Unicode character.
|
||||
|
||||
Both collections accept `max_bytes` (default 1048576), independently of their
|
||||
row/code-unit limit and MCP response limits. A byte refusal names the allowance
|
||||
and retained parent: select a smaller page or increase `max_bytes`, then retry
|
||||
without decoding again. A large individual identity may require a larger view
|
||||
allowance or complete export. Complete map text and diagnostics remain in the parent.
|
||||
An optional `view` can accompany `detail: "summary"` on the initial trace, returning
|
||||
that selected page immediately. `view` with complete delivery is rejected before
|
||||
artifact acquisition. Summary also refuses before acquisition when session retention
|
||||
or the CLI Evidence output is unavailable.
|
||||
|
||||
These views read retained facts even after the original map is removed. CLI can read
|
||||
the saved parent with `inspect-analysis-view` and an `evidence-file` source pointing
|
||||
to the written bundle; select its `evidence_id` when the bundle has multiple records.
|
||||
MCP can export the complete session with `export_evidence_bundle`. A complete MCP
|
||||
response that exceeds transport admission retains its parent and supplies a
|
||||
`source-location` matches continuation.
|
||||
|
||||
## Selecting budgets and recovering the same map
|
||||
|
||||
Both `trace_web_source_location` and `analyze_web_bundle` accept an optional
|
||||
`source_map_limits` object. Omitted fields retain the defaults above and stay
|
||||
omitted in Evidence parameters. An update applies on the next call without a
|
||||
REA restart. Failure text names the exact field, effective value, supported
|
||||
range and next action; increasing a deadline does not change any of these
|
||||
budgets.
|
||||
|
||||
| Field | Default | Scope |
|
||||
| ------------------ | -------- | ------------------------------------------------------------------------------------- |
|
||||
| `map_bytes` | 8388608 | Local selected map admission, enforced before reading and again by the codec |
|
||||
| `output_bytes` | 33554432 | Complete local request/reply or aggregate fetched-map representation |
|
||||
| `decoded_rows` | 262144 | Generated rows before upstream allocation |
|
||||
| `decoded_segments` | 262144 | Mapping segments before upstream allocation |
|
||||
| `decoded_records` | 262144 | Shared structural work across leaves, declarations, rows, segments and sections |
|
||||
| `section_depth` | 64 | Indexed-section nesting |
|
||||
| `codec_heap_mib` | 192 | Local codec old-generation request; process-memory enforcement remains host dependent |
|
||||
| `fetch_bytes` | 67108864 | Aggregate response bytes and, independently, resolved-identity bytes |
|
||||
|
||||
The local 8 MiB admission covers the pinned 4,284,329-byte main map and 5,529,840-byte
|
||||
DateSelect map from Actual Budget without an admission retry. This is an artifact
|
||||
compatibility allowance, not a context or latency optimization: complete Evidence
|
||||
retains raw map text, and those two measured replies estimate 1.59 and 2.23 million
|
||||
tokens. A smaller admission refuses the map; it does not compact successful
|
||||
Evidence. MCP retains complete Evidence before encoding; an oversized response
|
||||
returns its retained reference for `export_evidence_bundle`, or callers can use
|
||||
complete CLI JSON. Select opt-in compact delivery above to keep complete Evidence accessible while returning only point metadata or an explicitly selected text page.
|
||||
Browser collection keeps its separate conservative defaults; see the
|
||||
[calibration record](../../verification/source-map-budget-calibration.md).
|
||||
|
||||
Local tracing accepts all listed fields except `fetch_bytes`. Bundle fetching
|
||||
accepts all except `map_bytes` and `codec_heap_mib`: it uses its existing
|
||||
inspection process rather than launching the local codec. Unsupported fields
|
||||
are rejected rather than silently ignored. Byte fields are bounded below
|
||||
Node's single-string representation ceiling, record fields by the signed
|
||||
32-bit decoder coordinate range, section depth by the same supported count range and shared structural admission,
|
||||
and heap requests by the existing 128..16384 MiB worker envelope. These bounds
|
||||
remain independent; a larger map can require a larger encoded request budget
|
||||
and codec heap, and the outer MCP client still owns its receive buffer/deadline.
|
||||
|
||||
For example, a valid 10 MiB selected local map can be retried with:
|
||||
|
||||
```json
|
||||
{
|
||||
"source_map_limits": { "map_bytes": 16777216 }
|
||||
}
|
||||
```
|
||||
|
||||
Keep the original manifest/map/point selection in that retry. The CLI spelling
|
||||
is `--source-map-bytes 8388608`; the other local flags are
|
||||
`--source-map-output-bytes`, `--source-map-decoded-rows`,
|
||||
`--source-map-decoded-segments`, `--source-map-decoded-records`,
|
||||
`--source-map-section-depth` and `--source-map-codec-heap-mib`.
|
||||
`analyze-web-bundle` instead offers `--source-map-fetch-bytes` and the shared
|
||||
output/record/depth flags. Raising only `decoded_records` does not raise the
|
||||
independent row or segment budgets. Failed local decoding returns no partial
|
||||
point evidence; completed browser map observations retain their existing
|
||||
partial/unknown semantics. Wait for retained cleanup to settle before retrying.
|
||||
|
||||
## Verification
|
||||
|
||||
Browser bundle collection uses absolute section offsets for nested inline maps.
|
||||
@@ -257,10 +130,10 @@ its `original_sources` declaration, including repeated source URLs across indexe
|
||||
sections. Imports are deduplicated within each declaration. Older observations
|
||||
may omit the index; their edge declaration identity remains unknown.
|
||||
|
||||
Browser collection defaults to 64 MiB of map responses per inspection and
|
||||
Browser collection accepts up to 64 MiB of map responses per inspection and
|
||||
preflights indexed sections, sources, names, generated rows and mapping segments
|
||||
against a 262144-record decode budget before upstream flattening. Indexed maps
|
||||
default to 64 nested levels. These structural limits bound decoded object
|
||||
are limited to 64 nested levels. These structural limits bound decoded object
|
||||
growth while preserving large embedded `sourcesContent` strings within the
|
||||
response-byte budget. It also bounds the expanded inline evidence to 32 MiB
|
||||
across the inspection, checking resolved source identities and mapping rows
|
||||
|
||||
+3
-16
@@ -83,22 +83,9 @@ that `struct module`, symbol-table records, or certificate chains were decoded.
|
||||
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
|
||||
8192 `.modinfo` and `__versions` entries plus listed `__ksymtab*` sections are
|
||||
examined by default (65536 maximum); `coverage.examined_entries` counts all
|
||||
three. Exceeding that budget fails with no partial success. `.modinfo` keys
|
||||
retain at most 256 bytes and values at most 4096 bytes by default. A larger
|
||||
field becomes `null` and is named in the entry's `omitted_fields`; the field
|
||||
that fits is still copied. Structural `status` and `reason` remain independent:
|
||||
a terminated, nonempty `key=value` entry stays `observed` when its text is
|
||||
omitted, while overall coverage becomes `partial`. Source range and digest
|
||||
remain available.
|
||||
|
||||
Raise `max_modinfo_key_bytes` or `max_modinfo_value_bytes` on the next MCP call
|
||||
to retain the omitted text. The returned limitation names the required byte
|
||||
budget. CLI flags are `--max-modinfo-key-bytes` and `--max-modinfo-value-bytes`.
|
||||
Each accepts an integer from 1 through 536870912 bytes, matching the maximum
|
||||
input size. No restart is required; file, entry and raw-hex budgets do not
|
||||
change. These are per-call controls, not runtime settings.
|
||||
|
||||
A `__ksymtab*` section name longer than
|
||||
three. Exceeding that budget fails with no partial success. Keys longer
|
||||
than 256 bytes and values longer than 4096 bytes stay malformed per entry;
|
||||
the field that fits is still copied. A `__ksymtab*` section name longer than
|
||||
256 bytes keeps its range with `name: null` and `ksymtab_name_exceeds_limit`. Inline raw hex stops at 4096 bytes and
|
||||
the SHA-256 remains.
|
||||
|
||||
|
||||
+9
-53
@@ -72,60 +72,16 @@ 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.
|
||||
Zero padding after a complete blob is allowed. Producer admission defaults to
|
||||
a 256 MiB blob, 100,000 assets whose names total at most 8 MiB, and 4,096 exec
|
||||
arguments totaling at most 1 MiB of text. These are resource policies, not Node
|
||||
format limits. Select `artifact_limits.node_sea` on inspection or extraction to
|
||||
change them before decoding names or creating records. Asset and argument
|
||||
decoding, padding scans, and inventory construction
|
||||
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
|
||||
names total at most 8 MiB, and 4,096 exec arguments totaling at most 1 MiB of
|
||||
text. These are fixed resource bounds, not Node format limits, checked before
|
||||
decoding names or creating records; a hit fails as a limit with no runtime
|
||||
override. Asset and argument decoding, padding scans, and inventory construction
|
||||
yield in bounded batches and honor caller cancellation. Asset count and name
|
||||
budgets bound retained metadata independently of stored member body sizes.
|
||||
|
||||
| `artifact_limits.node_sea` field | Default | Unit and admission |
|
||||
| -------------------------------- | --------: | ------------------------------------------------------ |
|
||||
| `blob_bytes` | 268435456 | Bytes in one blob, checked before reading and decoding |
|
||||
| `asset_count` | 100000 | Asset records |
|
||||
| `asset_name_bytes` | 8388608 | Aggregate raw UTF-8 asset-name bytes |
|
||||
| `argument_count` | 4096 | Exec-argument records |
|
||||
| `argument_bytes` | 1048576 | Aggregate raw UTF-8 exec-argument bytes |
|
||||
| `recognition_header_count` | 4096 | ELF header rows admitted for recognition |
|
||||
| `recognition_load_command_bytes` | 8388608 | Mach-O load-command bytes admitted for recognition |
|
||||
|
||||
Exhausted recognition remains unknown, not examined absence. Inventory preserves
|
||||
native-root facts or another positively recognized pack and records the exact
|
||||
recognition limit inline. If extraction cannot establish a supported pack, it
|
||||
refuses with that admission cause. Confirmed SEA blob admission still returns a
|
||||
typed resource failure. ELF note-owner matching reads only the prefix needed to distinguish
|
||||
`NODE_SEA_BLOB`; an unrelated long owner does not consume retained string space
|
||||
or conceal a later SEA note. This does not change malformed framing checks.
|
||||
|
||||
For example, retain the same active carrier and retry an argument-count refusal:
|
||||
|
||||
```json
|
||||
{ "artifact_limits": { "node_sea": { "argument_count": 8192 } } }
|
||||
```
|
||||
|
||||
The CLI spelling is:
|
||||
|
||||
```sh
|
||||
rea inspect-artifact ./application --artifact-limits '{"node_sea":{"argument_count":8192}}' --json
|
||||
```
|
||||
|
||||
Failures report `reader_limit`, including the exact parameter, effective value,
|
||||
unit, supported range and required value when known. Changes apply on the next
|
||||
call without restarting REA. Delivery summary, paging and extraction selection
|
||||
cannot bypass whole-manifest admission. Omitted selections stay omitted in
|
||||
Evidence parameters; selecting `artifact_limits` records the accepted selection
|
||||
and `effective_limits`. Those are conditional policies for encountered readers,
|
||||
not a claim that every listed format was examined. The independent aggregate
|
||||
member-work override is `artifact_limits.expanded_bytes`; omission keeps the
|
||||
encoded-size-derived work allowance. Runtime Buffer/string representation limits
|
||||
and host memory remain independent of larger selected budgets.
|
||||
|
||||
PE carriers additionally use the shared 512 MiB input, 65,536 directory-entry
|
||||
and 8 MiB resource-metadata ceilings; the Node SEA selections do not raise those
|
||||
shared PE-parser boundaries. The PE name probe reads headers from the first
|
||||
64 KiB of the carrier; a PE whose
|
||||
budgets bound retained metadata independently of stored member body sizes. The PE
|
||||
name probe reads headers from the first 64 KiB of the carrier; a PE whose
|
||||
headers extend beyond that is left to native classification. ELF note tables
|
||||
are read for at most one note per 12 carrier bytes in total, the most that
|
||||
disjoint tables can hold, so program headers that repeat the same notes do not
|
||||
|
||||
@@ -96,40 +96,6 @@ and trace matches likewise retain Ghidra's external and thunk classification.
|
||||
The Ghidra analysis profile identifies this search projection, so older
|
||||
snapshots with unqualified search rows cannot replay under the new profile.
|
||||
|
||||
## Decompiler and value-flow budgets
|
||||
|
||||
Use `inspect_analysis_settings` or a result's named recovery to inspect
|
||||
`ghidra.value_flow_max_operations` (default 3000),
|
||||
`ghidra.value_flow_max_inputs_per_operation` (default 64), and
|
||||
`ghidra.value_flow_max_edges` (default 12000). Each accepts a positive Java
|
||||
integer count. `update_analysis_settings` applies changes to the next operation
|
||||
on the same Program without restarting REA. Launch seeds use the corresponding
|
||||
`REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS`,
|
||||
`REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION`, and
|
||||
`REA_GHIDRA_VALUE_FLOW_MAX_EDGES` environment settings.
|
||||
For a one-shot CLI retry, set the named seed in the environment that launches
|
||||
the next `rea` invocation; the completed invocation's Program is no longer
|
||||
active. Runtime MCP changes preserve the current Program. A changed launch seed
|
||||
only affects a running MCP server after that server restarts.
|
||||
|
||||
The returned `native_value_flow.limits` and `limits_reached` distinguish accepted
|
||||
budgets from actual exhaustion. Truncation keeps omitted counts and names the
|
||||
setting to adjust. The edge budget applies independently to def-use and
|
||||
parameter-use collections. New function dossiers are reobserved rather than
|
||||
replayed from snapshots; completed Evidence remains available for historical
|
||||
inspection. Other observation caches keep their existing policy.
|
||||
|
||||
`ghidra.decompile_max_payload_mib` controls the native decompiler's response
|
||||
payload, separately from p-code projection and decompile time. Its launch seed
|
||||
is `REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB`; explicit values are 1..2047 MiB to
|
||||
avoid overflow in Ghidra's signed integer byte conversion. Omission or runtime
|
||||
`null` preserves the installed provider's default. Changes apply before the
|
||||
next native decompile, and native option changes clear its old cached function
|
||||
state. A failed decompile preserves the original diagnostic and reports the
|
||||
effective payload allowance; payload recovery applies only when that diagnostic
|
||||
identifies payload exhaustion. Raising these controls does not raise the MCP
|
||||
client's deadline or receive buffer.
|
||||
|
||||
## PE debug-symbol coverage
|
||||
|
||||
The post-analysis handshake reports Ghidra's PDB Program Information attributes:
|
||||
|
||||
@@ -148,10 +148,7 @@ returned: `pseudocode`, `native_api` and `native_value_flow` are unavailable
|
||||
with the decompiler's failure as the reason, while the listing-backed facets
|
||||
are collected as usual. These failed decompiler observations and workflows
|
||||
derived from them retain Evidence but skip snapshot reuse, so a later request
|
||||
can observe backend recovery. Ghidra function dossiers also skip snapshot reuse
|
||||
so each new operation honors its accepted value-flow budgets; completed source
|
||||
Evidence remains accessible through retained views and bundle export. Other
|
||||
healthy provider observations keep their existing cache policy.
|
||||
can observe backend recovery; healthy dossiers keep their existing cache policy.
|
||||
`procedure_pseudo_code` still fails for that function. Hopper's unavailable reference-kind metadata and reported unresolved
|
||||
calls still prevent equality conclusions for those dimensions. Analysis profiles
|
||||
version these declarations so older snapshots are not reused as current results.
|
||||
|
||||
@@ -43,22 +43,14 @@ rea inspect-with-objdump ./program dwarf
|
||||
rea inspect-with-objdump ./program dwarf --follow-debug-links
|
||||
```
|
||||
|
||||
Captured stdout and stderr share `max_output_bytes` (CLI: `--max-output-bytes`), default 16777216 bytes (16 MiB). Select an integer from 1 to 44651859 bytes. The maximum leaves room for worst-case JSON escaping of both Evidence output mirrors under Node's single-string ceiling. The selected budget applies on the next call without a restart; `output_budget_bytes` reports it and `output_budget_source` distinguishes a default from an explicit request. On exhaustion REA retains a prefix, requests provider termination, and marks `output_truncated` with unknown completion. Narrow or page the native command, or raise the selected budget. Verify command effects and outstanding cleanup before deciding whether repeating it is safe. Client deadlines and receive buffers remain separate. Concurrent one-shot captures reserve their selected budgets within a 128 MiB aggregate admission and eight-operation capacity; wait for active work or select a smaller output budget when that capacity is occupied.
|
||||
Captured stdout and stderr are retained up to a 16 MiB combined limit; `output_truncated` marks output beyond that limit. That cap has no runtime override.
|
||||
|
||||
`inspect_with_objdump`, `rea list-objdump-architectures`, and one-shot `execute_rizin_command` accept `command_timeout_ms` (CLI: `--command-timeout-ms`). Omitted means 120000. The integer range is 1 to 2147483647. Target preflight shares one budget across stat and digest. The one-shot command receives that budget again. It applies on the next call; no restart is required. It does not raise the separately selected `max_output_bytes`, a Ghidra or Hopper startup deadline, or a debugger session deadline.
|
||||
`inspect_with_objdump`, `rea list-objdump-architectures`, and one-shot `execute_rizin_command` accept `command_timeout_ms` (CLI: `--command-timeout-ms`). Omitted means 120000. The integer range is 1 to 2147483647. Target preflight shares one budget across stat and digest. The one-shot command receives that budget again. It applies on the next call; no restart is required. It does not raise the 16 MiB cap, a Ghidra or Hopper startup deadline, or a debugger session deadline.
|
||||
|
||||
## GDB
|
||||
|
||||
Persistent GDB sessions, including the MCP workflow and one-shot `rea debug-with-gdb` CLI, are available only on Linux. REA verifies a live inferior's output descriptors through `/proc` before detaching it; it does not claim equivalent descriptor verification on macOS. Windows is also unsupported because owned Job Object cleanup can terminate debugger inferiors. On supported Linux hosts, `start_gdb_session`, `gdb_console`, `gdb_session_status`, and `close_gdb_session` manage a persistent GDB/MI session in MCP. The one-shot CLI requires a caller-selected durable TTY: `rea debug-with-gdb --inferior-tty /dev/pts/7 'file ./program'`. The TTY must remain available after GDB exits so a detached inferior can continue writing. MCP starts GDB with `--nx --quiet --interpreter=mi3`, retries initialization with MI2 if MI3 initialization returns an error and cleanup succeeds, waits for its prompt, and disables auto-loading. Pass optional `inferior_tty` to select the terminal that future inferior runs use; REA configures it before the session becomes ready and checks the exact value with GDB/MI. `gdb_session_status` reports the selected path, whether REA still considers the configuration verified, and whether invalid UTF-8 has been observed in MI bytes (`mi_invalid_utf8`) or stderr (`stderr_invalid_utf8`). Commands that can change the inferior TTY or redirect a run revoke that verification permanently for the session, so close retains GDB ownership if a live inferior remains. The complete built-in inspections `show inferior-tty` and `help inferior-tty` preserve verification; compound commands still use the conservative mutation checks. Closing a session with one live inferior and a verified TTY automatically detaches the target, which can resume execution; MCP clients receive destructive-operation annotations for this behavior. The console remains unrestricted and can run shell commands, change target state, access files, and use network in the REA process user's security context. Raw MI records and console/target/log streams are returned with Evidence; command output is retained up to the configured budget (16 MiB by default) and explicitly marked when truncated. A single console, target or log record over the 16 MiB MI line limit is dropped and the command is marked truncated, while an oversized result or async record still stops the session. GDB startup waits 10000 ms with no runtime override. Each MI command defaults to 120000 ms; its deadline, command-output budget, session capacity and pending-command queue use the [shared debugger launch settings](#debugger-launch-settings). `command_timeout_ms` does not change a persistent debugger session.
|
||||
|
||||
A truncated GDB command capture includes `output_limits` with the exact causes
|
||||
and effective maxima: `command_bytes`, `command_records`, or `mi_line_bytes`.
|
||||
Only byte-budget exhaustion recommends raising
|
||||
`REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES`. The 16384-record retention ceiling and
|
||||
16 MiB single-line protocol ceiling stay fixed; select a command with fewer or
|
||||
smaller records when either is reached. These cause facts survive timeout,
|
||||
cancellation and process-exit captures as well as successful commands.
|
||||
|
||||
With multiple live inferiors, or a live inferior without a verified TTY, manual
|
||||
close requests an explicit disposition and keeps the session ready. Use
|
||||
`gdb_console` to select and explicitly detach or terminate each remaining
|
||||
@@ -133,7 +125,7 @@ rea debug-with-rizin --inferior-tty /dev/pts/7 ./program dr
|
||||
rea execute-rizin-command ./program iI
|
||||
```
|
||||
|
||||
Captured stdout and stderr use the same `max_output_bytes` (CLI: `--max-output-bytes`) admission and recovery described above, default 16 MiB. One-shot commands independently use `command_timeout_ms`. Raising output admission cannot recover an earlier capture; command effects and cleanup must be checked before another invocation.
|
||||
Captured stdout and stderr are retained up to a 16 MiB combined limit; `output_truncated` marks output beyond that limit. One-shot commands use the `command_timeout_ms` budget described above. That 16 MiB cap has no runtime override.
|
||||
|
||||
### Decoder and map inputs
|
||||
|
||||
|
||||
+2
-18
@@ -173,26 +173,10 @@ present. `architecture` on any other image is rejected rather than ignored.
|
||||
|
||||
Package and dependency counts are checked against the minimum required JSON
|
||||
representation before graph allocation or schema cloning. Oversized graphs
|
||||
report the output byte budget.
|
||||
|
||||
The inferred compiler-path facet keeps at most `max_rustc_hashes` distinct
|
||||
hashes by default (64). Reaching that selection leaves the dependency report
|
||||
available and adds `rustc_commit.coverage` with partial status and the effective
|
||||
limit. Increase `max_rustc_hashes` (CLI `--max-rustc-hashes`) on the next call
|
||||
to retain a larger prefix; no restart is required. The complete-report byte
|
||||
allowance still applies. Optional compiler paths are shortened before they can
|
||||
displace independently observed dependency facts.
|
||||
report the output byte budget; the compiler-path capacity reports
|
||||
`maximum_rustc_hashes` in hashes, independently of byte limits.
|
||||
|
||||
Reader architecture names remain verbatim in the report. Evidence subjects use
|
||||
the recognized canonical family: `386` maps to `x86`, `amd64` to `x86_64`, and
|
||||
`ppc` to `powerpc`. Unknown families such as `ppc64` remain unknown rather than
|
||||
being inferred from a related spelling.
|
||||
|
||||
Input and complete-report admission are caller choices through `producer_limits`:
|
||||
`input_bytes` defaults to 256 MiB and `output_bytes` to 8 MiB. Both accept positive
|
||||
safe integers and apply to the next call without restarting REA. On CLI, use
|
||||
`--producer-limits '{"input_bytes":536870912,"output_bytes":16777216}'`.
|
||||
Graph expansion and compiler-path retention consume the same selected report
|
||||
allowance. The independent compressed dependency limit (1 MiB), inflated JSON
|
||||
limits (8 MiB and 64 times the consumed stream), parser nesting and transport
|
||||
budgets remain enforced; increasing report admission does not raise those.
|
||||
@@ -44,9 +44,7 @@ are outside admitted signature semantics and keep unknown mappings.
|
||||
## Resource admission
|
||||
|
||||
Typed producer rows, names and declaration tokenization are admitted before
|
||||
allocation against the selected `max_interface_bytes` allowance (default 32 MiB).
|
||||
Inspection and comparison select this allowance independently; comparison cannot
|
||||
restore producer facts omitted under a smaller inspection allowance.
|
||||
allocation against the existing 32 MiB WABT output representation capacity.
|
||||
Comparison uses linear grouping and cooperative transport yields; it never
|
||||
builds Cartesian duplicate pairings. CLI JSON input retains the shared file
|
||||
admission gate. Token usage and peak process-family memory remain unmeasured.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Implementation plan
|
||||
|
||||
Use the existing named domain contracts and runtime settings owner. Prefer
|
||||
per-call selection or cohesive resource policies where they express analyst
|
||||
intent. Extend the runtime registry for ASAR listing admission, which already
|
||||
flows through the shared archive admission owner.
|
||||
|
||||
Implement independent owner slices: kernel metadata; GDB/Ghidra; desktop and
|
||||
generated Python; Apktool; JavaScript precision; source maps; historical
|
||||
captures; artifacts/managed/Rust; live capture; and producer limits. Coordinate
|
||||
shared schema/configuration files centrally and review cross-slice changes
|
||||
before broad checks.
|
||||
|
||||
Remove superseded literals, representations and misleading recovery rather
|
||||
than adding downstream prose repairs. Use the repository's Result/error algebra,
|
||||
existing process supervision, and existing CLI/MCP registration paths.
|
||||
|
||||
Validate focused boundaries first, then build, type/lint checks and generated
|
||||
documentation consistency. Document unavailable native/provider lanes. No
|
||||
performance claim or benchmark is required for correctness and policy ownership.
|
||||
|
||||
For default calibration, compare representative completed tasks, including
|
||||
recovery calls and complete replies. Record artifact identity, an independent
|
||||
coverage oracle, estimated tokens, elapsed/CPU time and sampled process-family
|
||||
memory. Separate artifact admission from delivery size; do not raise defaults
|
||||
merely to make a large response fit. Keep unmeasured defaults explicitly
|
||||
provisional rather than presenting inherited numbers as optimal.
|
||||
@@ -1,69 +0,0 @@
|
||||
# Recoverable analysis budgets
|
||||
|
||||
## Problem
|
||||
|
||||
Several analysis workflows discard selected facts at fixed retention boundaries.
|
||||
Existing runtime settings control only some resources. Increasing a deadline,
|
||||
heap, or header allowance cannot repair an independent count or representation
|
||||
limit. Desktop readers also duplicate policy in generated Python.
|
||||
|
||||
Related discussions: [#6176](https://github.com/morluto/rea/issues/6176),
|
||||
[#8434](https://github.com/morluto/rea/issues/8434),
|
||||
[#5808](https://github.com/morluto/rea/issues/5808), and
|
||||
[#8978](https://github.com/morluto/rea/issues/8978).
|
||||
|
||||
## Requirements
|
||||
|
||||
1. Preserve defaults unless representative completed-task measurements justify
|
||||
a calibration; preserve caller selections, Evidence identity, omission,
|
||||
unknown coverage, and cleanup authority. Changes must not grant new target
|
||||
execution, injection, mutation, installation, or network authority.
|
||||
2. Keep source validity independent of retention completeness. In particular,
|
||||
an overlong but terminated kernel modinfo value is not malformed merely
|
||||
because REA omitted its text.
|
||||
3. Preserve exact exhaustion causes and give a next action addressing that
|
||||
resource. GDB byte, record-count, and single-line limits must remain distinct.
|
||||
4. Offer caller selection, continuation, or a bounded policy override for the
|
||||
reviewed desktop projections, Apktool string tables, JavaScript semantic
|
||||
precision, source maps, historical captures, provider capture retention,
|
||||
artifact/managed representations, and producer admission/output limits.
|
||||
5. Interpret each policy at its owner. Parent and child process checks consume
|
||||
the same accepted values; generated Python consumes the desktop policy rather
|
||||
than maintaining independent numeric defaults. Reject invalid values rather
|
||||
than clamping or silently falling back.
|
||||
6. Retain justified format, runtime, protocol, and ownership constraints.
|
||||
Distinguish them from adjustable resource policy and external client budgets.
|
||||
A larger delivery page cannot recover facts already discarded by a producer.
|
||||
7. A runtime default update affects new accepted operations only. Explicit
|
||||
per-call choices win. Report effective value, units, applicable control and
|
||||
retry/restart scope without claiming active-worker telemetry.
|
||||
8. Artifact recognition that cannot examine the selected carrier within its
|
||||
accepted resource policy remains limited/unknown, never examined absence.
|
||||
Preserve the exhausted cause through classification and retained probe facts.
|
||||
Add cohesive per-call Bun, .NET bundle, Node SEA and Godot admission controls
|
||||
at their producing owners, including preallocation and expansion checks.
|
||||
9. Desktop task checks must admit the actual shipped seed representation under
|
||||
the same selected view policy as the generated reader. Acquisition/export
|
||||
budgets remain separate from runtime view admission.
|
||||
10. Local source-location tracing offers opt-in compact retained delivery and
|
||||
bounded access to mapping alternatives and embedded text. Preserve complete
|
||||
Evidence, source identity, selected positions, unknowns and every equal-point
|
||||
mapping; compact output must not repeat the raw map or entire source text.
|
||||
11. Desktop SQLite projections share a configurable per-table encoded-view
|
||||
allowance with generated readers and check it before retaining rows. Keep
|
||||
the independent native SQLite value/record and worker-reply envelopes
|
||||
explicit; a projected-view override does not raise those constraints.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Exercise the exhausted boundary and its advertised repair through the strongest
|
||||
practical public or production workflow. Use small lowered-budget fixtures when
|
||||
they prove the same boundary without large allocations. Preserve independent
|
||||
malformed-input, cancellation, identity, cleanup and fixed-protocol controls.
|
||||
Record real-provider coverage separately from simulated process/protocol tests.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Unbounded analysis, changing all numeric constants, treating format bit widths as
|
||||
preferences, raising another application's client buffer/deadline, recovering
|
||||
lost live observations retroactively, or adding new execution authority.
|
||||
@@ -1,137 +0,0 @@
|
||||
# Tasks
|
||||
|
||||
- [x] Separate source validity from retained modinfo fields.
|
||||
- [x] Preserve GDB exhaustion causes and improve Ghidra value-flow recovery.
|
||||
- [x] Consolidate desktop policy with emitted Python and expose caller controls.
|
||||
- [x] Make Apktool string omissions reachable through caller selection.
|
||||
- [x] Thread JavaScript semantic precision policy through actual evaluators.
|
||||
- [x] Coordinate source-map parent, codec and fetch budgets.
|
||||
- [x] Coordinate historical-capture parent and decoder budgets.
|
||||
- [x] Expose independent artifact/managed admission and preserve partial Rust facts.
|
||||
- [x] Expose capture retention policy without changing target authority.
|
||||
- [x] Expose producer resource/output controls with exact recovery.
|
||||
- [x] Integrate ASAR runtime setting, launch validation and setup forwarding.
|
||||
- [x] Complete focused and integrated checks, update product docs and report gaps.
|
||||
- [x] Repair reviewed managed CLI decimal parsing and selected-budget provenance.
|
||||
- [x] Preserve Go/Rust owner-supplied exhaustion facts through summary views.
|
||||
- [x] Preserve selected desktop policy separately from effective policy and bound
|
||||
the emitted reader's initial probe during file growth.
|
||||
- [x] Compare representative completed Flutter and source-map tasks before
|
||||
calibrating defaults; retain reproducible inputs, oracles and receipts.
|
||||
- [x] Preserve limited Bun recognition through inventory and probe facts.
|
||||
- [x] Add per-call artifact admission policies and exact same-artifact recovery.
|
||||
- [x] Align shipped desktop seed task admission with generated reader policy.
|
||||
- [x] Add retained source-location summaries, paged matches and bounded text.
|
||||
- [x] Verify compact point tracing on the pinned public maps and refresh native
|
||||
provider recovery coverage; complete coordinated integration checks.
|
||||
|
||||
## Validation
|
||||
|
||||
Executed with the pinned Node 24.22.1 / npm 11.16.0 toolchain:
|
||||
|
||||
- Runtime build and packaged skill generation through the focused-test
|
||||
prerequisite.
|
||||
- `npm run check:fast`: typecheck, lint and module boundaries pass.
|
||||
- `npm run format:check`: passes.
|
||||
- `npm run test:python`: bridge and analysis-efficiency suites pass.
|
||||
- `npm run verify:test-discovery`: all 3,218 test files discovered exactly once.
|
||||
- `npm run knip:production`: passes; advisory candidates were inspected rather
|
||||
than removed without runtime/verifier ownership evidence.
|
||||
- `npm run docs:generate` and `npm run docs:check`: pass, including the managed
|
||||
conformance generator and README consistency checks. Removed obsolete ignored
|
||||
managed-verifier argument bags instead of adding permissive budget fallbacks.
|
||||
|
||||
Focused tests ran in batches across CLI, MCP, worker, parser, lifecycle and
|
||||
provider boundaries. The primary 28-file run passed 726 tests, with six failures
|
||||
and one host skip. Those failures were repaired and every affected file passed
|
||||
its targeted rerun. The supplementary producer run passed 512 tests with five
|
||||
failures and 120 platform skips; every affected file then passed its rerun.
|
||||
The final capture-only run passed all 45 tests. This is a record of separate
|
||||
executed runs, not an aggregate unique-test count or a claim that `check:pr` ran.
|
||||
|
||||
Recovery checks include the same retained target after raised admission,
|
||||
explicit selection reaching an omitted item, lower-budget cache re-admission,
|
||||
worker-policy changes without stale results, generated Python parity, exact
|
||||
exhaustion causes, and malformed/cancellation/cleanup controls. Integration
|
||||
review also found and fixed XML/JSON view and historical-capture nesting budgets
|
||||
that initially failed to reserve complete Evidence wrapper depth.
|
||||
|
||||
The subsequent review fixes passed all 50 tests across six compiled acceptance,
|
||||
CLI, MCP and emitted-Python boundary files. Typecheck and lint also passed after
|
||||
those fixes. Default calibration records its separate measurements and
|
||||
limitations in the [Flutter](../../../verification/flutter-budget-calibration.md)
|
||||
and [source-map](../../../verification/source-map-budget-calibration.md) reports;
|
||||
those measurements are not native-provider verification.
|
||||
The calibrated source-map default and explicit smaller-budget recovery passed
|
||||
all six compiled source-location CLI tests. The final formatting, type/lint,
|
||||
generated-documentation and README consistency checks passed. The observer's
|
||||
completed, timeout, surviving-child and signal-refusal controls retained
|
||||
receipts and confirmed empty process groups.
|
||||
|
||||
The final follow-up adds per-call artifact admission, limited recognition,
|
||||
shipped-seed policy agreement, bounded SQLite views and retained source-location
|
||||
delivery. The 12-file compiled CLI/MCP run passed 56 tests and exposed four stale
|
||||
fixed-limit expectations; those owner expectations were updated to structured
|
||||
resource facts and all five rerun files passed 60 tests, including the shared
|
||||
analysis-view integration suite. The SEA MCP matrix separately passed both full
|
||||
and compact schema profiles with same-session lower/raise/lower admission.
|
||||
The final SQLite worker/emitted-reader checks passed all four scenarios. These
|
||||
are separate executed runs, not an aggregate unique-test count.
|
||||
|
||||
The [source-location delivery qualification](../../../verification/source-location-delivery.md)
|
||||
retains 18 successful public MCP measurements with matching parent IDs, exact
|
||||
mapping/snippet oracles and process-tree samples. The
|
||||
[JavaScript precision calibration](../../../verification/javascript-precision-calibration.md)
|
||||
retains 12 real CLI runs supporting unchanged numeric defaults for that narrow
|
||||
corpus. Native capture qualification and the live same-Program Ghidra receipt
|
||||
are linked from the [runtime verification report](../../../verification/runtime-analysis-settings.md).
|
||||
The Python gate passed all 102 tests; architecture guards passed across 2,571
|
||||
source files. Formatting, type/lint, generated docs, README consistency and
|
||||
production reachability checks passed. Reachability advisory candidates were
|
||||
reviewed without deleting runtime/verifier-owned exports. The complete
|
||||
`check:pr` gate was not run.
|
||||
|
||||
## Integration onto current main for the PR
|
||||
|
||||
Integrated onto main `8dfc47b107afc0cf4b53322a10b94c666fe9e18b`, preserving its
|
||||
new desktop secret isolation, custom task readers, gzip support, Deno recognition,
|
||||
signed bundle validation and chunked I/O. Incomplete Bun/SEA recognition now
|
||||
keeps known native or other confirmed-pack observations with an explicit unknown
|
||||
cause; it never caches unexamined data as absence. Main's malformed-directory
|
||||
validation continues to take precedence over adjustable Godot retention.
|
||||
|
||||
The fresh 11-file compiled integration run passed 118 tests with one host skip,
|
||||
covering artifact admission, retained source locations, desktop seed/SQLite/CLI,
|
||||
Go/Rust snapshots, runtime settings and setup forwarding. Separate owner checks
|
||||
passed for new main's desktop secret/custom-reader/gzip and JavaScript
|
||||
enumerable-copy/require-drain behavior. Source-map fetch-budget omission, empty
|
||||
selection and partial selection now also remain distinct in browser Evidence.
|
||||
|
||||
On this integrated branch, Python passed 102 tests; discovery found all 3,343
|
||||
test files exactly once; architecture guards passed across 2,696 source files.
|
||||
The runtime/skill build, type/lint and generated-documentation checks passed.
|
||||
The retained native Ghidra lane was rerun against the integrated code and again
|
||||
confirmed live budget recovery, unchanged target bytes, Program continuity and
|
||||
cleanup; see its [integration receipt](https://github.com/morluto/rea/blob/main/docs/verification/runtime-analysis-settings-ghidra-integration-2026-11-09.json).
|
||||
Historical calibration and earlier receipts retain their original runtime pins;
|
||||
they are not performance measurements of the integrated head. `check:pr` was
|
||||
not run.
|
||||
|
||||
## Verification limits
|
||||
|
||||
The follow-up verified all packaged Java sources against installed Ghidra 12.1.4
|
||||
and exercised live operation/input/edge, native payload and deadline recovery on
|
||||
the same Program with unchanged target bytes. The target was not executed.
|
||||
The [dated native receipt](../../../verification/runtime-analysis-settings.md)
|
||||
records exact versions, observations and verified cleanup; this does not qualify
|
||||
Windows or every public transport/provider combination. Real local Frida and
|
||||
LLDB producer-boundary budget checks also passed with explicitly controlled test
|
||||
processes and confirmed cleanup. Full AgentEnv execution was not exercised;
|
||||
desktop checks ran real SQLite workers and emitted reader helpers. Linux-only
|
||||
native lanes and real Apktool, WABT, pwntools, pwndbg, sigrok and TShark remain
|
||||
unverified. Source-owned codec/worker and simulated checks are labeled separately.
|
||||
No native-provider performance improvement is claimed. Source-owned workflow
|
||||
calibration measures a bounded public-artifact corpus, including complete
|
||||
replies and recovery calls; it does not establish universally optimal defaults.
|
||||
Ghidra function dossiers now reobserve accepted budgets while retaining the
|
||||
native decompiler cache; the native latency cost of that change is unmeasured.
|
||||
@@ -1,6 +0,0 @@
|
||||
# Plan
|
||||
|
||||
1. Give EVM defaults and per-call policy one domain owner using the shared producer schema.
|
||||
2. Resolve the accepted policy before resource selection and carry byte limits to the worker request; retain tighter inherited soft and hard bounds.
|
||||
3. Preserve exact budgets through read/reply/diagnostic/resource recovery, CLI and Evidence. Replace the failure owner's positional argument list with a typed context.
|
||||
4. Verify policy recovery and existing malformed/cancellation/cleanup behavior with focused checks.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Per-call EVM producer budgets
|
||||
|
||||
An analyst can raise or lower carrier, reply, diagnostic, address-space, file-output and CPU budgets without changing the selected artifact or restarting REA. Defaults stay 4 MiB carrier, 16 MiB reply/file, 1 MiB diagnostics, 3 GiB address space and 30 CPU seconds. `timeout_ms` remains the separate worker deadline.
|
||||
|
||||
Use the shared typed `producer_limits` contract, advertising only fields enforced by EVM. If `file_bytes` is omitted, it follows the selected `output_bytes`; an explicit file budget stays independent. Linux soft/hard limits retain the tighter inherited value, and failures distinguish configured from unknown effective limits and signal causes. Resource guidance identifies the exhausted field, its selected value and whether inherited restrictions can prevent repair. The 256 MiB worker heap stays explicit and independent.
|
||||
|
||||
Preserve carrier/decoded identities, metadata, target-free offline authority, cancellation, final cleanup and partial facts. Verify same-target recovery with a small selected allowance and narrower inherited-limit controls; report unavailable real Linux coverage.
|
||||
@@ -1,6 +0,0 @@
|
||||
# Tasks
|
||||
|
||||
- [x] Declare defaults and exact per-call policy.
|
||||
- [x] Wire parent, worker and inherited resource enforcement.
|
||||
- [x] Update exact recovery, CLI and Evidence.
|
||||
- [x] Run focused regressions and report platform gaps.
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
Extend `goBinarySchema` in `src/domain/go/goBinary.ts` with optional
|
||||
`string_literals` (`evidence_kind: "inferred"`, `items`, `discovered_count`,
|
||||
`aggregate_bytes`, `max_items`, `truncated`, optional `limits_reached`), plus
|
||||
`include_string_literals` and the Go-owned per-call budget controls on `src/contracts/go/goToolContracts.ts` and
|
||||
`aggregate_bytes`, `max_items`, `truncated`), plus `include_string_literals` and
|
||||
`max_string_literals` on `src/contracts/go/goToolContracts.ts` and
|
||||
`src/cli/goCommands.ts`. Add the `string_literals` view projection to
|
||||
`src/domain/analysisView/goBinaryView.ts` so callers can page recovered literals
|
||||
from retained Evidence without repeating file parsing.
|
||||
@@ -18,9 +18,6 @@ pointer alignment phase, merge overlaps, and sort by ascending file offset so
|
||||
`reference_count` counts each physical header once and `first_header_offset` is
|
||||
minimal. Deduplicate target slices in memory with bounded candidate and rejection
|
||||
caches before sorting deterministically by `(location.offset, location.bytes)`.
|
||||
Use exact offset/length keys, so configurable string lengths cannot collide in
|
||||
an arithmetic key encoding. Resolve budget defaults once in the image reader
|
||||
and pass that policy to string recovery.
|
||||
|
||||
## Incremental work and validation
|
||||
|
||||
@@ -40,8 +37,4 @@ and pass that policy to string recovery.
|
||||
|
||||
Default `inspect_go_binary` calls omit `string_literals` and preserve historical
|
||||
Evidence compatibility. No external tool or target execution is introduced; only
|
||||
bounded offline byte slicing over the admitted stable image buffer runs. The
|
||||
input default remains `256 MiB`; callers can select byte and work budgets within
|
||||
the advertised runtime representability. Count normalized report bytes before
|
||||
allocating its JSON serialization. Every exhausted limit carries its own next
|
||||
request repair, independently of transport delivery.
|
||||
bounded offline byte slicing over the already read `64 MiB` image buffer runs.
|
||||
@@ -17,30 +17,15 @@ images so static string slices are recovered at their exact boundaries.
|
||||
`O(log R)` readable segment checks on Mach-O and sectionless/unnamed-ELF
|
||||
load-region fallbacks) in deduplicated ascending file-offset order.
|
||||
- Admit candidate `[ptr, ptr + len)` slices only inside single read-only
|
||||
non-executable mapped spans with `1 <= len <= max_string_literal_bytes`
|
||||
(default `4096`), non-overlapping with the
|
||||
non-executable mapped spans with `1 <= len <= 4096`, non-overlapping with the
|
||||
header itself, and valid printable UTF-8 (allowing `\t`, `\n`, `\r`, `U+200C`
|
||||
and `U+200D` while rejecting control, surrogate, noncharacter, private-use and
|
||||
invisible/bidi `\p{Cf}` code points).
|
||||
- Keep the existing defaults for header-scan work (`64 MiB`), tracked candidates
|
||||
(`100,000`), returned items (`2,000`), and aggregate returned UTF-8 bytes
|
||||
(`512 KiB`). Expose each independent boundary as the positive-safe-integer
|
||||
per-call controls `max_string_header_scan_bytes`, `max_string_candidates`,
|
||||
`max_string_literals`, `max_string_literal_bytes` and `max_string_bytes`.
|
||||
String-byte maxima follow the runtime's string representability; there is no
|
||||
separate fixed 10,000-item ceiling. The bounded negative-slice cache (`4,096`)
|
||||
affects reuse only and must not discard valid later candidates.
|
||||
- Report `evidence_kind: "inferred"`, `discovered_count`, `aggregate_bytes`,
|
||||
`truncated` and the exact exhausted `limits_reached` controls honestly.
|
||||
Increasing a named control and inspecting the same complete artifact can
|
||||
recover omitted facts; output paging cannot recover facts never retained.
|
||||
- Deduplicate candidates by their complete offset and length independently of
|
||||
the selected literal-length allowance; raising that allowance cannot merge
|
||||
distinct slices or inflate their reference counts.
|
||||
- Share one Go analysis budget owner with build-info input, representation and
|
||||
search controls. Omitted selections keep defaults and remain omitted from
|
||||
request Evidence. Representation maxima account for base64 expansion; no
|
||||
setting changes the separate shared native structural decoder or MCP budget.
|
||||
- Bound header-scan work (`64 MiB`), tracked candidates (`100,000`), negative
|
||||
slice cache (`4,096`), returned items (`max_string_literals`, `1..10,000`,
|
||||
default `2,000`), and aggregate returned UTF-8 bytes (`512 KiB`), reporting
|
||||
`evidence_kind: "inferred"`, `discovered_count`, `aggregate_bytes` and
|
||||
`truncated` honestly.
|
||||
- Record limitation notes when Mach-O `LC_DYLD_CHAINED_FIXUPS` encodes rebases in
|
||||
pointer slots or when truncation occurs, and expose a focused `string_literals`
|
||||
projection in `inspect_analysis_view`.
|
||||
@@ -50,9 +35,7 @@ images so static string slices are recovered at their exact boundaries.
|
||||
Unit, boundary and CLI acceptance tests cover ELF/PE/Mach-O (32/64-bit, little
|
||||
and big endian), sectionless and unreadable-`.shstrtab` ELF images, overlapping
|
||||
spans, `U+200C`/`U+200D` versus disallowed `\p{Cf}` controls, negative-cache and
|
||||
output defaults, named budget recovery, long-literal candidate identity, and
|
||||
25,000-region Mach-O indexing/cancellation. Service and provider tests cover
|
||||
malformed numeric controls and same-artifact retries with larger budgets. The real Go binary
|
||||
output caps, and 25,000-region Mach-O indexing/cancellation. The real Go binary
|
||||
verifier validates CLI and advertised-schema MCP parity across compiled targets.
|
||||
|
||||
## Out of scope
|
||||
|
||||
@@ -6,8 +6,3 @@
|
||||
- [x] Add unit, provider, MCP view and CLI acceptance tests for multi-format recovery, Unicode `\p{Cf}` / ZWNJ / ZWJ filtering, caps, negative-cache bounds and 25,000-section Mach-O scaling.
|
||||
- [x] Verify CLI and `artifactMcpCall` advertised-schema MCP parity across compiled targets in `scripts/verify-go-binary.mjs`.
|
||||
- [x] Update the Go binary guide, skill reference and generated catalogs; pass documentation route and static checks.
|
||||
|
||||
- [x] Consolidate Go-owned budgets and expose per-call input/report/build-info/search and inferred-string controls in CLI/MCP without changing defaults.
|
||||
- [x] Preserve source validity and exact exhausted controls, remove the fixed 10,000-item schema ceiling, and deduplicate variable-length candidates with complete offset/length identity.
|
||||
- [x] Run focused source string/service tests and production service budget-recovery checks over temporary offline fixture files.
|
||||
- [ ] Run the compiled CLI/MCP boundary lanes and real Go verifier for the new budget selections; source checks do not establish those workflows.
|
||||
@@ -1,5 +0,0 @@
|
||||
# Implementation plan
|
||||
|
||||
1. Declare caller budgets in the domain input and derive one immutable provider policy with existing defaults.
|
||||
2. Carry that policy in private child requests and scope all parser/read/reply consumers to it without global mutable state.
|
||||
3. Update exact recovery, CLI controls and Evidence parameters; verify selected-budget recovery and default regressions.
|
||||
@@ -1,9 +0,0 @@
|
||||
# Historical capture admission budgets
|
||||
|
||||
Allow an analyst to inspect the same retained HAR, SAZ, qlog or native mitmproxy capture after explicitly increasing an exhausted input, reply or nesting budget. Keep existing defaults, producer identity, sensitive-value handling and complete-or-failure semantics.
|
||||
|
||||
The public contract accepts positive safe-integer `max_input_bytes` and `max_reply_bytes`, and `max_nesting_depth` up to REA's JSON representation depth minus the four enclosing record/reply/Evidence levels (996). Selected values apply to the admitted operation and its child decoder, source and reply readers, archive expansion and protobuf interpretation. They do not raise the separately owned worker heap, CPU or client transport limits.
|
||||
|
||||
Failures name the exhausted parameter and value and the next-call repair. Omitted values retain 32 MiB input, 96 MiB reply and 64 nesting levels. Evidence preserves explicit selections and omission.
|
||||
|
||||
Acceptance: the same valid capture rejected by a deliberately small selected budget succeeds after changing only that budget. A malformed control remains malformed. The child and parent enforce the same selected facts, and parallel operations keep distinct policies.
|
||||
@@ -1,6 +0,0 @@
|
||||
# Tasks
|
||||
|
||||
- [x] Add input contracts and canonical policy selection.
|
||||
- [x] Connect parent, Node/Python decoder, archive and protobuf boundaries.
|
||||
- [x] Expose CLI controls and preserve request provenance.
|
||||
- [x] Verify same-input recovery, malformed controls and parser regressions.
|
||||
@@ -1,9 +0,0 @@
|
||||
# Plan
|
||||
|
||||
Use one domain policy and schema for defaults, selection and recovery. Publish
|
||||
local and fetch projections that reject fields their path does not consume.
|
||||
Thread selected values through existing acquisition/codec/process/format owners,
|
||||
including independently validated child request budgets. CLI exposes named
|
||||
numeric options and MCP derives its inputs from canonical source contracts.
|
||||
Existing guides own product behavior. Verify finite budget recovery through
|
||||
production boundaries and the public CLI; keep resource/lifecycle regressions.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Per-call source-map budget recovery
|
||||
|
||||
## Problem
|
||||
|
||||
Local tracing and bundle source-map collection discard or refuse useful facts
|
||||
at independent fixed byte, structural and codec-heap ceilings. A longer timeout
|
||||
cannot admit the same map. Related: #6176, #8434, #8978.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Preserve defaults unless representative calibration justifies a change;
|
||||
preserve selected artifact identities, source declarations,
|
||||
unknown coverage and cleanup ownership.
|
||||
- Admit explicit bounded per-call budgets at the producing owners, and propagate
|
||||
the same policy through acquisition, structural preflight, codec child and
|
||||
complete request/reply checks.
|
||||
- Reject inapplicable fields; preserve omission in Evidence parameters.
|
||||
- Explain the exact exhausted control, effective value, range, application point
|
||||
and recovery. Retain real representation and client constraints.
|
||||
- Keep upstream decoding unchanged and preflight rows/segments before allocation.
|
||||
|
||||
## Acceptance
|
||||
|
||||
A valid local map fails at an explicitly lowered admission, names map_bytes,
|
||||
and succeeds for the same point through the public CLI after increasing that
|
||||
budget. The calibrated 8 MiB default admits a complete map above 4 MiB while
|
||||
preserving an explicit smaller selection; see the
|
||||
[calibration record](../../../verification/source-map-budget-calibration.md).
|
||||
A finite fetched map similarly recovers after increasing decoded_records.
|
||||
Malformed maps, cancellation and failed cleanup retain their existing semantics.
|
||||
|
||||
## Non-goals
|
||||
|
||||
No source fetching beyond existing origin authority, application execution,
|
||||
implicit retries, unbounded allocation or change to client deadlines.
|
||||
@@ -1,10 +0,0 @@
|
||||
# Tasks
|
||||
|
||||
- [x] Define shared source-map policy with existing defaults and typed selections.
|
||||
- [x] Propagate local budgets through acquisition, service, codec and process owner.
|
||||
- [x] Propagate fetched response, record and representation budgets.
|
||||
- [x] Expose exact CLI/MCP contracts and actionable recovery.
|
||||
- [x] Add finite structural/fetch and public large-map recovery regressions.
|
||||
- [x] Update source-location and browser guides.
|
||||
- [x] Run compiled CLI/codec recovery and coordinated type/lint/documentation gates.
|
||||
- [x] Calibrate local map admission against shipped maps; retain other budgets.
|
||||
@@ -1,135 +0,0 @@
|
||||
# Flutter projection budget calibration
|
||||
|
||||
Keep the default `pool_policy.uri_limit=500`. Two pinned real APKs show that
|
||||
requesting 2048 immediately benefits a full bounded package-URI inventory, but
|
||||
increases complete overview delivery when the task only needs identity and
|
||||
snapshot facts. This is a task-specific selection, not evidence for a universal
|
||||
2048 default. No production default changed in this experiment.
|
||||
|
||||
## Tasks and correctness
|
||||
|
||||
The inventory task retrieves every distinct complete printable ASCII run that
|
||||
starts `package:` and has length 5–1024 in arm64
|
||||
`_kDartIsolateSnapshotData`. Both workflows select `contains:"package:"`.
|
||||
The baseline starts at the default 500 and retries with 2048 when the returned
|
||||
array is shorter than its count lower bound; the candidate starts at 2048.
|
||||
Both final arrays exactly match an independent Python ZIP/ELF dynamic-symbol and
|
||||
section-table parser plus byte-regex oracle. Gallery has 756 matches, LocalSend
|
||||
1407; 1024 is insufficient for the latter.
|
||||
|
||||
The overview control requires target/library identity, ABI and snapshot section
|
||||
observations, without individual strings. It compares unfiltered complete
|
||||
Evidence with URI 500 and URI 2048. All calls preserve `run_bytes=1024`,
|
||||
`token_bytes=128`, `token_limit=2000` and existing admission budgets. Strings
|
||||
remain byte-pattern observations, not recovered Dart types. Gallery has eight
|
||||
printable runs over 1024 bytes and LocalSend one. Recovering every bounded
|
||||
package URI does not make the overall string pool or snapshot coverage complete.
|
||||
|
||||
## Measurements
|
||||
|
||||
Three serial trials per case, 24 successful fresh source-service processes,
|
||||
Node 24.22.1, macOS 26.3 arm64. Each starts `FlutterBuildAnalysisService` and closes
|
||||
its provider. No app installation or execution occurred. OS caches were not
|
||||
flushed; these are neither cold-cache nor warm-runtime-cache claims. No small
|
||||
real Flutter artifact was available as a setup control. Eight task variants
|
||||
across two APKs do not establish universal workload fit.
|
||||
|
||||
Values below are medians of three runs; tokens were identical within each case.
|
||||
The numeric receipts, artifact manifest and portable reproduction tools are in
|
||||
[`scripts/verify/analysis/flutter-budget-calibration/`](https://github.com/morluto/rea/tree/main/scripts/verify/analysis/flutter-budget-calibration).
|
||||
|
||||
| APK / completed task | Calls | cl100k tokens | Wall s | CPU s | Peak group RSS MiB |
|
||||
| --------------------------------------- | ----: | ------------: | -----: | ----: | -----------------: |
|
||||
| Gallery inventory, default + recovery | 2 | 14,488 | 0.836 | 1.028 | 258.656 |
|
||||
| Gallery inventory, 2048 first | 1 | 8,631 | 0.480 | 0.601 | 238.859 |
|
||||
| Gallery overview, default | 1 | 15,995 | 0.448 | 0.639 | 241.922 |
|
||||
| Gallery overview, 2048 | 1 | 18,764 | 0.560 | 0.639 | 243.328 |
|
||||
| LocalSend inventory, default + recovery | 2 | 22,360 | 0.405 | 0.519 | 221.328 |
|
||||
| LocalSend inventory, 2048 first | 1 | 16,317 | 0.296 | 0.365 | 184.781 |
|
||||
| LocalSend overview, default | 1 | 16,885 | 0.292 | 0.403 | 189.766 |
|
||||
| LocalSend overview, 2048 | 1 | 27,158 | 0.293 | 0.388 | 189.641 |
|
||||
|
||||
Tokens use tiktoken 0.12.0 `cl100k_base`, separately encoding compact Unicode JSON
|
||||
for every logical request and complete reply, including retries. They are exact
|
||||
counts for that encoding and those records, not model usage. CLI/MCP discovery,
|
||||
adapter delivery, retained-resource paging, chat framing and model reasoning
|
||||
are unmeasured. Path spelling can change replay counts. The driver also emits
|
||||
separately labeled tokenx 1.3.0 estimates; the table uses cl100k counts.
|
||||
|
||||
Wall time includes observer overhead. CPU is OS `wait4` user+system usage of the
|
||||
waited subtree; it excludes sampler work and descendants not waited for. RSS is
|
||||
the simultaneous process-group sum sampled every 20 ms: short peaks can be
|
||||
missed, shared pages can be counted twice and descendants leaving the group
|
||||
are excluded. The 1536 MiB observer stop is not a kernel hard limit. All 24 runs
|
||||
exited 0 without a resource stop and left their groups empty. No timing or memory
|
||||
claim is made for an unmeasured MCP summary workflow; the separate retained
|
||||
local historical summary study (`experiments/analysis-efficiency/flutter-aot-summary.md`) measured that workflow historically.
|
||||
|
||||
The frozen baseline is HEAD `e2a2ede5440df396e44b3406ef511a31a680290e` plus
|
||||
tracked patch SHA256 `038b69c19bbd79a303c9ecd8d9d22a1251637e3886543ca4f5a4c1b70c724eec`
|
||||
and copied untracked policy sources. HEAD alone does not identify the measured
|
||||
runtime. Original driver/oracle hashes and per-run values are retained in
|
||||
`measurements.json`; retained portable scripts change path resolution without
|
||||
changing task semantics. Later observer cleanup fixes do not change the sampled
|
||||
RSS algorithm or these successful, empty-group receipts.
|
||||
|
||||
## Reproduce
|
||||
|
||||
Use the pinned Node toolchain and installed locked dependencies. Download the two
|
||||
URLs in `manifest.json` to an external input directory under their declared
|
||||
filenames. `oracle.py` verifies APK sizes/digests, libapp digests, section offsets
|
||||
and expected counts before generating exact arrays. Do not commit APKs or raw
|
||||
reply records. Run comparisons serially; replace the paths below with absolute
|
||||
paths to the selected source snapshot, Node 24 and external artifacts.
|
||||
|
||||
```sh
|
||||
REA_CAL_REPO=/absolute/path/to/rea
|
||||
REA_CAL_NODE=/absolute/path/to/node24
|
||||
REA_CAL_INPUTS=/absolute/path/to/apks
|
||||
REA_CAL_OUTPUT=/absolute/path/to/receipts
|
||||
REA_CAL_ASSETS="$REA_CAL_REPO/scripts/verify/analysis/flutter-budget-calibration"
|
||||
for trial in 1 2 3; do
|
||||
python3 "$REA_CAL_ASSETS/oracle.py" "$REA_CAL_INPUTS" "$REA_CAL_OUTPUT/r$trial"
|
||||
python3 "$REA_CAL_ASSETS/configs.py" "$REA_CAL_INPUTS" "$REA_CAL_OUTPUT/r$trial"
|
||||
for config in "$REA_CAL_OUTPUT/r$trial/"*.config.json; do
|
||||
python3 "$REA_CAL_REPO/scripts/verify/analysis/budget-calibration-run.py" \
|
||||
--output "${config%.config.json}.metrics.json" --cwd "$REA_CAL_REPO" \
|
||||
--timeout-seconds 90 --memory-mib 1536 -- \
|
||||
"$REA_CAL_NODE" --import "$REA_CAL_REPO/node_modules/tsx/dist/loader.mjs" \
|
||||
"$REA_CAL_ASSETS/task.mjs" "$REA_CAL_REPO" "$config"
|
||||
done
|
||||
done
|
||||
python3 "$REA_CAL_ASSETS/aggregate.py" "$REA_CAL_OUTPUT" "$REA_CAL_OUTPUT/aggregate.json"
|
||||
```
|
||||
|
||||
`aggregate.py` requires tiktoken 0.12.0. Install it separately if needed; the
|
||||
calibration scripts do not change the project's dependencies or host software.
|
||||
Replays on a later checkout are new measurements, not reproduction of the
|
||||
frozen runtime merely because artifact hashes match.
|
||||
|
||||
## Other defaults remain evidence-limited
|
||||
|
||||
Keep Apktool strings 2000/table and 10000 aggregate: the retained real
|
||||
three-APK native-decoder study (`experiments/analysis-efficiency/apktool-string-tables.md`, historical local output) has at most 1126 valid
|
||||
strings in one table, and three selected locale tables fit the aggregate.
|
||||
That does not calibrate all APKs or all-locales tasks. No fresh Apktool run was
|
||||
performed here; the current host lacks the executable.
|
||||
|
||||
Keep desktop sampling defaults pending a representative complete state-view
|
||||
corpus. The pinned official QGIS GPKG used in the historical local query study
|
||||
(`experiments/analysis-efficiency/sqlite-existing-controls.md`) has 26 declared tables,
|
||||
240 countries and 4556 states/provinces according to an independent read-only
|
||||
SQLite count. A 500-row sample covers the former and omits the latter. One
|
||||
all-column geometry row can expand to 485277 bytes; selected tables/columns are
|
||||
more appropriate than globally multiplying retained rows. Counts alone do not
|
||||
calibrate CPU/memory or text/XML defaults.
|
||||
|
||||
Keep JavaScript semantic candidate/depth/getter/alias defaults. They bound
|
||||
analysis work and precision, rather than merely display projection. No
|
||||
independent real-artifact sensitivity study crossing these defaults was found.
|
||||
Synthetic recovery regressions establish behavior, not representative workload
|
||||
fit. This calibration gap remains explicit.
|
||||
|
||||
The supporting QGIS input is pinned in `manifest.json`; the row counts were
|
||||
rechecked with Python SQLite in read-only mode. Historical local studies are
|
||||
context, not new provider measurements or checked-in raw receipts.
|
||||
@@ -1,127 +0,0 @@
|
||||
# JavaScript primitive-budget calibration — 2026-11-09
|
||||
|
||||
Keep the existing semantic precision defaults. A narrow real-source comparison
|
||||
found no additional required facts when doubling `primitive_bytes`; it does not
|
||||
justify a global increase or replacing the existing conservative admission rule.
|
||||
The input contract now explains that rule, and its list of defaults derives from
|
||||
the policy owner instead of repeating independent literals.
|
||||
|
||||
## What was checked
|
||||
|
||||
Two inert directories went through the actual compiled
|
||||
`analyze-javascript-application` CLI, including its isolated source worker,
|
||||
complete Evidence serialization and normal CLI cleanup:
|
||||
|
||||
- An original `theme.ts` embedded in the pinned Actual Budget Theme source map.
|
||||
The required task recovers all **237** literal CSS property/value pairs and
|
||||
their source identity. A separate Python parser accepts only the complete
|
||||
literal-only declaration and checks every name/value without using REA's
|
||||
evaluator. A small package descriptor constructs the analysis directory; this
|
||||
is one captured module, not the complete application or an authenticated
|
||||
browser capture.
|
||||
- The installed locked **ms 2.1.3** package's original `index.js` and package
|
||||
descriptor. The requested small control recovers its `s = 1000` millisecond
|
||||
base unit. Multiplication-derived coefficients remain unsupported and unknown;
|
||||
larger byte admission does not implement arithmetic or prove those values.
|
||||
|
||||
Input URLs/digests, a portable preparation/oracle script, CLI task driver and
|
||||
all twelve compact numeric receipts are in
|
||||
[the calibration assets](https://github.com/morluto/rea/tree/main/scripts/verify/analysis/javascript-precision-calibration).
|
||||
No downloaded code or complete large replies are committed. The theme original
|
||||
source SHA-256 is `05307dc6f927705a8c76e43579c910bf6415ce28b375eea19251dc51dd2b1726`.
|
||||
|
||||
## Results
|
||||
|
||||
Three fresh CLI/worker trials per case compare omitted precision with explicit
|
||||
`semantic_precision: {"primitive_bytes": 2097152}`. Both requests hold the worker
|
||||
and CLI host old-space allowance at 512 MiB. Each task makes one analysis call,
|
||||
with no retries. All twelve exit successfully, verify the required facts and
|
||||
leave the observer's process group empty. Theme has no semantic unknowns; ms
|
||||
keeps the same 133 unrelated unknowns in both variants. This is complete
|
||||
coverage of the requested facts, not complete semantic understanding of ms.
|
||||
|
||||
| Task / allowance | Required facts | Estimated CLI task tokens | Wall s | CPU s | Sampled host-group RSS MiB |
|
||||
| ----------------------------- | -------------: | ------------------------: | -----: | ----: | -------------------------: |
|
||||
| Theme / default 1 MiB | 237 | 259,396 | 0.937 | 1.226 | 330.6 |
|
||||
| Theme / explicit 2 MiB | 237 | 259,475 | 0.928 | 1.229 | 330.8 |
|
||||
| ms base unit / default 1 MiB | 1 | 234,613 | 0.899 | 1.238 | 322.2 |
|
||||
| ms base unit / explicit 2 MiB | 1 | 234,668 | 0.905 | 1.225 | 330.5 |
|
||||
|
||||
Values are medians of three serial trials, in fixed default-then-double order.
|
||||
Node 24.22.1 ran on macOS 26.3 arm64; OS caches were not flushed. Mixed small
|
||||
timing/memory differences do not establish a performance improvement.
|
||||
|
||||
Token estimates use tiktoken 0.12.0 `cl100k_base`: compact Unicode JSON of the
|
||||
actual argv and complete parsed stdout Evidence, plus literal progress stderr.
|
||||
The full complete replies are about 734 KB for Theme and 624 KB for ms. This
|
||||
measurement includes the CLI entrypoint, worker, serialized output, and the
|
||||
driver's correctness checks. It excludes artifact acquisition, one-off oracle
|
||||
preparation, tokenizer computation, MCP discovery/protocol, chat framing and
|
||||
model reasoning. These are not billed model tokens or a claim that full output
|
||||
is an efficient agent workflow; existing retained summary/views can change
|
||||
delivery independently of evaluator precision. Replays with other path spellings
|
||||
can have different tokens and identities.
|
||||
|
||||
CPU is OS `wait4` user+system usage of the waited subtree; sampler work and
|
||||
unwaited descendants are excluded. RSS sums the host process group every 20 ms:
|
||||
short peaks may be missed and shared pages counted more than once. **The isolated
|
||||
JavaScript worker launches into another process group, so its peak memory is
|
||||
unmeasured here.** The table is not total process-family peak memory. The
|
||||
observer's 1536 MiB early-stop guard is not a kernel hard limit; its 90-second
|
||||
deadline was not reached. Worker old-space flags are also not whole-process
|
||||
memory limits. Compiled entrypoint and owner hashes are retained because HEAD
|
||||
`e2a2ede5440df396e44b3406ef511a31a680290e` alone does not identify the measured
|
||||
uncommitted runtime. Portable scripts revise path handling after measurement
|
||||
without changing the requested facts; original driver/oracle hashes are retained.
|
||||
|
||||
## Why retain the conservative rule
|
||||
|
||||
`primitive_bytes` charges six estimated JSON bytes per UTF-16 code unit plus
|
||||
quotes, before creating concatenations. At the 1 MiB default, 174,762 ASCII units
|
||||
fit and one additional unit exhausts that estimate, even though actual ASCII
|
||||
JSON occupies far fewer bytes. Control characters and lone surrogates can need
|
||||
six escaped bytes, so this is a conservative precision/resource policy rather
|
||||
than an assertion about observed encoded bytes. Existing regressions retain the
|
||||
boundary and exact exhausted-control recovery.
|
||||
|
||||
A bounded Babel inventory of the pinned generated Theme, main and DateSelect
|
||||
scripts found their longest static strings/templates to be 152, 936 and 13,120
|
||||
UTF-16 units respectively. This inventory is not full semantic analysis and
|
||||
does not bound dynamically composed values or candidate cross-products. It
|
||||
nevertheless supplies no representative case requiring a higher string default.
|
||||
|
||||
Exact preallocation accounting would need to inspect string contents across
|
||||
every candidate concatenation, including pairs formed across piece boundaries,
|
||||
while preserving work bounds. Without an observed task failing this estimate,
|
||||
that added work and complexity are unjustified. The 256-candidate, 512-state and
|
||||
other depth/work defaults remain provisional: these two tasks do not establish
|
||||
their suitability for large unions, getters, aliases or adversarial sources.
|
||||
|
||||
## Reproduce
|
||||
|
||||
Download the pinned Theme map URL from `manifest.json` to external storage, use
|
||||
the pinned Node toolchain and installed locked dependencies, then run:
|
||||
|
||||
```sh
|
||||
REA_JS_REPO=/absolute/path/to/rea
|
||||
REA_JS_NODE=/absolute/path/to/node24
|
||||
REA_JS_DATA=/absolute/path/to/external-receipts
|
||||
REA_JS_ASSETS="$REA_JS_REPO/scripts/verify/analysis/javascript-precision-calibration"
|
||||
python3 "$REA_JS_ASSETS/prepare.py" "$REA_JS_REPO" /absolute/path/to/theme.js.map "$REA_JS_DATA"
|
||||
for trial in 1 2 3; do
|
||||
for task in theme ms; do
|
||||
for budget in default double; do
|
||||
python3 "$REA_JS_REPO/scripts/verify/analysis/budget-calibration-run.py" \
|
||||
--output "$REA_JS_DATA/r${trial}-${task}-${budget}.metrics.json" \
|
||||
--cwd "$REA_JS_REPO" --timeout-seconds 90 --memory-mib 1536 -- \
|
||||
python3 "$REA_JS_ASSETS/task.py" "$REA_JS_REPO" "$REA_JS_NODE" \
|
||||
"$REA_JS_DATA" "$task" "$budget" "$REA_JS_DATA/r${trial}-${task}-${budget}.replies.json"
|
||||
done
|
||||
done
|
||||
done
|
||||
python3 "$REA_JS_ASSETS/aggregate.py" "$REA_JS_DATA" "$REA_JS_DATA/aggregate.json"
|
||||
```
|
||||
|
||||
`aggregate.py` needs the separately installed tokenizer version above. These
|
||||
scripts neither install dependencies nor change host/provider configuration.
|
||||
Later checkouts are new measurements even when target hashes still match.
|
||||
@@ -1,168 +0,0 @@
|
||||
{
|
||||
"schema": "rea-native-budget-qualification/v1",
|
||||
"date": "2026-11-09",
|
||||
"host": "macOS 26.3 arm64",
|
||||
"node": "24.22.1",
|
||||
"scope": "Real application service/provider boundaries, not public MCP/CLI recovery. Controlled owned fixtures only.",
|
||||
"frida": {
|
||||
"package_version": "17.23.2",
|
||||
"cases": [
|
||||
{
|
||||
"session_bytes": 1,
|
||||
"messages": [],
|
||||
"messages_truncated": true,
|
||||
"effective_limits": {
|
||||
"session_bytes": 1,
|
||||
"message_type_bytes": 4096,
|
||||
"message_text_bytes": 32768,
|
||||
"json_nodes": 250000,
|
||||
"json_depth": 64
|
||||
},
|
||||
"limitations": [
|
||||
"Artifact identity is unavailable for this observation.",
|
||||
"The target operating system and architecture were not reported by this observation.",
|
||||
"Frida capture reached capture_limits.session_bytes (1 bytes including JSON/base64 expansion and metadata). Select a larger capture_limits.session_bytes before starting a new explicitly chosen session or instrument_with_frida experiment; no REA restart is required. Already discarded observations cannot be recovered. Session bytes and MCP/client delivery budgets remain independent."
|
||||
],
|
||||
"cleanup_error": null
|
||||
},
|
||||
{
|
||||
"session_bytes": 4096,
|
||||
"messages": [
|
||||
{
|
||||
"script_id": "2c901ff4-07b7-4a97-9784-bdc9f676c911",
|
||||
"type": "send",
|
||||
"payload": null,
|
||||
"payload_truncated": true,
|
||||
"payload_capture_limit": "payload_bytes"
|
||||
}
|
||||
],
|
||||
"messages_truncated": false,
|
||||
"effective_limits": {
|
||||
"session_bytes": 4096,
|
||||
"message_type_bytes": 4096,
|
||||
"message_text_bytes": 32768,
|
||||
"json_nodes": 250000,
|
||||
"json_depth": 64
|
||||
},
|
||||
"limitations": [
|
||||
"Artifact identity is unavailable for this observation.",
|
||||
"The target operating system and architecture were not reported by this observation.",
|
||||
"Frida capture reached capture_limits.session_bytes (4096 bytes including JSON/base64 expansion and metadata). Select a larger capture_limits.session_bytes before starting a new explicitly chosen session or instrument_with_frida experiment; no REA restart is required. Already discarded observations cannot be recovered. Session bytes and MCP/client delivery budgets remain independent."
|
||||
],
|
||||
"cleanup_error": null
|
||||
},
|
||||
{
|
||||
"session_bytes": 262144,
|
||||
"messages": [
|
||||
{
|
||||
"script_id": "6283235c-f8c4-40db-ad61-3375003e4362",
|
||||
"type": "send",
|
||||
"payload": {
|
||||
"rea_budget_probe": "ok"
|
||||
}
|
||||
}
|
||||
],
|
||||
"messages_truncated": false,
|
||||
"effective_limits": {
|
||||
"session_bytes": 262144,
|
||||
"message_type_bytes": 4096,
|
||||
"message_text_bytes": 32768,
|
||||
"json_nodes": 250000,
|
||||
"json_depth": 64
|
||||
},
|
||||
"limitations": [
|
||||
"Artifact identity is unavailable for this observation.",
|
||||
"The target operating system and architecture were not reported by this observation."
|
||||
],
|
||||
"cleanup_error": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"lldb": {
|
||||
"version": "lldb-2100.0.17.108",
|
||||
"cases": [
|
||||
{
|
||||
"output_bytes": 2,
|
||||
"target_sha256": "819c94d6cdf0aff72ff5cea9769a7caf900d223af23ef11d66a13baa8baf71aa",
|
||||
"outcome": "exited",
|
||||
"events": 1,
|
||||
"stdout": {
|
||||
"text": "re",
|
||||
"bytes": 20,
|
||||
"truncated": true,
|
||||
"complete": true
|
||||
},
|
||||
"stderr": {
|
||||
"text": "",
|
||||
"bytes": 0,
|
||||
"truncated": false,
|
||||
"complete": true
|
||||
},
|
||||
"terminated": true
|
||||
},
|
||||
{
|
||||
"output_bytes": 64,
|
||||
"target_sha256": "819c94d6cdf0aff72ff5cea9769a7caf900d223af23ef11d66a13baa8baf71aa",
|
||||
"outcome": "exited",
|
||||
"events": 1,
|
||||
"stdout": {
|
||||
"text": "rea_budget_probe_ok\n",
|
||||
"bytes": 20,
|
||||
"truncated": false,
|
||||
"complete": true
|
||||
},
|
||||
"stderr": {
|
||||
"text": "",
|
||||
"bytes": 0,
|
||||
"truncated": false,
|
||||
"complete": true
|
||||
},
|
||||
"terminated": true
|
||||
}
|
||||
],
|
||||
"cleanup": {
|
||||
"ok": true
|
||||
}
|
||||
},
|
||||
"working_controls": {
|
||||
"frida_existing_smoke": {
|
||||
"ok": true,
|
||||
"provider": "frida",
|
||||
"host_platform": "darwin",
|
||||
"target_mode": "local_spawn",
|
||||
"target_pid": 47641,
|
||||
"target_name": "node",
|
||||
"script_message_observed": true,
|
||||
"cleanup_error": null
|
||||
},
|
||||
"ghidra_java_compile": {
|
||||
"ghidra_version": "12.1.4",
|
||||
"javac": "javac 21.0.12.1",
|
||||
"class_path_jars": 195,
|
||||
"sources": [
|
||||
"bridge/ghidra/ReaGhidraAnalysisOptions.java",
|
||||
"bridge/ghidra/ReaGhidraApplySeeds.java",
|
||||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"bridge/ghidra/ReaGhidraBundleActivator.java",
|
||||
"bridge/ghidra/ReaGhidraExtensions.java",
|
||||
"bridge/ghidra/ReaGhidraMipsSoftFloat.java",
|
||||
"bridge/ghidra/ReaGhidraNoReturnFix.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareCom.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareDol.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareRaw.java",
|
||||
"bridge/ghidra/ReaGhidraRiscvCallingConvention.java"
|
||||
]
|
||||
}
|
||||
},
|
||||
"probe_source_sha256": {
|
||||
"frida-budget-probe.mjs": "43dd424e0750bc8f60468210d99caedaa1e0fa16148db3c2290a8ff593254ccb",
|
||||
"lldb-budget-probe.mjs": "f11e899698d6c219078b1e4a6b52d97bfd308ab9ee8fade6902cb2bdfd1826af"
|
||||
},
|
||||
"limitations": [
|
||||
"Each raised capture budget starts a new controlled process; discarded live observations are not recovered retroactively.",
|
||||
"Initial 4 KiB Frida marker assertion failed because a fixed 128 KiB metadata reserve leaves no payload allowance; tagged truncation is preserved.",
|
||||
"Initial 1-second LLDB observation elapsed before any event; a 10-second-window rerun isolated output retention.",
|
||||
"Successful observer receipts showed empty owned process groups; this is not a kernel process-family memory or performance qualification.",
|
||||
"Windows/Linux, full AgentEnv, remote Frida and public native-tool transport recovery are unverified."
|
||||
]
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"platform": "darwin",
|
||||
"architecture": "arm64",
|
||||
"node": "v24.22.1",
|
||||
"fixture": {
|
||||
"kind": "volatile-straight-line",
|
||||
"statements": 5400,
|
||||
"source_bytes": 297679
|
||||
},
|
||||
"target_sha256": "4004126020cc5517d75c47cacabc68422a12b3260ccd19c105f9a7df087c69d1",
|
||||
"source_sha256": "8bc279d5a4881ca87d1629fc26d73d191986d2a2fb014f77a8d711ea0eca9426",
|
||||
"compiler": {
|
||||
"command": "cc",
|
||||
"arguments": [
|
||||
"-dynamiclib",
|
||||
"-fPIC",
|
||||
"-O0",
|
||||
"/var/folders/xl/60j5sd4j6b99tn8q082l9t8r0000gn/T/rea-ghidra-decompile-timeout-c0NNHy/slow-decompile.c",
|
||||
"-o",
|
||||
"/var/folders/xl/60j5sd4j6b99tn8q082l9t8r0000gn/T/rea-ghidra-decompile-timeout-c0NNHy/slow-decompile.dylib"
|
||||
]
|
||||
},
|
||||
"provider": {
|
||||
"id": "ghidra",
|
||||
"name": "Ghidra",
|
||||
"version": "12.1.4"
|
||||
},
|
||||
"analysis_profile_digest": "43b27fd852aa4e8b9b08c60928f06e9961e04f17410d0246ca771c3d526248c4",
|
||||
"configured_jvm_max_heap": "1G",
|
||||
"elapsed_seconds": 16.066160042,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "operations_low",
|
||||
"available": true,
|
||||
"operations": 1,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 0,
|
||||
"parameter_uses": 1,
|
||||
"truncated": true,
|
||||
"limits": {
|
||||
"max_operations": 1,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": ["max_operations"],
|
||||
"omitted_operations_lower_bound": 1,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "operations_recovered",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 6,
|
||||
"parameter_uses": 1,
|
||||
"truncated": false,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": [],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "inputs_low",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 1,
|
||||
"def_use_edges": 4,
|
||||
"parameter_uses": 1,
|
||||
"truncated": true,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 1,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": ["max_inputs_per_operation"],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 5,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "inputs_recovered",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 6,
|
||||
"parameter_uses": 1,
|
||||
"truncated": false,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": [],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "edges_low",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 1,
|
||||
"parameter_uses": 1,
|
||||
"truncated": true,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 1
|
||||
},
|
||||
"limits_reached": ["max_edges"],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 5
|
||||
},
|
||||
{
|
||||
"stage": "edges_recovered",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 6,
|
||||
"parameter_uses": 1,
|
||||
"truncated": false,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": [],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "operations_lowered",
|
||||
"available": true,
|
||||
"operations": 2,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 1,
|
||||
"parameter_uses": 1,
|
||||
"truncated": true,
|
||||
"limits": {
|
||||
"max_operations": 2,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": ["max_operations"],
|
||||
"omitted_operations_lower_bound": 1,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "payload_1_mib",
|
||||
"seconds": 1.7718680000000004,
|
||||
"code": "execution_failure",
|
||||
"message": "Ghidra decompilation failed: Exception while decompiling 00000414: Response buffer size exceeded for: target-4004126020cc.dylib . The effective native decompiler payload allowance is 1 MiB. If the diagnostic reports payload exhaustion, in MCP raise ghidra.decompile_max_payload_mib with update_analysis_settings (integer MiB from 1 to 2047) and repeat the selected operation on this Program without restarting REA. For a one-shot CLI retry, set REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB to a larger MiB value in the environment that starts the next rea invocation, then repeat the selected operation; the previous invocation's Program is no longer active. Runtime null or an omitted launch seed restores the installed Ghidra default. This does not raise the decompile deadline, high-pcode projection budgets or MCP client limits. Only the decompiler failed for this procedure. The Ghidra session remains usable."
|
||||
},
|
||||
{
|
||||
"stage": "payload_64_mib_recovered",
|
||||
"seconds": 2.11943175,
|
||||
"pseudocode_bytes": 261131
|
||||
},
|
||||
{
|
||||
"stage": "deadline_partial_dossier",
|
||||
"seconds": 2.3652059170000013,
|
||||
"pseudocode": {
|
||||
"available": false,
|
||||
"reason": "Ghidra decompilation of 0x414 exceeded the 1-second configured decompilation limit (ghidra.decompile_timeout_seconds; launch seed REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS)"
|
||||
},
|
||||
"assembly_lines": 49912,
|
||||
"basic_blocks": 1,
|
||||
"cache_policy": "live"
|
||||
},
|
||||
{
|
||||
"stage": "deadline_pseudocode",
|
||||
"seconds": 1.2178966249999994,
|
||||
"code": "provider_timeout",
|
||||
"message": "Ghidra decompilation of 0x414 exceeded the 1-second configured decompilation limit (ghidra.decompile_timeout_seconds; launch seed REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS). Only this procedure's decompilation was stopped. The Ghidra session remains usable.",
|
||||
"limit": {
|
||||
"setting": "ghidra.decompile_timeout_seconds",
|
||||
"effective_value": 1,
|
||||
"unit": "seconds",
|
||||
"minimum": 1,
|
||||
"maximum": 2147483,
|
||||
"applies_at": "next_operation",
|
||||
"agent_editable": true,
|
||||
"scope": "rea-runtime",
|
||||
"rea_restart_required": false
|
||||
},
|
||||
"remediation": {
|
||||
"action": "Use analyze_function (CLI: `rea function`) for listing facts. Call update_analysis_settings with setting ghidra.decompile_timeout_seconds and action set to raise the per-function deadline for the next operation on this Program. For CLI or future REA processes, set REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS. Set the MCP tool-call deadline to cover startup and the full call; earlier cancellation closes the session. `rea setup` estimates deadlines for new Codex/Kimi registrations and preserves existing positive values; edit existing deadlines yourself."
|
||||
}
|
||||
},
|
||||
{
|
||||
"stage": "deadline_recovered_payload_null",
|
||||
"seconds": 2.0052460419999987,
|
||||
"selected_timeout_seconds": 20,
|
||||
"selected_payload_mib": null,
|
||||
"pseudocode_bytes": 261131
|
||||
}
|
||||
],
|
||||
"initial_session_run_id": "c03f277d-d679-4e85-965b-690474ec4e9c",
|
||||
"final_session_run_id": "c03f277d-d679-4e85-965b-690474ec4e9c",
|
||||
"same_session_next_decompile": "succeeded",
|
||||
"verifier_run": {
|
||||
"run_id": "7fea6f67-fb82-4c6a-b129-8bbe3de8b5b2",
|
||||
"verifier_pid": 52515,
|
||||
"parent_pid": 2850,
|
||||
"process_lineage": {
|
||||
"status": "verified",
|
||||
"observed_at": "2026-11-09T16:50:38.401Z",
|
||||
"launcher_pid": 52515,
|
||||
"launcher_parent_pid": 2850,
|
||||
"process_group_id": 52515,
|
||||
"descendants": []
|
||||
}
|
||||
},
|
||||
"scope": "Real Ghidra analysis of generated Mach-O/ELF through authenticated loopback: independent value-flow budgets, lowered admission, native payload exhaustion and recovery, natural decompile timeout with listing-backed partial facts, and deadline recovery on the same Program. No target execution. Windows and public CLI/MCP transport recovery are unverified. Timings are observations, not performance comparisons; CPU, peak process-family memory and agent tokens are unmeasured.",
|
||||
"cleanup": "complete"
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
{
|
||||
"integration_base": "8dfc47b107afc0cf4b53322a10b94c666fe9e18b",
|
||||
"tested_feature_head": "329df8b331ece4201e7ea0849643c4481611ac86",
|
||||
"qualification": "Post-main-integration correctness run; not a performance comparison.",
|
||||
"receipt": {
|
||||
"status": "passed",
|
||||
"platform": "darwin",
|
||||
"architecture": "arm64",
|
||||
"node": "v24.22.1",
|
||||
"fixture": {
|
||||
"kind": "volatile-straight-line",
|
||||
"statements": 5400,
|
||||
"source_bytes": 297679
|
||||
},
|
||||
"target_sha256": "0eabbda46fcb6bbb33a84df09f8a2f1b2e4621aa5c7b8c7216921188e3b55ad8",
|
||||
"source_sha256": "8bc279d5a4881ca87d1629fc26d73d191986d2a2fb014f77a8d711ea0eca9426",
|
||||
"compiler": {
|
||||
"command": "cc",
|
||||
"arguments": [
|
||||
"-dynamiclib",
|
||||
"-fPIC",
|
||||
"-O0",
|
||||
"/var/folders/xl/60j5sd4j6b99tn8q082l9t8r0000gn/T/rea-ghidra-decompile-timeout-8ppfRJ/slow-decompile.c",
|
||||
"-o",
|
||||
"/var/folders/xl/60j5sd4j6b99tn8q082l9t8r0000gn/T/rea-ghidra-decompile-timeout-8ppfRJ/slow-decompile.dylib"
|
||||
]
|
||||
},
|
||||
"provider": {
|
||||
"id": "ghidra",
|
||||
"name": "Ghidra",
|
||||
"version": "12.1.4"
|
||||
},
|
||||
"analysis_profile_digest": "a70b06ba1d73ceb5b6e7ee2d9f7587583c17e076134ebb96f0169f6a8b95956a",
|
||||
"configured_jvm_max_heap": "1G",
|
||||
"elapsed_seconds": 16.838551541999998,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "operations_low",
|
||||
"available": true,
|
||||
"operations": 1,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 0,
|
||||
"parameter_uses": 1,
|
||||
"truncated": true,
|
||||
"limits": {
|
||||
"max_operations": 1,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": ["max_operations"],
|
||||
"omitted_operations_lower_bound": 1,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "operations_recovered",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 6,
|
||||
"parameter_uses": 1,
|
||||
"truncated": false,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": [],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "inputs_low",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 1,
|
||||
"def_use_edges": 4,
|
||||
"parameter_uses": 1,
|
||||
"truncated": true,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 1,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": ["max_inputs_per_operation"],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 5,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "inputs_recovered",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 6,
|
||||
"parameter_uses": 1,
|
||||
"truncated": false,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": [],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "edges_low",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 1,
|
||||
"parameter_uses": 1,
|
||||
"truncated": true,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 1
|
||||
},
|
||||
"limits_reached": ["max_edges"],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 5
|
||||
},
|
||||
{
|
||||
"stage": "edges_recovered",
|
||||
"available": true,
|
||||
"operations": 8,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 6,
|
||||
"parameter_uses": 1,
|
||||
"truncated": false,
|
||||
"limits": {
|
||||
"max_operations": 3000,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": [],
|
||||
"omitted_operations_lower_bound": 0,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "operations_lowered",
|
||||
"available": true,
|
||||
"operations": 2,
|
||||
"largest_input_count": 2,
|
||||
"def_use_edges": 1,
|
||||
"parameter_uses": 1,
|
||||
"truncated": true,
|
||||
"limits": {
|
||||
"max_operations": 2,
|
||||
"max_inputs_per_operation": 64,
|
||||
"max_edges": 12000
|
||||
},
|
||||
"limits_reached": ["max_operations"],
|
||||
"omitted_operations_lower_bound": 1,
|
||||
"known_omitted_inputs": 0,
|
||||
"known_omitted_edges": 0
|
||||
},
|
||||
{
|
||||
"stage": "payload_1_mib",
|
||||
"seconds": 1.6774305839999988,
|
||||
"code": "execution_failure",
|
||||
"message": "Ghidra decompilation failed: Exception while decompiling 00000414: Response buffer size exceeded for: target-0eabbda46fcb.dylib . The effective native decompiler payload allowance is 1 MiB. If the diagnostic reports payload exhaustion, in MCP raise ghidra.decompile_max_payload_mib with update_analysis_settings (integer MiB from 1 to 2047) and repeat the selected operation on this Program without restarting REA. For a one-shot CLI retry, set REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB to a larger MiB value in the environment that starts the next rea invocation, then repeat the selected operation; the previous invocation's Program is no longer active. Runtime null or an omitted launch seed restores the installed Ghidra default. This does not raise the decompile deadline, high-pcode projection budgets or MCP client limits. Only the decompiler failed for this procedure. The Ghidra session remains usable."
|
||||
},
|
||||
{
|
||||
"stage": "payload_64_mib_recovered",
|
||||
"seconds": 2.1426570829999982,
|
||||
"pseudocode_bytes": 261131
|
||||
},
|
||||
{
|
||||
"stage": "deadline_partial_dossier",
|
||||
"seconds": 2.4800411670000013,
|
||||
"pseudocode": {
|
||||
"available": false,
|
||||
"reason": "Ghidra decompilation of 0x414 exceeded the 1-second configured decompilation limit (ghidra.decompile_timeout_seconds; launch seed REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS)"
|
||||
},
|
||||
"assembly_lines": 49912,
|
||||
"basic_blocks": 1,
|
||||
"cache_policy": "live"
|
||||
},
|
||||
{
|
||||
"stage": "deadline_pseudocode",
|
||||
"seconds": 1.2235721249999987,
|
||||
"code": "provider_timeout",
|
||||
"message": "Ghidra decompilation of 0x414 exceeded the 1-second configured decompilation limit (ghidra.decompile_timeout_seconds; launch seed REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS). Only this procedure's decompilation was stopped. The Ghidra session remains usable.",
|
||||
"limit": {
|
||||
"setting": "ghidra.decompile_timeout_seconds",
|
||||
"effective_value": 1,
|
||||
"unit": "seconds",
|
||||
"minimum": 1,
|
||||
"maximum": 2147483,
|
||||
"applies_at": "next_operation",
|
||||
"agent_editable": true,
|
||||
"scope": "rea-runtime",
|
||||
"rea_restart_required": false
|
||||
},
|
||||
"remediation": {
|
||||
"action": "Use analyze_function (CLI: `rea function`) for listing facts. Call update_analysis_settings with setting ghidra.decompile_timeout_seconds and action set to raise the per-function deadline for the next operation on this Program. For CLI or future REA processes, set REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS. Set the MCP tool-call deadline to cover startup and the full call; earlier cancellation closes the session. `rea setup` estimates deadlines for new Codex/Kimi registrations and preserves existing positive values; edit existing deadlines yourself."
|
||||
}
|
||||
},
|
||||
{
|
||||
"stage": "deadline_recovered_payload_null",
|
||||
"seconds": 2.361492542,
|
||||
"selected_timeout_seconds": 20,
|
||||
"selected_payload_mib": null,
|
||||
"pseudocode_bytes": 261131
|
||||
}
|
||||
],
|
||||
"initial_session_run_id": "89aac416-1a46-4eeb-9ddf-9652ba17262b",
|
||||
"final_session_run_id": "89aac416-1a46-4eeb-9ddf-9652ba17262b",
|
||||
"same_session_next_decompile": "succeeded",
|
||||
"verifier_run": {
|
||||
"run_id": "1c4699a5-d090-4cad-8271-961cdd41d23e",
|
||||
"verifier_pid": 9490,
|
||||
"parent_pid": 2850,
|
||||
"process_lineage": {
|
||||
"status": "verified",
|
||||
"observed_at": "2026-11-09T21:55:25.629Z",
|
||||
"launcher_pid": 9490,
|
||||
"launcher_parent_pid": 2850,
|
||||
"process_group_id": 9490,
|
||||
"descendants": []
|
||||
}
|
||||
},
|
||||
"scope": "Real Ghidra analysis of generated Mach-O/ELF through authenticated loopback: independent value-flow budgets, lowered admission, native payload exhaustion and recovery, natural decompile timeout with listing-backed partial facts, and deadline recovery on the same Program. No target execution. Windows and public CLI/MCP transport recovery are unverified. Timings are observations, not performance comparisons; CPU, peak process-family memory and agent tokens are unmeasured.",
|
||||
"cleanup": "complete"
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,11 @@
|
||||
|
||||
Historical report added in
|
||||
[commit f3ed1657f82188fb90914f745a08849537342a23](https://github.com/morluto/rea/commit/f3ed1657f82188fb90914f745a08849537342a23)
|
||||
on 2026-10-25. The exact source revisions used for the initial checks and
|
||||
measurements were not recorded here. Those results do not establish current
|
||||
verification. The initial checks describe the runtime-settings introduction;
|
||||
dated follow-ups below preserve their own receipts and coverage limitations.
|
||||
Rebasing the implementation does not requalify a later runtime against these
|
||||
historical measurements.
|
||||
on 2026-10-25. The exact source revisions used for the checks and measurements
|
||||
below were not recorded here. This record does not establish current verification.
|
||||
|
||||
The initial verification used macOS with Node 24.22.1, Java 21.0.12.1 and
|
||||
Ghidra 12.1.4. That implementation added two MCP tools and 11 runtime defaults
|
||||
without a new dependency.
|
||||
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
|
||||
|
||||
@@ -91,101 +86,3 @@ 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.
|
||||
|
||||
## Ghidra budget recovery — 2026-11-09
|
||||
|
||||
The existing
|
||||
[`verify-real-ghidra-decompile-timeout.mjs`](https://github.com/morluto/rea/blob/main/scripts/verify-real-ghidra-decompile-timeout.mjs)
|
||||
now covers p-code projection budgets and native payload admission in the same
|
||||
real Program as deadline recovery. It replaces the branch fixture in that lane
|
||||
with 5,400 straight-line volatile statements. A preliminary 1,800-statement
|
||||
calibration completed under one second, so it did not reproduce deadline
|
||||
exhaustion; this was fixture calibration, not a failed provider result.
|
||||
|
||||
The retained lane ran once on macOS arm64 with Node 24.22.1, Ghidra 12.1.4 and
|
||||
Homebrew OpenJDK 21.0.12.1. It compiled its own Mach-O dylib using
|
||||
`cc -dynamiclib -fPIC -O0`; the target was never executed. The lane requested
|
||||
a 1 GiB JVM heap, bounded compiler execution to 30 seconds, native recovery
|
||||
decompiles to 20 seconds, and the overall run to 180 seconds. The JVM request
|
||||
does not bound the separate native decompiler's memory.
|
||||
|
||||
Reproduce after the runtime build with an existing Ghidra/JDK installation:
|
||||
|
||||
```sh
|
||||
GHIDRA_INSTALL_DIR=/path/to/ghidra_12.1.4_PUBLIC \
|
||||
JAVA_HOME=/path/to/jdk-21 \
|
||||
node scripts/verify-real-ghidra-decompile-timeout.mjs
|
||||
```
|
||||
|
||||
`REA_DECOMPILE_TIMEOUT_STATEMENTS` selects another source-bounded fixture size
|
||||
when calibrating a different host. The verifier refuses a scenario that does
|
||||
not naturally exhaust the selected allowance instead of claiming a reproduced
|
||||
timeout or payload failure.
|
||||
|
||||
The [compact receipt](https://github.com/morluto/rea/blob/main/docs/verification/runtime-analysis-settings-ghidra-2026-11-09.json) retains
|
||||
the source/target hashes, profile digest, exact setting facts, native failure
|
||||
diagnostics, timings and cleanup lineage. Observed results were:
|
||||
|
||||
| Selected control | Observed failure or omission | Recovery in the same Program |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| P-code operations: 1 | One operation retained, omitted-operation lower bound 1 | Raising to 3000 returned eight operations without truncation; lowering to 2 subsequently returned two |
|
||||
| Inputs per operation: 1 | Five known inputs omitted | Restoring 64 returned complete input coverage for the selected function |
|
||||
| Def-use edges: 1 | One edge retained, five omitted | Restoring 12000 returned six edges without truncation |
|
||||
| Native payload: 1 MiB | Ghidra reported `Response buffer size exceeded` | Raising to 64 MiB returned 261131 bytes of pseudocode |
|
||||
| Native deadline: 1 second | The dossier retained 49912 assembly lines and one basic block with unavailable decompiler facets; direct pseudocode returned typed `provider_timeout` | Raising to 20 seconds and selecting payload `null` returned 261131 bytes of pseudocode; the next small function also decompiled |
|
||||
|
||||
The complete lane took 16.07 seconds. Initial and final authenticated session
|
||||
run IDs both equal `c03f277d-d679-4e85-965b-690474ec4e9c`; the target SHA-256
|
||||
remained `4004126020cc5517d75c47cacabc68422a12b3260ccd19c105f9a7df087c69d1`.
|
||||
Provider close succeeded, token-verified lineage reported no live descendants,
|
||||
and the fixture workspace was removed. Ghidra may replace its native decompiler
|
||||
after a timeout; this check establishes Program/session continuity, not native
|
||||
child-process continuity.
|
||||
|
||||
This lane uses the real provider and authenticated loopback bridge with the
|
||||
shared runtime setting store. It does not exercise public CLI or MCP transport
|
||||
recovery, Windows, another Ghidra release, or every parameter-use edge boundary.
|
||||
The successful payload-null call does not measure the installed default's
|
||||
effective ceiling. Timings are observations, not a before/after performance
|
||||
comparison. CPU time, peak process-family memory and agent token usage were not
|
||||
measured in the retained run.
|
||||
|
||||
## Native capture qualification — 2026-11-09
|
||||
|
||||
The installed Frida Node SDK and system LLDB also exercised the new capture
|
||||
controls through their real application service/provider boundaries on macOS
|
||||
arm64. The [compact receipt](https://github.com/morluto/rea/blob/main/docs/verification/native-capture-budget-qualification-2026-11-09.json)
|
||||
records the selected/effective policies, observations and cleanup.
|
||||
|
||||
Frida's existing local-spawn verifier observed its marker and reported no cleanup
|
||||
error. Additional controlled runs selected session allowances of 1 byte, 4 KiB
|
||||
and 256 KiB. They respectively retained no event, a tagged payload-truncated
|
||||
event, and the complete marker. The 4 KiB case exposes conservative accounting:
|
||||
the existing fixed 128 KiB metadata reservation leaves no payload allowance.
|
||||
This is explicit truncation, not evidence that the nominal 4 KiB is filled with
|
||||
retained bytes. Its first marker assertion failed for that explained reason;
|
||||
the subsequent larger-budget experiment does not recover discarded observations.
|
||||
|
||||
The production LLDB tracer ran a newly compiled arm64 C fixture with one known
|
||||
function entry and a 20-byte stdout line. With `output_bytes: 2`, it retained
|
||||
`re`, reported 20 drained bytes and truncation. A new run of the same unchanged
|
||||
binary with `output_bytes: 64` returned the complete line, one expected event and
|
||||
normal exit. Both targets were confirmed terminated and tracer cleanup succeeded.
|
||||
An initial one-second observation elapsed during startup before any event; the
|
||||
ten-second-window rerun isolated output retention. No caller target was run.
|
||||
|
||||
These checks establish real local producer behavior and cleanup, not public
|
||||
MCP/CLI recovery, full AgentEnv execution, remote Frida, Windows/Linux behavior,
|
||||
or optimal default sizes. Group cleanup observations are not whole-family peak
|
||||
memory measurements. The live Ghidra lane above separately establishes same-Program
|
||||
recovery for read-only static analysis.
|
||||
|
||||
## Main integration qualification — 2026-11-09
|
||||
|
||||
After integration onto main `8dfc47b107afc0cf4b53322a10b94c666fe9e18b`, the
|
||||
retained Ghidra lane ran again against the integrated feature code. Its
|
||||
[integration receipt](https://github.com/morluto/rea/blob/main/docs/verification/runtime-analysis-settings-ghidra-integration-2026-11-09.json)
|
||||
records the new source/target identities and observations. Payload and deadline
|
||||
recovery again used one Program with unchanged target bytes, and owned cleanup
|
||||
completed. This is a fresh correctness qualification, not a new performance
|
||||
comparison or a replacement for the historical measurements above.
|
||||
@@ -1,206 +0,0 @@
|
||||
# Source-location delivery qualification — 2026-11-09
|
||||
|
||||
Opt-in summary delivery substantially reduces the measured main-map task's
|
||||
serialized transcript and its median elapsed time, CPU and sampled memory. The
|
||||
small Theme control exposes a tradeoff: summary adds a call and becomes more
|
||||
expensive when a client must first load the complete `inspect_analysis_view`
|
||||
definition. Complete delivery remains the compatibility default. These results
|
||||
support explicit summary selection for large point answers or already available
|
||||
views; they do not establish a uniform whole-task token or latency improvement.
|
||||
|
||||
The [product guide](../guides/web/web-source-location.md#compact-point-delivery-and-retained-source-text)
|
||||
explains summary, matches pages, explicit UTF-16 source text selection and retained
|
||||
complete Evidence. This record measures the public compiled stdio MCP workflow.
|
||||
CLI publication performance is unmeasured here.
|
||||
|
||||
## Task, oracle and execution
|
||||
|
||||
The task returns every alternative at one generated point, preserving source/map
|
||||
identity and original source positions, plus a 512 UTF-16-code-unit snippet around
|
||||
each mapped point. Inputs are the pinned public Actual Budget Theme, main
|
||||
`index.CMrE7aQb`, and `DateSelect.BVYaCNUe` maps and generated scripts in the existing
|
||||
[artifact manifest](https://github.com/morluto/rea/blob/main/docs/verification/source-map-budgets-2026-11-09/artifacts.json). Acquisition used
|
||||
verified cached public bytes and the existing acquisition helper's explicitly
|
||||
incomplete analyst-manifest construction. No application or browser was executed.
|
||||
The independent [VLQ oracle](https://github.com/morluto/rea/blob/main/docs/verification/source-map-budgets-2026-11-09/oracle.py) supplies the
|
||||
expected source declarations, positions, alternatives and content digests.
|
||||
|
||||
Both variants use the same compiled runtime, files and portable driver:
|
||||
|
||||
- **Complete:** request complete Evidence. Theme and main return it inline; the
|
||||
driver selects the required snippet locally from the returned source content.
|
||||
DateSelect exceeds the default 10 MiB transport allowance, then follows its
|
||||
retained matches continuation and requests the selected source text: three tool
|
||||
calls, including the failed delivery.
|
||||
- **Summary:** request `detail: summary`, then explicitly request the selected
|
||||
source-text page: two tool calls. Match paging remains part of the driver when
|
||||
required. All three selected real points have one alternative.
|
||||
|
||||
This compares current **complete delivery with bounded retained-view recovery**
|
||||
against opt-in summary. It does not compare against the former export-only fallback.
|
||||
The measurement includes driver artifact hashing/reference parsing, initialization,
|
||||
full catalog discovery, trace, necessary continuation/text calls, answer validation
|
||||
and shutdown. Export is not needed to answer this task and was not included.
|
||||
|
||||
Three fresh-process runs per case/variant completed. Trial two reversed variant
|
||||
order; OS caches were not flushed. All 18 runs exited zero, with empty owned groups
|
||||
and no resource stop. Aggregation independently verifies all snippets and pinned
|
||||
map/generated-script/content hashes, identical answers and parent Evidence IDs
|
||||
across variants/trials, and exact raw-map SHA-256 whenever the initial complete
|
||||
response includes raw text. Runtime execution and deployment authenticity remain
|
||||
unknown.
|
||||
|
||||
## Serialized token estimates
|
||||
|
||||
Counts use tiktoken 0.12.0 `cl100k_base` on compact Unicode JSON-RPC request/reply
|
||||
lines, including initialization, the initialized notification, `tools/list`, failed
|
||||
calls, continuations and selected text. Replies include both MCP text and structured
|
||||
content. These are serialized-transcript estimates, **not actual model usage or
|
||||
billing**; client presentation can omit or transform those representations.
|
||||
|
||||
The observed full catalog uses compact input schemas and full output schemas.
|
||||
`tools/list` returns 4,487,850 wire bytes. Initialization, notification and catalog
|
||||
account for **1,145,053 estimated tokens** in every run. Do not add selected
|
||||
contracts again to the catalog-included totals. Post-discovery counts exclude only
|
||||
that discovery prefix, so they include all task retries and subsequent calls.
|
||||
|
||||
| Map / delivery | Tool calls | Catalog-included task tokens | Post-discovery tokens |
|
||||
| ----------------------------------- | ---------: | ---------------------------: | --------------------: |
|
||||
| Theme / complete | 1 | 1,196,400 | 51,347 |
|
||||
| Theme / summary | 2 | 1,154,878 | 9,825 |
|
||||
| Main / complete | 1 | 4,354,894 | 3,209,841 |
|
||||
| Main / summary | 2 | 1,154,851 | 9,798 |
|
||||
| DateSelect / complete with recovery | 3 | 1,155,932 | 10,879 |
|
||||
| DateSelect / summary | 2 | 1,154,974 | 9,921 |
|
||||
|
||||
Selected complete wired definitions cost **8,408 tokens** for
|
||||
`trace_web_source_location` and **180,576** for `inspect_analysis_view`. When those
|
||||
definitions are uncached and a client loads only the tools it uses, Theme complete
|
||||
costs 8,408 + 51,347 = **59,755**, whereas summary costs
|
||||
8,408 + 180,576 + 9,825 = **198,809**. Summary loses in this first-use accounting
|
||||
scenario. The measured full-catalog scenario already loads the view definition,
|
||||
so its Theme total instead falls from 1,196,400 to 1,154,878. Main still improves
|
||||
with selected complete definitions: 3,218,249 to 198,782.
|
||||
|
||||
As a separate **hypothetical client projection**, retaining only each definition's
|
||||
`{name, description, inputSchema}` costs 548 and 3,533 tokens respectively. Adding
|
||||
those selected projected definitions to the same post-discovery transcripts gives:
|
||||
|
||||
| Case | Complete → summary, selected complete wired definitions | Complete → summary, hypothetical input-only definitions |
|
||||
| ---------- | ------------------------------------------------------: | ------------------------------------------------------: |
|
||||
| Theme | 59,755 → 198,809 | 51,895 → 13,906 |
|
||||
| Main | 3,218,249 → 198,782 | 3,210,389 → 13,879 |
|
||||
| DateSelect | 199,863 → 198,905 | 14,960 → 14,002 |
|
||||
|
||||
This projection is an offline accounting calculation, not an observed client/model
|
||||
transformation or an alternate measured discovery workflow. Cached or deferred tool
|
||||
loading, output-schema profiles and model-facing reply presentation can change the
|
||||
costs. No actual model run or billing comparison was performed.
|
||||
|
||||
## Elapsed time, CPU and sampled memory
|
||||
|
||||
Values are medians of three runs on macOS 26.3 arm64 with Node v24.22.1. The small
|
||||
control has a slight elapsed/CPU increase; DateSelect memory is similar across
|
||||
variants. Three runs do not establish general performance guarantees.
|
||||
|
||||
| Map / delivery | Wall seconds | CPU seconds, waited subtree | Sampled group peak MiB | Sampled tree peak MiB |
|
||||
| ----------------------------------- | -----------: | --------------------------: | ---------------------: | --------------------: |
|
||||
| Theme / complete | 0.972 | 1.128 | 498.0 | 555.8 |
|
||||
| Theme / summary | 0.985 | 1.143 | 483.9 | 542.1 |
|
||||
| Main / complete | 1.625 | 1.815 | 776.5 | 776.5 |
|
||||
| Main / summary | 1.134 | 1.294 | 549.7 | 661.4 |
|
||||
| DateSelect / complete with recovery | 1.199 | 1.351 | 580.6 | 695.6 |
|
||||
| DateSelect / summary | 1.150 | 1.326 | 588.6 | 694.2 |
|
||||
|
||||
The observer samples simultaneous `ps` RSS sums at nominal 20 ms intervals, plus
|
||||
sampling work. **Tree RSS includes
|
||||
codec descendants outside the root PGID while their PPID ancestry remains linked.**
|
||||
Short peaks, exited/reparented/orphaned descendants can be missed; shared pages can
|
||||
be counted repeatedly. These are sampled sums, not kernel peak or private-memory
|
||||
measurements. CPU is OS `wait4` user + system for the waited subtree, excluding
|
||||
sampler CPU and unwaited/orphaned descendants. Wall time includes observer overhead.
|
||||
|
||||
The 90-second deadline, 1536 MiB observed memory early stop and cleanup signals apply
|
||||
only to the **owned process group**. Observational tree membership grants no signal
|
||||
authority. The group guard does not enforce a tree-wide or kernel memory hard cap;
|
||||
empty-group cleanup does not prove that an unobserved orphan cannot exist.
|
||||
|
||||
A preliminary 18-run completed-task matrix used group-only RSS and a temporary
|
||||
driver that decoded UTF-8 separately per incoming chunk. It is superseded: codec
|
||||
processes can change PGID, and chunk boundaries can split UTF-8. Its local method
|
||||
receipts are preserved; **none of its result statistics appear in these tables**.
|
||||
All final runs use the retained driver's UTF-8 stream decoder, raw-map digest
|
||||
check and the observer's additional read-only tree RSS measurement. The earlier
|
||||
[map-admission calibration](source-map-budget-calibration.md) is a separate study.
|
||||
|
||||
## Retained evidence and reproduction
|
||||
|
||||
The compact [measurement record](https://github.com/morluto/rea/blob/main/scripts/verify/analysis/source-location-delivery/measurements.json)
|
||||
contains 18 numeric records, per-call bytes/token estimates, oracle/answer checks,
|
||||
full SHA-256 pins and the compiled-tree digest. Large replies, artifacts and model
|
||||
transcripts are not committed. Runtime pins include `dist/mcp/main.js`
|
||||
(`9bc4c070f63adc9963f276da20b36ffbbcc77b4079585cadf90d792dd2cc04dd`)
|
||||
and the 2,921-file compiled `dist` tree
|
||||
(`a8a510499dabc07b3cdf6b164d02e55f2fc6917b696dcc4d23449510e91e80fa`).
|
||||
The recorded CLI digest is separately bound to `dist/cli.js`. These pins identify
|
||||
the measured build and selected source-location owners. A later unrelated generated
|
||||
desktop SQLite diagnostic change and rebuild are outside this snapshot; later
|
||||
whole-tree hashes do not replace the measured one.
|
||||
|
||||
The [portable driver](https://github.com/morluto/rea/blob/main/scripts/verify/analysis/source-location-delivery/task.mjs)
|
||||
has SHA-256 `b21ec9d5315682337eb55a4ad208e2145a7397d8d567cdb6653facd3f0bec90e`.
|
||||
The [observer](https://github.com/morluto/rea/blob/main/scripts/verify/analysis/budget-calibration-run.py)
|
||||
has SHA-256 `c18a06c44715c97ec9cbc76248dfbcd862d0e5ca3cd0c159d868bc7fa1fa9e9f`.
|
||||
Use the repository's built runtime and pinned Node. Acquisition refuses changed
|
||||
publisher bytes; the existing acquisition helper creates `artifacts.json` and
|
||||
analyst manifests under the selected artifact directory.
|
||||
|
||||
Create a fresh receipt directory and its `runtime.json` before measuring. Keep
|
||||
source files and the compiled runtime unchanged throughout all 18 runs and
|
||||
aggregation; do not build or edit between them. The compiled-tree hash collected
|
||||
afterward identifies the measured runtime under that condition. Aggregate before
|
||||
any subsequent rebuild.
|
||||
|
||||
```sh
|
||||
python3 docs/verification/source-map-budgets-2026-11-09/acquire.py \
|
||||
docs/verification/source-map-budgets-2026-11-09/artifacts.json /tmp/rea-point-artifacts
|
||||
python3 - <<'PY'
|
||||
import hashlib, json
|
||||
from pathlib import Path
|
||||
receipts = Path('/tmp/rea-source-location-measurements')
|
||||
receipts.mkdir()
|
||||
paths = {
|
||||
'driver_sha256': Path('scripts/verify/analysis/source-location-delivery/task.mjs'),
|
||||
'observer_sha256': Path('scripts/verify/analysis/budget-calibration-run.py'),
|
||||
'cli_sha256': Path('dist/cli.js'),
|
||||
}
|
||||
metadata = {key: hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
for key, path in paths.items() if key != 'cli_sha256'}
|
||||
cli = paths['cli_sha256']
|
||||
metadata['cli_sha256'] = hashlib.sha256(cli.read_bytes()).hexdigest() if cli.is_file() else None
|
||||
(receipts / 'runtime.json').write_text(json.dumps(metadata, indent=2) + '\n')
|
||||
PY
|
||||
python3 scripts/verify/analysis/budget-calibration-run.py \
|
||||
--output /tmp/rea-source-location-measurements/r1-index-summary.metrics.json --cwd "$PWD" \
|
||||
--timeout-seconds 90 --memory-mib 1536 -- \
|
||||
node scripts/verify/analysis/source-location-delivery/task.mjs "$PWD" \
|
||||
'{"artifactDir":"/tmp/rea-point-artifacts","select":"index.","detail":"summary","resultPath":"/tmp/rea-source-location-measurements/r1-index-summary.replies.json"}'
|
||||
```
|
||||
|
||||
Run Theme, main and DateSelect with both detail modes serially, three times, reversing
|
||||
variant order for trial two. Use unique receipt/result paths named
|
||||
`r1-index-summary.metrics.json` / `.replies.json` (and the corresponding case/trial
|
||||
names), preserving the runtime metadata. After measurements, the reusable
|
||||
[aggregator](https://github.com/morluto/rea/blob/main/scripts/verify/analysis/source-location-delivery/aggregate.py)
|
||||
requires Python with tiktoken 0.12.0 and regenerates the compact record:
|
||||
|
||||
```sh
|
||||
python3 scripts/verify/analysis/source-location-delivery/aggregate.py \
|
||||
/tmp/rea-source-location-measurements --repo-root "$PWD" --output /tmp/point-measurements.json
|
||||
```
|
||||
|
||||
The aggregator hashes the compiled tree after performance runs, verifies every
|
||||
answer and successful complete raw-map response, and excludes large payloads from
|
||||
its output. Analysis still decodes and retains complete Evidence; this study does
|
||||
not qualify arbitrary larger maps, batch source-map fetching, browser execution or
|
||||
disk-cold behavior.
|
||||
@@ -1,109 +0,0 @@
|
||||
# Source-map admission calibration — 2026-11-09
|
||||
|
||||
The local map default changes from 4 to **8 MiB** to admit the pinned Actual
|
||||
Budget main and DateSelect maps on the first call. Other source-map defaults
|
||||
remain unchanged. This is a compatibility improvement for the measured artifacts,
|
||||
not an established latency, memory or context optimization or a universal optimum.
|
||||
|
||||
[Issue #8978](https://github.com/morluto/rea/issues/8978) identifies Actual Budget
|
||||
v26.11.0. We acquired eight public shipped maps from
|
||||
[the public demo](https://demo.actualbudget.org/) without running the application.
|
||||
These eight preload-selected maps total 18,319,814 bytes and 834,659 structural
|
||||
records; they are not the complete eleven-map scope of the issue. Pinned URLs,
|
||||
SHA-256 values, counts and independently decoded lookup expectations are in
|
||||
[artifacts.json](https://github.com/morluto/rea/blob/main/docs/verification/source-map-budgets-2026-11-09/artifacts.json).
|
||||
|
||||
The completed point task reads and verifies a selected generated script, analyst
|
||||
manifest and map, runs the actual owned source codec, checks every equal-position
|
||||
match and embedded-source digest, and creates and serializes complete Evidence.
|
||||
The acquisition manifest is constructed and explicitly incomplete: it establishes
|
||||
no browser capture, runtime execution or deployment authenticity. The independent
|
||||
[Python VLQ oracle](https://github.com/morluto/rea/blob/main/docs/verification/source-map-budgets-2026-11-09/oracle.py) derives mapping positions
|
||||
and counts directly from artifact bytes, separately from REA's decoder.
|
||||
|
||||
## Admission-only measurements before compact delivery
|
||||
|
||||
Each cell below is the median of three fresh-process runs. A baseline call uses
|
||||
4 MiB admission and, when refused, retries the same complete map with 8 MiB. The
|
||||
candidate selects 8 MiB on its first call. Other policies are identical. All
|
||||
18 point runs exit successfully, verify the oracle and leave their process groups
|
||||
empty. The baseline source snapshot and individual measurements/digests are in
|
||||
[measurements.json](https://github.com/morluto/rea/blob/main/docs/verification/source-map-budgets-2026-11-09/measurements.json).
|
||||
|
||||
| Map / workflow | Calls | Estimated task tokens | Wall s | CPU s | Sampled host-group RSS MiB |
|
||||
| ---------------------------- | ----: | --------------------: | -----: | ----: | -------------------------: |
|
||||
| Theme / default | 1 | 25,103 | 0.368 | 0.359 | 98.6 |
|
||||
| Theme / 8 MiB | 1 | 25,126 | 0.367 | 0.391 | 99.3 |
|
||||
| Main / default + retry | 2 | 1,590,278 | 0.553 | 0.573 | 174.0 |
|
||||
| Main / 8 MiB | 1 | 1,589,746 | 0.539 | 0.559 | 181.5 |
|
||||
| DateSelect / default + retry | 2 | 2,232,370 | 0.582 | 0.586 | 205.0 |
|
||||
| DateSelect / 8 MiB | 1 | 2,231,835 | 0.599 | 0.617 | 192.4 |
|
||||
|
||||
Tokens are tiktoken 0.12.0 `cl100k_base` estimates of compact UTF-8 JSON arguments
|
||||
and complete replies, including failed calls and retries; they are not actual
|
||||
model usage. Catalog discovery was not performed. Catalog/tool schemas, JSON-RPC
|
||||
envelopes, network acquisition and actual model billing are unmeasured. Complete
|
||||
Evidence retains raw map text: eliminating a retry saves only about 532–535
|
||||
estimated tokens against replies exceeding 1.5–2.2 million tokens. A map admission
|
||||
cap is therefore not a suitable agent context budget. These measurements do not
|
||||
qualify a smaller public reply projection or a live browser investigation.
|
||||
Source inspection confirms that the production MCP handler records complete
|
||||
Evidence before response encoding. Overflow recovery names its retained
|
||||
reference and offers session export or complete CLI JSON; facts are not lost.
|
||||
At the time of this admission-only experiment, local tracing had no summary,
|
||||
paged matches view or native analysis-view continuation. This experiment did not
|
||||
exercise a large reply through a live MCP client. The subsequent
|
||||
[compact-delivery qualification](source-location-delivery.md) measures real
|
||||
public MCP summaries, continuations and source-text selection separately.
|
||||
|
||||
Node v24.22.1 ran on macOS 26.3 arm64. Workers/processes were fresh; OS caches were
|
||||
not flushed, and no cold-cache comparison is claimed. Wall time includes the
|
||||
observer. CPU is OS `wait4` user +
|
||||
system usage for the waited subtree; orphaned/unwaited descendants and sampler
|
||||
CPU are excluded. RSS is a simultaneous process-group sum sampled every 20 ms;
|
||||
short peaks and descendants leaving the group can be missed and shared pages can
|
||||
be counted repeatedly. In particular, the owned codec launches in a separate
|
||||
group, so its peak is unmeasured in this older table; this is not whole-family
|
||||
memory. The 1536 MiB observer threshold is an early-stop guard,
|
||||
not a kernel memory limit. Mixed timing/memory differences do not establish a
|
||||
performance improvement.
|
||||
|
||||
## Unsuccessful full-fetch sizing
|
||||
|
||||
An eight-map sizing attempt selected 128 MiB expanded output and 1,048,576
|
||||
records/segments. It generated a 123 MB full-Evidence file, but the observer's
|
||||
signal race failed while the sampled process group exceeded its 1536 MiB guard.
|
||||
No valid complete measurement receipt was produced; this is an unsuccessful,
|
||||
unmeasured sizing attempt, not evidence of a safe completed task or a calibrated
|
||||
fetch default. The observer was subsequently repaired. A small Theme fetch
|
||||
control completed, but one control does not justify raising collection defaults.
|
||||
Full-fetch output, records, heap and response-byte defaults stay conservative and
|
||||
configurable. Local tracing retains its existing 262,144-record budget; the
|
||||
largest measured individual map consumes 260,561 records, so broader map coverage
|
||||
remains unqualified.
|
||||
|
||||
## Reproduction
|
||||
|
||||
No downloaded artifacts or complete large replies are committed. Publisher URLs
|
||||
may change; acquisition refuses a digest mismatch. Use a separate baseline source
|
||||
snapshot identified in `measurements.json`, then select the same explicit policies.
|
||||
The retained driver uses production source through the pinned tsx loader and runs
|
||||
the real source-owned codec. Fetch mode replays pinned HTTP artifact bytes through
|
||||
the fetcher and includes schema/Evidence/serialization costs, while excluding
|
||||
browser capture and generated-script analysis.
|
||||
|
||||
```sh
|
||||
python3 docs/verification/source-map-budgets-2026-11-09/acquire.py \
|
||||
docs/verification/source-map-budgets-2026-11-09/artifacts.json /tmp/rea-map-calibration
|
||||
python3 docs/verification/source-map-budgets-2026-11-09/oracle.py \
|
||||
/tmp/rea-map-calibration/artifacts.json
|
||||
node --import ./node_modules/tsx/dist/loader.mjs \
|
||||
docs/verification/source-map-budgets-2026-11-09/workload.mjs "$PWD" \
|
||||
'{"artifactDir":"/tmp/rea-map-calibration","mode":"point","select":"index.","strategy":"recover","policy":{"map_bytes":8388608},"resultPath":"/tmp/map-replies.json"}'
|
||||
```
|
||||
|
||||
For candidate-first, omit `strategy`. Use `select: "theme."` or `"DateSelect."`
|
||||
for the other point cases. Wrap each invocation with
|
||||
[the process-group observer](https://github.com/morluto/rea/blob/main/scripts/verify/analysis/budget-calibration-run.py) for bounded process-group
|
||||
observation; keep runs serial. After the default change, reconstruct the baseline
|
||||
snapshot or explicitly select `map_bytes: 4194304` for the initial baseline call.
|
||||
@@ -1,44 +0,0 @@
|
||||
"""Acquire pinned public artifacts read-only and construct explicitly incomplete analyst manifests."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
manifest_path, destination = map(Path, sys.argv[1:])
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
manifest_bytes = manifest_path.read_bytes()
|
||||
manifest = json.loads(manifest_bytes)
|
||||
(destination / 'artifacts.json').write_bytes(manifest_bytes)
|
||||
for item in manifest:
|
||||
for artifact in [item, *([item['generated_script']] if 'generated_script' in item else [])]:
|
||||
with urllib.request.urlopen(artifact['url'], timeout=30) as response:
|
||||
data = response.read(16 * 1024 * 1024)
|
||||
assert len(data) == artifact['bytes']
|
||||
assert hashlib.sha256(data).hexdigest() == artifact['sha256'], artifact['url']
|
||||
(destination / artifact['path']).write_bytes(data)
|
||||
if 'generated_script' not in item:
|
||||
continue
|
||||
script = item['generated_script']
|
||||
(destination / 'files').mkdir(exist_ok=True)
|
||||
(destination / 'files' / script['path']).write_bytes((destination / script['path']).read_bytes())
|
||||
context = {
|
||||
'capture_path': str((destination / 'artifacts.json').resolve()),
|
||||
'capture_sha256': hashlib.sha256(manifest_bytes).hexdigest(),
|
||||
'capture_kind': 'browser-scenario',
|
||||
'source_evidence_id': None,
|
||||
'capture_completeness': {
|
||||
'status': 'incomplete', 'equality_eligible': False,
|
||||
'missing_sections': ['action', 'screenshot', 'dom', 'accessibility', 'url', 'history', 'storage', 'events', 'frames', 'workers', 'popups', 'websockets'],
|
||||
'truncated_sections': [],
|
||||
},
|
||||
'output_directory': str(destination.resolve()),
|
||||
'analysis_input': {'input_path': str(destination.resolve()), 'format': 'directory'},
|
||||
'limitations': ['Constructed analyst manifest over pinned public HTTP bytes. capture_path identifies the artifact manifest, not a browser capture. No capture, deployment authenticity or execution is asserted.'],
|
||||
'scripts': [{
|
||||
'url': script['url'],
|
||||
'source': {'kind': 'scenario-response', 'transaction_id': 'read-only-public-http', 'request_sequence': 1, 'response_sequence': 2, 'status': 200},
|
||||
'content': {'state': 'exported', 'relative_path': script['path'], 'layout': 'isolated', 'layout_reason': 'explicit-public-artifact-selection', 'sha256': script['sha256'], 'bytes': script['bytes'], 'media_type': 'text/javascript', 'redacted': False, 'representation': 'browser-decoded-response-bytes'},
|
||||
}],
|
||||
}
|
||||
(destination / (script['path'] + '.manifest.json')).write_text(json.dumps(context))
|
||||
@@ -1,252 +0,0 @@
|
||||
[
|
||||
{
|
||||
"url": "https://demo.actualbudget.org/static/js/index.CMrE7aQb.js.map",
|
||||
"path": "index.CMrE7aQb.js.map",
|
||||
"bytes": 4284329,
|
||||
"sha256": "f0de6886f6a49758f53c7f2de0b77fbff6d72c719a7c32ac23d13453ebe7b3e8",
|
||||
"sources": 790,
|
||||
"names": 1050,
|
||||
"generated_lines": 97,
|
||||
"headers": {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Etag": "\"e9cabe1eb63edd2868b94f9e315c2b15-ssl\""
|
||||
},
|
||||
"segment_count": 201344,
|
||||
"record_count": 204072,
|
||||
"point": {
|
||||
"line": 2,
|
||||
"column": 15579
|
||||
},
|
||||
"expected_matches": [
|
||||
{
|
||||
"source_index": 0,
|
||||
"source": "../../../../../node_modules/absurd-sql/dist/indexeddb-main-thread.js",
|
||||
"original_line": 10,
|
||||
"original_column": 0,
|
||||
"content_sha256": "d6bf964929e31ceeb301a174d85f4f58de4d4a73f65336488aac7b50fce0a1cd"
|
||||
}
|
||||
],
|
||||
"mapped_count": 201344,
|
||||
"generated_script": {
|
||||
"path": "index.CMrE7aQb.js",
|
||||
"url": "https://demo.actualbudget.org/static/js/index.CMrE7aQb.js",
|
||||
"bytes": 2078745,
|
||||
"sha256": "937f792d1849b69a6e0741234428b22b4884ec0157ea31dd6949dd1bacc46456"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "https://demo.actualbudget.org/static/js/theme.EvD-JhP6.chunk.js.map",
|
||||
"path": "theme.EvD-JhP6.chunk.js.map",
|
||||
"bytes": 49264,
|
||||
"sha256": "ceef8194eaca339e55f35cf2decf7ceffa121c7744ee1fffd976a07d39477b32",
|
||||
"sources": 7,
|
||||
"names": 0,
|
||||
"generated_lines": 1,
|
||||
"headers": {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Etag": "\"f9829244cb0a7c333e7bbe83b110c2ab-ssl\""
|
||||
},
|
||||
"segment_count": 2339,
|
||||
"record_count": 2355,
|
||||
"point": {
|
||||
"line": 1,
|
||||
"column": 1643
|
||||
},
|
||||
"expected_matches": [
|
||||
{
|
||||
"source_index": 0,
|
||||
"source": "../../../../../node_modules/react/cjs/react.production.js",
|
||||
"original_line": 12,
|
||||
"original_column": 0,
|
||||
"content_sha256": "6b28feb9dbe9781ab1e1b64dea7b29a7c70f03d2dc10eaa966363053d8a9b4e5"
|
||||
}
|
||||
],
|
||||
"mapped_count": 2339,
|
||||
"generated_script": {
|
||||
"path": "theme.EvD-JhP6.chunk.js",
|
||||
"url": "https://demo.actualbudget.org/static/js/theme.EvD-JhP6.chunk.js",
|
||||
"bytes": 29718,
|
||||
"sha256": "10ecf4c66ae103222a7c146ad9b945dc83e78b6c78ce05021296e3c2f799bd5b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "https://demo.actualbudget.org/static/js/months.CGmeIa7I.chunk.js.map",
|
||||
"path": "months.CGmeIa7I.chunk.js.map",
|
||||
"bytes": 1630058,
|
||||
"sha256": "f18a2a2f4d6360b944a8c57432e3f3898314f1cf39d69ba5d049233ab2f39e8d",
|
||||
"sources": 267,
|
||||
"names": 208,
|
||||
"generated_lines": 7,
|
||||
"headers": {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Etag": "\"8e5c8614ccacde6bd659f89916deba34-ssl\""
|
||||
},
|
||||
"segment_count": 65705,
|
||||
"record_count": 66455,
|
||||
"point": {
|
||||
"line": 1,
|
||||
"column": 230
|
||||
},
|
||||
"expected_matches": [
|
||||
{
|
||||
"source_index": 0,
|
||||
"source": "../../../src/build-shims.js",
|
||||
"original_line": 1,
|
||||
"original_column": 6,
|
||||
"content_sha256": "1039d33f95ace766ed5731b2775e957198f2c9ad102989ca09cbd297d94b2863"
|
||||
}
|
||||
],
|
||||
"mapped_count": 65705
|
||||
},
|
||||
{
|
||||
"url": "https://demo.actualbudget.org/static/js/DateSelect.BVYaCNUe.chunk.js.map",
|
||||
"path": "DateSelect.BVYaCNUe.chunk.js.map",
|
||||
"bytes": 5529840,
|
||||
"sha256": "e1939ef36214ff46263d3bdaeb99d60a46c75714a6b6220ed27c9be2d24d6cc8",
|
||||
"sources": 1001,
|
||||
"names": 3893,
|
||||
"generated_lines": 116,
|
||||
"headers": {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Etag": "\"407e8912b56411e3b2b9a55824a6bb7b-ssl\""
|
||||
},
|
||||
"segment_count": 254549,
|
||||
"record_count": 260561,
|
||||
"point": {
|
||||
"line": 1,
|
||||
"column": 3866
|
||||
},
|
||||
"expected_matches": [
|
||||
{
|
||||
"source_index": 0,
|
||||
"source": "../../../../../node_modules/date-fns/isSameWeek.js",
|
||||
"original_line": 40,
|
||||
"original_column": 0,
|
||||
"content_sha256": "436cf6fa9365c1cbf783a422c8da3a3b82e55edeac248f987cca22c0ba950902"
|
||||
}
|
||||
],
|
||||
"mapped_count": 254549,
|
||||
"generated_script": {
|
||||
"path": "DateSelect.BVYaCNUe.chunk.js",
|
||||
"url": "https://demo.actualbudget.org/static/js/DateSelect.BVYaCNUe.chunk.js",
|
||||
"bytes": 2191517,
|
||||
"sha256": "9e45139dd0b510ad7014188a5e9d11b307c2ac15dfdc761050388e76e94759bf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"url": "https://demo.actualbudget.org/static/js/useLocalPref.CMSDl-Rj.chunk.js.map",
|
||||
"path": "useLocalPref.CMSDl-Rj.chunk.js.map",
|
||||
"bytes": 443520,
|
||||
"sha256": "341c7d72797abdda122b935c0a5fc80718a79457e02d315fed2cf32f4bbd6f38",
|
||||
"sources": 32,
|
||||
"names": 9,
|
||||
"generated_lines": 1,
|
||||
"headers": {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Etag": "\"ffaaea8a6a08415032b4cba4f266624f-ssl\""
|
||||
},
|
||||
"segment_count": 8976,
|
||||
"record_count": 9051,
|
||||
"point": {
|
||||
"line": 1,
|
||||
"column": 352
|
||||
},
|
||||
"expected_matches": [
|
||||
{
|
||||
"source_index": 0,
|
||||
"source": "../../../../../node_modules/react-router/dist/production/lib/router/url.js",
|
||||
"original_line": 12,
|
||||
"original_column": 0,
|
||||
"content_sha256": "493e1c3475d74a528af0c1aaae7574e841fe21019fb9e801dc8fe60f212570c7"
|
||||
}
|
||||
],
|
||||
"mapped_count": 8976
|
||||
},
|
||||
{
|
||||
"url": "https://demo.actualbudget.org/static/js/Value.FdYRYOLz.chunk.js.map",
|
||||
"path": "Value.FdYRYOLz.chunk.js.map",
|
||||
"bytes": 4367424,
|
||||
"sha256": "8428453a8a2aa4366a719e37f6aeb93e75f70aea2e8713529ec6f898d4cddcfa",
|
||||
"sources": 538,
|
||||
"names": 717,
|
||||
"generated_lines": 52,
|
||||
"headers": {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Etag": "\"6adb2ff5f177a9236937e770097bbeef-ssl\""
|
||||
},
|
||||
"segment_count": 181768,
|
||||
"record_count": 183614,
|
||||
"point": {
|
||||
"line": 2,
|
||||
"column": 7624
|
||||
},
|
||||
"expected_matches": [
|
||||
{
|
||||
"source_index": 0,
|
||||
"source": "../../../../loot-core/src/shared/query.ts",
|
||||
"original_line": 22,
|
||||
"original_column": 13,
|
||||
"content_sha256": "e4b93dea21ab9d73af8eeffcee818935c00c9a10427549a643ac133be0dfc685"
|
||||
}
|
||||
],
|
||||
"mapped_count": 181768
|
||||
},
|
||||
{
|
||||
"url": "https://demo.actualbudget.org/static/js/columns.BR9M2Oqz.chunk.js.map",
|
||||
"path": "columns.BR9M2Oqz.chunk.js.map",
|
||||
"bytes": 1549581,
|
||||
"sha256": "39f0c008302908e9648f492f073570e76f03a978db25c7b527192e4936cc255f",
|
||||
"sources": 346,
|
||||
"names": 626,
|
||||
"generated_lines": 53,
|
||||
"headers": {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Etag": "\"c23d08378a207ac086f12075af6703b4-ssl\""
|
||||
},
|
||||
"segment_count": 83166,
|
||||
"record_count": 84538,
|
||||
"point": {
|
||||
"line": 1,
|
||||
"column": 843
|
||||
},
|
||||
"expected_matches": [
|
||||
{
|
||||
"source_index": 0,
|
||||
"source": "../../../../../node_modules/reselect/dist/reselect.mjs",
|
||||
"original_line": 63,
|
||||
"original_column": 0,
|
||||
"content_sha256": "ce215e8758151f87b03d651c8ebdd097a8b658ffbe56e0162e0154bd00b92f12"
|
||||
}
|
||||
],
|
||||
"mapped_count": 83166
|
||||
},
|
||||
{
|
||||
"url": "https://demo.actualbudget.org/static/js/TransactionEdit.C3L723rq.chunk.js.map",
|
||||
"path": "TransactionEdit.C3L723rq.chunk.js.map",
|
||||
"bytes": 465798,
|
||||
"sha256": "cf04088c6542a020fadc284264cb3cb52763de9f352b2e630fbe10278ceab4c8",
|
||||
"sources": 69,
|
||||
"names": 227,
|
||||
"generated_lines": 1,
|
||||
"headers": {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Etag": "\"35e992601467cfecec49b2eeb6925d92-ssl\""
|
||||
},
|
||||
"segment_count": 23646,
|
||||
"record_count": 24013,
|
||||
"point": {
|
||||
"line": 1,
|
||||
"column": 6411
|
||||
},
|
||||
"expected_matches": [
|
||||
{
|
||||
"source_index": 0,
|
||||
"source": "../../../../../node_modules/react-aria-components/dist/private/Checkbox.mjs",
|
||||
"original_line": 46,
|
||||
"original_column": 6,
|
||||
"content_sha256": "8319abfad740fe6665fc33f1ca8790251fb58d144497b856f7073c10eef62974"
|
||||
}
|
||||
],
|
||||
"mapped_count": 23646
|
||||
}
|
||||
]
|
||||
@@ -1,370 +0,0 @@
|
||||
{
|
||||
"tokenizer": "tiktoken0.12.0 cl100k_base; compact UTF8 JSON args and complete replies; estimates not model usage; catalog and protocol unmeasured",
|
||||
"rows": [
|
||||
{
|
||||
"name": "point-r1-DateSelect-default-retry",
|
||||
"elapsed_s": 0.5818042080063606,
|
||||
"cpu_s": 0.586226,
|
||||
"rss_bytes": 214908928,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 2,
|
||||
"tokens": {
|
||||
"arguments": 193,
|
||||
"replies": 2232177
|
||||
},
|
||||
"reply_bytes": 5775790,
|
||||
"measurement_receipt_sha256": "5f7c3a7b1bbcacb046f0caf136d41b6afa43a805d14f85365f265df9ac0015ae",
|
||||
"reply_trace_sha256": "04b9aa60f9843ce5441ea30abf0c4d2e803a2764949ca9cf01136d5d720b424d"
|
||||
},
|
||||
{
|
||||
"name": "point-r1-DateSelect-map8",
|
||||
"elapsed_s": 0.5986265420069685,
|
||||
"cpu_s": 0.616543,
|
||||
"rss_bytes": 203620352,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 102,
|
||||
"replies": 2231733
|
||||
},
|
||||
"reply_bytes": 5774116,
|
||||
"measurement_receipt_sha256": "85b6a677a2c542ca55ef156ed2cfb9a1af92fc6783921d22c118d0a28b2c4866",
|
||||
"reply_trace_sha256": "ec9abc70ca71f5e91911147879f97df2d999ad2c4370e6c975b081577fdd5969"
|
||||
},
|
||||
{
|
||||
"name": "point-r1-index-default-retry",
|
||||
"elapsed_s": 0.5493043329915963,
|
||||
"cpu_s": 0.573376,
|
||||
"rss_bytes": 184139776,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 2,
|
||||
"tokens": {
|
||||
"arguments": 181,
|
||||
"replies": 1590097
|
||||
},
|
||||
"reply_bytes": 4428697,
|
||||
"measurement_receipt_sha256": "cab96c90a933d2ccf8a854308b2de0b2593a678413afd5d95dd9d1628df277b0",
|
||||
"reply_trace_sha256": "d613c618414f970b94b17ffee783768cedf45e3c414162dc4c408943522ac585"
|
||||
},
|
||||
{
|
||||
"name": "point-r1-index-map8",
|
||||
"elapsed_s": 0.5393435839941958,
|
||||
"cpu_s": 0.559231,
|
||||
"rss_bytes": 190300160,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 96,
|
||||
"replies": 1589650
|
||||
},
|
||||
"reply_bytes": 4427067,
|
||||
"measurement_receipt_sha256": "abf76eb747e00c26eab5807944224ed55d5fd3b08bc4c8b147732e70180dceca",
|
||||
"reply_trace_sha256": "abc7c820c6d627553f3e2d3549899d4ff7160271a0b6a937a5719d87420190bf"
|
||||
},
|
||||
{
|
||||
"name": "point-r1-theme-default-retry",
|
||||
"elapsed_s": 0.6525920830026735,
|
||||
"cpu_s": 0.72032,
|
||||
"rss_bytes": 167591936,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 88,
|
||||
"replies": 25015
|
||||
},
|
||||
"reply_bytes": 77129,
|
||||
"measurement_receipt_sha256": "c108ab41544816f7706e6343584264bf3db938a97dfd51b52d4495f579efeb30",
|
||||
"reply_trace_sha256": "d086b3579fbd099df1a32c7d1e54e2af6ea6a5b244747d1a012fa19ed541d4e5"
|
||||
},
|
||||
{
|
||||
"name": "point-r1-theme-map8",
|
||||
"elapsed_s": 0.3669995829986874,
|
||||
"cpu_s": 0.391014,
|
||||
"rss_bytes": 104103936,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 99,
|
||||
"replies": 25027
|
||||
},
|
||||
"reply_bytes": 77171,
|
||||
"measurement_receipt_sha256": "146b3d877d36fea192e0442f3ae06d9e05cfbaf8c75902d8b1db914cea818cf2",
|
||||
"reply_trace_sha256": "6a905573af8e55b34bbbd1618b48aec7d3cc12860f59643f28024002e81fcf00"
|
||||
},
|
||||
{
|
||||
"name": "point-r2-DateSelect-default-retry",
|
||||
"elapsed_s": 0.5365884999919217,
|
||||
"cpu_s": 0.5738759999999999,
|
||||
"rss_bytes": 206454784,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 2,
|
||||
"tokens": {
|
||||
"arguments": 193,
|
||||
"replies": 2232177
|
||||
},
|
||||
"reply_bytes": 5775790,
|
||||
"measurement_receipt_sha256": "415deab64b1490e8300b63fa926a517f8630f68117c5735c126f7a8f8f02e635",
|
||||
"reply_trace_sha256": "ae1055e47e9b2e3ef6ded1aa124950d220ba0bbe7a27afe68b9f46039299ce1e"
|
||||
},
|
||||
{
|
||||
"name": "point-r2-DateSelect-map8",
|
||||
"elapsed_s": 0.5848246670066146,
|
||||
"cpu_s": 0.5841609999999999,
|
||||
"rss_bytes": 196968448,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 102,
|
||||
"replies": 2231733
|
||||
},
|
||||
"reply_bytes": 5774116,
|
||||
"measurement_receipt_sha256": "18c256638988915c3029f1cf6c19b1ce6cde5e2b7e046a93d47d0afbf5c74c6f",
|
||||
"reply_trace_sha256": "ee27a03f5b3580d54ad8ad33bc7f22f552c724c9e1674f8c39aac4dd7b8fffab"
|
||||
},
|
||||
{
|
||||
"name": "point-r2-index-default-retry",
|
||||
"elapsed_s": 0.5531810840038816,
|
||||
"cpu_s": 0.5698139999999999,
|
||||
"rss_bytes": 182484992,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 2,
|
||||
"tokens": {
|
||||
"arguments": 181,
|
||||
"replies": 1590097
|
||||
},
|
||||
"reply_bytes": 4428697,
|
||||
"measurement_receipt_sha256": "e97c8b561949f6e6615f9684e8187c7eb2a47833e836532891631b56ac26526a",
|
||||
"reply_trace_sha256": "35a4f9f5f868083cd1c45767282e5d7555f07a2962fbdf647de538a07452ae02"
|
||||
},
|
||||
{
|
||||
"name": "point-r2-index-map8",
|
||||
"elapsed_s": 0.4954192500008503,
|
||||
"cpu_s": 0.53103,
|
||||
"rss_bytes": 176865280,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 96,
|
||||
"replies": 1589650
|
||||
},
|
||||
"reply_bytes": 4427067,
|
||||
"measurement_receipt_sha256": "d9f3c788ee7de0508ba42f3a37fea9df282d195a634e4e523b673eed6f813b3a",
|
||||
"reply_trace_sha256": "e19bbc0e4534d60d9577c8c5871243604439f1e84f6e6e1c5a763cf5001665f1"
|
||||
},
|
||||
{
|
||||
"name": "point-r2-theme-default-retry",
|
||||
"elapsed_s": 0.36803100000543054,
|
||||
"cpu_s": 0.358402,
|
||||
"rss_bytes": 103415808,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 88,
|
||||
"replies": 25015
|
||||
},
|
||||
"reply_bytes": 77129,
|
||||
"measurement_receipt_sha256": "ad7ff37773f7a383a2ba1376863358451959a535d802c3300495dba4d8c0ef2e",
|
||||
"reply_trace_sha256": "4b9afba56e44a75d3e1f674b70ddd2b030ace630ad8bf8bde2950cf866890880"
|
||||
},
|
||||
{
|
||||
"name": "point-r2-theme-map8",
|
||||
"elapsed_s": 0.38679324999975506,
|
||||
"cpu_s": 0.40970799999999996,
|
||||
"rss_bytes": 104267776,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 99,
|
||||
"replies": 25027
|
||||
},
|
||||
"reply_bytes": 77171,
|
||||
"measurement_receipt_sha256": "3958afd848f3cc5529bb1a56f8ba8ea82eea34818713c8b045d327ea0aae9f65",
|
||||
"reply_trace_sha256": "241cf2e478a29f069c1a17fdb5b14f35e3430ee9c276d818189a7de789608c89"
|
||||
},
|
||||
{
|
||||
"name": "point-r3-DateSelect-default-retry",
|
||||
"elapsed_s": 0.6513657079922268,
|
||||
"cpu_s": 0.613527,
|
||||
"rss_bytes": 220790784,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 2,
|
||||
"tokens": {
|
||||
"arguments": 193,
|
||||
"replies": 2232177
|
||||
},
|
||||
"reply_bytes": 5775790,
|
||||
"measurement_receipt_sha256": "19e779011951101b141f48e84b427161bf86f41998e79a2fb368d9e129aa6b00",
|
||||
"reply_trace_sha256": "7d1916049662bf0a278ef0bf341874e35a317182846d4ac4eca1a09a166a3e58"
|
||||
},
|
||||
{
|
||||
"name": "point-r3-DateSelect-map8",
|
||||
"elapsed_s": 0.6560826670029201,
|
||||
"cpu_s": 0.686476,
|
||||
"rss_bytes": 201785344,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 102,
|
||||
"replies": 2231733
|
||||
},
|
||||
"reply_bytes": 5774116,
|
||||
"measurement_receipt_sha256": "32af3c6720329c991dc434844878aba283e0c6001087ed0f93a1bfaabbdecc67",
|
||||
"reply_trace_sha256": "5feb55a1ff8c433f5dee38a7c3ae877b9260d1fbd0d5ef8501ed38d8e5d46d60"
|
||||
},
|
||||
{
|
||||
"name": "point-r3-index-default-retry",
|
||||
"elapsed_s": 0.5955084579909453,
|
||||
"cpu_s": 0.6005269999999999,
|
||||
"rss_bytes": 182140928,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 2,
|
||||
"tokens": {
|
||||
"arguments": 181,
|
||||
"replies": 1590097
|
||||
},
|
||||
"reply_bytes": 4428697,
|
||||
"measurement_receipt_sha256": "ab049ddfe685398068198b13ed317eb6c51aff104d88d9d11a0cc7dab6fab900",
|
||||
"reply_trace_sha256": "7fd9f40f0e16debbffb65c9b5bff8043e586b7aa493daf376eefb0348676fd1e"
|
||||
},
|
||||
{
|
||||
"name": "point-r3-index-map8",
|
||||
"elapsed_s": 0.6677014579909155,
|
||||
"cpu_s": 0.6370119999999999,
|
||||
"rss_bytes": 194920448,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 96,
|
||||
"replies": 1589650
|
||||
},
|
||||
"reply_bytes": 4427067,
|
||||
"measurement_receipt_sha256": "fa78f3707dda0387d07a8cfee9f4591d64d5a79b49f097c4501de143e32f89e5",
|
||||
"reply_trace_sha256": "184a5536859edd8e990e89a785048200847aff85de28008fb6f972ec3455e491"
|
||||
},
|
||||
{
|
||||
"name": "point-r3-theme-default-retry",
|
||||
"elapsed_s": 0.3368863329960732,
|
||||
"cpu_s": 0.358882,
|
||||
"rss_bytes": 102940672,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 88,
|
||||
"replies": 25015
|
||||
},
|
||||
"reply_bytes": 77129,
|
||||
"measurement_receipt_sha256": "ab076949d4ba9ffc11e1f677e6af805d8dae976ee642a1344fdf252c710c5ec6",
|
||||
"reply_trace_sha256": "ae3742c091752393a0fae881f1203287fdbb2bde08838edf989e809ae278e246"
|
||||
},
|
||||
{
|
||||
"name": "point-r3-theme-map8",
|
||||
"elapsed_s": 0.3349004170013359,
|
||||
"cpu_s": 0.36136099999999993,
|
||||
"rss_bytes": 103153664,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"calls": 1,
|
||||
"tokens": {
|
||||
"arguments": 99,
|
||||
"replies": 25027
|
||||
},
|
||||
"reply_bytes": 77171,
|
||||
"measurement_receipt_sha256": "eff22ab422018535d88a0e0d3c135139a6930a18a24a3d32f9c68defaeae8dce",
|
||||
"reply_trace_sha256": "4af542e52f3377ad3410a069a60c64cf4db76771a0bc686075ebea31c2733032"
|
||||
}
|
||||
],
|
||||
"medians": {
|
||||
"DateSelect-default-retry": {
|
||||
"runs": 3,
|
||||
"elapsed_s": 0.5818042080063606,
|
||||
"cpu_s": 0.586226,
|
||||
"rss_mib": 204.953125,
|
||||
"calls": 2,
|
||||
"argument_tokens": 193,
|
||||
"reply_tokens": 2232177,
|
||||
"reply_bytes": 5775790
|
||||
},
|
||||
"DateSelect-map8": {
|
||||
"runs": 3,
|
||||
"elapsed_s": 0.5986265420069685,
|
||||
"cpu_s": 0.616543,
|
||||
"rss_mib": 192.4375,
|
||||
"calls": 1,
|
||||
"argument_tokens": 102,
|
||||
"reply_tokens": 2231733,
|
||||
"reply_bytes": 5774116
|
||||
},
|
||||
"index-default-retry": {
|
||||
"runs": 3,
|
||||
"elapsed_s": 0.5531810840038816,
|
||||
"cpu_s": 0.573376,
|
||||
"rss_mib": 174.03125,
|
||||
"calls": 2,
|
||||
"argument_tokens": 181,
|
||||
"reply_tokens": 1590097,
|
||||
"reply_bytes": 4428697
|
||||
},
|
||||
"index-map8": {
|
||||
"runs": 3,
|
||||
"elapsed_s": 0.5393435839941958,
|
||||
"cpu_s": 0.559231,
|
||||
"rss_mib": 181.484375,
|
||||
"calls": 1,
|
||||
"argument_tokens": 96,
|
||||
"reply_tokens": 1589650,
|
||||
"reply_bytes": 4427067
|
||||
},
|
||||
"theme-default-retry": {
|
||||
"runs": 3,
|
||||
"elapsed_s": 0.36803100000543054,
|
||||
"cpu_s": 0.358882,
|
||||
"rss_mib": 98.625,
|
||||
"calls": 1,
|
||||
"argument_tokens": 88,
|
||||
"reply_tokens": 25015,
|
||||
"reply_bytes": 77129
|
||||
},
|
||||
"theme-map8": {
|
||||
"runs": 3,
|
||||
"elapsed_s": 0.3669995829986874,
|
||||
"cpu_s": 0.391014,
|
||||
"rss_mib": 99.28125,
|
||||
"calls": 1,
|
||||
"argument_tokens": 99,
|
||||
"reply_tokens": 25027,
|
||||
"reply_bytes": 77171
|
||||
}
|
||||
},
|
||||
"baseline": {
|
||||
"baseline_worktree": "/var/folders/xl/60j5sd4j6b99tn8q082l9t8r0000gn/T/rea-budget-calibration-baseline-2rg1qcde",
|
||||
"head": "e2a2ede5440df396e44b3406ef511a31a680290e",
|
||||
"tracked_patch_sha256": "038b69c19bbd79a303c9ecd8d9d22a1251637e3886543ca4f5a4c1b70c724eec",
|
||||
"source_map_policy_sha256": "6d28cba1ff7936dee640474a4d5e1e456a3acb3c63432ea6ab4698181a53ca05"
|
||||
},
|
||||
"conditions": {
|
||||
"host": "macOS26.3 arm64",
|
||||
"node": "v24.22.1",
|
||||
"workers": "fresh process and owned codec each run; OS caches warm, not flushed",
|
||||
"rss": "sampled simultaneous process-group RSS sum at20ms, shared pages may count twice; short peaks and descendants leaving group excluded",
|
||||
"cpu": "OS wait4 user+system waited subtree; orphaned or unwaited descendants excluded; sampler excluded",
|
||||
"guard": "1536MiB observed early stop, not kernel hard limit",
|
||||
"catalog": "not counted; no catalog discovery in replay; tool schemas, JSON-RPC envelopes, actual billed tokens, network, browser capture, and generated-script analysis unmeasured"
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
"""Independent source-map VLQ artifact oracle; standard library only."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
|
||||
def values(segment):
|
||||
output, value, shift = [], 0, 0
|
||||
for character in segment:
|
||||
digit = ALPHABET.index(character)
|
||||
value += (digit & 31) << shift
|
||||
if digit & 32:
|
||||
shift += 5
|
||||
else:
|
||||
output.append(-(value >> 1) if value & 1 else value >> 1)
|
||||
value, shift = 0, 0
|
||||
assert shift == 0
|
||||
return output
|
||||
|
||||
def inspect(path):
|
||||
data = Path(path).read_bytes()
|
||||
raw = json.loads(data)
|
||||
source = original_line = original_column = 0
|
||||
count, mapped_count, first, matches = 0, 0, None, []
|
||||
for line, row in enumerate(raw['mappings'].split(';'), 1):
|
||||
column = 0
|
||||
for segment in row.split(','):
|
||||
if not segment:
|
||||
continue
|
||||
count += 1
|
||||
fields = values(segment)
|
||||
column += fields[0]
|
||||
if len(fields) > 1:
|
||||
assert len(fields) in (4, 5)
|
||||
mapped_count += 1
|
||||
source += fields[1]
|
||||
original_line += fields[2]
|
||||
original_column += fields[3]
|
||||
if first is None and len(fields) > 1:
|
||||
first = {'line': line, 'column': column}
|
||||
if first == {'line': line, 'column': column}:
|
||||
if len(fields) == 1:
|
||||
matches.append({'generated_only': True})
|
||||
else:
|
||||
text = raw['sourcesContent'][source]
|
||||
matches.append({'source_index':source,'source':raw['sources'][source],
|
||||
'original_line':original_line+1,'original_column':original_column,
|
||||
'content_sha256':hashlib.sha256(text.encode()).hexdigest() if text is not None else None})
|
||||
lines = raw['mappings'].count(';')+1
|
||||
return {'sha256':hashlib.sha256(data).hexdigest(), 'bytes':len(data),
|
||||
'segment_count':count, 'mapped_count':mapped_count, 'record_count':1+len(raw['sources'])+len(raw['names'])+len(raw['sourcesContent'])+lines+count,
|
||||
'point':first,'expected_matches':matches}
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
manifest = json.loads(Path(sys.argv[1]).read_text())
|
||||
for source in manifest:
|
||||
observed = inspect(Path(sys.argv[1]).resolve().parent / source['path'])
|
||||
for key, value in observed.items():
|
||||
assert source[key] == value, (source['path'], key, source[key], value)
|
||||
print(json.dumps({'verified_maps':len(manifest), 'sha256s':[source['sha256'] for source in manifest]}))
|
||||
@@ -1,288 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
const [projectRoot, configText] = process.argv.slice(2);
|
||||
const config = JSON.parse(configText);
|
||||
const require = createRequire(join(projectRoot, "package.json"));
|
||||
assert(
|
||||
config.artifactDir,
|
||||
"Select artifactDir containing the acquired artifacts.json",
|
||||
);
|
||||
const manifest = JSON.parse(
|
||||
await readFile(join(config.artifactDir, "artifacts.json"), "utf8"),
|
||||
).map((source) => ({ ...source, path: join(config.artifactDir, source.path) }));
|
||||
const selected =
|
||||
config.select === "all"
|
||||
? manifest
|
||||
: manifest.filter((x) => x.path.includes(config.select));
|
||||
assert(selected.length > 0);
|
||||
const load = (path) => import(pathToFileURL(join(projectRoot, path)).href);
|
||||
const { projectAnalysisError } = await load(
|
||||
"src/domain/analysisErrorProjection.ts",
|
||||
);
|
||||
const calls = [];
|
||||
let finalResult;
|
||||
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
||||
const start = performance.now();
|
||||
if (config.mode === "point") {
|
||||
const { SourceMapDecoder } = await load(
|
||||
"src/javascript/sourceMaps/SourceMapDecoder.ts",
|
||||
);
|
||||
const { WebSourceLocationService } = await load(
|
||||
"src/application/WebSourceLocationService.ts",
|
||||
);
|
||||
const { LocalWebSourceLocationArtifacts } = await load(
|
||||
"src/browser/assets/WebSourceLocationArtifacts.ts",
|
||||
);
|
||||
const { spawnOwnedProviderProcess } = await load(
|
||||
"src/process/ProviderProcess.ts",
|
||||
);
|
||||
const decoder = new SourceMapDecoder({
|
||||
launcher: (options) =>
|
||||
spawnOwnedProviderProcess({
|
||||
...options,
|
||||
arguments: [
|
||||
"--import",
|
||||
require.resolve("tsx"),
|
||||
...options.arguments.map((x) =>
|
||||
x.endsWith("/SourceMapCodecProcess.js")
|
||||
? join(
|
||||
projectRoot,
|
||||
"src/javascript/sourceMaps/SourceMapCodecProcess.ts",
|
||||
)
|
||||
: x,
|
||||
),
|
||||
],
|
||||
}),
|
||||
});
|
||||
const service = new WebSourceLocationService(
|
||||
new LocalWebSourceLocationArtifacts(),
|
||||
decoder,
|
||||
);
|
||||
try {
|
||||
for (const source of selected) {
|
||||
const text = await readFile(source.path, "utf8");
|
||||
const policies =
|
||||
config.strategy === "recover"
|
||||
? [{}, config.policy]
|
||||
: [config.policy ?? {}];
|
||||
let completed = false;
|
||||
for (const policy of policies) {
|
||||
const args = {
|
||||
manifest_path: source.path.slice(0, -4) + ".manifest.json",
|
||||
script_index: 0,
|
||||
source_map: { path: source.path, url: source.url },
|
||||
generated_position: source.point,
|
||||
...(Object.keys(policy).length ? { source_map_limits: policy } : {}),
|
||||
};
|
||||
const began = performance.now();
|
||||
const result = await service.trace(args);
|
||||
calls.push({
|
||||
operation: "trace_web_source_location",
|
||||
args,
|
||||
reply: result.ok ? result.value : projectAnalysisError(result.error),
|
||||
elapsed_ms: performance.now() - began,
|
||||
});
|
||||
if (!result.ok) continue;
|
||||
const report = result.value.normalized_result;
|
||||
assert.equal(report.source_map_sha256, source.sha256);
|
||||
assert.equal(report.matches.length, source.expected_matches.length);
|
||||
for (let i = 0; i < source.expected_matches.length; i++) {
|
||||
const expected = source.expected_matches[i],
|
||||
actual = report.matches[i];
|
||||
assert.equal(
|
||||
actual.state,
|
||||
expected.generated_only ? "generated-only" : "mapped",
|
||||
);
|
||||
if (expected.generated_only) continue;
|
||||
assert.equal(actual.source_index, expected.source_index);
|
||||
assert.equal(actual.reported_source, expected.source);
|
||||
assert.equal(actual.original_position.line, expected.original_line);
|
||||
assert.equal(
|
||||
actual.original_position.column,
|
||||
expected.original_column,
|
||||
);
|
||||
assert.equal(actual.content.state, "embedded");
|
||||
assert.equal(actual.content.utf8_sha256, expected.content_sha256);
|
||||
}
|
||||
finalResult = result.value;
|
||||
completed = true;
|
||||
break;
|
||||
}
|
||||
assert(
|
||||
completed,
|
||||
`point incomplete for ${source.path}: ${JSON.stringify(calls.at(-1)?.reply)}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await service.close();
|
||||
}
|
||||
} else {
|
||||
const { fetchWebSourceMaps } = await load(
|
||||
"src/browser/WebSourceMapFetcher.ts",
|
||||
);
|
||||
const { analyzeWebBundleInputSchema, webBundleAnalysisSchema } = await load(
|
||||
"src/domain/webBundleAnalysis.ts",
|
||||
);
|
||||
const { jsonValueSchema } = await load("src/domain/jsonValue.ts");
|
||||
const { createBrowserEvidence } = await load(
|
||||
"src/application/BrowserEvidence.ts",
|
||||
);
|
||||
const requests = selected.map((s, i) => ({
|
||||
scriptKey: `scr_${String(i + 1).padStart(64, "0")}`,
|
||||
declaredUrl: s.url,
|
||||
fetchUrl: s.url,
|
||||
}));
|
||||
const policies =
|
||||
config.strategy === "recover" ? [{}, config.policy] : [config.policy ?? {}];
|
||||
const byUrl = new Map(selected.map((s) => [s.url, s]));
|
||||
let completed = false;
|
||||
for (const policy of policies) {
|
||||
const input = analyzeWebBundleInputSchema.parse({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
allowed_origins: ["https://demo.actualbudget.org"],
|
||||
target_id: "calibration-artifact-replay",
|
||||
fetch_source_maps: true,
|
||||
...(Object.keys(policy).length ? { source_map_limits: policy } : {}),
|
||||
});
|
||||
const began = performance.now();
|
||||
const result = await fetchWebSourceMaps(requests, input, undefined, {
|
||||
fetch: async (url) =>
|
||||
new Response(await readFile(byUrl.get(String(url)).path), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/source-map+json" },
|
||||
}),
|
||||
});
|
||||
let reply = result.ok ? result.value : projectAnalysisError(result.error);
|
||||
if (result.ok && config.evidence !== false) {
|
||||
// Isolate source-map retention cost: script capture/analysis are explicitly outside this replay.
|
||||
const bundle = webBundleAnalysisSchema.parse({
|
||||
capture: {
|
||||
target_url: "https://demo.actualbudget.org/",
|
||||
target_origin: "https://demo.actualbudget.org",
|
||||
scripts_observed: 0,
|
||||
scripts_analyzed: 0,
|
||||
source_artifacts: [],
|
||||
},
|
||||
observations: {
|
||||
chunks: { nodes: [], edges: [] },
|
||||
routes: [],
|
||||
endpoints: [],
|
||||
webmcp_declarations: [],
|
||||
source_maps: result.value,
|
||||
},
|
||||
inferences: [],
|
||||
unknowns: [],
|
||||
completeness: {
|
||||
status: result.value.status === "included" ? "complete" : "partial",
|
||||
parsed_scripts: 0,
|
||||
parse_failures: 0,
|
||||
visited_ast_nodes: 0,
|
||||
},
|
||||
limitations: [
|
||||
"Pinned public source-map transport replay. Browser capture and generated-script static analysis were not performed.",
|
||||
],
|
||||
});
|
||||
jsonValueSchema.parse(bundle);
|
||||
const evidence = createBrowserEvidence(
|
||||
"analyze_web_bundle",
|
||||
input,
|
||||
bundle,
|
||||
{
|
||||
id: "source-map-calibration-replay",
|
||||
name: "Pinned public source-map replay",
|
||||
version: "1",
|
||||
},
|
||||
);
|
||||
// Include actual Evidence envelope cost rather than only the producer report.
|
||||
reply = evidence;
|
||||
}
|
||||
calls.push({
|
||||
operation: "analyze_web_bundle.source_maps",
|
||||
args: {
|
||||
source_urls: selected.map((s) => s.url),
|
||||
source_sha256s: selected.map((s) => s.sha256),
|
||||
...(Object.keys(policy).length ? { source_map_limits: policy } : {}),
|
||||
},
|
||||
reply,
|
||||
elapsed_ms: performance.now() - began,
|
||||
});
|
||||
if (
|
||||
!result.ok ||
|
||||
result.value.items.length !== selected.length ||
|
||||
result.value.items.some(
|
||||
(item) => !["included", "partial"].includes(item.status),
|
||||
)
|
||||
)
|
||||
continue;
|
||||
for (let i = 0; i < selected.length; i++) {
|
||||
const original = selected[i],
|
||||
item = result.value.items[i],
|
||||
raw = JSON.parse(await readFile(original.path, "utf8"));
|
||||
assert(["included", "partial"].includes(item.status));
|
||||
assert.equal(item.mappings.length, original.mapped_count);
|
||||
assert.equal(item.artifact.sha256, original.sha256);
|
||||
assert.equal(item.original_sources.length, raw.sources.length);
|
||||
const expected = original.expected_matches.filter(
|
||||
(x) => !x.generated_only,
|
||||
);
|
||||
const actual = item.mappings.filter(
|
||||
(x) =>
|
||||
x.generated_line === original.point.line &&
|
||||
x.generated_column === original.point.column,
|
||||
);
|
||||
assert.equal(actual.length, expected.length);
|
||||
for (let j = 0; j < expected.length; j++) {
|
||||
assert.equal(actual[j].source_index, expected[j].source_index);
|
||||
assert.equal(actual[j].original_line, expected[j].original_line);
|
||||
assert.equal(actual[j].original_column, expected[j].original_column);
|
||||
}
|
||||
for (let j = 0; j < raw.sources.length; j++) {
|
||||
assert.equal(item.original_sources[j].source_index, j);
|
||||
if (raw.sourcesContent[j] !== null)
|
||||
assert.equal(
|
||||
item.original_sources[j].artifact.sha256,
|
||||
digest(raw.sourcesContent[j]),
|
||||
);
|
||||
}
|
||||
}
|
||||
finalResult = result.value;
|
||||
completed = true;
|
||||
break;
|
||||
}
|
||||
if (!completed && config.allowIncomplete !== true)
|
||||
throw Error(
|
||||
`fetch incomplete: ${JSON.stringify(calls.at(-1)?.reply?.items?.map((i) => ({ status: i.status, limitation: i.limitation })))}`,
|
||||
);
|
||||
}
|
||||
if (config.resultPath)
|
||||
await writeFile(config.resultPath, JSON.stringify({ config, calls }));
|
||||
const rows = calls.map((c) => {
|
||||
const report =
|
||||
c.reply.normalized_result?.observations?.source_maps ?? c.reply;
|
||||
return {
|
||||
operation: c.operation,
|
||||
elapsed_ms: c.elapsed_ms,
|
||||
reply_bytes: Buffer.byteLength(JSON.stringify(c.reply)),
|
||||
state: report.status ?? (c.reply.code ? "error" : "complete"),
|
||||
error: c.reply.code ? c.reply.message : undefined,
|
||||
items: report.items?.map((i) => ({
|
||||
status: i.status,
|
||||
sources: i.original_sources?.length,
|
||||
mappings: i.mappings?.length,
|
||||
limitation: i.limitation,
|
||||
})),
|
||||
};
|
||||
});
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
config,
|
||||
operation_ms: performance.now() - start,
|
||||
calls: rows,
|
||||
result_sha256: finalResult ? digest(JSON.stringify(finalResult)) : null,
|
||||
resource_usage: process.resourceUsage(),
|
||||
}),
|
||||
);
|
||||
@@ -30,31 +30,31 @@ class Generator:
|
||||
assert "payload must not enter diagnostics" not in json.dumps(reply)
|
||||
for state, expected in [({"metadata": {1: b"bad-key"}}, "format"), ({"metadata": object()}, "decoder")]:
|
||||
try:
|
||||
adapter.project_record(state, [], 64)
|
||||
adapter.project_record(state, [])
|
||||
except adapter.CaptureFailure as error:
|
||||
assert error.reason == expected, (error.reason, expected)
|
||||
assert error.pointer == "/metadata", error.pointer
|
||||
else:
|
||||
raise AssertionError("Invalid native representation was accepted")
|
||||
non_utf8 = adapter.project_record({"request": {"headers": [(b"Referer", b"https://byte-user:[email protected]/path\xff")] }}, [], 64)
|
||||
non_utf8 = adapter.project_record({"request": {"headers": [(b"Referer", b"https://byte-user:[email protected]/path\xff")] }}, [])
|
||||
assert non_utf8["reported"]["request"]["headers"][0][1] is None, non_utf8
|
||||
assert non_utf8["binary_fields"][-1]["state"] == "redacted", non_utf8
|
||||
for value in [" \tHTTPS://ows-user:[email protected]/path\t ", b" \tHTTPS://ows-user:[email protected]/path\t "]:
|
||||
projected = adapter.project_record({"request": {"headers": [(b"Referer", value)]}}, [], 64)
|
||||
projected = adapter.project_record({"request": {"headers": [(b"Referer", value)]}}, [])
|
||||
assert projected["reported"]["request"]["headers"][0][1] == " \tHTTPS://example.test/path\t ", projected
|
||||
assert "ows-secret" not in json.dumps(projected), projected
|
||||
if isinstance(value, bytes):
|
||||
assert projected["binary_fields"][-1]["state"] == "redacted", projected
|
||||
body = b"A" * 12345
|
||||
state = {"response": {"content": body}}
|
||||
control = adapter.project_record(state, [], 64)
|
||||
control = adapter.project_record(state, [])
|
||||
field = control["binary_fields"][0]
|
||||
assert field["state"] == "retained" and field["bytes"] == len(body), control
|
||||
for literal in ["12345", "2345"]:
|
||||
# Only the generated byte count matches this exclusion.
|
||||
assert literal not in control["reported"]["response"]["content"]
|
||||
assert literal not in field["content_base64"] and literal not in field["sha256"]
|
||||
projected = adapter.project_record(state, [literal], 64)
|
||||
projected = adapter.project_record(state, [literal])
|
||||
assert projected["reported"]["response"]["content"] is None, projected
|
||||
assert projected["binary_fields"] == [{"pointer": "/response/content", "representation": "producer-bytes", "state": "redacted", "content_base64": None, "bytes": None, "sha256": None}], projected
|
||||
assert {"pointer": "/response/content", "reason": "explicit-sensitive-value"} in projected["redactions"], projected
|
||||
|
||||
@@ -24,71 +24,41 @@ import {
|
||||
createVerifierRun,
|
||||
} from "./lib/verifier-run.mjs";
|
||||
|
||||
// Volatile straight-line work produces a natural native response above 1 MiB
|
||||
// and a decompile above one second, without the old branch-heavy fixture.
|
||||
// Admit the generated source before allocating its line array.
|
||||
// A natural slow decompile: one function with many independent branches on
|
||||
// volatile state. Ghidra decompiles it correctly but takes far longer than the
|
||||
// configured limit, so the limit, not an injected result, stops it.
|
||||
const LIMIT_SECONDS = 1;
|
||||
const RECOVERY_SECONDS = 20;
|
||||
// This verifier budgets one MiB for its generated C source. Bound the number
|
||||
// of lines before allocating them, including the largest admitted integer's
|
||||
// decimal representation; the joined source and line array remain bounded.
|
||||
const FIXTURE_SOURCE_BYTES = 1024 * 1024;
|
||||
const FIXTURE_PREFIX = [
|
||||
"volatile unsigned rea_state[128];",
|
||||
"__attribute__((noinline, used)) unsigned rea_small(unsigned x) {",
|
||||
" unsigned a = x + rea_state[0];",
|
||||
" unsigned b = a ^ rea_state[1];",
|
||||
" return b + (a * 3u);",
|
||||
"}",
|
||||
"volatile unsigned rea_state[64];",
|
||||
"__attribute__((noinline, used)) unsigned rea_slow(unsigned x) {",
|
||||
" unsigned a = x;",
|
||||
];
|
||||
const FIXTURE_SUFFIX = [" return a;", "}", ""];
|
||||
const MAX_STATEMENT_BYTES = Buffer.byteLength(
|
||||
` a += rea_state[127] ^ (x + ${Number.MAX_SAFE_INTEGER}u); rea_state[127] = a;\n`,
|
||||
const FIXTURE_SUFFIX = [
|
||||
" return a;",
|
||||
"}",
|
||||
"__attribute__((noinline, used)) int rea_small(int v) {",
|
||||
" return v + (int)rea_state[0];",
|
||||
"}",
|
||||
"",
|
||||
];
|
||||
const MAX_BRANCH_LINE_BYTES = Buffer.byteLength(
|
||||
` if (rea_state[63] & 1073741824u) a = a * 31u + ${Number.MAX_SAFE_INTEGER}u; else a ^= rea_state[63];\n`,
|
||||
);
|
||||
const FIXTURE_FIXED_BYTES = Buffer.byteLength(
|
||||
[...FIXTURE_PREFIX, ...FIXTURE_SUFFIX].join("\n"),
|
||||
);
|
||||
const MAX_STATEMENTS = Math.floor(
|
||||
(FIXTURE_SOURCE_BYTES - FIXTURE_FIXED_BYTES) / MAX_STATEMENT_BYTES,
|
||||
const MAX_BRANCHES = Math.floor(
|
||||
(FIXTURE_SOURCE_BYTES - FIXTURE_FIXED_BYTES) / MAX_BRANCH_LINE_BYTES,
|
||||
);
|
||||
const STATEMENTS = Number(process.env.REA_DECOMPILE_TIMEOUT_STATEMENTS ?? 5400);
|
||||
const BRANCHES = Number(process.env.REA_DECOMPILE_TIMEOUT_BRANCHES ?? 6000);
|
||||
assert.ok(
|
||||
Number.isSafeInteger(STATEMENTS) &&
|
||||
STATEMENTS > 0 &&
|
||||
STATEMENTS <= MAX_STATEMENTS,
|
||||
`REA_DECOMPILE_TIMEOUT_STATEMENTS must be an integer from 1 to ${MAX_STATEMENTS} (generated source budget: ${FIXTURE_SOURCE_BYTES} bytes)`,
|
||||
Number.isSafeInteger(BRANCHES) && BRANCHES > 0 && BRANCHES <= MAX_BRANCHES,
|
||||
`REA_DECOMPILE_TIMEOUT_BRANCHES must be an integer from 1 to ${MAX_BRANCHES} (generated source budget: ${FIXTURE_SOURCE_BYTES} bytes)`,
|
||||
);
|
||||
const fixtureSource = () => {
|
||||
const lines = [...FIXTURE_PREFIX];
|
||||
for (let index = 0; index < STATEMENTS; index += 1)
|
||||
lines.push(
|
||||
` a += rea_state[${index % 128}] ^ (x + ${index}u); rea_state[${(index * 7) % 128}] = a;`,
|
||||
);
|
||||
lines.push(...FIXTURE_SUFFIX);
|
||||
const source = lines.join("\n");
|
||||
assert.ok(Buffer.byteLength(source) <= FIXTURE_SOURCE_BYTES);
|
||||
return source;
|
||||
};
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const value = (result) => {
|
||||
if (!result.ok) throw result.error;
|
||||
return result.value;
|
||||
};
|
||||
const flowSummary = (flow) => ({
|
||||
available: flow.available,
|
||||
operations: flow.operations?.length,
|
||||
largest_input_count: flow.operations?.reduce(
|
||||
(largest, operation) => Math.max(largest, operation.inputs.length),
|
||||
0,
|
||||
),
|
||||
def_use_edges: flow.def_use?.length,
|
||||
parameter_uses: flow.parameter_uses?.length,
|
||||
truncated: flow.truncated,
|
||||
limits: flow.limits,
|
||||
limits_reached: flow.limits_reached,
|
||||
omitted_operations_lower_bound: flow.omitted_operations_lower_bound,
|
||||
known_omitted_inputs: flow.known_omitted_inputs,
|
||||
known_omitted_edges: flow.known_omitted_edges,
|
||||
});
|
||||
|
||||
const run = createVerifierRun();
|
||||
assert.ok(
|
||||
@@ -97,30 +67,36 @@ assert.ok(
|
||||
);
|
||||
const environment = {
|
||||
...process.env,
|
||||
GHIDRA_HEADLESS_MAXMEM:
|
||||
process.env.GHIDRA_HEADLESS_MAXMEM || process.env.GHIDRA_MAXMEM || "1G",
|
||||
REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS: String(RECOVERY_SECONDS),
|
||||
REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS: String(LIMIT_SECONDS),
|
||||
};
|
||||
const settings = new AnalysisSettings(environment);
|
||||
const config = value(parseConfig(environment));
|
||||
const config = parseConfig(environment);
|
||||
if (!config.ok) throw config.error;
|
||||
assert.equal(config.value.ghidraDecompileTimeoutSeconds, LIMIT_SECONDS);
|
||||
const installation = await inspectVerifierGhidraInstallation({
|
||||
environment,
|
||||
installDir: config.ghidraInstallDir,
|
||||
javaHome: config.ghidraJavaHome,
|
||||
installDir: config.value.ghidraInstallDir,
|
||||
javaHome: config.value.ghidraJavaHome,
|
||||
});
|
||||
assert.equal(installation.status, "available", JSON.stringify(installation));
|
||||
assert.equal(installation.providerVersion, SUPPORTED_GHIDRA_VERSION);
|
||||
|
||||
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const fixtureSource = () => {
|
||||
const lines = [...FIXTURE_PREFIX];
|
||||
for (let index = 0; index < BRANCHES; index += 1)
|
||||
lines.push(
|
||||
` if (rea_state[${index % 64}] & ${(1 << (index % 31)) >>> 0}u) a = a * 31u + ${index}u; else a ^= rea_state[${(index * 7) % 64}];`,
|
||||
);
|
||||
lines.push(...FIXTURE_SUFFIX);
|
||||
const source = lines.join("\n");
|
||||
assert.ok(Buffer.byteLength(source) <= FIXTURE_SOURCE_BYTES);
|
||||
return source;
|
||||
};
|
||||
|
||||
const workspace = await mkdtemp(
|
||||
join(tmpdir(), "rea-ghidra-decompile-timeout-"),
|
||||
);
|
||||
const began = performance.now();
|
||||
const controller = new AbortController();
|
||||
const outerDeadline = setTimeout(
|
||||
() => controller.abort(new Error("Ghidra verifier exceeded 180000 ms")),
|
||||
180_000,
|
||||
);
|
||||
outerDeadline.unref();
|
||||
let cleaned = true;
|
||||
let report;
|
||||
try {
|
||||
@@ -148,11 +124,7 @@ try {
|
||||
try {
|
||||
await runOwnedCommand(
|
||||
{ ...compiler, runId: run.run_id, expectedCommand: null },
|
||||
{
|
||||
timeoutMs: 30_000,
|
||||
diagnosticBytes: 64 * 1024,
|
||||
signal: controller.signal,
|
||||
},
|
||||
{ timeoutMs: 300_000, diagnosticBytes: 1024 * 1024 },
|
||||
);
|
||||
cleaned = true;
|
||||
} catch (cause) {
|
||||
@@ -161,13 +133,17 @@ try {
|
||||
throw cause;
|
||||
}
|
||||
const targetDigest = digest(await readFile(targetPath));
|
||||
|
||||
// One real provider session over the read-only authenticated TCP bridge.
|
||||
let bridgeClient;
|
||||
const provider = new GhidraProvider(
|
||||
runtimeAnalysisConfig(config, settings.read),
|
||||
runtimeAnalysisConfig(config.value, settings.read),
|
||||
silentLogger,
|
||||
environment,
|
||||
undefined,
|
||||
(options) => {
|
||||
// This verifier has no caller-selected run ID: each new launch receives
|
||||
// a fresh owner-generated identity, so equality detects session restarts.
|
||||
assert.equal(options.runId, undefined);
|
||||
bridgeClient = new GhidraClient({
|
||||
...options,
|
||||
@@ -176,196 +152,113 @@ try {
|
||||
return bridgeClient;
|
||||
},
|
||||
);
|
||||
const target = value(await parseBinaryTarget(targetPath));
|
||||
const profile = value(await provider.resolveAnalysisProfile(target));
|
||||
assert.ok(profile.profile);
|
||||
const client = provider.createClient(target, profile.profile);
|
||||
const execute = (operation, input) =>
|
||||
settings.run(() =>
|
||||
client.execute(operation, input, { signal: controller.signal }),
|
||||
);
|
||||
const update = (changes) =>
|
||||
value(
|
||||
settings.update({
|
||||
changes: changes.map(([setting, selected]) => ({
|
||||
setting,
|
||||
action: "set",
|
||||
value: selected,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
const observed = { stages: [] };
|
||||
const record = (stage, facts) => observed.stages.push({ stage, ...facts });
|
||||
const failures = [];
|
||||
const target = await parseBinaryTarget(targetPath);
|
||||
if (!target.ok) throw target.error;
|
||||
const profile = await provider.resolveAnalysisProfile(target.value);
|
||||
if (!profile.ok) throw profile.error;
|
||||
assert.ok(profile.value.profile);
|
||||
const client = provider.createClient(target.value, profile.value.profile);
|
||||
cleaned = false;
|
||||
const failures = [];
|
||||
const observed = {};
|
||||
try {
|
||||
const listed = value(await execute("list_procedures", {}));
|
||||
// Mach-O symbols carry a leading underscore; address the functions by
|
||||
// their entry instead of guessing the recovered name.
|
||||
const listed = await client.execute("list_procedures", {});
|
||||
if (!listed.ok) throw listed.error;
|
||||
const entry = (name) => {
|
||||
const found = listed.result.find(
|
||||
const found = listed.value.result.find(
|
||||
(item) => item.value === name || item.value === `_${name}`,
|
||||
);
|
||||
assert.ok(found, `Missing fixture function ${name}`);
|
||||
return found.address;
|
||||
};
|
||||
const smallEntry = entry("rea_small");
|
||||
const slowEntry = entry("rea_slow");
|
||||
const initial = value(
|
||||
await bridgeClient.ping({ signal: controller.signal }),
|
||||
);
|
||||
assert.equal(initial.target.sha256, targetDigest);
|
||||
observed.initial_session_run_id = initial.run_id;
|
||||
const flow = async () =>
|
||||
value(await execute("analyze_function", { procedure: smallEntry })).result
|
||||
.native_value_flow;
|
||||
|
||||
update([["ghidra.value_flow_max_operations", 1]]);
|
||||
const operationsLow = await flow();
|
||||
assert.equal(operationsLow.operations.length, 1);
|
||||
assert.ok(operationsLow.limits_reached.includes("max_operations"));
|
||||
record("operations_low", flowSummary(operationsLow));
|
||||
update([["ghidra.value_flow_max_operations", 3000]]);
|
||||
const complete = await flow();
|
||||
assert.equal(complete.truncated, false);
|
||||
assert.ok(complete.operations.length > 2);
|
||||
record("operations_recovered", flowSummary(complete));
|
||||
|
||||
update([["ghidra.value_flow_max_inputs_per_operation", 1]]);
|
||||
const inputsLow = await flow();
|
||||
assert.ok(inputsLow.known_omitted_inputs > 0);
|
||||
assert.ok(inputsLow.limits_reached.includes("max_inputs_per_operation"));
|
||||
record("inputs_low", flowSummary(inputsLow));
|
||||
update([["ghidra.value_flow_max_inputs_per_operation", 64]]);
|
||||
const inputsRecovered = await flow();
|
||||
assert.equal(inputsRecovered.truncated, false);
|
||||
record("inputs_recovered", flowSummary(inputsRecovered));
|
||||
|
||||
update([["ghidra.value_flow_max_edges", 1]]);
|
||||
const edgesLow = await flow();
|
||||
assert.ok(edgesLow.known_omitted_edges > 0);
|
||||
assert.ok(edgesLow.limits_reached.includes("max_edges"));
|
||||
record("edges_low", flowSummary(edgesLow));
|
||||
update([["ghidra.value_flow_max_edges", 12000]]);
|
||||
const edgesRecovered = await flow();
|
||||
assert.equal(edgesRecovered.truncated, false);
|
||||
assert.ok(edgesRecovered.def_use.length > 1);
|
||||
record("edges_recovered", flowSummary(edgesRecovered));
|
||||
|
||||
update([["ghidra.value_flow_max_operations", 2]]);
|
||||
const lowered = await flow();
|
||||
assert.equal(lowered.operations.length, 2);
|
||||
assert.ok(lowered.limits_reached.includes("max_operations"));
|
||||
record("operations_lowered", flowSummary(lowered));
|
||||
update([
|
||||
["ghidra.value_flow_max_operations", 3000],
|
||||
["ghidra.decompile_max_payload_mib", 1],
|
||||
]);
|
||||
|
||||
const smallEntry = entry("rea_small");
|
||||
assert.ok(bridgeClient);
|
||||
const initialSession = await bridgeClient.ping();
|
||||
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)`;
|
||||
let started = performance.now();
|
||||
const payloadLow = await execute("procedure_pseudo_code", {
|
||||
const slow = await client.execute("analyze_function", {
|
||||
procedure: slowEntry,
|
||||
});
|
||||
assert.equal(
|
||||
payloadLow.ok,
|
||||
false,
|
||||
"Fixture did not exhaust the 1 MiB payload allowance; calibrate REA_DECOMPILE_TIMEOUT_STATEMENTS rather than reporting a provider failure.",
|
||||
);
|
||||
const payloadError = projectAnalysisError(payloadLow.error);
|
||||
observed.analyze_seconds = (performance.now() - started) / 1000;
|
||||
if (!slow.ok) throw slow.error;
|
||||
const dossier = slow.value.result;
|
||||
assert.equal(dossier.facet_availability.pseudocode.available, false);
|
||||
assert.ok(
|
||||
payloadError.message.includes("Response buffer size exceeded"),
|
||||
payloadError.message,
|
||||
dossier.facet_availability.pseudocode.reason.endsWith(limitText),
|
||||
dossier.facet_availability.pseudocode.reason,
|
||||
);
|
||||
assert.ok(
|
||||
payloadError.message.includes("ghidra.decompile_max_payload_mib"),
|
||||
);
|
||||
record("payload_1_mib", {
|
||||
seconds: (performance.now() - started) / 1000,
|
||||
code: payloadError.code,
|
||||
message: payloadError.message,
|
||||
});
|
||||
update([["ghidra.decompile_max_payload_mib", 64]]);
|
||||
started = performance.now();
|
||||
const payloadRecovered = value(
|
||||
await execute("procedure_pseudo_code", { procedure: slowEntry }),
|
||||
);
|
||||
assert.ok(String(payloadRecovered.result).includes("rea_state"));
|
||||
record("payload_64_mib_recovered", {
|
||||
seconds: (performance.now() - started) / 1000,
|
||||
pseudocode_bytes: Buffer.byteLength(String(payloadRecovered.result)),
|
||||
});
|
||||
|
||||
update([["ghidra.decompile_timeout_seconds", LIMIT_SECONDS]]);
|
||||
const limitText = `exceeded the ${LIMIT_SECONDS}-second configured decompilation limit`;
|
||||
started = performance.now();
|
||||
const degraded = value(
|
||||
await execute("analyze_function", { procedure: slowEntry }),
|
||||
);
|
||||
const dossier = degraded.result;
|
||||
assert.equal(
|
||||
dossier.facet_availability.pseudocode.available,
|
||||
false,
|
||||
"Fixture completed within the one-second native deadline; calibrate REA_DECOMPILE_TIMEOUT_STATEMENTS rather than reporting a provider failure.",
|
||||
);
|
||||
assert.ok(dossier.facet_availability.pseudocode.reason.includes(limitText));
|
||||
assert.equal(dossier.native_api.available, false);
|
||||
assert.equal(dossier.native_value_flow.available, false);
|
||||
assert.ok(dossier.assembly.length > STATEMENTS);
|
||||
assert.ok(dossier.assembly.length > BRANCHES);
|
||||
assert.ok(dossier.basic_blocks.length > 0);
|
||||
assert.equal(degraded.cachePolicy, "live");
|
||||
record("deadline_partial_dossier", {
|
||||
seconds: (performance.now() - started) / 1000,
|
||||
assert.equal(slow.value.cachePolicy, "live");
|
||||
observed.degraded = {
|
||||
pseudocode: dossier.facet_availability.pseudocode,
|
||||
assembly_lines: dossier.assembly.length,
|
||||
basic_blocks: dossier.basic_blocks.length,
|
||||
cache_policy: degraded.cachePolicy,
|
||||
});
|
||||
cache_policy: slow.value.cachePolicy,
|
||||
};
|
||||
|
||||
started = performance.now();
|
||||
const timedOut = await execute("procedure_pseudo_code", {
|
||||
const pseudocode = await client.execute("procedure_pseudo_code", {
|
||||
procedure: slowEntry,
|
||||
});
|
||||
assert.equal(timedOut.ok, false);
|
||||
assert.equal(timedOut.error._tag, "AnalysisTimeoutError");
|
||||
const timeout = projectAnalysisError(timedOut.error);
|
||||
assert.equal(timeout.code, "provider_timeout");
|
||||
assert.equal(timeout.retryable, true);
|
||||
assert.equal(timeout.details.timeout_ms, 1000);
|
||||
assert.equal(
|
||||
timeout.details.limit.setting,
|
||||
"ghidra.decompile_timeout_seconds",
|
||||
observed.pseudocode_seconds = (performance.now() - started) / 1000;
|
||||
assert.equal(pseudocode.ok, false);
|
||||
const projected = projectAnalysisError(pseudocode.error);
|
||||
assert.equal(pseudocode.error._tag, "AnalysisTimeoutError");
|
||||
assert.equal(projected.code, "provider_timeout");
|
||||
assert.equal(projected.retryable, true);
|
||||
assert.equal(projected.details.timeout_ms, LIMIT_SECONDS * 1000);
|
||||
assert.ok(projected.message.includes(limitText), projected.message);
|
||||
assert.ok(
|
||||
projected.remediation.action.includes(
|
||||
"REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS",
|
||||
),
|
||||
);
|
||||
assert.equal(timeout.details.limit.scope, "rea-runtime");
|
||||
assert.ok(timeout.message.includes(limitText), timeout.message);
|
||||
record("deadline_pseudocode", {
|
||||
seconds: (performance.now() - started) / 1000,
|
||||
code: timeout.code,
|
||||
message: timeout.message,
|
||||
limit: timeout.details.limit,
|
||||
remediation: timeout.remediation,
|
||||
});
|
||||
observed.pseudocode_error = {
|
||||
code: projected.code,
|
||||
retryable: projected.retryable,
|
||||
message: projected.message,
|
||||
};
|
||||
|
||||
update([
|
||||
["ghidra.decompile_timeout_seconds", RECOVERY_SECONDS],
|
||||
["ghidra.decompile_max_payload_mib", null],
|
||||
]);
|
||||
started = performance.now();
|
||||
const recovered = value(
|
||||
await execute("procedure_pseudo_code", { procedure: slowEntry }),
|
||||
);
|
||||
assert.ok(String(recovered.result).includes("rea_state"));
|
||||
record("deadline_recovered_payload_null", {
|
||||
seconds: (performance.now() - started) / 1000,
|
||||
selected_timeout_seconds: RECOVERY_SECONDS,
|
||||
selected_payload_mib: null,
|
||||
pseudocode_bytes: Buffer.byteLength(String(recovered.result)),
|
||||
const changed = settings.update({
|
||||
changes: [
|
||||
{
|
||||
setting: "ghidra.decompile_timeout_seconds",
|
||||
action: "set",
|
||||
value: 60,
|
||||
},
|
||||
],
|
||||
});
|
||||
const small = value(
|
||||
await execute("procedure_pseudo_code", { procedure: smallEntry }),
|
||||
assert.equal(changed.ok, true);
|
||||
const recovered = await settings.run(() =>
|
||||
client.execute("procedure_pseudo_code", { procedure: slowEntry }),
|
||||
);
|
||||
assert.ok(String(small.result).includes("rea_state"));
|
||||
const final = value(await bridgeClient.ping({ signal: controller.signal }));
|
||||
assert.equal(final.run_id, initial.run_id);
|
||||
assert.equal(final.target.sha256, targetDigest);
|
||||
observed.final_session_run_id = final.run_id;
|
||||
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,
|
||||
});
|
||||
if (!small.ok) throw small.error;
|
||||
assert.ok(String(small.value.result).includes("rea_state"));
|
||||
const continuedSession = await bridgeClient.ping();
|
||||
if (!continuedSession.ok) throw continuedSession.error;
|
||||
assert.equal(continuedSession.value.run_id, initialSession.value.run_id);
|
||||
assert.equal(continuedSession.value.target.sha256, targetDigest);
|
||||
observed.final_session_run_id = continuedSession.value.run_id;
|
||||
observed.same_session_next_decompile = "succeeded";
|
||||
} catch (cause) {
|
||||
failures.push(cause);
|
||||
@@ -373,38 +266,33 @@ try {
|
||||
const closed = await client.close();
|
||||
if (!closed.ok) failures.push(closed.error);
|
||||
if (failures.length > 0)
|
||||
throw new AggregateError(
|
||||
failures,
|
||||
"Ghidra runtime budget verification failed",
|
||||
);
|
||||
throw new AggregateError(failures, "Decompile timeout verification failed");
|
||||
assert.equal(digest(await readFile(targetPath)), targetDigest);
|
||||
const verifierRun = await completeVerifierRun(run);
|
||||
assertVerifierCleanupComplete(verifierRun.process_lineage);
|
||||
try {
|
||||
assertVerifierCleanupComplete(verifierRun.process_lineage);
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Verifier cleanup unresolved; retained ${workspace}; run ${run.run_id}`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
cleaned = true;
|
||||
report = {
|
||||
status: "passed",
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
node: process.version,
|
||||
fixture: {
|
||||
kind: "volatile-straight-line",
|
||||
statements: STATEMENTS,
|
||||
source_bytes: Buffer.byteLength(source),
|
||||
},
|
||||
limit_seconds: LIMIT_SECONDS,
|
||||
branches: BRANCHES,
|
||||
target_sha256: targetDigest,
|
||||
source_sha256: digest(Buffer.from(source)),
|
||||
compiler,
|
||||
provider: profile.profile.provider,
|
||||
analysis_profile_digest: profile.profile.digest,
|
||||
configured_jvm_max_heap: environment.GHIDRA_HEADLESS_MAXMEM,
|
||||
elapsed_seconds: (performance.now() - began) / 1000,
|
||||
provider: profile.value.profile.provider,
|
||||
analysis_profile_digest: profile.value.profile.digest,
|
||||
...observed,
|
||||
verifier_run: verifierRun,
|
||||
scope:
|
||||
"Real Ghidra analysis of generated Mach-O/ELF through authenticated loopback: independent value-flow budgets, lowered admission, native payload exhaustion and recovery, natural decompile timeout with listing-backed partial facts, and deadline recovery on the same Program. No target execution. Windows and public CLI/MCP transport recovery are unverified. Timings are observations, not performance comparisons; CPU, peak process-family memory and agent tokens are unmeasured.",
|
||||
"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.",
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(outerDeadline);
|
||||
if (cleaned) await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
console.log(JSON.stringify({ ...report, cleanup: "complete" }));
|
||||
@@ -543,12 +543,10 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
|
||||
"readback-",
|
||||
);
|
||||
try {
|
||||
const written = "runtime-write-readback";
|
||||
const sourceBytes = await readFile(source);
|
||||
readbackRuntime.writeFile("written.txt", written);
|
||||
readbackRuntime.writeFile("written.txt", "runtime-write-readback");
|
||||
assert.equal(
|
||||
readbackRuntime.readFile("written.txt", Buffer.byteLength(written)),
|
||||
written,
|
||||
readbackRuntime.readFile("written.txt"),
|
||||
"runtime-write-readback",
|
||||
);
|
||||
const readbackSnapshot = await readbackRuntime.snapshot(
|
||||
source,
|
||||
@@ -556,19 +554,21 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
|
||||
);
|
||||
assert.equal(
|
||||
readbackSnapshot.sha256,
|
||||
createHash("sha256").update(sourceBytes).digest("hex"),
|
||||
createHash("sha256")
|
||||
.update(await readFile(source))
|
||||
.digest("hex"),
|
||||
);
|
||||
assert.equal(
|
||||
readbackRuntime.readFile("snapshot.bin", sourceBytes.byteLength),
|
||||
sourceBytes.toString("utf8"),
|
||||
readbackRuntime.readFile("snapshot.bin"),
|
||||
await readFile(source, "utf8"),
|
||||
);
|
||||
const writtenPath = join(readbackRuntime.observation.path, "written.txt");
|
||||
await assert.rejects(writeFile(writtenPath, "replace"));
|
||||
await assert.rejects(rename(writtenPath, `${writtenPath}.renamed`));
|
||||
await assert.rejects(rm(writtenPath));
|
||||
assert.equal(
|
||||
readbackRuntime.readFile("written.txt", Buffer.byteLength(written)),
|
||||
written,
|
||||
readbackRuntime.readFile("written.txt"),
|
||||
"runtime-write-readback",
|
||||
);
|
||||
const snapshotPath = join(
|
||||
readbackRuntime.observation.path,
|
||||
@@ -590,8 +590,8 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
|
||||
await assert.rejects(rename(snapshotPath, `${snapshotPath}.renamed`));
|
||||
await assert.rejects(rm(snapshotPath));
|
||||
assert.equal(
|
||||
readbackRuntime.readFile("snapshot.bin", sourceBytes.byteLength),
|
||||
sourceBytes.toString("utf8"),
|
||||
readbackRuntime.readFile("snapshot.bin"),
|
||||
await readFile(source, "utf8"),
|
||||
);
|
||||
report.controls = {
|
||||
...report.controls,
|
||||
@@ -622,11 +622,7 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
|
||||
assert.equal(await exists(heldPath), true);
|
||||
assert.throws(
|
||||
() => retryRuntime.writeFile("after-close.txt", "forbidden"),
|
||||
/Windows private runtime cleanup is in progress or complete/u,
|
||||
);
|
||||
assert.equal(
|
||||
await exists(join(retryRuntime.observation.path, "after-close.txt")),
|
||||
false,
|
||||
/cleanup is pending/u,
|
||||
);
|
||||
holder.child.stdin.end("\n");
|
||||
const [exitCode] = await holder.closed;
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
"""Observe one selected experiment's process group and descendant tree.
|
||||
|
||||
macOS has no cgroup peak counter. RSS is a sampled sum, not an exact kernel
|
||||
peak or aggregate memory hard limit. Only the owned group is stopped early;
|
||||
descendant-tree sampling is observational and does not confer signal authority.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def process_snapshot():
|
||||
rows = subprocess.run(
|
||||
["ps", "-axo", "pid=,ppid=,pgid=,rss="],
|
||||
text=True, capture_output=True, check=True,
|
||||
).stdout.splitlines()
|
||||
return [tuple(map(int, row.split())) for row in rows]
|
||||
|
||||
|
||||
def group_rss(group, snapshot=None):
|
||||
if snapshot is None:
|
||||
snapshot = process_snapshot()
|
||||
return [(pid, kib * 1024) for pid, _, pgid, kib in snapshot if pgid == group]
|
||||
|
||||
|
||||
def tree_rss(root, snapshot):
|
||||
children = {}
|
||||
for pid, ppid, _, _ in snapshot:
|
||||
children.setdefault(ppid, []).append(pid)
|
||||
pending = [root]
|
||||
descendants = set()
|
||||
while pending:
|
||||
pid = pending.pop()
|
||||
if pid not in descendants:
|
||||
descendants.add(pid)
|
||||
pending.extend(children.get(pid, []))
|
||||
return [(pid, kib * 1024) for pid, _, _, kib in snapshot if pid in descendants]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", required=True)
|
||||
parser.add_argument("--cwd", required=True)
|
||||
parser.add_argument("--timeout-seconds", type=float, default=90)
|
||||
parser.add_argument("--memory-mib", type=int, default=1536)
|
||||
parser.add_argument("command", nargs=argparse.REMAINDER)
|
||||
options = parser.parse_args()
|
||||
command = options.command
|
||||
if command[:1] == ["--"]:
|
||||
command = command[1:]
|
||||
if not command or options.timeout_seconds <= 0 or options.memory_mib <= 0:
|
||||
parser.error("Select an argv target and positive observation bounds")
|
||||
output = Path(options.output).resolve()
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
stdout_path = output.with_suffix(".stdout")
|
||||
stderr_path = output.with_suffix(".stderr")
|
||||
peak = 0
|
||||
maximum_members = 0
|
||||
tree_peak = 0
|
||||
maximum_tree_members = 0
|
||||
samples = 0
|
||||
stopped = None
|
||||
usage = None
|
||||
status = None
|
||||
signal_errors = []
|
||||
observer_error = None
|
||||
started = time.monotonic()
|
||||
with stdout_path.open("wb") as stdout, stderr_path.open("wb") as stderr:
|
||||
process = subprocess.Popen(
|
||||
command, cwd=options.cwd, stdout=stdout, stderr=stderr,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
def reap():
|
||||
nonlocal status, usage
|
||||
if process.returncode is not None:
|
||||
return True
|
||||
pid, observed_status, observed_usage = os.wait4(process.pid, os.WNOHANG)
|
||||
if not pid:
|
||||
return False
|
||||
status, usage = observed_status, observed_usage
|
||||
process.returncode = os.waitstatus_to_exitcode(status)
|
||||
return True
|
||||
|
||||
def root_exited():
|
||||
# Keep the session leader waitable until group cleanup finishes.
|
||||
# Its reserved PID prevents our process-group identity being reused.
|
||||
return os.waitid(
|
||||
os.P_PID, process.pid, os.WEXITED | os.WNOHANG | os.WNOWAIT,
|
||||
) is not None
|
||||
|
||||
def group_has_work():
|
||||
exited = root_exited()
|
||||
return any(
|
||||
pid != process.pid or not exited
|
||||
for pid, _ in group_rss(process.pid)
|
||||
)
|
||||
|
||||
def signal_owned_group(selected_signal):
|
||||
# A sample and the following signal are not atomic. A completed
|
||||
# root may still own live children. Do not reap it before their
|
||||
# cleanup, and preserve receipts when the OS refuses a signal.
|
||||
if not group_has_work():
|
||||
return
|
||||
try:
|
||||
os.killpg(process.pid, selected_signal)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
except OSError as error:
|
||||
signal_errors.append({
|
||||
"signal": selected_signal.name,
|
||||
"errno": error.errno,
|
||||
"reason": str(error),
|
||||
})
|
||||
|
||||
def stop_owned_group():
|
||||
signal_owned_group(signal.SIGTERM)
|
||||
time.sleep(0.2)
|
||||
signal_owned_group(signal.SIGKILL)
|
||||
deadline = time.monotonic() + 1
|
||||
while group_has_work() and time.monotonic() < deadline:
|
||||
time.sleep(0.02)
|
||||
reap()
|
||||
|
||||
try:
|
||||
while True:
|
||||
if root_exited():
|
||||
if group_has_work():
|
||||
stopped = "target_exited_with_group_members"
|
||||
stop_owned_group()
|
||||
else:
|
||||
reap()
|
||||
break
|
||||
# One OS snapshot supplies both scopes. Ancestry is an
|
||||
# observation, never permission to signal detached children.
|
||||
snapshot = process_snapshot()
|
||||
members = group_rss(process.pid, snapshot)
|
||||
tree_members = tree_rss(process.pid, snapshot)
|
||||
samples += 1
|
||||
peak = max(peak, sum(rss for _, rss in members))
|
||||
maximum_members = max(maximum_members, len(members))
|
||||
tree_peak = max(tree_peak, sum(rss for _, rss in tree_members))
|
||||
maximum_tree_members = max(maximum_tree_members, len(tree_members))
|
||||
elapsed = time.monotonic() - started
|
||||
if peak > options.memory_mib * 1024 * 1024:
|
||||
stopped = "sampled_group_rss_early_stop"
|
||||
elif elapsed > options.timeout_seconds:
|
||||
stopped = "wall_deadline"
|
||||
if stopped:
|
||||
stop_owned_group()
|
||||
break
|
||||
time.sleep(0.02)
|
||||
except BaseException as error:
|
||||
observer_error = {"type": type(error).__name__, "reason": str(error)}
|
||||
stopped = stopped or "observer_error"
|
||||
stop_owned_group()
|
||||
remaining = group_rss(process.pid)
|
||||
receipt = {
|
||||
"schema": "rea-budget-calibration/v1",
|
||||
"argv": command,
|
||||
"cwd": options.cwd,
|
||||
"platform": platform.platform(),
|
||||
"elapsed_seconds": time.monotonic() - started,
|
||||
"cpu_seconds_waited_subtree": usage.ru_utime + usage.ru_stime if usage else None,
|
||||
"peak_process_group_rss_bytes_sampled": peak,
|
||||
"maximum_group_members_sampled": maximum_members,
|
||||
"peak_process_tree_rss_bytes_sampled": tree_peak,
|
||||
"maximum_tree_members_sampled": maximum_tree_members,
|
||||
"sample_count": samples,
|
||||
"sample_interval_seconds": 0.02,
|
||||
"memory_early_stop_bytes": options.memory_mib * 1024 * 1024,
|
||||
"memory_early_stop_scope": "owned_process_group",
|
||||
"wall_deadline_seconds": options.timeout_seconds,
|
||||
"wall_deadline_signal_scope": "owned_process_group",
|
||||
"stop_reason": stopped,
|
||||
"signal_errors": signal_errors,
|
||||
"observer_error": observer_error,
|
||||
"exit_code": process.returncode,
|
||||
"cleanup": "group_empty" if not remaining else "group_members_remaining",
|
||||
"cleanup_scope": "owned_process_group",
|
||||
"remaining_group_pids": [pid for pid, _ in remaining],
|
||||
"stdout": str(stdout_path),
|
||||
"stderr": str(stderr_path),
|
||||
"stdout_sha256": hashlib.sha256(stdout_path.read_bytes()).hexdigest(),
|
||||
"stderr_sha256": hashlib.sha256(stderr_path.read_bytes()).hexdigest(),
|
||||
"limitations": [
|
||||
"RSS scopes are simultaneous sums within each ps snapshot; short peaks can be missed.",
|
||||
"Shared pages can be counted in multiple members' RSS.",
|
||||
"Group RSS excludes descendants that leave this group; tree RSS includes detached descendants while PPID ancestry remains linked to the waitable root.",
|
||||
"Tree samples can miss exited, reparented, or orphaned descendants; no cumulative PID list or descendant signal authority is inferred.",
|
||||
"CPU is OS wait4 user+system usage of the waited subtree; sampler CPU is excluded.",
|
||||
"CPU excludes descendants the target did not wait for, including orphaned children.",
|
||||
"Memory early stop and cleanup signals apply only to the owned group, not the observed tree; neither RSS metric is a kernel hard limit.",
|
||||
"Wall time includes observation overhead; OS caches are not flushed.",
|
||||
],
|
||||
}
|
||||
output.write_text(json.dumps(receipt, indent=2) + "\n")
|
||||
print(json.dumps({key: receipt[key] for key in [
|
||||
"elapsed_seconds", "cpu_seconds_waited_subtree",
|
||||
"peak_process_group_rss_bytes_sampled", "exit_code", "cleanup", "stop_reason",
|
||||
"peak_process_tree_rss_bytes_sampled",
|
||||
]}))
|
||||
return process.returncode if not stopped and not remaining and process.returncode is not None else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -1,45 +0,0 @@
|
||||
"""Join serial observer receipts with exact tokenization of every logical call.
|
||||
|
||||
Requires tiktoken 0.12.0 for comparison with the retained measurements.
|
||||
Token counts exclude chat framing, catalogs, adapters, retained delivery and
|
||||
model reasoning. Artifact and repository path spelling can affect tokens.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import tiktoken
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('receipts', type=Path)
|
||||
parser.add_argument('output', type=Path)
|
||||
options = parser.parse_args()
|
||||
encoding = tiktoken.get_encoding('cl100k_base')
|
||||
rows = []
|
||||
for path in sorted(options.receipts.rglob('*.metrics.json')):
|
||||
metrics = json.loads(path.read_text())
|
||||
key = str(path.relative_to(options.receipts)).removesuffix('.metrics.json')
|
||||
# The observer argv ends with the selected configuration file.
|
||||
config = json.loads(Path(metrics['argv'][-1]).read_text())
|
||||
reply = json.loads(Path(config['reply_receipt']).read_text())
|
||||
assert metrics['exit_code'] == 0 and metrics['stop_reason'] is None, path
|
||||
assert metrics['cleanup'] == 'group_empty' and reply['success'], path
|
||||
tokens = sum(
|
||||
len(encoding.encode(json.dumps(call[field], ensure_ascii=False, separators=(',', ':'))))
|
||||
for call in reply['calls']
|
||||
for field in ['request', 'reply']
|
||||
)
|
||||
rows.append({
|
||||
'key': key,
|
||||
'calls': reply['call_count'],
|
||||
'tokens_cl100k_base': tokens,
|
||||
'seconds': metrics['elapsed_seconds'],
|
||||
'cpu': metrics['cpu_seconds_waited_subtree'],
|
||||
'peak_mib': metrics['peak_process_group_rss_bytes_sampled'] / 1024**2,
|
||||
'delivered': reply['package_uris_delivered_final'],
|
||||
'cleanup': metrics['cleanup'],
|
||||
'scope': reply['boundary'],
|
||||
})
|
||||
assert rows, 'No *.metrics.json receipts found'
|
||||
options.output.write_text(json.dumps(rows, indent=2) + '\n')
|
||||
@@ -1,37 +0,0 @@
|
||||
"""Generate eight task configs after oracle.py has verified the pinned APKs."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('inputs', type=Path)
|
||||
parser.add_argument('output', type=Path)
|
||||
options = parser.parse_args()
|
||||
options.output.mkdir(parents=True, exist_ok=True)
|
||||
manifest = json.loads((Path(__file__).parent / 'manifest.json').read_text())
|
||||
cases = [
|
||||
('inventory-default-retry', 'package-inventory', None, True),
|
||||
('inventory-2048', 'package-inventory', 2048, False),
|
||||
('overview-default', 'overview', None, False),
|
||||
('overview-2048', 'overview', 2048, False),
|
||||
]
|
||||
for item in manifest['artifacts']:
|
||||
stem = Path(item['file']).stem
|
||||
oracle = (options.output / f'{stem}.oracle.json').resolve()
|
||||
if not oracle.exists():
|
||||
parser.error(f'Run oracle.py first: {oracle}')
|
||||
for case, task, limit, recover in cases:
|
||||
config = {
|
||||
'artifact': str((options.inputs / item['file']).resolve()),
|
||||
'oracle': str(oracle),
|
||||
'task': task,
|
||||
'recover': recover,
|
||||
'recovery_uri_limit': 2048,
|
||||
'reply_receipt': str((options.output / f'{stem}.{case}.replies.json').resolve()),
|
||||
}
|
||||
if limit is not None:
|
||||
config['first_uri_limit'] = limit
|
||||
(options.output / f'{stem}.{case}.config.json').write_text(
|
||||
json.dumps(config, indent=2) + '\n'
|
||||
)
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"artifacts": [
|
||||
{
|
||||
"file": "flutter_gallery_Android.apk",
|
||||
"url": "https://github.com/flutter-team-archive/gallery/releases/download/v2.9.2/flutter_gallery_Android.apk",
|
||||
"bytes": 117817550,
|
||||
"sha256": "659b5fc6a4319dfe4f117d00ad4e7ec004915ebcadcb96628797fb3544040e29",
|
||||
"expected_libapp_sha256": "0ae30443b66dcf579d024d2f121f77204639bda9897116a8b2f66a6385189fd9",
|
||||
"expected_package_count_1024": 756,
|
||||
"expected_dart_count_1024": 201,
|
||||
"expected_isolate_section": {
|
||||
"name": "_kDartIsolateSnapshotData",
|
||||
"offset": 10420224,
|
||||
"size": 14263200
|
||||
}
|
||||
},
|
||||
{
|
||||
"file": "LocalSend-1.18.2-android-arm64v8.apk",
|
||||
"url": "https://github.com/localsend/localsend/releases/download/v1.18.2/LocalSend-1.18.2-android-arm64v8.apk",
|
||||
"bytes": 46558706,
|
||||
"sha256": "82ec3568fba2aa5295b9aae8b76f701d7a4703d86b9f8bad749472038fbaeab3",
|
||||
"expected_libapp_sha256": "8f46efd1f78d65ebc0ff630655f274f3c36be89aa13316fb7322a3dda8a497d6",
|
||||
"expected_package_count_1024": 1407,
|
||||
"expected_dart_count_1024": 216,
|
||||
"expected_isolate_section": {
|
||||
"name": "_kDartIsolateSnapshotData",
|
||||
"offset": 19264,
|
||||
"size": 6742144
|
||||
}
|
||||
}
|
||||
],
|
||||
"supporting_artifacts": [
|
||||
{
|
||||
"file": "qgis-world-map.gpkg",
|
||||
"url": "https://raw.githubusercontent.com/qgis/QGIS/e1223b8e9e71fc50ee995ed556f711c54ccc101b/resources/data/world_map.gpkg",
|
||||
"bytes": 12914688,
|
||||
"sha256": "01d293159d1b77bd9212b5252587853f61aa9e25b2387135f34afaba8d057190",
|
||||
"independent_sqlite_row_counts": {
|
||||
"declared_tables_including_virtual_and_shadow": 26,
|
||||
"countries": 240,
|
||||
"states_provinces": 4556,
|
||||
"disputed_borders": 46,
|
||||
"layer_styles": 4
|
||||
},
|
||||
"role": "Supporting counts only, not a new timed desktop calibration"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
{
|
||||
"schema": "rea-flutter-budget-calibration/v1",
|
||||
"baseline": {
|
||||
"head": "e2a2ede5440df396e44b3406ef511a31a680290e",
|
||||
"tracked_patch_sha256": "038b69c19bbd79a303c9ecd8d9d22a1251637e3886543ca4f5a4c1b70c724eec",
|
||||
"description": "Frozen in-progress source snapshot; HEAD alone does not identify this runtime."
|
||||
},
|
||||
"host": "macOS-26.3-arm64-arm-64bit-Mach-O",
|
||||
"node": "24.22.1",
|
||||
"driver_original_sha256": "b696f10ddef5bab2dc966587bf362a4f93e7a5c662db1f4c12d107b090e78cba",
|
||||
"oracle_original_sha256": "82e7e68bea71929ebfce3e607d13bea88fb6bd0259892bfa3655d2cd17a1a49a",
|
||||
"tokenizer": {
|
||||
"encoding": "cl100k_base",
|
||||
"tiktoken_version": "0.12.0",
|
||||
"scope": "logical request and reply JSON, including retries; no chat framing/catalog/adapters/model usage",
|
||||
"serialization": "json.dumps(call[field], ensure_ascii=False, separators=(',', ':')) for request and reply separately on every call"
|
||||
},
|
||||
"method": {
|
||||
"trials": 3,
|
||||
"cases": 8,
|
||||
"cache": "fresh process per case; OS caches not flushed; no cold-cache or warm-runtime-cache claims",
|
||||
"process_group_rss_sample_seconds": 0.02,
|
||||
"cpu": "wait4 waited-subtree user+system; sampler excluded",
|
||||
"memory_early_stop_mib": 1536,
|
||||
"wall_deadline_seconds": 90,
|
||||
"memory_limit_note": "sampled early-stop threshold, not kernel hard limit"
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"key": "r1-LocalSend-1.18.2-android-arm64v8.inventory-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 16317,
|
||||
"seconds": 0.299053416994866,
|
||||
"cpu": 0.374338,
|
||||
"peak_mib": 184.78125,
|
||||
"delivered": 1407,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r1-LocalSend-1.18.2-android-arm64v8.inventory-default-retry",
|
||||
"calls": 2,
|
||||
"tokens_cl100k_base": 22360,
|
||||
"seconds": 0.40635258400288876,
|
||||
"cpu": 0.515262,
|
||||
"peak_mib": 221.328125,
|
||||
"delivered": 1407,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r1-LocalSend-1.18.2-android-arm64v8.overview-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 27158,
|
||||
"seconds": 0.29257937498914544,
|
||||
"cpu": 0.388501,
|
||||
"peak_mib": 189.515625,
|
||||
"delivered": 1407,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r1-LocalSend-1.18.2-android-arm64v8.overview-default",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 16885,
|
||||
"seconds": 0.2934171250090003,
|
||||
"cpu": 0.413556,
|
||||
"peak_mib": 189.625,
|
||||
"delivered": 500,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r1-flutter_gallery_Android.inventory-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 8631,
|
||||
"seconds": 0.5524000000004889,
|
||||
"cpu": 0.598673,
|
||||
"peak_mib": 238.859375,
|
||||
"delivered": 756,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r1-flutter_gallery_Android.inventory-default-retry",
|
||||
"calls": 2,
|
||||
"tokens_cl100k_base": 14488,
|
||||
"seconds": 1.01431600000069,
|
||||
"cpu": 1.250315,
|
||||
"peak_mib": 299.453125,
|
||||
"delivered": 756,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r1-flutter_gallery_Android.overview-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 18764,
|
||||
"seconds": 0.5649452920042677,
|
||||
"cpu": 0.639156,
|
||||
"peak_mib": 243.328125,
|
||||
"delivered": 756,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r1-flutter_gallery_Android.overview-default",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 15995,
|
||||
"seconds": 0.5576133750000736,
|
||||
"cpu": 0.6388879999999999,
|
||||
"peak_mib": 240.71875,
|
||||
"delivered": 500,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r2-LocalSend-1.18.2-android-arm64v8.inventory-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 16317,
|
||||
"seconds": 0.2958210000069812,
|
||||
"cpu": 0.36518599999999996,
|
||||
"peak_mib": 183.703125,
|
||||
"delivered": 1407,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r2-LocalSend-1.18.2-android-arm64v8.inventory-default-retry",
|
||||
"calls": 2,
|
||||
"tokens_cl100k_base": 22360,
|
||||
"seconds": 0.4046505839942256,
|
||||
"cpu": 0.530767,
|
||||
"peak_mib": 216.390625,
|
||||
"delivered": 1407,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r2-LocalSend-1.18.2-android-arm64v8.overview-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 27158,
|
||||
"seconds": 0.3230182090046583,
|
||||
"cpu": 0.38625,
|
||||
"peak_mib": 189.640625,
|
||||
"delivered": 1407,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r2-LocalSend-1.18.2-android-arm64v8.overview-default",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 16885,
|
||||
"seconds": 0.289660875001573,
|
||||
"cpu": 0.400553,
|
||||
"peak_mib": 189.765625,
|
||||
"delivered": 500,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r2-flutter_gallery_Android.inventory-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 8631,
|
||||
"seconds": 0.47969675000058487,
|
||||
"cpu": 0.606711,
|
||||
"peak_mib": 241.25,
|
||||
"delivered": 756,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r2-flutter_gallery_Android.inventory-default-retry",
|
||||
"calls": 2,
|
||||
"tokens_cl100k_base": 14488,
|
||||
"seconds": 0.8361371669889195,
|
||||
"cpu": 1.028465,
|
||||
"peak_mib": 258.65625,
|
||||
"delivered": 756,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r2-flutter_gallery_Android.overview-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 18764,
|
||||
"seconds": 0.5597699580102926,
|
||||
"cpu": 0.633868,
|
||||
"peak_mib": 241.53125,
|
||||
"delivered": 756,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r2-flutter_gallery_Android.overview-default",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 15995,
|
||||
"seconds": 0.4476122909982223,
|
||||
"cpu": 0.641888,
|
||||
"peak_mib": 245.84375,
|
||||
"delivered": 500,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r3-LocalSend-1.18.2-android-arm64v8.inventory-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 16317,
|
||||
"seconds": 0.29069650000019465,
|
||||
"cpu": 0.362932,
|
||||
"peak_mib": 185.859375,
|
||||
"delivered": 1407,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r3-LocalSend-1.18.2-android-arm64v8.inventory-default-retry",
|
||||
"calls": 2,
|
||||
"tokens_cl100k_base": 22360,
|
||||
"seconds": 0.3999212499911664,
|
||||
"cpu": 0.519303,
|
||||
"peak_mib": 230.84375,
|
||||
"delivered": 1407,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r3-LocalSend-1.18.2-android-arm64v8.overview-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 27158,
|
||||
"seconds": 0.2895570419932483,
|
||||
"cpu": 0.388245,
|
||||
"peak_mib": 189.984375,
|
||||
"delivered": 1407,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r3-LocalSend-1.18.2-android-arm64v8.overview-default",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 16885,
|
||||
"seconds": 0.29197387500607874,
|
||||
"cpu": 0.402588,
|
||||
"peak_mib": 190.25,
|
||||
"delivered": 500,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r3-flutter_gallery_Android.inventory-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 8631,
|
||||
"seconds": 0.447617124998942,
|
||||
"cpu": 0.600641,
|
||||
"peak_mib": 238.6875,
|
||||
"delivered": 756,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r3-flutter_gallery_Android.inventory-default-retry",
|
||||
"calls": 2,
|
||||
"tokens_cl100k_base": 14488,
|
||||
"seconds": 0.8252949169982458,
|
||||
"cpu": 1.0068,
|
||||
"peak_mib": 254.765625,
|
||||
"delivered": 756,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r3-flutter_gallery_Android.overview-2048",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 18764,
|
||||
"seconds": 0.4492089169943938,
|
||||
"cpu": 0.6431779999999999,
|
||||
"peak_mib": 249.65625,
|
||||
"delivered": 756,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
},
|
||||
{
|
||||
"key": "r3-flutter_gallery_Android.overview-default",
|
||||
"calls": 1,
|
||||
"tokens_cl100k_base": 15995,
|
||||
"seconds": 0.44429433399636764,
|
||||
"cpu": 0.6304479999999999,
|
||||
"peak_mib": 241.921875,
|
||||
"delivered": 500,
|
||||
"cleanup": "group_empty",
|
||||
"scope": "shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
"""Independently enumerate bounded printable runs in two pinned Flutter APKs.
|
||||
|
||||
This intentionally small ELF64LE parser is for these verified artifacts, not a
|
||||
replacement for production parsing. It uses no REA implementation code.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import zipfile
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('inputs', type=Path)
|
||||
parser.add_argument('output', type=Path)
|
||||
options = parser.parse_args()
|
||||
options.output.mkdir(parents=True, exist_ok=True)
|
||||
manifest = json.loads((Path(__file__).parent / 'manifest.json').read_text())
|
||||
for item in manifest['artifacts']:
|
||||
apk = options.inputs / item['file']
|
||||
assert apk.stat().st_size == item['bytes'], apk
|
||||
with apk.open('rb') as source:
|
||||
artifact_digest = hashlib.file_digest(source, 'sha256').hexdigest()
|
||||
assert artifact_digest == item['sha256'], apk
|
||||
with zipfile.ZipFile(apk) as archive:
|
||||
data = archive.read('lib/arm64-v8a/libapp.so')
|
||||
assert data[:6] == b'\x7fELF\x02\x01', apk
|
||||
section_offset = struct.unpack_from('<Q', data, 40)[0]
|
||||
section_size, section_count = struct.unpack_from('<HH', data, 58)
|
||||
sections = [
|
||||
struct.unpack_from('<IIQQQQIIQQ', data, section_offset + i * section_size)
|
||||
for i in range(section_count)
|
||||
]
|
||||
found = []
|
||||
for section in sections:
|
||||
if section[1] != 11: # SHT_DYNSYM
|
||||
continue
|
||||
strings = sections[section[6]]
|
||||
names = data[strings[4]:strings[4] + strings[5]]
|
||||
for at in range(section[4], section[4] + section[5], section[9]):
|
||||
name, _, _, index, value, size = struct.unpack_from('<IBBHQQ', data, at)
|
||||
end = names.find(b'\0', name)
|
||||
if names[name:end] != b'_kDartIsolateSnapshotData':
|
||||
continue
|
||||
owner = sections[index]
|
||||
offset = owner[4] + value - owner[3]
|
||||
assert 0 <= offset <= len(data) - size
|
||||
found.append((offset, size))
|
||||
assert len(found) == 1, (apk, found)
|
||||
offset, size = found[0]
|
||||
runs = re.findall(rb'[\x20-\x7e]+', data[offset:offset + size])
|
||||
oracle = {
|
||||
'artifact': str(apk),
|
||||
'artifact_sha256': artifact_digest,
|
||||
'libapp_sha256': hashlib.sha256(data).hexdigest(),
|
||||
'abi': 'arm64-v8a',
|
||||
'section': {
|
||||
'name': '_kDartIsolateSnapshotData',
|
||||
'offset': offset,
|
||||
'size': size,
|
||||
},
|
||||
'oracle': 'Independent Python ELF DYNSYM/section mapping and byte regex; no REA parser used',
|
||||
'policies': {},
|
||||
}
|
||||
for run_bytes in [1024, 4096]:
|
||||
selected = [run for run in runs if 5 <= len(run) <= run_bytes]
|
||||
packages = sorted({run.decode('ascii') for run in selected if run.startswith(b'package:')})
|
||||
darts = sorted({run.decode('ascii') for run in selected if run.startswith(b'dart:')})
|
||||
oracle['policies'][str(run_bytes)] = {
|
||||
'package_uris': packages,
|
||||
'dart_uris': darts,
|
||||
'package_count': len(packages),
|
||||
'dart_count': len(darts),
|
||||
'longest_printable_run': max(map(len, runs)),
|
||||
'overflowed_runs': sum(len(run) > run_bytes for run in runs),
|
||||
}
|
||||
assert oracle['libapp_sha256'] == item['expected_libapp_sha256']
|
||||
assert oracle['section'] == item['expected_isolate_section']
|
||||
assert oracle['policies']['1024']['package_count'] == item['expected_package_count_1024']
|
||||
assert oracle['policies']['1024']['dart_count'] == item['expected_dart_count_1024']
|
||||
target = options.output / (apk.stem + '.oracle.json')
|
||||
target.write_text(json.dumps(oracle, indent=2) + '\n')
|
||||
print(json.dumps({
|
||||
'oracle': str(target),
|
||||
'artifact_sha256': artifact_digest,
|
||||
'libapp_sha256': oracle['libapp_sha256'],
|
||||
'section': oracle['section'],
|
||||
'policies': {
|
||||
key: {name: value for name, value in policy.items() if not isinstance(value, list)}
|
||||
for key, policy in oracle['policies'].items()
|
||||
},
|
||||
}))
|
||||
@@ -1,103 +0,0 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { join } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
const [repository, configPath] = process.argv.slice(2);
|
||||
const config = JSON.parse(await readFile(configPath, "utf8"));
|
||||
const { FlutterBuildAnalysisService } = await import(
|
||||
pathToFileURL(
|
||||
join(repository, "src/application/flutter/FlutterBuildAnalysisService.ts"),
|
||||
)
|
||||
);
|
||||
const { createFlutterBuildAnalysisProvider } = await import(
|
||||
pathToFileURL(join(repository, "src/composition/flutter.ts"))
|
||||
);
|
||||
const { estimateTokenCount } = await import(
|
||||
pathToFileURL(join(repository, "node_modules/tokenx/dist/index.mjs"))
|
||||
);
|
||||
const oracle = JSON.parse(await readFile(config.oracle, "utf8"));
|
||||
const calls = [];
|
||||
const provider = createFlutterBuildAnalysisProvider();
|
||||
const service = new FlutterBuildAnalysisService(provider);
|
||||
try {
|
||||
let uriLimit = config.first_uri_limit;
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
const input = {
|
||||
path: config.artifact,
|
||||
abi: "arm64-v8a",
|
||||
pool_policy: {
|
||||
...(uriLimit === undefined ? {} : { uri_limit: uriLimit }),
|
||||
...(config.task === "package-inventory"
|
||||
? { contains: "package:" }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
const request = { operation: "inspect_dart_aot", input };
|
||||
const reply = await service.execute(request.operation, request.input);
|
||||
calls.push({
|
||||
request,
|
||||
reply,
|
||||
request_token_estimate: estimateTokenCount(JSON.stringify(request)),
|
||||
reply_token_estimate: estimateTokenCount(JSON.stringify(reply)),
|
||||
});
|
||||
assert.equal(reply.ok, true, JSON.stringify(reply));
|
||||
const result = reply.value.normalized_result;
|
||||
assert.equal(result.target.sha256, oracle.artifact_sha256);
|
||||
assert.equal(result.libapp.sha256, oracle.libapp_sha256);
|
||||
assert.equal(result.abi, oracle.abi);
|
||||
assert.equal(result.string_pool.policy.run_bytes, 1024);
|
||||
assert.equal(result.string_pool.policy.token_bytes, 128);
|
||||
assert.equal(result.string_pool.policy.token_limit, 2000);
|
||||
if (config.task === "package-inventory") {
|
||||
const expected = oracle.policies["1024"].package_uris;
|
||||
if (
|
||||
result.string_pool.package_uris.length <
|
||||
result.string_pool.package_uri_count_at_least
|
||||
) {
|
||||
assert.equal(
|
||||
config.recover,
|
||||
true,
|
||||
"Candidate failed inventory completeness",
|
||||
);
|
||||
uriLimit = config.recovery_uri_limit;
|
||||
continue;
|
||||
}
|
||||
assert.deepEqual([...result.string_pool.package_uris].sort(), expected);
|
||||
} else {
|
||||
// Overview controls verify identity, ABI and snapshot section identities;
|
||||
// they deliberately do not require any printable string values.
|
||||
assert.ok(result.libapp.sections.length > 0);
|
||||
}
|
||||
const tokenEstimate = calls.reduce(
|
||||
(n, call) => n + call.request_token_estimate + call.reply_token_estimate,
|
||||
0,
|
||||
);
|
||||
const receipt = {
|
||||
task: config.task,
|
||||
boundary:
|
||||
"shared application service; excludes CLI/MCP discovery, adapter delivery, retained resource overhead and model reasoning",
|
||||
token_estimator:
|
||||
"tokenx 1.3.0, estimate only; all logical requests and replies including retries",
|
||||
token_estimate: tokenEstimate,
|
||||
call_count: calls.length,
|
||||
success: true,
|
||||
pool_coverage: result.string_pool.coverage,
|
||||
package_uris_delivered_final: result.string_pool.package_uris.length,
|
||||
package_uri_count_at_least_final:
|
||||
result.string_pool.package_uri_count_at_least,
|
||||
config,
|
||||
calls,
|
||||
};
|
||||
await writeFile(
|
||||
config.reply_receipt,
|
||||
JSON.stringify(receipt, null, 2) + "\n",
|
||||
);
|
||||
process.stdout.write(
|
||||
JSON.stringify({ ...receipt, calls: undefined }) + "\n",
|
||||
);
|
||||
break;
|
||||
}
|
||||
assert.ok(calls.length < 5, "Recovery did not complete");
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
"""Aggregate retained observer receipts and complete CLI records; needs tiktoken."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
|
||||
import tiktoken
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('inputs', type=Path)
|
||||
parser.add_argument('output', type=Path)
|
||||
options = parser.parse_args()
|
||||
encoder = tiktoken.get_encoding('cl100k_base')
|
||||
def compact(value):
|
||||
return json.dumps(value, ensure_ascii=False, separators=(',', ':'))
|
||||
rows = []
|
||||
for path in sorted(options.inputs.glob('*.metrics.json')):
|
||||
metrics = json.loads(path.read_text())
|
||||
record = json.loads(path.with_name(path.name.replace('.metrics.json', '.replies.json')).read_text())
|
||||
summary = json.loads(path.with_suffix('.stdout').read_text())
|
||||
tokens = {
|
||||
'arguments': len(encoder.encode(compact(record['argv']))),
|
||||
'reply': len(encoder.encode(compact(json.loads(record['stdout'])))),
|
||||
'progress_stderr': len(encoder.encode(record['stderr'])),
|
||||
}
|
||||
rows.append({
|
||||
'name': path.stem.removesuffix('.metrics'),
|
||||
'task': summary['case'], 'budget': summary['budget'],
|
||||
'verified_facts': summary['verified_facts'], 'unknowns': summary['unknowns'],
|
||||
'wall_s': metrics['elapsed_seconds'],
|
||||
'cpu_s': metrics['cpu_seconds_waited_subtree'],
|
||||
'host_group_rss_bytes': metrics['peak_process_group_rss_bytes_sampled'],
|
||||
'tokens': tokens, 'reply_bytes': summary['reply_bytes'],
|
||||
'exit_code': metrics['exit_code'], 'cleanup': metrics['cleanup'],
|
||||
'stop_reason': metrics['stop_reason'],
|
||||
})
|
||||
medians = []
|
||||
for task in ['theme', 'ms']:
|
||||
for budget in ['default', 'double']:
|
||||
selected = [row for row in rows if row['task'] == task and row['budget'] == budget]
|
||||
if not selected:
|
||||
continue
|
||||
medians.append({
|
||||
'task': task, 'budget': budget, 'trials': len(selected),
|
||||
'facts': selected[0]['verified_facts'], 'unknowns': selected[0]['unknowns'],
|
||||
'wall_s': statistics.median(row['wall_s'] for row in selected),
|
||||
'cpu_s': statistics.median(row['cpu_s'] for row in selected),
|
||||
'host_group_rss_mib': statistics.median(row['host_group_rss_bytes'] / 1048576 for row in selected),
|
||||
'tokens': statistics.median(sum(row['tokens'].values()) for row in selected),
|
||||
'reply_bytes': selected[0]['reply_bytes'],
|
||||
})
|
||||
options.output.write_text(json.dumps({'rows': rows, 'medians': medians}, indent=2) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"theme": {
|
||||
"url": "https://demo.actualbudget.org/static/js/theme.EvD-JhP6.chunk.js.map",
|
||||
"map_sha256": "ceef8194eaca339e55f35cf2decf7ceffa121c7744ee1fffd976a07d39477b32",
|
||||
"reported_source": "../../../../component-library/src/theme.ts",
|
||||
"source_sha256": "05307dc6f927705a8c76e43579c910bf6415ce28b375eea19251dc51dd2b1726"
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.3",
|
||||
"source": "locked installed node_modules/ms",
|
||||
"files": {
|
||||
"index.js": "e5f0b6a946a9b2b356a28557728410717df54ea2f599edb619f9839df6b7b0e9",
|
||||
"package.json": "1a6b4d9739790c0b94ab96c8cc0507e281c164c311ff4fbf5e57fb8d26290b40"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
{
|
||||
"method": "tiktoken0.12.0 cl100k_base compactUnicodeJSON argv and full parsedstdoutEvidence plusliteralprogressstderr; no MCPcatalog/chat/modelbilling",
|
||||
"runtime_hashes": {
|
||||
"scripts/rea.mjs": "11120fdc741c2eeca9ce903de5fd18cd1c7c3fffa15ae2beccca750f161d9ee3",
|
||||
"dist/cli.js": "acd23e0f186188c99fb905cba36f81e11f92d7f280acd684a9ee93ccc75eb9ce",
|
||||
"dist/domain/javascript/javascriptSemanticResourceLimits.js": "567ecef0cdb593c75ac156075a43c0e3fc8fb6b5ade3991614f58feb256d075c",
|
||||
"dist/domain/javascript/javascriptSemanticPrecision.js": "56cec74b6fbe0c2dd6e4fb058811f35635d63ae3235f74d2b408e1b7c6dcc366",
|
||||
"dist/domain/javascript/javascriptSemanticAnalysis.js": "c21da46e866ce49f69a45e0b5ab923fcb8eb6746a50e8f6c8b77ee61d0ca0bbd",
|
||||
"dist/javascript/analysis/JavaScriptAnalysisWorker.js": "132f199041291d70ae304474a86143fb4bba3e8df4d0114607bce0f5c3f2fe61",
|
||||
"dist/cli-runtime/cli.js": "b24d2408a52483a8e9b93435110c243deb53bb7ef94757c802a0c6246d8df20c",
|
||||
"dist/cli-runtime/cliBuiltinArguments.js": "7b0c81902f568cdd7f8003b7baadcb4f493828b1e472ee613940168213e15090",
|
||||
"./dist/composition/javascriptAnalysisWorkerProcess.js": "ab927845ccfe917549198b5438d4d206c3ecfb087b0e79e431519bbf4ef1f9fd"
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"name": "r1-ms-default",
|
||||
"task": "ms",
|
||||
"budget": "default",
|
||||
"verified_facts": 1,
|
||||
"unknowns": 133,
|
||||
"wall_s": 0.9129875000071479,
|
||||
"cpu_s": 1.239214,
|
||||
"host_group_rss_bytes": 344621056,
|
||||
"tokens": {
|
||||
"arguments": 67,
|
||||
"reply": 233873,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 624004,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r1-ms-double",
|
||||
"task": "ms",
|
||||
"budget": "double",
|
||||
"verified_facts": 1,
|
||||
"unknowns": 133,
|
||||
"wall_s": 0.9050752910115989,
|
||||
"cpu_s": 1.2302469999999999,
|
||||
"host_group_rss_bytes": 348717056,
|
||||
"tokens": {
|
||||
"arguments": 81,
|
||||
"reply": 233914,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 624244,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r1-theme-default",
|
||||
"task": "theme",
|
||||
"budget": "default",
|
||||
"verified_facts": 237,
|
||||
"unknowns": 0,
|
||||
"wall_s": 0.9374774580064695,
|
||||
"cpu_s": 1.221098,
|
||||
"host_group_rss_bytes": 346652672,
|
||||
"tokens": {
|
||||
"arguments": 67,
|
||||
"reply": 258656,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 733950,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r1-theme-double",
|
||||
"task": "theme",
|
||||
"budget": "double",
|
||||
"verified_facts": 237,
|
||||
"unknowns": 0,
|
||||
"wall_s": 0.9283738329977496,
|
||||
"cpu_s": 1.236008,
|
||||
"host_group_rss_bytes": 346423296,
|
||||
"tokens": {
|
||||
"arguments": 81,
|
||||
"reply": 258721,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 734190,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r2-ms-default",
|
||||
"task": "ms",
|
||||
"budget": "default",
|
||||
"verified_facts": 1,
|
||||
"unknowns": 133,
|
||||
"wall_s": 0.8994753339939052,
|
||||
"cpu_s": 1.211395,
|
||||
"host_group_rss_bytes": 326647808,
|
||||
"tokens": {
|
||||
"arguments": 67,
|
||||
"reply": 233873,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 624004,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r2-ms-double",
|
||||
"task": "ms",
|
||||
"budget": "double",
|
||||
"verified_facts": 1,
|
||||
"unknowns": 133,
|
||||
"wall_s": 0.9041235420008888,
|
||||
"cpu_s": 1.206674,
|
||||
"host_group_rss_bytes": 326893568,
|
||||
"tokens": {
|
||||
"arguments": 81,
|
||||
"reply": 233914,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 624244,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r2-theme-default",
|
||||
"task": "theme",
|
||||
"budget": "default",
|
||||
"verified_facts": 237,
|
||||
"unknowns": 0,
|
||||
"wall_s": 0.9415311659977306,
|
||||
"cpu_s": 1.231308,
|
||||
"host_group_rss_bytes": 348160000,
|
||||
"tokens": {
|
||||
"arguments": 67,
|
||||
"reply": 258656,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 733950,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r2-theme-double",
|
||||
"task": "theme",
|
||||
"budget": "double",
|
||||
"verified_facts": 237,
|
||||
"unknowns": 0,
|
||||
"wall_s": 0.9384306669962825,
|
||||
"cpu_s": 1.228734,
|
||||
"host_group_rss_bytes": 350060544,
|
||||
"tokens": {
|
||||
"arguments": 81,
|
||||
"reply": 258721,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 734190,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r3-ms-default",
|
||||
"task": "ms",
|
||||
"budget": "default",
|
||||
"verified_facts": 1,
|
||||
"unknowns": 133,
|
||||
"wall_s": 0.8975688329956029,
|
||||
"cpu_s": 1.2376939999999998,
|
||||
"host_group_rss_bytes": 337887232,
|
||||
"tokens": {
|
||||
"arguments": 67,
|
||||
"reply": 233873,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 624004,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r3-ms-double",
|
||||
"task": "ms",
|
||||
"budget": "double",
|
||||
"verified_facts": 1,
|
||||
"unknowns": 133,
|
||||
"wall_s": 0.9211560420080787,
|
||||
"cpu_s": 1.22497,
|
||||
"host_group_rss_bytes": 346587136,
|
||||
"tokens": {
|
||||
"arguments": 81,
|
||||
"reply": 233914,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 624244,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r3-theme-default",
|
||||
"task": "theme",
|
||||
"budget": "default",
|
||||
"verified_facts": 237,
|
||||
"unknowns": 0,
|
||||
"wall_s": 0.9006596249964787,
|
||||
"cpu_s": 1.225968,
|
||||
"host_group_rss_bytes": 340787200,
|
||||
"tokens": {
|
||||
"arguments": 67,
|
||||
"reply": 258656,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 733950,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
},
|
||||
{
|
||||
"name": "r3-theme-double",
|
||||
"task": "theme",
|
||||
"budget": "double",
|
||||
"verified_facts": 237,
|
||||
"unknowns": 0,
|
||||
"wall_s": 0.9200200000050245,
|
||||
"cpu_s": 1.2206029999999999,
|
||||
"host_group_rss_bytes": 346898432,
|
||||
"tokens": {
|
||||
"arguments": 81,
|
||||
"reply": 258721,
|
||||
"progress_stderr": 673
|
||||
},
|
||||
"reply_bytes": 734190,
|
||||
"exit_code": 0,
|
||||
"cleanup": "group_empty",
|
||||
"stop_reason": null
|
||||
}
|
||||
],
|
||||
"medians": [
|
||||
{
|
||||
"task": "theme",
|
||||
"budget": "default",
|
||||
"trials": 3,
|
||||
"facts": 237,
|
||||
"unknowns": 0,
|
||||
"wall_s": 0.9374774580064695,
|
||||
"cpu_s": 1.225968,
|
||||
"host_group_rss_mib": 330.59375,
|
||||
"tokens": 259396,
|
||||
"reply_bytes": 733950
|
||||
},
|
||||
{
|
||||
"task": "theme",
|
||||
"budget": "double",
|
||||
"trials": 3,
|
||||
"facts": 237,
|
||||
"unknowns": 0,
|
||||
"wall_s": 0.9283738329977496,
|
||||
"cpu_s": 1.228734,
|
||||
"host_group_rss_mib": 330.828125,
|
||||
"tokens": 259475,
|
||||
"reply_bytes": 734190
|
||||
},
|
||||
{
|
||||
"task": "ms",
|
||||
"budget": "default",
|
||||
"trials": 3,
|
||||
"facts": 1,
|
||||
"unknowns": 133,
|
||||
"wall_s": 0.8994753339939052,
|
||||
"cpu_s": 1.2376939999999998,
|
||||
"host_group_rss_mib": 322.234375,
|
||||
"tokens": 234613,
|
||||
"reply_bytes": 624004
|
||||
},
|
||||
{
|
||||
"task": "ms",
|
||||
"budget": "double",
|
||||
"trials": 3,
|
||||
"facts": 1,
|
||||
"unknowns": 133,
|
||||
"wall_s": 0.9050752910115989,
|
||||
"cpu_s": 1.22497,
|
||||
"host_group_rss_mib": 330.53125,
|
||||
"tokens": 234668,
|
||||
"reply_bytes": 624244
|
||||
}
|
||||
],
|
||||
"baseline": {
|
||||
"head": "e2a2ede5440df396e44b3406ef511a31a680290e",
|
||||
"working_tree": "uncommitted runtime; relevant compiled entrypoint/owner hashes retained; HEAD alone does not identify it",
|
||||
"original_task_sha256": "a17526f4b4325139896488b1d64106e5f955f7a5f998a062cb6d319425136782",
|
||||
"original_oracle_sha256": "1f4070c8814a1cd45b5ab205efd9cbf797f4b82aff195abb41877f79642c5388"
|
||||
},
|
||||
"measurement_scope": {
|
||||
"observer": "sampled simultaneous process-group RSS; not a kernel hard cap",
|
||||
"excluded_memory": "isolated JavaScript worker launches detached into another process group; its peak memory is unmeasured",
|
||||
"cpu": "wait4 waited-subtree user+system; sampler and unwaited descendants excluded",
|
||||
"cache": "fresh CLI/worker per trial; OS caches not flushed; fixed default-then-double ordering",
|
||||
"node": "24.22.1",
|
||||
"host": "macOS26.3 arm64",
|
||||
"heap_mib": "CLI and isolated worker both held512 via NODE_OPTIONS and --max-heap-mb",
|
||||
"deadline_seconds": 90,
|
||||
"group_early_stop_mib": 1536,
|
||||
"calls_per_task": 1,
|
||||
"retries": 0
|
||||
},
|
||||
"unknown_equivalence": {
|
||||
"theme": {
|
||||
"unknown_meaning_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",
|
||||
"equivalent_trials": 6
|
||||
},
|
||||
"ms": {
|
||||
"unknown_meaning_sha256": "9868f30b3b2ffc97cea3b26908a54673ff0137970a41c8c6ab95356ae48a9f19",
|
||||
"equivalent_trials": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
"""Verify pinned inputs and prepare inert application directories and an oracle."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
|
||||
def sha256(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def theme_values(source):
|
||||
prefix, suffix = 'export const theme = {', '};\n'
|
||||
if not source.startswith(prefix) or not source.endswith(suffix):
|
||||
raise ValueError('Expected the pinned literal-only theme declaration')
|
||||
body = source[len(prefix):-len(suffix)]
|
||||
pair = re.compile(r"\s*([A-Za-z_]\w*)\s*:\s*'([^'\\]*)'\s*,")
|
||||
values = {}
|
||||
position = 0
|
||||
while body[position:].strip():
|
||||
item = pair.match(body, position)
|
||||
if item is None or item[1] in values:
|
||||
raise ValueError('Oracle only accepts distinct literal CSS property declarations')
|
||||
values[item[1]] = item[2]
|
||||
position = item.end()
|
||||
if len(values) != 237:
|
||||
raise ValueError('The pinned source must declare 237 theme properties')
|
||||
return values
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('repo', type=Path)
|
||||
parser.add_argument('source_map', type=Path)
|
||||
parser.add_argument('output', type=Path)
|
||||
options = parser.parse_args()
|
||||
manifest = json.loads((Path(__file__).parent / 'manifest.json').read_text())
|
||||
pinned = manifest['theme']
|
||||
if sha256(options.source_map) != pinned['map_sha256']:
|
||||
parser.error('Source-map digest does not match the pinned artifact')
|
||||
document = json.loads(options.source_map.read_text())
|
||||
matching = [text for name, text in zip(document['sources'], document['sourcesContent'])
|
||||
if name == pinned['reported_source']]
|
||||
if len(matching) != 1 or matching[0] is None:
|
||||
parser.error('Expected one embedded original theme source')
|
||||
source = matching[0]
|
||||
if hashlib.sha256(source.encode()).hexdigest() != pinned['source_sha256']:
|
||||
parser.error('Embedded source digest mismatch')
|
||||
theme = options.output / 'cases/theme'
|
||||
control = options.output / 'cases/ms'
|
||||
theme.mkdir(parents=True, exist_ok=True)
|
||||
control.mkdir(parents=True, exist_ok=True)
|
||||
(theme / 'theme.ts').write_text(source)
|
||||
(theme / 'package.json').write_text(json.dumps({
|
||||
'name': 'rea-captured-theme-calibration', 'type': 'module', 'main': 'theme.ts',
|
||||
}))
|
||||
for name, digest in manifest['ms']['files'].items():
|
||||
path = options.repo / 'node_modules/ms' / name
|
||||
if sha256(path) != digest:
|
||||
parser.error(f'Installed ms input digest mismatch: {name}')
|
||||
shutil.copyfile(path, control / name)
|
||||
oracle = {
|
||||
'theme': {'source_file': 'theme.ts', 'source_sha256': pinned['source_sha256'],
|
||||
'expected': theme_values(source)},
|
||||
'ms': {'source_file': 'index.js',
|
||||
'source_sha256': manifest['ms']['files']['index.js'], 'expected': {'s': 1000}},
|
||||
}
|
||||
options.output.mkdir(parents=True, exist_ok=True)
|
||||
(options.output / 'oracle.json').write_text(json.dumps(oracle, indent=2) + '\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,68 +0,0 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { parse } from "@babel/parser";
|
||||
const [output, ...files] = process.argv.slice(2);
|
||||
const rows = [];
|
||||
for (const file of files) {
|
||||
const text = await readFile(file, "utf8");
|
||||
const ast = parse(text, { sourceType: "unambiguous" });
|
||||
const strings = [];
|
||||
let nodes = 0;
|
||||
const stack = [ast];
|
||||
while (stack.length) {
|
||||
const n = stack.pop();
|
||||
if (!n || typeof n !== "object") continue;
|
||||
if (Array.isArray(n)) {
|
||||
stack.push(...n);
|
||||
continue;
|
||||
}
|
||||
if (typeof n.type === "string") nodes++;
|
||||
if (n.type === "StringLiteral") {
|
||||
let oracle = false;
|
||||
try {
|
||||
oracle = JSON.parse(text.slice(n.start, n.end)) === n.value;
|
||||
} catch {}
|
||||
strings.push({
|
||||
start: n.start,
|
||||
end: n.end,
|
||||
units: n.value.length,
|
||||
jsonBytes: Buffer.byteLength(JSON.stringify(n.value)),
|
||||
oracle,
|
||||
});
|
||||
}
|
||||
if (
|
||||
n.type === "TemplateLiteral" &&
|
||||
n.expressions.length === 0 &&
|
||||
n.quasis[0].value.cooked !== null
|
||||
) {
|
||||
const value = n.quasis[0].value.cooked;
|
||||
const raw = text.slice(n.start + 1, n.end - 1);
|
||||
strings.push({
|
||||
start: n.start,
|
||||
end: n.end,
|
||||
units: value.length,
|
||||
jsonBytes: Buffer.byteLength(JSON.stringify(value)),
|
||||
oracle: !raw.includes("\\") && raw === value,
|
||||
template: true,
|
||||
});
|
||||
}
|
||||
for (const [k, v] of Object.entries(n))
|
||||
if (
|
||||
!["loc", "extra", "comments", "tokens"].includes(k) &&
|
||||
v &&
|
||||
typeof v === "object"
|
||||
)
|
||||
stack.push(v);
|
||||
}
|
||||
strings.sort((a, b) => b.units - a.units);
|
||||
rows.push({
|
||||
file,
|
||||
bytes: Buffer.byteLength(text),
|
||||
sha256: createHash("sha256").update(text).digest("hex"),
|
||||
nodes,
|
||||
strings: strings.length,
|
||||
largest: strings.slice(0, 5),
|
||||
});
|
||||
}
|
||||
await writeFile(output, JSON.stringify(rows, null, 2));
|
||||
console.log(JSON.stringify(rows));
|
||||
@@ -1,192 +0,0 @@
|
||||
[
|
||||
{
|
||||
"file": "/tmp/rea-source-map-calibration/theme.EvD-JhP6.chunk.js",
|
||||
"bytes": 29718,
|
||||
"sha256": "10ecf4c66ae103222a7c146ad9b945dc83e78b6c78ce05021296e3c2f799bd5b",
|
||||
"nodes": 4022,
|
||||
"strings": 349,
|
||||
"largest": [
|
||||
{
|
||||
"start": 1434,
|
||||
"end": 1589,
|
||||
"units": 152,
|
||||
"jsonBytes": 155,
|
||||
"oracle": true
|
||||
},
|
||||
{
|
||||
"start": 3123,
|
||||
"end": 3227,
|
||||
"units": 102,
|
||||
"jsonBytes": 104,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 7255,
|
||||
"end": 7330,
|
||||
"units": 73,
|
||||
"jsonBytes": 75,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 18225,
|
||||
"end": 18297,
|
||||
"units": 70,
|
||||
"jsonBytes": 72,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 10083,
|
||||
"end": 10154,
|
||||
"units": 69,
|
||||
"jsonBytes": 71,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "/tmp/rea-source-map-calibration/index.CMrE7aQb.js",
|
||||
"bytes": 2078745,
|
||||
"sha256": "937f792d1849b69a6e0741234428b22b4884ec0157ea31dd6949dd1bacc46456",
|
||||
"nodes": 460754,
|
||||
"strings": 15897,
|
||||
"largest": [
|
||||
{
|
||||
"start": 78956,
|
||||
"end": 79894,
|
||||
"units": 936,
|
||||
"jsonBytes": 938,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 104891,
|
||||
"end": 105550,
|
||||
"units": 657,
|
||||
"jsonBytes": 659,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 1752523,
|
||||
"end": 1753169,
|
||||
"units": 644,
|
||||
"jsonBytes": 662,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 1751921,
|
||||
"end": 1752324,
|
||||
"units": 401,
|
||||
"jsonBytes": 413,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 583954,
|
||||
"end": 584343,
|
||||
"units": 387,
|
||||
"jsonBytes": 391,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "/tmp/rea-source-map-calibration/DateSelect.BVYaCNUe.chunk.js",
|
||||
"bytes": 2191517,
|
||||
"sha256": "9e45139dd0b510ad7014188a5e9d11b307c2ac15dfdc761050388e76e94759bf",
|
||||
"nodes": 363578,
|
||||
"strings": 29000,
|
||||
"largest": [
|
||||
{
|
||||
"start": 34808,
|
||||
"end": 47930,
|
||||
"units": 13120,
|
||||
"jsonBytes": 13122,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 21787,
|
||||
"end": 34790,
|
||||
"units": 13001,
|
||||
"jsonBytes": 13003,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 8827,
|
||||
"end": 21772,
|
||||
"units": 12943,
|
||||
"jsonBytes": 12945,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 47947,
|
||||
"end": 50426,
|
||||
"units": 2477,
|
||||
"jsonBytes": 2479,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
},
|
||||
{
|
||||
"start": 50458,
|
||||
"end": 52394,
|
||||
"units": 1934,
|
||||
"jsonBytes": 1936,
|
||||
"oracle": true,
|
||||
"template": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "node_modules/ms/index.js",
|
||||
"bytes": 3024,
|
||||
"sha256": "e5f0b6a946a9b2b356a28557728410717df54ea2f599edb619f9839df6b7b0e9",
|
||||
"nodes": 427,
|
||||
"strings": 48,
|
||||
"largest": [
|
||||
{
|
||||
"start": 702,
|
||||
"end": 757,
|
||||
"units": 53,
|
||||
"jsonBytes": 55,
|
||||
"oracle": false
|
||||
},
|
||||
{
|
||||
"start": 1851,
|
||||
"end": 1865,
|
||||
"units": 12,
|
||||
"jsonBytes": 14,
|
||||
"oracle": false
|
||||
},
|
||||
{
|
||||
"start": 1876,
|
||||
"end": 1889,
|
||||
"units": 11,
|
||||
"jsonBytes": 13,
|
||||
"oracle": false
|
||||
},
|
||||
{
|
||||
"start": 1745,
|
||||
"end": 1754,
|
||||
"units": 7,
|
||||
"jsonBytes": 9,
|
||||
"oracle": false
|
||||
},
|
||||
{
|
||||
"start": 1639,
|
||||
"end": 1648,
|
||||
"units": 7,
|
||||
"jsonBytes": 9,
|
||||
"oracle": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,70 +0,0 @@
|
||||
"""Run one complete CLI task and independently check the required source facts."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('repo', type=Path)
|
||||
parser.add_argument('node')
|
||||
parser.add_argument('inputs', type=Path)
|
||||
parser.add_argument('case', choices=['theme', 'ms'])
|
||||
parser.add_argument('budget', choices=['default', 'double'])
|
||||
parser.add_argument('output', type=Path)
|
||||
options = parser.parse_args()
|
||||
oracle = json.loads((options.inputs / 'oracle.json').read_text())[options.case]
|
||||
target = options.inputs / 'cases' / options.case
|
||||
source = target / oracle['source_file']
|
||||
if hashlib.sha256(source.read_bytes()).hexdigest() != oracle['source_sha256']:
|
||||
raise ValueError('Selected source digest does not match the oracle')
|
||||
argv = [options.node, str(options.repo / 'scripts/rea.mjs'),
|
||||
'analyze-javascript-application', str(target), '--max-heap-mb', '512',
|
||||
'--format', 'json']
|
||||
if options.budget == 'double':
|
||||
argv += ['--semantic-precision', '{"primitive_bytes":2097152}']
|
||||
environment = {**os.environ, 'NODE_OPTIONS': '--max-old-space-size=512'}
|
||||
result = subprocess.run(argv, cwd=options.repo, env=environment, capture_output=True,
|
||||
text=True, timeout=45)
|
||||
options.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
options.output.write_text(json.dumps({
|
||||
'argv': argv, 'stdout': result.stdout, 'stderr': result.stderr,
|
||||
'exit_code': result.returncode,
|
||||
}) + '\n')
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f'CLI failed: {result.stderr[:500]}')
|
||||
evidence = json.loads(result.stdout)
|
||||
graph = evidence['normalized_result']['semantic_graph']
|
||||
nodes = {node['node_id']: node for node in graph['nodes']}
|
||||
if any(node['identity']['artifact_sha256'] != oracle['source_sha256']
|
||||
for node in nodes.values()
|
||||
if node['identity']['module_path'] == oracle['source_file']):
|
||||
raise ValueError('Selected graph source identity differs from the oracle')
|
||||
facts = {}
|
||||
for edge in graph['relations']:
|
||||
if edge['relation'] != 'defines':
|
||||
continue
|
||||
left, right = nodes[edge['source_node_id']], nodes[edge['target_node_id']]
|
||||
if left['kind'] != 'literal':
|
||||
continue
|
||||
if options.case == 'theme' and right['kind'] == 'property-slot':
|
||||
facts[right['properties']['name']] = left['properties']['value']
|
||||
if options.case == 'ms' and right['kind'] == 'binding' and right['label'] == 's':
|
||||
facts['s'] = left['properties']['value']
|
||||
if facts != oracle['expected']:
|
||||
raise ValueError('Required source facts differ from the independent oracle')
|
||||
if any('primitive-bytes' in json.dumps(unknown) for unknown in graph['unknowns']):
|
||||
raise ValueError('Selected byte allowance unexpectedly limited the task')
|
||||
print(json.dumps({
|
||||
'case': options.case, 'budget': options.budget, 'verified_facts': len(facts),
|
||||
'source_sha256': oracle['source_sha256'],
|
||||
'reply_bytes': len(result.stdout.encode()), 'unknowns': len(graph['unknowns']),
|
||||
}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,199 +0,0 @@
|
||||
"""Validate and summarize bounded public source-location task receipts.
|
||||
|
||||
Requires tiktoken 0.12.0. Full replies stay in the caller-selected measurement
|
||||
directory; the output retains only identities, hashes, numeric measurements and
|
||||
per-call serialized JSON-RPC token/byte estimates.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import statistics
|
||||
|
||||
import tiktoken
|
||||
|
||||
|
||||
def compact(value, *, ordered=False):
|
||||
return json.dumps(value, ensure_ascii=False, separators=(',', ':'), sort_keys=ordered)
|
||||
|
||||
|
||||
def sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open('rb') as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('measurement_dir', type=Path)
|
||||
parser.add_argument('--repo-root', type=Path, required=True)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
repo = args.repo_root.resolve()
|
||||
assert tiktoken.__version__ == '0.12.0', 'Use the recorded tokenizer version'
|
||||
encoder = tiktoken.get_encoding('cl100k_base')
|
||||
token_cache = {}
|
||||
|
||||
def tokens(value, *, line=False):
|
||||
document = compact(value) + ('\n' if line else '')
|
||||
digest = hashlib.sha256(document.encode()).hexdigest()
|
||||
if digest not in token_cache:
|
||||
token_cache[digest] = len(encoder.encode(document, disallowed_special=()))
|
||||
return token_cache[digest]
|
||||
|
||||
pins_path = repo / 'docs/verification/source-map-budgets-2026-11-09/artifacts.json'
|
||||
artifacts = json.loads(pins_path.read_text())
|
||||
records, semantic_answers, evidence_ids = [], {}, {}
|
||||
definitions = None
|
||||
input_only_definitions = None
|
||||
artifact_cache = {}
|
||||
receipts = sorted(args.measurement_dir.glob('r*-*.metrics.json'), key=lambda p: p.stat().st_mtime_ns)
|
||||
assert len(receipts) == 18, f'Expected 18 final receipts, found {len(receipts)}'
|
||||
for path in receipts:
|
||||
name = path.name.removesuffix('.metrics.json')
|
||||
selected = re.fullmatch(r'r([123])-(theme|index|DateSelect)-(complete|summary)', name)
|
||||
assert selected, name
|
||||
trial, case, detail = int(selected[1]), selected[2], selected[3]
|
||||
metric = json.loads(path.read_text())
|
||||
trace_path = path.with_name(name + '.replies.json')
|
||||
trace = json.loads(trace_path.read_text())
|
||||
assert metric['exit_code'] == 0 and metric['cleanup'] == 'group_empty'
|
||||
assert metric['stop_reason'] is None and metric['observer_error'] is None
|
||||
assert metric['signal_errors'] == [] and metric['remaining_group_pids'] == []
|
||||
assert trace['config']['select'] == case + '.' and trace['config']['detail'] == detail
|
||||
answer = trace['answer']
|
||||
artifact = next(item for item in artifacts if case + '.' in item['path'])
|
||||
assert answer['source_map_sha256'] == artifact['sha256']
|
||||
assert answer['lookup']['requested'] == artifact['point']
|
||||
assert answer['lookup']['matched_generated_position'] == artifact['point']
|
||||
assert len(answer['matches']) == len(artifact['expected_matches'])
|
||||
if case not in artifact_cache:
|
||||
raw_path = Path(trace['config']['artifactDir']) / artifact['path']
|
||||
assert sha256(raw_path) == artifact['sha256']
|
||||
artifact_cache[case] = json.loads(raw_path.read_text())
|
||||
raw = artifact_cache[case]
|
||||
for index, (actual, expected) in enumerate(zip(answer['matches'], artifact['expected_matches'])):
|
||||
assert actual['state'] == ('generated-only' if expected.get('generated_only') else 'mapped')
|
||||
if expected.get('generated_only'):
|
||||
continue
|
||||
assert actual['match_index'] == index
|
||||
assert actual['source_index'] == expected['source_index']
|
||||
assert actual['reported_source'] == expected['source']
|
||||
assert actual['original_position']['line'] == expected['original_line']
|
||||
assert actual['original_position']['column'] == expected['original_column']
|
||||
body = raw['sourcesContent'][expected['source_index']]
|
||||
assert hashlib.sha256(body.encode()).hexdigest() == expected['content_sha256']
|
||||
assert actual['content']['utf8_sha256'] == expected['content_sha256']
|
||||
snippet = actual['content']['snippet']
|
||||
encoded = body.encode('utf-16-le')
|
||||
start, length = snippet['offset_utf16'], snippet['length_utf16']
|
||||
assert snippet['text'] == encoded[start * 2:(start + length) * 2].decode('utf-16-le')
|
||||
assert length == 512, 'These pinned tasks require a 512-unit snippet'
|
||||
assert start <= actual['original_position']['offset'] <= start + length
|
||||
assert actual['content']['code_units'] == len(encoded) // 2
|
||||
semantic = {key: answer[key] for key in ['evidence_id', 'source_map_sha256', 'lookup', 'matches']}
|
||||
semantic_digest = hashlib.sha256(compact(semantic, ordered=True).encode()).hexdigest()
|
||||
if case in semantic_answers:
|
||||
assert semantic_digest == semantic_answers[case], f'Answer mismatch in {name}'
|
||||
assert answer['evidence_id'] == evidence_ids[case]
|
||||
else:
|
||||
semantic_answers[case], evidence_ids[case] = semantic_digest, answer['evidence_id']
|
||||
catalog = next(call['reply']['result'] for call in trace['calls'] if call['request']['method'] == 'tools/list')
|
||||
chosen_definitions = {tool: next(item for item in catalog['tools'] if item['name'] == tool) for tool in ['trace_web_source_location', 'inspect_analysis_view']}
|
||||
definition_counts = {tool: tokens(definition) for tool, definition in chosen_definitions.items()}
|
||||
input_only_counts = {tool: tokens({key: definition[key] for key in ['name', 'description', 'inputSchema']}) for tool, definition in chosen_definitions.items()}
|
||||
if definitions is None:
|
||||
definitions = definition_counts
|
||||
input_only_definitions = input_only_counts
|
||||
else:
|
||||
assert definitions == definition_counts
|
||||
assert input_only_definitions == input_only_counts
|
||||
rows, seen_catalog = [], False
|
||||
used_tools = set()
|
||||
for call in trace['calls']:
|
||||
request, reply = call['request'], call['reply']
|
||||
operation = request.get('params', {}).get('name', request['method'])
|
||||
request_count = tokens(request, line=True)
|
||||
reply_count = tokens(reply, line=True) if reply is not None else 0
|
||||
post_discovery = seen_catalog
|
||||
if request['method'] == 'tools/list':
|
||||
seen_catalog = True
|
||||
if request['method'] == 'tools/call':
|
||||
used_tools.add(operation)
|
||||
if reply is not None and request['method'] == 'tools/call' and not reply['result'].get('isError'):
|
||||
normalized = reply['result']['structuredContent']['normalized_result']
|
||||
source_report = normalized.get('source_location', normalized.get('summary', normalized))
|
||||
assert source_report['source']['sha256'] == artifact['generated_script']['sha256']
|
||||
assert source_report['source_map']['sha256'] == artifact['sha256']
|
||||
rows.append({
|
||||
'operation': operation, 'post_discovery': post_discovery,
|
||||
'request_tokens_estimated': request_count, 'reply_tokens_estimated': reply_count,
|
||||
'request_bytes_compact_json_line': len((compact(request) + '\n').encode()),
|
||||
'reply_bytes_observed_wire': call['wire_bytes'],
|
||||
'reply_bytes_reserialized_json_line': len((compact(reply) + '\n').encode()) if reply is not None else 0,
|
||||
'elapsed_ms': call['elapsed_ms'],
|
||||
'is_error': reply is not None and reply.get('result', {}).get('isError', False),
|
||||
})
|
||||
if operation == 'trace_web_source_location' and reply is not None:
|
||||
result = reply['result']
|
||||
if not result.get('isError') and detail == 'complete':
|
||||
text = result['structuredContent']['raw_result']['source_map_text']
|
||||
assert hashlib.sha256(text.encode()).hexdigest() == artifact['sha256']
|
||||
total = sum(row['request_tokens_estimated'] + row['reply_tokens_estimated'] for row in rows)
|
||||
post = sum(row['request_tokens_estimated'] + row['reply_tokens_estimated'] for row in rows if row['post_discovery'])
|
||||
assert used_tools <= chosen_definitions.keys()
|
||||
records.append({
|
||||
'name': name, 'trial': trial, 'artifact': case, 'detail': detail,
|
||||
'evidence_id': answer['evidence_id'], 'map_sha256': artifact['sha256'], 'generated_script_sha256': artifact['generated_script']['sha256'], 'answer_sha256': semantic_digest,
|
||||
'receipt_sha256': sha256(path), 'reply_trace_sha256': sha256(trace_path),
|
||||
'task_tokens_catalog_included_estimated': total, 'post_discovery_tokens_estimated': post,
|
||||
'used_tool_definitions_tokens_estimated': sum(definitions[tool] for tool in used_tools),
|
||||
'post_discovery_plus_used_definitions_tokens_estimated': post + sum(definitions[tool] for tool in used_tools),
|
||||
'hypothetical_used_input_only_definitions_tokens_estimated': sum(input_only_definitions[tool] for tool in used_tools),
|
||||
'hypothetical_post_discovery_plus_used_input_only_definitions_tokens_estimated': post + sum(input_only_definitions[tool] for tool in used_tools),
|
||||
'tool_calls': sum(call['request']['method'] == 'tools/call' for call in trace['calls']),
|
||||
'elapsed_seconds': metric['elapsed_seconds'], 'cpu_seconds_waited_subtree': metric['cpu_seconds_waited_subtree'],
|
||||
'peak_process_group_rss_bytes_sampled': metric['peak_process_group_rss_bytes_sampled'],
|
||||
'peak_process_tree_rss_bytes_sampled': metric['peak_process_tree_rss_bytes_sampled'],
|
||||
'maximum_group_members_sampled': metric['maximum_group_members_sampled'],
|
||||
'maximum_tree_members_sampled': metric['maximum_tree_members_sampled'],
|
||||
'sample_count': metric['sample_count'], 'exit_code': metric['exit_code'], 'cleanup': metric['cleanup'], 'stop_reason': metric['stop_reason'],
|
||||
'calls': rows,
|
||||
})
|
||||
groups = {}
|
||||
for record in records:
|
||||
groups.setdefault(record['artifact'] + '-' + record['detail'], []).append(record)
|
||||
fields = ['post_discovery_plus_used_definitions_tokens_estimated', 'hypothetical_used_input_only_definitions_tokens_estimated', 'hypothetical_post_discovery_plus_used_input_only_definitions_tokens_estimated', 'task_tokens_catalog_included_estimated', 'post_discovery_tokens_estimated', 'used_tool_definitions_tokens_estimated', 'tool_calls', 'elapsed_seconds', 'cpu_seconds_waited_subtree', 'peak_process_group_rss_bytes_sampled', 'peak_process_tree_rss_bytes_sampled']
|
||||
medians = {name: {'runs': len(group), **{field: statistics.median(record[field] for record in group) for field in fields}} for name, group in groups.items()}
|
||||
assert all(len(group) == 3 for group in groups.values()) and len(groups) == 6
|
||||
recorded_runtime = json.loads((args.measurement_dir / 'runtime.json').read_text())
|
||||
paths = ['scripts/verify/analysis/source-location-delivery/task.mjs', 'scripts/verify/analysis/budget-calibration-run.py', 'scripts/rea.mjs', 'dist/cli.js', 'dist/mcp/main.js', 'src/application/WebSourceLocationService.ts', 'src/domain/analysisView/webSourceLocationView.ts', 'src/application/analysisView/AnalysisViewService.ts', 'src/contracts/webScriptToolContracts.ts', 'src/server/ToolResultDelivery.ts', 'src/server/toolResult.ts', 'src/domain/sourceMapLimits.ts', 'src/server/registerWebSourceLocationTool.ts', 'src/cli/webSourceLocationCommands.ts', 'scripts/verify/analysis/source-location-delivery/aggregate.py', 'package-lock.json', 'docs/verification/source-map-budgets-2026-11-09/artifacts.json']
|
||||
hashes = {path: sha256(repo / path) for path in paths}
|
||||
assert hashes[paths[0]] == recorded_runtime['driver_sha256']
|
||||
assert hashes[paths[1]] == recorded_runtime['observer_sha256']
|
||||
if recorded_runtime.get('cli_sha256') is not None:
|
||||
assert hashes['dist/cli.js'] == recorded_runtime['cli_sha256']
|
||||
tree = [(path.relative_to(repo / 'dist').as_posix(), sha256(path)) for path in sorted((repo / 'dist').rglob('*')) if path.is_file()]
|
||||
tree_digest = hashlib.sha256(''.join(path + '\0' + digest + '\n' for path, digest in tree).encode()).hexdigest()
|
||||
output = {
|
||||
'schema': 'rea-source-location-delivery-calibration/v1',
|
||||
'token_accounting': {'library': 'tiktoken', 'version': tiktoken.__version__, 'encoding': 'cl100k_base', 'representation': 'compact Unicode JSON-RPC request/reply envelopes with trailing newline; initialized notification has no reply; estimates, not model billing', 'whole_catalog_included': 'initialize + initialized notification + tools/list + every trace/continuation/text call', 'post_discovery': 'all requests/replies after tools/list; excludes discovery only, not recovery', 'selected_definitions': 'complete serialized trace_web_source_location and inspect_analysis_view definitions counted separately; do not add them again to whole-catalog totals; client cached/deferred tool presentation may differ'},
|
||||
'runtime': {'recorded_runtime': recorded_runtime, 'sha256': hashes, 'compiled_tree': {'root': 'dist', 'files': len(tree), 'sha256': tree_digest, 'encoding': 'sorted relative UTF-8 filename + NUL + lowercase SHA256 + newline for every regular file, then SHA256'}},
|
||||
'conditions': {'host': metric['platform'], 'node': 'v24.22.1', 'runs': 'three fresh-process runs; second trial reverses variant order; OS caches not flushed; no disk-cold claim', 'task': 'all equal-position mappings and exact source/map identity plus 512 UTF-16 units around each mapped point', 'comparison': 'current complete delivery including bounded retained-view overflow recovery versus opt-in summary; not the older export-only fallback', 'profiles': {'tool_catalog': 'full', 'input_schema': 'compact', 'output_schema': 'full', 'response_budget_bytes': 10485760}, 'observer': {'interval_seconds': 0.02, 'wall_deadline_seconds': 90, 'memory_early_stop_bytes': 1536 * 1024 * 1024, 'signal_scope': 'owned process group only', 'rss': 'simultaneous ps-snapshot group/tree sums; tree includes linked-PPID codec descendants outside root PGID; can miss short peaks/exited/reparented/orphaned descendants and double-count shared pages', 'cpu': 'wait4 user+system waited subtree, excluding sampler and unwaited/orphaned descendants', 'memory_guard': 'group early stop; observational tree RSS is not signal authority or a hard/kernel peak limit'}},
|
||||
'selected_definition_tokens_estimated': definitions,
|
||||
'hypothetical_input_only_definition_tokens_estimated': input_only_definitions,
|
||||
'hypothetical_input_only_definition_projection': 'Only {name, description, inputSchema} from captured definitions; not an observed client/model transformation or a measured alternate discovery workflow',
|
||||
'observed_completion_order': [record['name'] for record in records],
|
||||
'validation': {'completed_runs': len(records), 'same_answer_and_parent_id_across_variants_and_trials': True, 'independent_artifact_oracle_and_snippets_verified': True, 'successful_complete_initial_raw_map_sha256_verified': True, 'all_owned_groups_empty': True, 'all_resource_stops': None},
|
||||
'medians': medians, 'records': records,
|
||||
}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(output, ensure_ascii=False, indent=2) + '\n')
|
||||
print(json.dumps({'medians': medians, 'selected_definitions': definitions, 'compiled_tree': output['runtime']['compiled_tree']}, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,426 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import assert from "node:assert/strict";
|
||||
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
|
||||
// Acquire artifactDir with docs/verification/source-map-budgets-2026-11-09/acquire.py.
|
||||
// This driver runs the public compiled stdio workflow; no target application is executed.
|
||||
const [rootArgument, configText, ...extra] = process.argv.slice(2);
|
||||
assert(
|
||||
rootArgument && configText && extra.length === 0,
|
||||
"Usage: node task.mjs REPO_ROOT CONFIG_JSON",
|
||||
);
|
||||
const root = resolve(rootArgument);
|
||||
const config = JSON.parse(configText);
|
||||
assert(
|
||||
config && typeof config === "object" && !Array.isArray(config),
|
||||
"CONFIG_JSON must be an object",
|
||||
);
|
||||
const allowed = new Set([
|
||||
"artifactDir",
|
||||
"select",
|
||||
"detail",
|
||||
"resultPath",
|
||||
"exportPath",
|
||||
"maxResponseBytes",
|
||||
]);
|
||||
assert(
|
||||
Object.keys(config).every((key) => allowed.has(key)),
|
||||
"Unknown task configuration field",
|
||||
);
|
||||
assert(
|
||||
typeof config.artifactDir === "string" && isAbsolute(config.artifactDir),
|
||||
"artifactDir must be an absolute acquired-artifact directory",
|
||||
);
|
||||
assert(
|
||||
typeof config.select === "string" &&
|
||||
config.select.length > 0 &&
|
||||
config.select.length <= 256,
|
||||
"select must be a nonempty artifact filename selector, at most 256 characters",
|
||||
);
|
||||
assert(
|
||||
config.detail === "complete" || config.detail === "summary",
|
||||
"detail must be complete or summary",
|
||||
);
|
||||
for (const key of ["resultPath", "exportPath"]) {
|
||||
assert(
|
||||
config[key] === undefined ||
|
||||
(typeof config[key] === "string" && isAbsolute(config[key])),
|
||||
`${key} must be an absolute path when selected`,
|
||||
);
|
||||
}
|
||||
const MAX_LINE_BYTES = 32 * 1024 * 1024;
|
||||
const RPC_TIMEOUT_MS = 45000;
|
||||
assert(
|
||||
config.maxResponseBytes === undefined ||
|
||||
(Number.isSafeInteger(config.maxResponseBytes) &&
|
||||
config.maxResponseBytes >= 10 * 1024 * 1024 &&
|
||||
config.maxResponseBytes <= MAX_LINE_BYTES),
|
||||
"maxResponseBytes must fit the driver receive guard: integer 10..32 MiB",
|
||||
);
|
||||
const sources = JSON.parse(
|
||||
await readFile(join(config.artifactDir, "artifacts.json"), "utf8"),
|
||||
);
|
||||
assert(
|
||||
Array.isArray(sources),
|
||||
"artifacts.json must be the retained artifact list",
|
||||
);
|
||||
const selected = sources.filter(
|
||||
(source) =>
|
||||
typeof source.path === "string" && source.path.includes(config.select),
|
||||
);
|
||||
assert.equal(
|
||||
selected.length,
|
||||
1,
|
||||
"select must identify exactly one pinned artifact",
|
||||
);
|
||||
const source = selected[0];
|
||||
assert(
|
||||
source.generated_script,
|
||||
"Selected point task requires an acquired generated script and analyst manifest",
|
||||
);
|
||||
const localFile = (path) => {
|
||||
assert(
|
||||
typeof path === "string" && !isAbsolute(path),
|
||||
"Artifact manifest paths must be relative",
|
||||
);
|
||||
const resolved = resolve(config.artifactDir, path);
|
||||
assert(
|
||||
!relative(config.artifactDir, resolved).startsWith(".." + sep) &&
|
||||
resolved !== resolve(config.artifactDir, ".."),
|
||||
"Artifact path must stay within artifactDir",
|
||||
);
|
||||
return resolved;
|
||||
};
|
||||
const mapPath = localFile(source.path);
|
||||
const bytes = await readFile(mapPath);
|
||||
assert.equal(bytes.length, source.bytes);
|
||||
assert.equal(createHash("sha256").update(bytes).digest("hex"), source.sha256);
|
||||
const artifact = JSON.parse(bytes.toString("utf8"));
|
||||
const manifestPath = localFile(source.generated_script.path + ".manifest.json");
|
||||
const child = spawn(process.execPath, [join(root, "scripts/rea.mjs"), "mcp"], {
|
||||
cwd: root,
|
||||
env: {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_MCP_TOOL_CATALOG_PROFILE: "full",
|
||||
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
|
||||
REA_MCP_OUTPUT_SCHEMA_PROFILE: "full",
|
||||
...(config.maxResponseBytes === undefined
|
||||
? {}
|
||||
: { REA_MCP_MAX_RESPONSE_BYTES: String(config.maxResponseBytes) }),
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
GHIDRA_INSTALL_DIR: process.execPath,
|
||||
},
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
let buffer = "",
|
||||
sequence = 0,
|
||||
stderr = "";
|
||||
const pending = new Map(),
|
||||
calls = [];
|
||||
const failPending = (error) => {
|
||||
for (const request of pending.values()) request.reject(error);
|
||||
pending.clear();
|
||||
};
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk).slice(-8192);
|
||||
});
|
||||
child.once("error", failPending);
|
||||
child.once("exit", (code, signal) =>
|
||||
failPending(
|
||||
new Error(
|
||||
`MCP server exited (${String(code)}, ${String(signal)}): ${stderr}`,
|
||||
),
|
||||
),
|
||||
);
|
||||
child.stdin.on("error", failPending);
|
||||
child.stdout.on("data", (chunk) => {
|
||||
try {
|
||||
buffer += chunk;
|
||||
assert(
|
||||
Buffer.byteLength(buffer) < MAX_LINE_BYTES,
|
||||
"32 MiB RPC line guard exceeded",
|
||||
);
|
||||
let end;
|
||||
while ((end = buffer.indexOf("\n")) >= 0) {
|
||||
const line = buffer.slice(0, end);
|
||||
buffer = buffer.slice(end + 1);
|
||||
if (!line) continue;
|
||||
const payload = JSON.parse(line),
|
||||
request = pending.get(payload.id);
|
||||
if (request) {
|
||||
pending.delete(payload.id);
|
||||
request.resolve({ payload, wireBytes: Buffer.byteLength(line) + 1 });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
failPending(error);
|
||||
}
|
||||
});
|
||||
const rpc = async (method, params = {}) => {
|
||||
const request = { jsonrpc: "2.0", id: ++sequence, method, params },
|
||||
start = performance.now();
|
||||
const waiting = new Promise((resolveReply, rejectReply) => {
|
||||
const timer = setTimeout(() => {
|
||||
pending.delete(request.id);
|
||||
rejectReply(new Error(`RPC timeout ${method}: ${stderr}`));
|
||||
}, RPC_TIMEOUT_MS);
|
||||
pending.set(request.id, {
|
||||
resolve: (value) => {
|
||||
clearTimeout(timer);
|
||||
resolveReply(value);
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timer);
|
||||
rejectReply(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
child.stdin.write(JSON.stringify(request) + "\n");
|
||||
const { payload, wireBytes } = await waiting;
|
||||
assert(!payload.error, JSON.stringify(payload.error));
|
||||
calls.push({
|
||||
request,
|
||||
reply: payload,
|
||||
wire_bytes: wireBytes,
|
||||
elapsed_ms: performance.now() - start,
|
||||
});
|
||||
return payload.result;
|
||||
};
|
||||
const errorOf = (r) =>
|
||||
r.isError
|
||||
? JSON.parse(r.content.find((c) => c.type === "text").text).error
|
||||
: undefined;
|
||||
const contentOf = (r) => {
|
||||
assert(!r.isError, JSON.stringify(errorOf(r)));
|
||||
return r.structuredContent;
|
||||
};
|
||||
try {
|
||||
await rpc("initialize", {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "source-location-completed-task", version: "1" },
|
||||
});
|
||||
const initialized = { jsonrpc: "2.0", method: "notifications/initialized" };
|
||||
child.stdin.write(JSON.stringify(initialized) + "\n");
|
||||
calls.push({
|
||||
request: initialized,
|
||||
reply: null,
|
||||
wire_bytes: 0,
|
||||
elapsed_ms: 0,
|
||||
});
|
||||
const catalog = await rpc("tools/list");
|
||||
const definition = catalog.tools.find(
|
||||
(t) => t.name === "trace_web_source_location",
|
||||
);
|
||||
assert(definition);
|
||||
const args = {
|
||||
manifest_path: manifestPath,
|
||||
script_index: 0,
|
||||
source_map: { path: mapPath, url: source.url },
|
||||
generated_position: source.point,
|
||||
...(config.detail === "summary" ? { detail: "summary" } : {}),
|
||||
};
|
||||
const initial = await rpc("tools/call", {
|
||||
name: "trace_web_source_location",
|
||||
arguments: args,
|
||||
});
|
||||
const error = errorOf(initial);
|
||||
let evidenceId,
|
||||
report,
|
||||
metadata = [],
|
||||
coverage;
|
||||
if (error) {
|
||||
assert.equal(error.code, "resource_constraint");
|
||||
const limits = error.details.reported_limits;
|
||||
evidenceId = limits.evidence_id;
|
||||
assert(evidenceId);
|
||||
const continuation = limits.continuation;
|
||||
assert(continuation, "new source-location continuation required");
|
||||
const first = contentOf(
|
||||
await rpc("tools/call", {
|
||||
name: continuation.name,
|
||||
arguments: continuation.arguments,
|
||||
}),
|
||||
).normalized_result;
|
||||
report = first.source_location;
|
||||
metadata = [...first.items];
|
||||
coverage = first.coverage;
|
||||
} else {
|
||||
const evidence = contentOf(initial),
|
||||
result = evidence.normalized_result;
|
||||
if (config.detail === "summary") {
|
||||
evidenceId = result.parent_evidence_id;
|
||||
report = result.summary;
|
||||
metadata = [...report.matches];
|
||||
coverage = result.coverage;
|
||||
} else {
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(evidence.raw_result.source_map_text)
|
||||
.digest("hex"),
|
||||
source.sha256,
|
||||
"Complete initial MCP response must preserve exact raw map bytes",
|
||||
);
|
||||
evidenceId = evidence.evidence_id;
|
||||
report = result;
|
||||
metadata = [...report.matches];
|
||||
coverage = { exhausted: true };
|
||||
}
|
||||
}
|
||||
const reference = { kind: "retained-evidence", evidence_id: evidenceId };
|
||||
while (!coverage.exhausted) {
|
||||
const page = contentOf(
|
||||
await rpc("tools/call", {
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: reference,
|
||||
view: {
|
||||
kind: "source-location",
|
||||
collection: "matches",
|
||||
offset: coverage.next_offset,
|
||||
limit: 64,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).normalized_result;
|
||||
metadata.push(...page.items);
|
||||
coverage = page.coverage;
|
||||
}
|
||||
assert.equal(report.source_map.sha256, source.sha256);
|
||||
assert.equal(metadata.length, source.expected_matches.length);
|
||||
const answer = [];
|
||||
for (let index = 0; index < metadata.length; index++) {
|
||||
const match = metadata[index],
|
||||
expected = source.expected_matches[index];
|
||||
assert.equal(
|
||||
match.state,
|
||||
expected.generated_only ? "generated-only" : "mapped",
|
||||
);
|
||||
if (match.state === "generated-only") {
|
||||
answer.push(match);
|
||||
continue;
|
||||
}
|
||||
assert.equal(match.source_index, expected.source_index);
|
||||
assert.equal(match.reported_source, expected.source);
|
||||
assert.equal(match.original_position.line, expected.original_line);
|
||||
assert.equal(match.original_position.column, expected.original_column);
|
||||
assert.equal(match.content.utf8_sha256, expected.content_sha256);
|
||||
const full = artifact.sourcesContent[expected.source_index],
|
||||
point = match.original_position.offset;
|
||||
assert(point !== null);
|
||||
let offset = Math.max(0, point - 256);
|
||||
if (
|
||||
offset > 0 &&
|
||||
full.charCodeAt(offset) >= 0xdc00 &&
|
||||
full.charCodeAt(offset) <= 0xdfff
|
||||
)
|
||||
offset--;
|
||||
let snippet;
|
||||
if ("text" in match.content) {
|
||||
assert.equal(
|
||||
createHash("sha256").update(match.content.text).digest("hex"),
|
||||
expected.content_sha256,
|
||||
);
|
||||
let end = Math.min(full.length, offset + 512);
|
||||
if (
|
||||
end < full.length &&
|
||||
full.charCodeAt(end - 1) >= 0xd800 &&
|
||||
full.charCodeAt(end - 1) <= 0xdbff
|
||||
)
|
||||
end--;
|
||||
snippet = match.content.text.slice(offset, end);
|
||||
} else {
|
||||
const text = contentOf(
|
||||
await rpc("tools/call", {
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: reference,
|
||||
view: {
|
||||
kind: "source-location",
|
||||
collection: "source-text",
|
||||
match_index: match.match_index,
|
||||
offset,
|
||||
limit: 512,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).normalized_result;
|
||||
assert.equal(text.match.content.utf8_sha256, expected.content_sha256);
|
||||
snippet = text.item.text;
|
||||
}
|
||||
assert.equal(snippet, full.slice(offset, offset + snippet.length));
|
||||
assert(offset <= point && offset + snippet.length >= point);
|
||||
const { text: _text, ...identity } = match.content;
|
||||
answer.push({
|
||||
...match,
|
||||
match_index: index,
|
||||
content: {
|
||||
...identity,
|
||||
code_units: full.length,
|
||||
snippet: {
|
||||
offset_utf16: offset,
|
||||
length_utf16: snippet.length,
|
||||
text: snippet,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
let exported = false;
|
||||
if (config.exportPath) {
|
||||
const result = await rpc("tools/call", {
|
||||
name: "export_evidence_bundle",
|
||||
arguments: { path: config.exportPath },
|
||||
});
|
||||
assert(!result.isError);
|
||||
const bundle = JSON.parse(await readFile(config.exportPath, "utf8"));
|
||||
const parent = bundle.records.find((r) => r.evidence_id === evidenceId);
|
||||
assert(parent);
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(parent.raw_result.source_map_text)
|
||||
.digest("hex"),
|
||||
source.sha256,
|
||||
);
|
||||
exported = true;
|
||||
}
|
||||
const result = {
|
||||
config,
|
||||
evidence_id: evidenceId,
|
||||
source_map_sha256: source.sha256,
|
||||
lookup: report.lookup,
|
||||
matches: answer,
|
||||
export_verified: exported,
|
||||
calls: calls.map((c) => ({
|
||||
method: c.request.method,
|
||||
tool: c.request.params?.name,
|
||||
wire_bytes: c.wire_bytes,
|
||||
elapsed_ms: c.elapsed_ms,
|
||||
isError: c.reply?.result?.isError ?? false,
|
||||
})),
|
||||
};
|
||||
if (config.resultPath)
|
||||
await writeFile(
|
||||
config.resultPath,
|
||||
JSON.stringify({ config, definition, calls, answer: result }),
|
||||
{ flag: "wx" },
|
||||
);
|
||||
console.log(JSON.stringify(result));
|
||||
} finally {
|
||||
child.stdin.end();
|
||||
if (child.exitCode === null && child.signalCode === null)
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
child.kill("SIGTERM");
|
||||
reject(Error("MCP close timeout"));
|
||||
}, 5000);
|
||||
child.once("exit", () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -140,10 +140,10 @@ check(
|
||||
// inputBytes values with a different policy (an overridable pwntools admission,
|
||||
// historical capture intake) are not this budget.
|
||||
check(
|
||||
"inline resident-input budget (use BOUNDED_PRODUCER_LIMITS from src/domain/producerResourceLimits)",
|
||||
"inline resident-input budget (use BOUNDED_PRODUCER_LIMITS from src/process/LinuxResourceLimits)",
|
||||
files,
|
||||
/input_bytes:\s*32 \* 1024 \* 1024/,
|
||||
["src/domain/producerResourceLimits.ts"],
|
||||
["src/process/LinuxResourceLimits.ts"],
|
||||
);
|
||||
|
||||
// Fixed-arity backtick keys audited 2026-10-10: every prefix field is NUL-free
|
||||
|
||||
@@ -1,3 +1,52 @@
|
||||
export const inspectionLimits = {
|
||||
referenceOffset: 0,
|
||||
referenceLimit: 100,
|
||||
resourceOffset: 0,
|
||||
resourceLimit: 100,
|
||||
attributeOffset: 0,
|
||||
attributeLimit: 100,
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
maxTableRows: 1_000,
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
export const memberLimits = {
|
||||
typeOffset: 0,
|
||||
typeLimit: 100,
|
||||
methodOffset: 0,
|
||||
methodLimit: 100,
|
||||
fieldOffset: 0,
|
||||
fieldLimit: 100,
|
||||
memberRefOffset: 0,
|
||||
memberRefLimit: 100,
|
||||
edgeOffset: 0,
|
||||
edgeLimit: 100,
|
||||
instructionAnchorLimit: 100,
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
maxTableRows: 1_000,
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
maxMethodBodyBytes: 1024 * 1024,
|
||||
maxMethodInstructions: 1_000,
|
||||
};
|
||||
|
||||
export const nativeBoundaryLimits = {
|
||||
moduleRefOffset: 0,
|
||||
moduleRefLimit: 100,
|
||||
importOffset: 0,
|
||||
importLimit: 100,
|
||||
implementationOffset: 0,
|
||||
implementationLimit: 100,
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
maxTableRows: 1_000,
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
export const comparisonLimits = {
|
||||
max_method_matches: 100,
|
||||
max_field_matches: 100,
|
||||
max_candidates: 20,
|
||||
};
|
||||
|
||||
export const defaultIlBody = Buffer.from([
|
||||
0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a, 0x2a,
|
||||
]);
|
||||
@@ -19,7 +19,13 @@ import {
|
||||
buildNativePeFixture,
|
||||
} from "../../fixtures/managed/pe.mjs";
|
||||
import { createManagedConformanceSupport } from "./support.mjs";
|
||||
import { defaultIlBody } from "./config.mjs";
|
||||
import {
|
||||
comparisonLimits,
|
||||
defaultIlBody,
|
||||
inspectionLimits,
|
||||
memberLimits,
|
||||
nativeBoundaryLimits,
|
||||
} from "./config.mjs";
|
||||
import { createManagedCompletionReport } from "./completion-report.mjs";
|
||||
import {
|
||||
completeVerifierRun,
|
||||
@@ -39,6 +45,8 @@ const {
|
||||
sha256,
|
||||
} = createManagedConformanceSupport({
|
||||
workspace,
|
||||
inspectionLimits,
|
||||
memberLimits,
|
||||
defaultIlBody,
|
||||
});
|
||||
|
||||
@@ -50,6 +58,7 @@ try {
|
||||
const modernArtifact = inspectManagedArtifactBytes(
|
||||
modern.bytes,
|
||||
modern.target,
|
||||
inspectionLimits,
|
||||
);
|
||||
assert.equal(modernArtifact.classification.status, "managed");
|
||||
assert.equal(modernArtifact.classification.runtime_family, "modern-dotnet");
|
||||
@@ -74,6 +83,7 @@ try {
|
||||
const frameworkArtifact = inspectManagedArtifactBytes(
|
||||
framework.bytes,
|
||||
framework.target,
|
||||
inspectionLimits,
|
||||
);
|
||||
assert.equal(
|
||||
frameworkArtifact.classification.runtime_family,
|
||||
@@ -83,6 +93,7 @@ try {
|
||||
const frameworkBoundaries = inspectManagedNativeBoundariesBytes(
|
||||
framework.bytes,
|
||||
framework.target,
|
||||
nativeBoundaryLimits,
|
||||
);
|
||||
assert.equal(frameworkBoundaries.pinvoke_imports.length, 1);
|
||||
assert.equal(
|
||||
@@ -97,13 +108,14 @@ try {
|
||||
const frameworkMembers = inspectManagedMembersBytes(
|
||||
framework.bytes,
|
||||
framework.target,
|
||||
memberLimits,
|
||||
);
|
||||
const frameworkArtifactEvidence = createEvidence(
|
||||
framework.target,
|
||||
MANAGED_STATIC_PROVIDER,
|
||||
{
|
||||
operation: "inspect_managed_artifact",
|
||||
parameters: {},
|
||||
parameters: inspectionLimits,
|
||||
result: frameworkArtifact,
|
||||
rawResult: null,
|
||||
limitations: frameworkArtifact.limitations,
|
||||
@@ -115,7 +127,7 @@ try {
|
||||
MANAGED_STATIC_PROVIDER,
|
||||
{
|
||||
operation: "inspect_managed_members",
|
||||
parameters: {},
|
||||
parameters: memberLimits,
|
||||
result: frameworkMembers,
|
||||
rawResult: null,
|
||||
limitations: frameworkMembers.limitations,
|
||||
@@ -127,7 +139,7 @@ try {
|
||||
MANAGED_STATIC_PROVIDER,
|
||||
{
|
||||
operation: "inspect_managed_native_boundaries",
|
||||
parameters: {},
|
||||
parameters: nativeBoundaryLimits,
|
||||
result: frameworkBoundaries,
|
||||
rawResult: null,
|
||||
limitations: frameworkBoundaries.limitations,
|
||||
@@ -209,6 +221,7 @@ try {
|
||||
const r2rArtifact = inspectManagedArtifactBytes(
|
||||
readyToRun.bytes,
|
||||
readyToRun.target,
|
||||
inspectionLimits,
|
||||
);
|
||||
assert.equal(r2rArtifact.pe.architecture, "x86_64");
|
||||
assert.equal(
|
||||
@@ -218,6 +231,7 @@ try {
|
||||
const r2rBoundaries = inspectManagedNativeBoundariesBytes(
|
||||
readyToRun.bytes,
|
||||
readyToRun.target,
|
||||
nativeBoundaryLimits,
|
||||
);
|
||||
assert.equal(r2rBoundaries.summary.ready_to_run, true);
|
||||
assert.equal(r2rBoundaries.native_implementations[0]?.code_type, "native");
|
||||
@@ -234,6 +248,7 @@ try {
|
||||
const obfuscatedMembers = inspectManagedMembersBytes(
|
||||
obfuscated.bytes,
|
||||
obfuscated.target,
|
||||
memberLimits,
|
||||
);
|
||||
assert.equal(obfuscatedMembers.types[0]?.full_name, "Fixture.ꙮType");
|
||||
assert.equal(obfuscatedMembers.methods[0]?.name, "λ⛧");
|
||||
@@ -294,6 +309,11 @@ try {
|
||||
const comparison = await compareManagedMemberPaths({
|
||||
leftPath: left.path,
|
||||
rightPath: right.path,
|
||||
memberLimits: {
|
||||
maxFileBytes: 1024 * 1024,
|
||||
...memberLimits,
|
||||
},
|
||||
comparisonLimits,
|
||||
});
|
||||
assert.equal(comparison.ok, true);
|
||||
const comparisonResult = comparison.value.normalized_result;
|
||||
@@ -321,6 +341,11 @@ try {
|
||||
const partialBodyComparison = await compareManagedMemberPaths({
|
||||
leftPath: unavailableBodyLeft.path,
|
||||
rightPath: unavailableBodyRight.path,
|
||||
memberLimits: {
|
||||
maxFileBytes: 1024 * 1024,
|
||||
...memberLimits,
|
||||
},
|
||||
comparisonLimits,
|
||||
});
|
||||
assert.equal(partialBodyComparison.ok, true);
|
||||
const partialBodyMethod =
|
||||
@@ -338,6 +363,7 @@ try {
|
||||
const nativeResult = inspectManagedArtifactBytes(
|
||||
nativeOnly.bytes,
|
||||
nativeOnly.target,
|
||||
inspectionLimits,
|
||||
);
|
||||
assert.equal(nativeResult.classification.status, "not-managed");
|
||||
|
||||
@@ -347,6 +373,7 @@ try {
|
||||
const malformedResult = inspectManagedArtifactBytes(
|
||||
malformed.bytes,
|
||||
malformed.target,
|
||||
inspectionLimits,
|
||||
);
|
||||
assert.equal(malformedResult.classification.status, "malformed");
|
||||
|
||||
|
||||
@@ -14,7 +14,13 @@ import {
|
||||
parseManagedAppManifest,
|
||||
} from "./manifest-parser.mjs";
|
||||
|
||||
export async function verifyManagedAppManifest(rawManifest, manifestPath) {
|
||||
export const createManagedManifestVerifier = (context) => ({
|
||||
verifyManagedAppManifest: (rawManifest, manifestPath) =>
|
||||
verifyManagedAppManifest(context, rawManifest, manifestPath),
|
||||
});
|
||||
|
||||
async function verifyManagedAppManifest(context, rawManifest, manifestPath) {
|
||||
const { appManifestInspectionLimits } = context;
|
||||
const manifest = parseManagedAppManifest(rawManifest);
|
||||
const targetPath = isAbsolute(manifest.target.path)
|
||||
? manifest.target.path
|
||||
@@ -30,15 +36,25 @@ export async function verifyManagedAppManifest(rawManifest, manifestPath) {
|
||||
`target sha256 mismatch: expected ${manifest.target.sha256}, observed ${target.sha256}`,
|
||||
);
|
||||
const bytes = await readFile(targetPath);
|
||||
const artifact = inspectManagedArtifactBytes(bytes, target);
|
||||
const artifact = inspectManagedArtifactBytes(
|
||||
bytes,
|
||||
target,
|
||||
appManifestInspectionLimits,
|
||||
);
|
||||
verifyManifestTarget(manifest, artifact);
|
||||
const methodVerification = verifyManifestMethods(manifest, bytes, target);
|
||||
const methodVerification = verifyManifestMethods(
|
||||
context,
|
||||
manifest,
|
||||
bytes,
|
||||
target,
|
||||
);
|
||||
const { methods, inspectedMethods } = methodVerification;
|
||||
let assertions = 1 + methodVerification.assertions;
|
||||
const applicationGraph =
|
||||
manifest.application_graph === undefined
|
||||
? undefined
|
||||
: verifyManagedAppManifestApplicationGraph({
|
||||
context,
|
||||
graphManifest: manifest.application_graph,
|
||||
bytes,
|
||||
target,
|
||||
@@ -93,12 +109,17 @@ function verifyManifestTarget(manifest, artifact) {
|
||||
);
|
||||
}
|
||||
|
||||
function verifyManifestMethods(manifest, bytes, target) {
|
||||
function verifyManifestMethods(context, manifest, bytes, target) {
|
||||
const methods = [];
|
||||
let assertions = 0;
|
||||
const inspectedMethods = new Map();
|
||||
for (const expectedMethod of manifest.methods) {
|
||||
const method = inspectManagedMethodByToken(bytes, target, expectedMethod);
|
||||
const method = inspectManagedMethodByToken(
|
||||
context,
|
||||
bytes,
|
||||
target,
|
||||
expectedMethod,
|
||||
);
|
||||
ensure(
|
||||
method.signature.raw_sha256 === expectedMethod.signature_sha256,
|
||||
`method ${expectedMethod.token} signature sha256 mismatch: expected ${expectedMethod.signature_sha256}, observed ${method.signature.raw_sha256}`,
|
||||
@@ -133,6 +154,7 @@ function verifyManifestMethods(manifest, bytes, target) {
|
||||
}
|
||||
|
||||
function verifyManagedAppManifestApplicationGraph({
|
||||
context,
|
||||
graphManifest,
|
||||
bytes,
|
||||
target,
|
||||
@@ -142,6 +164,7 @@ function verifyManagedAppManifestApplicationGraph({
|
||||
}) {
|
||||
const graphByToken = new Map();
|
||||
const projectionState = {
|
||||
context,
|
||||
graphByToken,
|
||||
inspectedMethods,
|
||||
bytes,
|
||||
@@ -215,6 +238,8 @@ function verifyManagedAppManifestApplicationGraph({
|
||||
}
|
||||
|
||||
const projectManifestMethod = (state, token) => {
|
||||
const { appManifestInspectionLimits, appManifestMemberLimits } =
|
||||
state.context;
|
||||
const existing = state.graphByToken.get(token);
|
||||
if (existing !== undefined) return existing;
|
||||
const method = state.inspectedMethods.get(token);
|
||||
@@ -223,6 +248,7 @@ const projectManifestMethod = (state, token) => {
|
||||
`application graph method ${token} must also be declared in manifest.methods`,
|
||||
);
|
||||
const members = inspectManagedMethodPageByToken(
|
||||
state.context,
|
||||
state.bytes,
|
||||
state.target,
|
||||
token,
|
||||
@@ -232,7 +258,7 @@ const projectManifestMethod = (state, token) => {
|
||||
MANAGED_STATIC_PROVIDER,
|
||||
{
|
||||
operation: "inspect_managed_artifact",
|
||||
parameters: {},
|
||||
parameters: appManifestInspectionLimits,
|
||||
result: state.artifact,
|
||||
rawResult: null,
|
||||
limitations: state.artifact.limitations,
|
||||
@@ -242,7 +268,7 @@ const projectManifestMethod = (state, token) => {
|
||||
const memberEvidence = createEvidence(state.target, MANAGED_STATIC_PROVIDER, {
|
||||
operation: "inspect_managed_members",
|
||||
parameters: {
|
||||
methodLimit: 1,
|
||||
...appManifestMemberLimits,
|
||||
methodOffset: methodTokenRow(token) - 1,
|
||||
},
|
||||
result: members,
|
||||
@@ -272,8 +298,9 @@ const projectManifestMethod = (state, token) => {
|
||||
return projection;
|
||||
};
|
||||
|
||||
function inspectManagedMethodByToken(bytes, target, expectedMethod) {
|
||||
function inspectManagedMethodByToken(context, bytes, target, expectedMethod) {
|
||||
const members = inspectManagedMethodPageByToken(
|
||||
context,
|
||||
bytes,
|
||||
target,
|
||||
expectedMethod.token,
|
||||
@@ -292,10 +319,11 @@ function inspectManagedMethodByToken(bytes, target, expectedMethod) {
|
||||
return method;
|
||||
}
|
||||
|
||||
function inspectManagedMethodPageByToken(bytes, target, token) {
|
||||
function inspectManagedMethodPageByToken(context, bytes, target, token) {
|
||||
const { appManifestMemberLimits } = context;
|
||||
const row = methodTokenRow(token);
|
||||
return inspectManagedMembersBytes(bytes, target, {
|
||||
methodLimit: 1,
|
||||
...appManifestMemberLimits,
|
||||
methodOffset: row - 1,
|
||||
});
|
||||
}
|
||||
@@ -10,12 +10,13 @@ import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberIn
|
||||
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/domain/investigationProviders.js";
|
||||
import { verifyManagedAppManifest } from "./manifest.mjs";
|
||||
import { createManagedManifestVerifier } from "./manifest.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const createManagedConformanceOracleSupport = (context) => {
|
||||
const support = { ...context, verifyManagedAppManifest };
|
||||
const verifier = createManagedManifestVerifier(context);
|
||||
const support = { ...context, ...verifier };
|
||||
return {
|
||||
runManagedAppManifestSelfTest: () => runManagedAppManifestSelfTest(support),
|
||||
runOptionalManagedAppManifest: () => runOptionalManagedAppManifest(support),
|
||||
@@ -28,8 +29,16 @@ async function runManagedAppManifestSelfTest(context) {
|
||||
methodName: "PinnedSemanticSlice",
|
||||
ilBody: context.defaultIlBody,
|
||||
});
|
||||
const artifact = inspectManagedArtifactBytes(sample.bytes, sample.target);
|
||||
const members = inspectManagedMembersBytes(sample.bytes, sample.target);
|
||||
const artifact = inspectManagedArtifactBytes(
|
||||
sample.bytes,
|
||||
sample.target,
|
||||
context.inspectionLimits,
|
||||
);
|
||||
const members = inspectManagedMembersBytes(
|
||||
sample.bytes,
|
||||
sample.target,
|
||||
context.memberLimits,
|
||||
);
|
||||
const method = members.methods[0];
|
||||
assert.ok(method);
|
||||
const summary = await context.verifyManagedAppManifest(
|
||||
@@ -123,8 +132,16 @@ async function runOptionalIlspyOracle(context) {
|
||||
const sample = await context.fixture("ilspy-oracle.exe", {
|
||||
methodName: "PinnedSemanticSlice",
|
||||
});
|
||||
const artifact = inspectManagedArtifactBytes(sample.bytes, sample.target);
|
||||
const members = inspectManagedMembersBytes(sample.bytes, sample.target);
|
||||
const artifact = inspectManagedArtifactBytes(
|
||||
sample.bytes,
|
||||
sample.target,
|
||||
context.inspectionLimits,
|
||||
);
|
||||
const members = inspectManagedMembersBytes(
|
||||
sample.bytes,
|
||||
sample.target,
|
||||
context.memberLimits,
|
||||
);
|
||||
const method = members.methods[0];
|
||||
ensureIlspy(method !== undefined, "source-owned fixture has no method");
|
||||
const listOutput = await runIlspy(
|
||||
@@ -150,6 +167,7 @@ async function runOptionalIlspyOracle(context) {
|
||||
"ilspycmd C# output exceeded the managed reconstruction import bound",
|
||||
);
|
||||
const reconstructionImport = importIlspyReconstruction({
|
||||
context,
|
||||
sample,
|
||||
method,
|
||||
members,
|
||||
@@ -192,6 +210,7 @@ async function runOptionalIlspyOracle(context) {
|
||||
}
|
||||
|
||||
function importIlspyReconstruction({
|
||||
context,
|
||||
sample,
|
||||
method,
|
||||
members,
|
||||
@@ -204,7 +223,7 @@ function importIlspyReconstruction({
|
||||
MANAGED_STATIC_PROVIDER,
|
||||
{
|
||||
operation: "inspect_managed_members",
|
||||
parameters: {},
|
||||
parameters: context.memberLimits,
|
||||
result: members,
|
||||
rawResult: null,
|
||||
limitations: members.limitations,
|
||||
|
||||
@@ -17,11 +17,32 @@ export const createManagedConformanceSupport = (context) => {
|
||||
const fixture = (name, options) =>
|
||||
fixtureBytes(name, buildManagedPeFixture(options));
|
||||
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const appManifestInspectionLimits = {
|
||||
...context.inspectionLimits,
|
||||
maxMetadataBytes: 64 * 1024 * 1024,
|
||||
maxTableRows: 250_000,
|
||||
maxHeapItemBytes: 16 * 1024 * 1024,
|
||||
};
|
||||
const appManifestMemberLimits = {
|
||||
...context.memberLimits,
|
||||
typeLimit: 1,
|
||||
methodLimit: 1,
|
||||
fieldLimit: 1,
|
||||
memberRefLimit: 1,
|
||||
edgeLimit: 1,
|
||||
maxMetadataBytes: 64 * 1024 * 1024,
|
||||
maxTableRows: 250_000,
|
||||
maxHeapItemBytes: 16 * 1024 * 1024,
|
||||
maxMethodBodyBytes: 4 * 1024 * 1024,
|
||||
maxMethodInstructions: 20_000,
|
||||
};
|
||||
const base = {
|
||||
...context,
|
||||
fixture,
|
||||
fixtureBytes,
|
||||
sha256,
|
||||
appManifestInspectionLimits,
|
||||
appManifestMemberLimits,
|
||||
};
|
||||
return { ...base, ...createManagedConformanceOracleSupport(base) };
|
||||
};
|
||||
@@ -177,64 +177,6 @@ const apkFixture = async (
|
||||
};
|
||||
};
|
||||
|
||||
it("recovers later string pages and an exhausted XML admission on the same APK", async () => {
|
||||
const stringsXml = `<resources>${Array.from({ length: 2001 }, (_, index) => `<string name="s_${String(index)}">value_${String(index)}</string>`).join("")}</resources>`;
|
||||
const { command, root } = await writeApktoolStub(decodeStub({ stringsXml }));
|
||||
const apk = await apkFixture(root);
|
||||
const provider = providerOver(command);
|
||||
onTestFinished(() => provider.close());
|
||||
const service = new ApktoolResourceAnalysisService(provider);
|
||||
const inspect = async (input: Record<string, unknown>) => {
|
||||
const outcome = await service.execute("decode_android_resources", {
|
||||
path: apk.path,
|
||||
...input,
|
||||
});
|
||||
if (!outcome.ok) throw outcome.error;
|
||||
expect(outcome.value.subject).toMatchObject({
|
||||
digest: { sha256: apk.sha256 },
|
||||
});
|
||||
return apktoolResultSchemas.decode_android_resources.parse(
|
||||
outcome.value.normalized_result,
|
||||
);
|
||||
};
|
||||
|
||||
const first = await inspect({ string_tables_only: true, string_limit: 2000 });
|
||||
expect(first.strings).toEqual([]);
|
||||
expect(first.string_tables?.[0]).toMatchObject({
|
||||
total_count: 2001,
|
||||
unparsed_count: 0,
|
||||
page: { offset: 0, returned_count: 2000, next_offset: 2000 },
|
||||
coverage: "partial",
|
||||
});
|
||||
const next = await inspect({
|
||||
string_tables_only: true,
|
||||
string_offset: first.string_tables?.[0]?.page?.next_offset,
|
||||
string_limit: 2000,
|
||||
});
|
||||
expect(next.string_tables?.[0]).toMatchObject({
|
||||
strings: [{ name: "s_2000", value: "value_2000" }],
|
||||
total_count: 2001,
|
||||
page: { offset: 2000, returned_count: 1, next_offset: null },
|
||||
coverage: "partial",
|
||||
});
|
||||
|
||||
const refused = await inspect({ max_string_table_bytes: 100 });
|
||||
expect(refused.string_tables?.[0]).toMatchObject({
|
||||
status: "oversized",
|
||||
total_count: null,
|
||||
});
|
||||
const recovered = await inspect({
|
||||
string_limit: 2001,
|
||||
max_string_table_bytes: Buffer.byteLength(stringsXml) + 1,
|
||||
});
|
||||
expect(recovered.strings).toHaveLength(2001);
|
||||
expect(recovered.string_tables?.[0]).toMatchObject({
|
||||
total_count: 2001,
|
||||
page: { next_offset: null },
|
||||
coverage: "complete",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns table-only facts for a single selected primary table", async () => {
|
||||
const { command, root } = await writeApktoolStub(decodeStub());
|
||||
const apk = await apkFixture(root);
|
||||
|
||||
@@ -31,7 +31,6 @@ import { providerConfigurationRefusal } from "../domain/providerConfigurationRef
|
||||
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
|
||||
import {
|
||||
apktoolResultSchemas,
|
||||
apktoolStringTablesRequested,
|
||||
DEFAULT_APKTOOL_DECODE_TIMEOUT_MS,
|
||||
MAX_APKTOOL_DECODE_TIMEOUT_MS,
|
||||
type ApktoolRequest,
|
||||
@@ -667,7 +666,7 @@ export class ApktoolProvider {
|
||||
: "complete";
|
||||
|
||||
completedDecode.strings = [...projectedStrings.strings];
|
||||
if (apktoolStringTablesRequested(input))
|
||||
if (input.locales !== undefined || input.string_tables_only === true)
|
||||
completedDecode.string_tables = [...projectedStrings.tables];
|
||||
completedDecode.coverage = coverage;
|
||||
if (!projection.ok) throw projection.error.cause;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
DEFAULT_APKTOOL_TOTAL_STRINGS,
|
||||
DEFAULT_APKTOOL_STRING_LIMIT,
|
||||
MAX_AGGREGATE_STRINGS,
|
||||
MAX_STRINGS,
|
||||
} from "../domain/apktool/apktoolResourceAnalysis.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { projectApktoolStringTables } from "./ApktoolStringTables.js";
|
||||
@@ -24,7 +24,7 @@ it("charges table-only output without reserving a duplicate primary copy", async
|
||||
{
|
||||
async readText(path) {
|
||||
paths.push(path);
|
||||
return textRead(xml(DEFAULT_APKTOOL_STRING_LIMIT));
|
||||
return textRead(xml(MAX_STRINGS));
|
||||
},
|
||||
},
|
||||
"decoded",
|
||||
@@ -42,7 +42,7 @@ it("charges table-only output without reserving a duplicate primary copy", async
|
||||
);
|
||||
expect(
|
||||
outcome.value.tables.reduce((sum, table) => sum + table.strings.length, 0),
|
||||
).toBe(DEFAULT_APKTOOL_TOTAL_STRINGS);
|
||||
).toBe(MAX_AGGREGATE_STRINGS);
|
||||
expect(outcome.value.coverage).toBe("complete");
|
||||
});
|
||||
|
||||
@@ -115,43 +115,12 @@ describe("Apktool locale alias table selection", () => {
|
||||
});
|
||||
|
||||
describe("physical Apktool string facts", () => {
|
||||
it("applies the selected page before aggregate allocation while preserving alias counts", async () => {
|
||||
const result = await projectApktoolStringTables(
|
||||
{ readText: async () => textRead(xml(4)) },
|
||||
"decoded",
|
||||
{
|
||||
include_strings: true,
|
||||
locale: "pt-BR",
|
||||
locales: ["pt-rBR"],
|
||||
string_tables_only: true,
|
||||
string_offset: 1,
|
||||
string_limit: 2,
|
||||
max_total_strings: 3,
|
||||
},
|
||||
);
|
||||
if (!result.ok) throw result.error.cause;
|
||||
expect(result.value.tables).toMatchObject([
|
||||
{
|
||||
strings: [{ name: "s_1" }, { name: "s_2" }],
|
||||
total_count: 4,
|
||||
page: { offset: 1, returned_count: 2, next_offset: 3 },
|
||||
coverage: "partial",
|
||||
},
|
||||
{
|
||||
strings: [{ name: "s_1" }],
|
||||
total_count: 4,
|
||||
page: { offset: 1, returned_count: 1, next_offset: 2 },
|
||||
coverage: "partial",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("counts complete source entries beyond the retained table prefix", async () => {
|
||||
const outcome = await projectApktoolStringTables(
|
||||
{
|
||||
async readText() {
|
||||
return textRead(
|
||||
xml(DEFAULT_APKTOOL_STRING_LIMIT + 1).replace(
|
||||
xml(MAX_STRINGS + 1).replace(
|
||||
"</resources>",
|
||||
'<string name="styled"><b>omitted</b></string></resources>',
|
||||
),
|
||||
@@ -164,13 +133,13 @@ describe("physical Apktool string facts", () => {
|
||||
if (!outcome.ok) throw outcome.error.cause;
|
||||
const result = outcome.value;
|
||||
expect(result.tables[0]).toMatchObject({
|
||||
total_count: DEFAULT_APKTOOL_STRING_LIMIT + 1,
|
||||
total_count: MAX_STRINGS + 1,
|
||||
unparsed_count: 1,
|
||||
coverage: "partial",
|
||||
});
|
||||
expect(result.strings).toHaveLength(DEFAULT_APKTOOL_STRING_LIMIT);
|
||||
expect(result.strings).toHaveLength(MAX_STRINGS);
|
||||
expect(result.limitations).toContain(
|
||||
`strings reports the first ${String(DEFAULT_APKTOOL_STRING_LIMIT)} of ${String(DEFAULT_APKTOOL_STRING_LIMIT + 1)} entries`,
|
||||
`strings reports the first ${String(MAX_STRINGS)} of ${String(MAX_STRINGS + 1)} entries`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -182,7 +151,7 @@ describe("physical Apktool string facts", () => {
|
||||
paths.push(path);
|
||||
if (path.includes("values-fr"))
|
||||
throw new Error("Unexamined table was read");
|
||||
return textRead(xml(DEFAULT_APKTOOL_STRING_LIMIT));
|
||||
return textRead(xml(MAX_STRINGS));
|
||||
},
|
||||
},
|
||||
"decoded",
|
||||
@@ -202,7 +171,7 @@ describe("physical Apktool string facts", () => {
|
||||
expect(
|
||||
result.strings.length +
|
||||
result.tables.reduce((count, table) => count + table.strings.length, 0),
|
||||
).toBe(DEFAULT_APKTOOL_TOTAL_STRINGS);
|
||||
).toBe(MAX_AGGREGATE_STRINGS);
|
||||
});
|
||||
|
||||
it("keeps a known empty alias present after the output budget is spent", async () => {
|
||||
@@ -212,9 +181,7 @@ describe("physical Apktool string facts", () => {
|
||||
async readText(path) {
|
||||
paths.push(path);
|
||||
return textRead(
|
||||
xml(
|
||||
path.includes("values-pt-rBR") ? 0 : DEFAULT_APKTOOL_STRING_LIMIT,
|
||||
),
|
||||
xml(path.includes("values-pt-rBR") ? 0 : MAX_STRINGS),
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,11 +2,9 @@ import { join } from "node:path";
|
||||
import { ok, err, type Result } from "../domain/result.js";
|
||||
|
||||
import {
|
||||
DEFAULT_APKTOOL_STRING_LIMIT,
|
||||
DEFAULT_APKTOOL_TOTAL_STRINGS,
|
||||
DEFAULT_APKTOOL_STRING_TABLE_BYTES,
|
||||
DEFAULT_APKTOOL_TOTAL_STRING_TABLE_BYTES,
|
||||
apktoolStringTablesRequested,
|
||||
AGGREGATE_STRINGS_XML_MAX_BYTES,
|
||||
MAX_AGGREGATE_STRINGS,
|
||||
MAX_STRINGS,
|
||||
androidLocaleDirectorySuffix,
|
||||
type ApktoolRequest,
|
||||
type StringTable,
|
||||
@@ -18,17 +16,10 @@ import {
|
||||
} from "./ApktoolDecodeOutput.js";
|
||||
import type { ApktoolWorkspace } from "./ApktoolWorkspace.js";
|
||||
|
||||
const STRINGS_XML_MAX_BYTES = 8 * 1024 * 1024;
|
||||
type Selection = Pick<
|
||||
Extract<ApktoolRequest, { operation: "decode_android_resources" }>["input"],
|
||||
| "include_strings"
|
||||
| "locale"
|
||||
| "locales"
|
||||
| "string_tables_only"
|
||||
| "string_offset"
|
||||
| "string_limit"
|
||||
| "max_total_strings"
|
||||
| "max_string_table_bytes"
|
||||
| "max_total_string_table_bytes"
|
||||
"include_strings" | "locale" | "locales" | "string_tables_only"
|
||||
>;
|
||||
type PresentTable = {
|
||||
readonly status: "present";
|
||||
@@ -61,8 +52,6 @@ const readTable = async (
|
||||
resources: Pick<ApktoolWorkspace, "readText">,
|
||||
path: string,
|
||||
budget: number,
|
||||
offset: number,
|
||||
limit: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ readonly fact: PhysicalTable; readonly bytes: number }> => {
|
||||
const source = await resources.readText(path, budget, signal);
|
||||
@@ -97,13 +86,9 @@ const readTable = async (
|
||||
bytes,
|
||||
fact: {
|
||||
status: "present",
|
||||
// Cache only the requested page for aliases. Counts describe the whole
|
||||
// examined table, independently of selection and output allocation.
|
||||
entries: parsed.value.entries.slice(
|
||||
offset,
|
||||
offset +
|
||||
Math.min(limit, Math.max(0, parsed.value.entries.length - offset)),
|
||||
),
|
||||
// Future aliases need counts and at most one table's output, not every
|
||||
// parsed entry. The complete source was examined before this retention cut.
|
||||
entries: parsed.value.entries.slice(0, MAX_STRINGS),
|
||||
totalCount: parsed.value.entries.length,
|
||||
unparsedCount: parsed.value.unparsedCount,
|
||||
},
|
||||
@@ -121,12 +106,9 @@ const unavailableTableLimitation = (
|
||||
? "No default res/values/strings.xml was found in the decoded workspace"
|
||||
: `No strings.xml was found for locale ${locale}`;
|
||||
if (fact.status === "oversized")
|
||||
return (
|
||||
(input.locales === undefined || locale === null
|
||||
? `strings.xml exceeds the ${String(fact.byteBudget)}-byte projection budget`
|
||||
: `strings.xml for locale ${locale} exceeds the ${String(fact.byteBudget)}-byte projection budget`) +
|
||||
"; raise max_string_table_bytes and max_total_string_table_bytes as needed on the next decode, or select fewer locales. No REA restart is required; decode_timeout_ms does not raise these read budgets."
|
||||
);
|
||||
return input.locales === undefined || locale === null
|
||||
? `strings.xml exceeds the ${String(fact.byteBudget)}-byte projection budget`
|
||||
: `strings.xml for locale ${locale} exceeds the ${String(fact.byteBudget)}-byte projection budget`;
|
||||
return `String table ${JSON.stringify(`res/${directory}/strings.xml`)} could not be projected: ${fact.message}; entry counts are unknown`;
|
||||
};
|
||||
|
||||
@@ -157,44 +139,18 @@ export const projectApktoolStringTables = async (
|
||||
coverage: "complete",
|
||||
});
|
||||
const primary = input.locale ?? null;
|
||||
const offset = input.string_offset ?? 0;
|
||||
const limit = input.string_limit ?? DEFAULT_APKTOOL_STRING_LIMIT;
|
||||
const maxTotalStrings =
|
||||
input.max_total_strings ?? DEFAULT_APKTOOL_TOTAL_STRINGS;
|
||||
const tableBytes =
|
||||
input.max_string_table_bytes ?? DEFAULT_APKTOOL_STRING_TABLE_BYTES;
|
||||
const totalTableBytes =
|
||||
input.max_total_string_table_bytes ??
|
||||
DEFAULT_APKTOOL_TOTAL_STRING_TABLE_BYTES;
|
||||
const includeTables = apktoolStringTablesRequested(input);
|
||||
const includePage =
|
||||
input.string_offset !== undefined ||
|
||||
input.string_limit !== undefined ||
|
||||
input.max_total_strings !== undefined ||
|
||||
input.max_string_table_bytes !== undefined ||
|
||||
input.max_total_string_table_bytes !== undefined;
|
||||
const locales = [...new Set([primary, ...(input.locales ?? [])])];
|
||||
const physical = new Map<string, PhysicalTable>();
|
||||
const tables: StringTable[] = [];
|
||||
const limitations = new Set<string>();
|
||||
let xmlBytes = 0;
|
||||
let outputEntries = 0;
|
||||
const exhausted = new Set<
|
||||
"max_total_strings" | "max_total_string_table_bytes"
|
||||
>();
|
||||
let aggregateCapped = false;
|
||||
const projection = (): ApktoolStringProjection => {
|
||||
for (const setting of exhausted) {
|
||||
const value =
|
||||
setting === "max_total_strings" ? maxTotalStrings : totalTableBytes;
|
||||
const unit =
|
||||
setting === "max_total_strings"
|
||||
? "returned string copies"
|
||||
: "XML source bytes";
|
||||
const source = input[setting] === undefined ? "default" : "request";
|
||||
if (aggregateCapped)
|
||||
limitations.add(
|
||||
`Aggregate string table projection reached ${setting}=${String(value)} ${unit} (source ${source}). Raise ${setting} or select fewer locales for the next decode. ${setting === "max_total_strings" ? "string_tables_only avoids a duplicate primary copy. " : "max_string_table_bytes independently bounds each XML source read. "}No REA restart is required.`,
|
||||
"Aggregate string table projection stopped due to resource limits",
|
||||
);
|
||||
}
|
||||
return {
|
||||
strings:
|
||||
input.string_tables_only === true ? [] : (tables[0]?.strings ?? []),
|
||||
@@ -223,8 +179,11 @@ export const projectApktoolStringTables = async (
|
||||
: `res/${directory}/strings.xml`;
|
||||
let fact = physical.get(directory);
|
||||
if (fact === undefined) {
|
||||
const budget = Math.min(tableBytes, totalTableBytes - xmlBytes);
|
||||
if (budget <= 0 || outputEntries >= maxTotalStrings) {
|
||||
const budget = Math.min(
|
||||
STRINGS_XML_MAX_BYTES,
|
||||
AGGREGATE_STRINGS_XML_MAX_BYTES - xmlBytes,
|
||||
);
|
||||
if (budget <= 0 || outputEntries >= MAX_AGGREGATE_STRINGS) {
|
||||
tables.push({
|
||||
locale,
|
||||
status: "skipped",
|
||||
@@ -236,23 +195,19 @@ export const projectApktoolStringTables = async (
|
||||
limitations.add(
|
||||
`Projection for ${label} skipped: aggregate resource limit reached`,
|
||||
);
|
||||
if (budget <= 0) exhausted.add("max_total_string_table_bytes");
|
||||
if (outputEntries >= maxTotalStrings)
|
||||
exhausted.add("max_total_strings");
|
||||
aggregateCapped = true;
|
||||
continue;
|
||||
}
|
||||
const read = await readTable(
|
||||
resources,
|
||||
join(workspace, "res", directory, "strings.xml"),
|
||||
budget,
|
||||
offset,
|
||||
limit,
|
||||
signal,
|
||||
);
|
||||
fact = read.fact;
|
||||
xmlBytes += read.bytes;
|
||||
if (fact.status === "oversized" && budget < tableBytes)
|
||||
exhausted.add("max_total_string_table_bytes");
|
||||
if (fact.status === "oversized" && budget < STRINGS_XML_MAX_BYTES)
|
||||
aggregateCapped = true;
|
||||
physical.set(directory, fact);
|
||||
}
|
||||
if (fact.status !== "present") {
|
||||
@@ -272,15 +227,17 @@ export const projectApktoolStringTables = async (
|
||||
// The default batch shape repeats primary entries in strings and tables;
|
||||
// table-only output charges one copy. Account before selecting entries.
|
||||
const copies =
|
||||
index === 0 && includeTables && input.string_tables_only !== true
|
||||
index === 0 &&
|
||||
input.locales !== undefined &&
|
||||
input.string_tables_only !== true
|
||||
? 2
|
||||
: 1;
|
||||
const remaining = Math.max(0, maxTotalStrings - outputEntries);
|
||||
const remaining = Math.max(0, MAX_AGGREGATE_STRINGS - outputEntries);
|
||||
const selected = fact.entries.slice(0, Math.floor(remaining / copies));
|
||||
outputEntries += selected.length * copies;
|
||||
const skipped = selected.length === 0 && fact.entries.length > 0;
|
||||
const limited = selected.length < fact.entries.length;
|
||||
if (limited) exhausted.add("max_total_strings");
|
||||
const skipped = selected.length === 0 && fact.totalCount > 0;
|
||||
const limited = selected.length < Math.min(fact.totalCount, MAX_STRINGS);
|
||||
if (limited) aggregateCapped = true;
|
||||
tables.push({
|
||||
locale,
|
||||
status: skipped ? "skipped" : "present",
|
||||
@@ -288,20 +245,6 @@ export const projectApktoolStringTables = async (
|
||||
// Output exhaustion cannot erase counts from an already examined table.
|
||||
total_count: fact.totalCount,
|
||||
unparsed_count: fact.unparsedCount,
|
||||
...(includePage
|
||||
? {
|
||||
page: {
|
||||
offset,
|
||||
limit,
|
||||
returned_count: selected.length,
|
||||
next_offset:
|
||||
offset < fact.totalCount &&
|
||||
selected.length < fact.totalCount - offset
|
||||
? offset + selected.length
|
||||
: null,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
coverage:
|
||||
selected.length < fact.totalCount || fact.unparsedCount > 0
|
||||
? "partial"
|
||||
@@ -321,16 +264,12 @@ export const projectApktoolStringTables = async (
|
||||
limitations.add(
|
||||
`Locale ${locale ?? "default"} strings reports ${String(selected.length)} entries due to aggregate strings limit`,
|
||||
);
|
||||
else if (fact.totalCount > limit && offset === 0 && !skipped)
|
||||
else if (fact.totalCount > MAX_STRINGS && !skipped)
|
||||
limitations.add(
|
||||
input.locales === undefined || locale === null
|
||||
? `strings reports the first ${String(selected.length)} of ${String(fact.totalCount)} entries`
|
||||
: `Locale ${locale} strings reports the first ${String(selected.length)} of ${String(fact.totalCount)} entries`,
|
||||
);
|
||||
if (selected.length < fact.totalCount && !skipped)
|
||||
limitations.add(
|
||||
`String page for ${label} starts at string_offset=${String(offset)} and returns ${String(selected.length)} of ${String(fact.totalCount)} valid entries. ${offset + selected.length < fact.totalCount ? `Continue with string_offset=${String(offset + selected.length)} and the same APK digest and locale;` : "This page reaches the end of the table;"} string_limit=${String(limit)} controls each page and max_total_strings=${String(maxTotalStrings)} controls returned copies. Table coverage describes the entire source table.`,
|
||||
);
|
||||
}
|
||||
} catch (cause) {
|
||||
limitations.add(
|
||||
|
||||
@@ -181,7 +181,7 @@ export class ApktoolWorkspace {
|
||||
signal,
|
||||
async ({ handle: file, initial }) => {
|
||||
admitted = true;
|
||||
const bytes = Buffer.alloc(Math.min(initial.size, budget) + 1);
|
||||
const bytes = Buffer.alloc(budget + 1);
|
||||
let length = 0;
|
||||
while (length < bytes.length) {
|
||||
assertArtifactReadActive(signal);
|
||||
|
||||
Loaded 100 of 518 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user