mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 16:57:19 +00:00
* refactor(analysis): centralize resource budgets and recovery facts * feat(analysis): expose ASAR and Ghidra runtime admission controls * feat(artifacts): expose reader admission and preserve limited recognition * feat(javascript): expose recoverable semantic precision budgets * feat(web): add recoverable source-map budgets and compact retained views * feat(web): make historical capture decoding budgets recoverable * fix(desktop): enforce selected view policies across capture and export * feat(runtime): make Frida and LLDB retention budgets selectable * feat(native): expose diagnostic budgets and preserve exhaustion causes * feat(managed): expose recoverable snapshot and representation budgets * feat(metadata): make Go and Rust inspection budgets recoverable * feat(wasm): expose representation and producer admissions * feat(evm): expose per-call worker resource budgets * feat(android): make resource and string-pool omissions recoverable * feat(producers): expose command and recorded-capture budgets * docs(analysis): record recovery contracts and integrated validation * fix(docs): link calibration oracle from published verification pages * fix(docs): restore desktop task anchors and receipt links * test(analysis): align fixtures with selectable budget contracts * fix(analysis): remove source-location projector dependency cycle * test(analysis): refresh budget recovery and verifier regressions * test(analysis): preserve format and publication recovery expectations * test(windows): align conformance with owned runtime admission * fix(desktop): preserve selected alias kinds in exported state * test(analysis): align provider fixtures with recoverable budgets * test(desktop): validate emitted alias metadata structurally * fix(ghidra): classify bounded Windows endpoint refusals * test(cli): assert selectable artifact admission recovery * fix(cli): preserve capture provenance in environment builds
427 lines
13 KiB
JavaScript
427 lines
13 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
}
|