mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 17:59:08 +00:00
Merge upstream/main into fix/release-exposure-audit
Resolve the docs/index.md guide table conflict by taking main's table (Linux kernel modules and wxapkg links) and keeping this branch's Release exposure link.
This commit is contained in:
601 files changed
+28500
-3060
No files matched your search
@@ -3,6 +3,7 @@ export interface ReadinessPullRequest {
|
||||
readonly state: "open" | "closed";
|
||||
readonly draft: boolean;
|
||||
readonly body: string | null;
|
||||
readonly user: { readonly id: number } | null;
|
||||
readonly head: { readonly sha: string };
|
||||
readonly base: { readonly ref: string };
|
||||
}
|
||||
@@ -53,7 +54,7 @@ export interface ReadinessGitHub {
|
||||
>;
|
||||
}
|
||||
export function evaluateReadiness(
|
||||
pull: Pick<ReadinessPullRequest, "body" | "draft">,
|
||||
pull: Pick<ReadinessPullRequest, "body" | "draft" | "user">,
|
||||
template: string,
|
||||
): ReadinessResult;
|
||||
export function runReadinessCheck(input: {
|
||||
|
||||
@@ -2,6 +2,14 @@ import { readFile } from "node:fs/promises";
|
||||
|
||||
const result = (state, description) => ({ state, description });
|
||||
|
||||
// GitHub account IDs remain stable when a login changes.
|
||||
const EXEMPT_AUTHOR_IDS = new Set([
|
||||
76467478, // morluto
|
||||
208281322, // kaoru0822-kitauji
|
||||
161784452, // N0zoM1z0
|
||||
342580529, // gensokyo-dev
|
||||
]);
|
||||
|
||||
const OPEN_HEADS_QUERY = `query ReadinessOpenHeads($owner: String!, $name: String!, $after: String) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
pullRequests(first: 100, after: $after, states: OPEN, orderBy: {field: CREATED_AT, direction: ASC}) {
|
||||
@@ -105,6 +113,11 @@ export function evaluateReadiness(pull, template) {
|
||||
"pending",
|
||||
"Draft PR: complete Author readiness before requesting review.",
|
||||
);
|
||||
if (EXEMPT_AUTHOR_IDS.has(pull.user?.id))
|
||||
return result(
|
||||
"success",
|
||||
"PR author is exempt from the readiness declaration.",
|
||||
);
|
||||
const expected = readinessLines(template).flatMap((line) => {
|
||||
const match = /^- \[ \] (.+)$/u.exec(line);
|
||||
return match ? [match[1]] : [];
|
||||
@@ -210,6 +223,7 @@ export async function runReadinessCheck({ github, context, core }) {
|
||||
latest.head.sha !== pull.head.sha ||
|
||||
latest.body !== pull.body ||
|
||||
latest.draft !== pull.draft ||
|
||||
latest.user?.id !== pull.user?.id ||
|
||||
latest.base.ref !== pull.base.ref
|
||||
)
|
||||
verdict = result(
|
||||
|
||||
@@ -76,6 +76,7 @@ const PATH_SCOPES = {
|
||||
"src/frida/frida-bindings.d.ts",
|
||||
"src/objdump/**",
|
||||
"src/cli/reverseEngineeringCommands.ts",
|
||||
"src/process/ownedDebuggerCleanupError.ts",
|
||||
"src/process/ownedDebuggerSession.ts",
|
||||
"src/rizin/**",
|
||||
"src/contracts/*ToolContracts.ts",
|
||||
@@ -103,6 +104,7 @@ const PATH_SCOPES = {
|
||||
"tests/boundary/providers/RizinInferiorTty.test.ts",
|
||||
"tests/boundary/providers/RizinTestInferiorTty.ts",
|
||||
"tests/boundary/process/ownedDebuggerSession.test.ts",
|
||||
"tests/boundary/cli/debuggerOneShotExit.test.ts",
|
||||
"tests/boundary/cli/gdbStartupCleanupCli.test.ts",
|
||||
"tests/boundary/cli/reverseEngineeringCommands.test.ts",
|
||||
"tests/boundary/mcp/cutter*.test.ts",
|
||||
@@ -163,6 +165,9 @@ const PATH_SCOPES = {
|
||||
"tests/boundary/mcp/nativeDemangleMcp.test.ts",
|
||||
"tests/boundary/mcp/nativeMachoSummaryMcp.test.ts",
|
||||
"tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts",
|
||||
"tests/boundary/providers/native/nativeUiOffscreenWindow.test.ts",
|
||||
"tests/boundary/process/nativeUiVerifierLane.test.ts",
|
||||
"tests/conformance/native/ui-offscreen.swift",
|
||||
"tests/boundary/cli/nativeSignatureCli.test.ts",
|
||||
"tests/boundary/cli/nativeMachoArm64_32Cli.test.ts",
|
||||
"tests/boundary/cli/nativeMachoEncryptionCli.test.ts",
|
||||
@@ -699,7 +704,9 @@ export const CI_LANES = {
|
||||
agentenv: sameJobs(PATH_SCOPES.agentenv, ["agentenv"]),
|
||||
nativeaot: sameJobs(PATH_SCOPES.nativeaot, ["nativeaot"]),
|
||||
ghidra_bridge: sameJobs(PATH_SCOPES.ghidra_bridge, ["ghidra_bridge"]),
|
||||
website: sameJobs(PATH_SCOPES.website, ["website"]),
|
||||
website: sameJobs(PATH_SCOPES.website, ["website"], {
|
||||
earlyInputs: [".agents/skills/reverse-engineer-anything/**"],
|
||||
}),
|
||||
};
|
||||
// An input owner omitted from the registry must never silently lose a lane.
|
||||
for (const [name, paths] of Object.entries(PATH_SCOPES))
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
"snapshot_hash": "65817c30a78bb44c3dc3771876b6010a",
|
||||
"engines": {
|
||||
"x86_64": {
|
||||
"dart_version": null,
|
||||
"dart_version": "2.14.0-151.0.dev (dev) (Tue May 25 10:58:39 2021 -0700)",
|
||||
"toolchain_lines": [
|
||||
"Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)"
|
||||
]
|
||||
},
|
||||
"armeabi-v7a": {
|
||||
"dart_version": null,
|
||||
"dart_version": "2.14.0-151.0.dev (dev) (Tue May 25 10:58:39 2021 -0700)",
|
||||
"toolchain_lines": [
|
||||
"Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)"
|
||||
]
|
||||
},
|
||||
"arm64-v8a": {
|
||||
"dart_version": null,
|
||||
"dart_version": "2.14.0-151.0.dev (dev) (Tue May 25 10:58:39 2021 -0700)",
|
||||
"toolchain_lines": [
|
||||
"Android (5900059 based on r365631c) clang version 9.0.8 (https://android.googlesource.com/toolchain/llvm-project 207d7abc1a2abf3ef8d4301736d6a7ebc224a290) (based on LLVM 9.0.8svn)"
|
||||
]
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { webSourceLocationResultSchema } from "../../dist/domain/webSourceLocation.js";
|
||||
import { PrivateRuntimeRoot } from "../../dist/process/PrivateRuntimeRoot.js";
|
||||
import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
|
||||
import { createPackageVerifierCommands } from "./verify-package-commands.mjs";
|
||||
import { importVerifierSource } from "./verifier-source-loader.mjs";
|
||||
|
||||
const { BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS } = await importVerifierSource(
|
||||
new URL("./browser-process-startup.mts", import.meta.url).href,
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
/** Verify real Chrome capture → export → original compiler point through packaged CLI/MCP. */
|
||||
export async function verifyBrowserSourceMap(
|
||||
@@ -21,21 +26,35 @@ export async function verifyBrowserSourceMap(
|
||||
) {
|
||||
let site;
|
||||
let root;
|
||||
let commands;
|
||||
let transport;
|
||||
let client;
|
||||
const closeResource = async (owner, resource) => {
|
||||
if (owner === undefined) return;
|
||||
try {
|
||||
await owner.close();
|
||||
} catch (cause) {
|
||||
commands?.adopt(owner, resource);
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
return withVerifierCleanup(async () => {
|
||||
site = await startBrowserSourceMapSite();
|
||||
root = await mkdtemp(join(tmpdir(), "rea-browser-source-map-"));
|
||||
root = await PrivateRuntimeRoot.create({
|
||||
prefix: "rea-browser-source-map-",
|
||||
});
|
||||
commands = await createPackageVerifierCommands({ workspace: root });
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = async (...args) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
const { stdout } = await commands.exec(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--json"],
|
||||
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
|
||||
// Allow the shared cold-start budget plus capture, cleanup and delivery.
|
||||
{ env, timeout: 120_000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
return parseEvidence(JSON.parse(stdout));
|
||||
};
|
||||
@@ -50,6 +69,7 @@ export async function verifyBrowserSourceMap(
|
||||
"capture-browser-scenario",
|
||||
JSON.stringify({
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
command_timeout_ms: BROWSER_DEVTOOLS_STARTUP_TIMEOUT_MS,
|
||||
start_url: { url: site.origin },
|
||||
actions: [
|
||||
{
|
||||
@@ -66,12 +86,12 @@ export async function verifyBrowserSourceMap(
|
||||
capture: { network: { response_body: true } },
|
||||
}),
|
||||
);
|
||||
const capturePath = join(root, "capture.json");
|
||||
const capturePath = join(root.path, "capture.json");
|
||||
await writeFile(capturePath, JSON.stringify(capture));
|
||||
const exported = await cli(
|
||||
"export-web-scripts",
|
||||
capturePath,
|
||||
join(root, "export"),
|
||||
join(root.path, "export"),
|
||||
);
|
||||
const manifest = exported.normalized_result;
|
||||
const manifestBytes = await readFile(manifest.manifest.path);
|
||||
@@ -86,7 +106,7 @@ export async function verifyBrowserSourceMap(
|
||||
scriptIndex >= 0,
|
||||
"Actual browser capture omitted compiled source",
|
||||
);
|
||||
const mapPath = join(root, "app.js.map");
|
||||
const mapPath = join(root.path, "app.js.map");
|
||||
await writeFile(mapPath, site.map);
|
||||
const before = site.mapRequests();
|
||||
await client.connect(transport);
|
||||
@@ -132,13 +152,11 @@ export async function verifyBrowserSourceMap(
|
||||
public_point_cases: 4,
|
||||
};
|
||||
}, [
|
||||
() => client?.close(),
|
||||
() => transport?.close(),
|
||||
() => site?.close(),
|
||||
() =>
|
||||
root === undefined
|
||||
? undefined
|
||||
: rm(root, { recursive: true, force: true }),
|
||||
() => closeResource(client, "source-map MCP client"),
|
||||
() => closeResource(transport, "source-map stdio transport"),
|
||||
() => closeResource(site, "source-map fixture site"),
|
||||
() => commands?.close(),
|
||||
() => (commands === undefined ? root?.close() : undefined),
|
||||
]);
|
||||
}
|
||||
function assertPoint(evidence, { site, input, capture, identity }) {
|
||||
|
||||
@@ -114,6 +114,46 @@ export const verifyKunaReferences = async ({
|
||||
);
|
||||
}
|
||||
assert.equal(trace.value.truncated, false);
|
||||
const summary = await call("trace_feature", {
|
||||
query: name,
|
||||
detail: "summary",
|
||||
});
|
||||
assert.equal(
|
||||
summary.value.parent_evidence_id,
|
||||
trace.structured.evidence_id,
|
||||
);
|
||||
assert.equal(
|
||||
summary.value.summary.references,
|
||||
trace.value.references.length,
|
||||
);
|
||||
assert.equal(
|
||||
summary.value.summary.unknowns,
|
||||
trace.value.residual_unknowns.length,
|
||||
);
|
||||
for (const [collection, values] of [
|
||||
["matches", trace.value.matches],
|
||||
["references", trace.value.references],
|
||||
["unknowns", trace.value.residual_unknowns],
|
||||
]) {
|
||||
const view = await call("inspect_analysis_view", {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: trace.structured.evidence_id,
|
||||
},
|
||||
view: {
|
||||
kind: "native-feature-trace",
|
||||
collection,
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
assert.equal(view.value.coverage.total, values.length);
|
||||
assert.deepEqual(
|
||||
view.value.items.rows.map(({ value }) => value),
|
||||
values.slice(0, 1),
|
||||
);
|
||||
assert.equal(view.value.items.collection, collection);
|
||||
}
|
||||
}
|
||||
composed = {
|
||||
graph_ms: graph.ms,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
requireMcpOutput,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
|
||||
import { verifyGhidraProjectIdentity } from "./real-ghidra-project-identity.mjs";
|
||||
|
||||
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const environment = () => ({
|
||||
@@ -214,6 +215,19 @@ async function verifyNativeValueAdapters(
|
||||
);
|
||||
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
|
||||
assert.deepEqual(mcpGraph, cliGraph);
|
||||
const warmReply = await client.callTool(
|
||||
{
|
||||
name: "trace_native_values",
|
||||
arguments: { procedure, max_depth: 2, max_functions: 8, limit: 3 },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
const warmEvidence = parseEvidence(
|
||||
requireMcpOutput(warmReply, "trace_native_values"),
|
||||
);
|
||||
const warmGraph = nativeValueTraceSchema.parse(
|
||||
warmEvidence.normalized_result,
|
||||
);
|
||||
const analyzed = await client.callTool(
|
||||
{
|
||||
name: "analyze_function",
|
||||
@@ -253,20 +267,9 @@ async function verifyNativeValueAdapters(
|
||||
functionEvidence.analysis_profile,
|
||||
);
|
||||
assert.deepEqual(functionProfile, upstreamProfile);
|
||||
const comparison = await client.callTool(
|
||||
{
|
||||
name: "compare_functions",
|
||||
arguments: {
|
||||
left: analyzed.structuredContent,
|
||||
right: analyzed.structuredContent,
|
||||
},
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(comparison.isError, true, JSON.stringify(comparison));
|
||||
assert.notEqual(comparison.structuredContent, undefined);
|
||||
const comparisonEvidence = parseEvidence(
|
||||
requireMcpOutput(comparison, "compare_functions"),
|
||||
const comparisonEvidence = await compareRetainedFunctionEvidence(
|
||||
client,
|
||||
functionEvidence,
|
||||
);
|
||||
assert.equal(
|
||||
comparisonEvidence.normalized_result.status,
|
||||
@@ -286,6 +289,25 @@ async function verifyNativeValueAdapters(
|
||||
"MCP omitted structured Evidence bundle",
|
||||
);
|
||||
const bundle = requireMcpEvidenceBundle(bundleReply);
|
||||
for (const [evidence, graph] of [
|
||||
[mcpEvidence, mcpGraph],
|
||||
[warmEvidence, warmGraph],
|
||||
]) {
|
||||
for (const source of [
|
||||
...graph.procedures,
|
||||
...graph.nodes,
|
||||
...graph.edges,
|
||||
]) {
|
||||
assert.ok(evidence.evidence_links.includes(source.evidence_id));
|
||||
assert.equal(
|
||||
bundle.records.find(
|
||||
(record) => record.evidence_id === source.evidence_id,
|
||||
)?.operation,
|
||||
"analyze_function",
|
||||
`Missing source Evidence ${source.evidence_id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const evidence of [
|
||||
mcpEvidence,
|
||||
functionEvidence,
|
||||
@@ -328,6 +350,11 @@ async function verifyNativeValueAdapters(
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
const projectIdentity = await verifyGhidraProjectIdentity({
|
||||
target,
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
@@ -335,6 +362,7 @@ async function verifyNativeValueAdapters(
|
||||
inline_evidence: true,
|
||||
direct_function_comparison: true,
|
||||
boundaries,
|
||||
project_identity: projectIdentity,
|
||||
nodes: cliGraph.total_nodes,
|
||||
edges: cliGraph.total_edges,
|
||||
decompilations: cliGraph.decompilations,
|
||||
@@ -395,21 +423,27 @@ async function verifyUnresolvedCallEvidence(client) {
|
||||
assert.equal(trace.goal_status, "unknown");
|
||||
assert.equal(trace.truncated, true);
|
||||
assert.ok(trace.residual_unknowns.length > 0);
|
||||
const compared = await compareRetainedFunctionEvidence(client, evidence);
|
||||
assert.equal(compared.normalized_result.status, "unknown");
|
||||
}
|
||||
|
||||
/** Compare an already admitted function record without retransmitting its payload. */
|
||||
export async function compareRetainedFunctionEvidence(client, evidence) {
|
||||
const reference = {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: evidence.evidence_id,
|
||||
};
|
||||
const compared = await client.callTool(
|
||||
{
|
||||
name: "compare_functions",
|
||||
arguments: {
|
||||
left: analyzed.structuredContent,
|
||||
right: analyzed.structuredContent,
|
||||
left: reference,
|
||||
right: reference,
|
||||
},
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(compared.isError, true, JSON.stringify(compared));
|
||||
assert.notEqual(compared.structuredContent, undefined);
|
||||
assert.equal(
|
||||
parseEvidence(requireMcpOutput(compared, "compare_functions"))
|
||||
.normalized_result.status,
|
||||
"unknown",
|
||||
);
|
||||
return parseEvidence(requireMcpOutput(compared, "compare_functions"));
|
||||
}
|
||||
@@ -504,6 +504,9 @@ export async function verifyGhidraBoundaries(
|
||||
// Prime both inventories before editing so stale names cannot hide in caches.
|
||||
await call("list_names");
|
||||
await call("list_procedures");
|
||||
const traceInput = { procedure: address, max_depth: 0, max_functions: 1 };
|
||||
const traceBefore = await evidence("trace_native_values", traceInput);
|
||||
const sourceBefore = traceBefore.normalized_result.procedures[0].evidence_id;
|
||||
const changes = {
|
||||
procedure: address,
|
||||
name: "dead",
|
||||
@@ -517,6 +520,16 @@ export async function verifyGhidraBoundaries(
|
||||
comment: changes.comment,
|
||||
inline_comment: changes.inline_comment,
|
||||
});
|
||||
const traceAfter = await evidence("trace_native_values", traceInput);
|
||||
const sourceAfter = traceAfter.normalized_result.procedures[0].evidence_id;
|
||||
assert.notEqual(sourceBefore, sourceAfter);
|
||||
const sourceProcedure = (evidenceId) =>
|
||||
call("inspect_analysis_view", {
|
||||
source: { kind: "retained-evidence", evidence_id: evidenceId },
|
||||
view: { kind: "native", facet: "procedure", offset: 0, limit: 1 },
|
||||
});
|
||||
assert.equal((await sourceProcedure(sourceBefore)).item.name, name);
|
||||
assert.equal((await sourceProcedure(sourceAfter)).item.name, changes.name);
|
||||
const regexPattern = "^(a|aa)*b$";
|
||||
const regexFailure = await client.callTool(
|
||||
{
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
appendFile,
|
||||
copyFile,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
/** Verify selected-project identity refusal through real CLI and MCP adapters. */
|
||||
export async function verifyGhidraProjectIdentity({ target, entrypoint, env }) {
|
||||
if (process.platform === "win32")
|
||||
return {
|
||||
status: "not-applicable",
|
||||
reason: "Existing Ghidra projects are not admitted on Windows.",
|
||||
};
|
||||
assert.ok(env.GHIDRA_INSTALL_DIR, "Ghidra installation is required");
|
||||
const root = await mkdtemp(join(env.TMPDIR, "rea-project-identity-"));
|
||||
const targetPath = target.path;
|
||||
const different = join(root, "different-target");
|
||||
const headless = join(env.GHIDRA_INSTALL_DIR, "support", "analyzeHeadless");
|
||||
const fixtureEnv = {
|
||||
...env,
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: join(root, "config"),
|
||||
XDG_CACHE_HOME: join(root, "cache"),
|
||||
XDG_DATA_HOME: join(root, "data"),
|
||||
JDK_JAVA_OPTIONS: `-Duser.home="${root}"`,
|
||||
};
|
||||
const program = basename(targetPath);
|
||||
const known = join(root, "known.gpr");
|
||||
const unknown = join(root, "unknown.gpr");
|
||||
const runtimeRootsBefore = (await readdir(env.TMPDIR))
|
||||
.filter((name) => name.startsWith("rea-ghidra-"))
|
||||
.sort();
|
||||
const sha = async (path) =>
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
try {
|
||||
await copyFile(targetPath, different);
|
||||
await appendFile(different, "\nREA_IDENTITY_VERIFIER\n");
|
||||
assert.notEqual(await sha(different), await sha(targetPath));
|
||||
await writeFile(
|
||||
join(root, "ClearImportDigest.java"),
|
||||
`import ghidra.app.script.GhidraScript;
|
||||
public class ClearImportDigest extends GhidraScript {
|
||||
@Override public void run() {
|
||||
currentProgram.setExecutableSHA256(null);
|
||||
println("REA_DIGEST_AFTER=" + currentProgram.getExecutableSHA256());
|
||||
}
|
||||
}\n`,
|
||||
);
|
||||
await exec(
|
||||
headless,
|
||||
[root, "known", "-import", targetPath, "-noanalysis"],
|
||||
{ env: fixtureEnv, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
const cleared = await exec(
|
||||
headless,
|
||||
[
|
||||
root,
|
||||
"unknown",
|
||||
"-import",
|
||||
targetPath,
|
||||
"-noanalysis",
|
||||
"-scriptPath",
|
||||
root,
|
||||
"-postScript",
|
||||
"ClearImportDigest.java",
|
||||
],
|
||||
{ env: fixtureEnv, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
assert.match(cleared.stdout, /REA_DIGEST_AFTER=unknown/u);
|
||||
const before = await projectState([known, unknown, targetPath, different]);
|
||||
|
||||
const knownEnv = {
|
||||
...env,
|
||||
REA_GHIDRA_PROJECT_PATH: known,
|
||||
REA_GHIDRA_PROGRAM_NAME: program,
|
||||
};
|
||||
const unknownEnv = {
|
||||
...env,
|
||||
REA_GHIDRA_PROJECT_PATH: unknown,
|
||||
REA_GHIDRA_PROGRAM_NAME: program,
|
||||
};
|
||||
const mismatch = await selectedProjectMcp({
|
||||
entrypoint,
|
||||
env: knownEnv,
|
||||
first: different,
|
||||
matching: targetPath,
|
||||
expected: "import_identity_mismatch",
|
||||
});
|
||||
const unavailable = await selectedProjectMcp({
|
||||
entrypoint,
|
||||
env: unknownEnv,
|
||||
first: targetPath,
|
||||
expected: "import_identity_unavailable",
|
||||
});
|
||||
for (const [label, selectedEnv, input, expected] of [
|
||||
["mismatch", knownEnv, different, "import_identity_mismatch"],
|
||||
["unavailable", unknownEnv, targetPath, "import_identity_unavailable"],
|
||||
]) {
|
||||
let failed;
|
||||
try {
|
||||
await exec(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"decompile",
|
||||
input,
|
||||
"0x0",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
{ env: selectedEnv, timeout: 180000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
} catch (cause) {
|
||||
assert.equal(cause.code, 1, `${label} CLI exit`);
|
||||
failed = JSON.parse(cause.stdout);
|
||||
}
|
||||
assert.ok(failed, `${label} CLI must refuse`);
|
||||
assert.equal(failed.code, "configuration_invalid");
|
||||
assert.equal(failed.details.diagnostics.remote_code, expected);
|
||||
}
|
||||
assert.deepEqual(
|
||||
await projectState([known, unknown, targetPath, different]),
|
||||
before,
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await readdir(env.TMPDIR))
|
||||
.filter((name) => name.startsWith("rea-ghidra-"))
|
||||
.sort(),
|
||||
runtimeRootsBefore,
|
||||
"Selected-project checks left a Ghidra runtime directory",
|
||||
);
|
||||
return {
|
||||
status: "verified",
|
||||
mismatch,
|
||||
unavailable,
|
||||
cli_refusals: true,
|
||||
source_files_unchanged: true,
|
||||
};
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function selectedProjectMcp({
|
||||
entrypoint,
|
||||
env,
|
||||
first,
|
||||
matching,
|
||||
expected,
|
||||
}) {
|
||||
const client = new Client({
|
||||
name: "rea-project-identity-verifier",
|
||||
version: "1",
|
||||
});
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const tools = new Map(
|
||||
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
|
||||
);
|
||||
const call = (name, arguments_) =>
|
||||
client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 180000, toolDefinition: tools.get(name) },
|
||||
);
|
||||
assert.notEqual(
|
||||
(await call("open_binary", { path: first, provider_id: "ghidra" }))
|
||||
.isError,
|
||||
true,
|
||||
);
|
||||
const rejected = requireMcpToolError(await call("list_documents", {}));
|
||||
assert.equal(
|
||||
rejected.code,
|
||||
"configuration_invalid",
|
||||
JSON.stringify({
|
||||
code: rejected.code,
|
||||
message: rejected.message,
|
||||
failure_kind: rejected.details?.diagnostics?.failure_kind,
|
||||
failure_message: rejected.details?.diagnostics?.failure_message,
|
||||
stdout: rejected.details?.diagnostics?.stdout?.text?.slice(-2500),
|
||||
stderr: rejected.details?.diagnostics?.stderr?.text?.slice(-1000),
|
||||
}),
|
||||
);
|
||||
assert.equal(rejected.details.diagnostics.remote_code, expected);
|
||||
if (matching !== undefined) {
|
||||
assert.notEqual(
|
||||
(await call("open_binary", { path: matching, provider_id: "ghidra" }))
|
||||
.isError,
|
||||
true,
|
||||
);
|
||||
assert.notEqual((await call("list_documents", {})).isError, true);
|
||||
}
|
||||
assert.notEqual((await call("close_binary", {})).isError, true);
|
||||
return {
|
||||
refused: expected,
|
||||
matching_target_reopened: matching !== undefined,
|
||||
};
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function projectState(paths) {
|
||||
const files = [];
|
||||
const walk = async (dir) => {
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) await walk(path);
|
||||
else if (entry.isFile()) files.push(path);
|
||||
}
|
||||
};
|
||||
for (const path of paths) {
|
||||
files.push(path);
|
||||
if (path.endsWith(".gpr")) await walk(`${path.slice(0, -4)}.rep`);
|
||||
}
|
||||
files.sort();
|
||||
return Promise.all(
|
||||
files.map(async (path) => [
|
||||
path,
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex"),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export function applyReleasePleasePatch(directory?: string): Promise<void>;
|
||||
|
||||
export function describeReleaseProposalError(error: unknown): string;
|
||||
|
||||
export function releaseProposalSearchDepth(
|
||||
run: (command: string, args: string[]) => Promise<string>,
|
||||
sourceSha: string,
|
||||
|
||||
@@ -13,6 +13,15 @@ const patch = fileURLToPath(
|
||||
new URL("../../patches/release-please+17.3.0.patch", import.meta.url),
|
||||
);
|
||||
|
||||
export function describeReleaseProposalError(error) {
|
||||
if (!(error instanceof Error)) return String(error);
|
||||
const description = error.message || error.name || "Unknown error";
|
||||
const status = error.status;
|
||||
return Number.isInteger(status) && status >= 400 && status <= 599
|
||||
? `${description} (HTTP ${status})`
|
||||
: description;
|
||||
}
|
||||
|
||||
export async function applyReleasePleasePatch(
|
||||
directory = dirname(require.resolve("release-please/package.json")),
|
||||
) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import xml.etree.ElementTree as ET
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from website import PageMetadata, page_url
|
||||
from website_agents import prepare_discovery, prepare_markdown
|
||||
|
||||
|
||||
EXAMPLE_FILES = (
|
||||
@@ -81,6 +82,8 @@ def prepare_assets(site):
|
||||
prepare_sharing_image(site)
|
||||
prepare_sitemap(site)
|
||||
prepare_notes_example(site)
|
||||
prepare_discovery(Path(__file__).resolve().parent.parent, site)
|
||||
prepare_markdown(site)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+2
-9
@@ -70,15 +70,8 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
guardClosedStdout();
|
||||
// Doctor runs outside Incur, so handle its global timing flag here. Keep a
|
||||
// token consumed by --format as a value, even when it looks like a flag.
|
||||
const outputValues = new Set(
|
||||
builtinArguments
|
||||
.splitIncurBuiltinArguments(doctorArguments)
|
||||
.outputValues.map(({ index }) => index),
|
||||
);
|
||||
const doctorCliArguments = doctorArguments.filter(
|
||||
(argument, index) => argument !== "--timings" || outputValues.has(index),
|
||||
);
|
||||
const timingRequested = doctorCliArguments.length !== doctorArguments.length;
|
||||
const { arguments: doctorCliArguments, enabled: timingRequested } =
|
||||
builtinArguments.splitCliTimingArguments(doctorArguments);
|
||||
const started = performance.now();
|
||||
try {
|
||||
const { runProductionMcpDoctorCli } =
|
||||
|
||||
@@ -15,12 +15,16 @@ const [
|
||||
{ parseConfig },
|
||||
{ silentLogger },
|
||||
{ PROMPT_CONTRACTS },
|
||||
{ TOOL_CONTRACTS },
|
||||
{ getEvidenceInputContract },
|
||||
] = await Promise.all([
|
||||
read("server/createServer.js"),
|
||||
read("composition/binary.js"),
|
||||
read("config/parseConfig.js"),
|
||||
read("logger.js"),
|
||||
read("contracts/promptContracts.js"),
|
||||
read("contracts/toolContracts.js"),
|
||||
read("domain/evidenceInputContract.js"),
|
||||
]);
|
||||
const recordCatalog = async (profile) => {
|
||||
// Catalog generation is independent of the host account and user configuration.
|
||||
@@ -71,6 +75,11 @@ const recordCatalog = async (profile) => {
|
||||
instructions: client.getInstructions(),
|
||||
identity: server.catalogIdentity,
|
||||
tools,
|
||||
evidenceReferenceAlternatives: TOOL_CONTRACTS.filter(
|
||||
(contract) =>
|
||||
getEvidenceInputContract(contract.inputSchema)
|
||||
.hasEvidenceReferenceAlternative,
|
||||
).map((contract) => contract.name),
|
||||
prompts,
|
||||
};
|
||||
const encoded = JSON.stringify(catalog);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { appendFile } from "node:fs/promises";
|
||||
import { parseArgs, promisify } from "node:util";
|
||||
import {
|
||||
applyReleasePleasePatch,
|
||||
describeReleaseProposalError,
|
||||
releaseProposalSearchDepth,
|
||||
} from "./lib/release-proposal.mjs";
|
||||
|
||||
@@ -63,7 +64,7 @@ async function main() {
|
||||
|
||||
await main().catch((error) => {
|
||||
process.stderr.write(
|
||||
`Release proposal failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
||||
`Release proposal failed: ${describeReleaseProposalError(error)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Check the public build artifacts and Markdown content contract."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from zipfile import ZipFile
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import yaml
|
||||
|
||||
from website_agents import prepare_discovery, prepare_markdown
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class AgentPublishingChecks(unittest.TestCase):
|
||||
def test_published_skill_digest_covers_every_reference_and_catalogs_resolve(self):
|
||||
site = ROOT / "website/_site"
|
||||
index = json.loads((site / ".well-known/agent-skills/index.json").read_text())
|
||||
self.assertEqual(index["$schema"], "https://schemas.agentskills.io/discovery/0.2.0/schema.json")
|
||||
skill, = index["skills"]
|
||||
self.assertEqual(skill["name"], "reverse-engineer-anything")
|
||||
self.assertEqual(skill["type"], "archive")
|
||||
artifact = site / skill["url"].removeprefix("https://rea.tools/")
|
||||
self.assertEqual(skill["digest"], "sha256:" + hashlib.sha256(artifact.read_bytes()).hexdigest())
|
||||
source = ROOT / ".agents/skills/reverse-engineer-anything"
|
||||
with ZipFile(artifact) as archive:
|
||||
expected = {f"{source.name}/{p.relative_to(source).as_posix()}": p.read_bytes()
|
||||
for p in source.rglob("*.md")}
|
||||
self.assertEqual(set(archive.namelist()), set(expected))
|
||||
for name, data in expected.items():
|
||||
self.assertEqual(archive.read(name), data)
|
||||
manifest = archive.read(f"{source.name}/SKILL.md").decode()
|
||||
metadata = yaml.safe_load(manifest[4:].split("\n---\n", 1)[0])
|
||||
self.assertEqual(skill["description"], metadata["description"])
|
||||
catalog = json.loads((site / ".well-known/ai-catalog.json").read_text())
|
||||
self.assertEqual(catalog["specVersion"], "1.0")
|
||||
entry, = catalog["entries"]
|
||||
self.assertEqual(entry["identifier"], "urn:air:rea.tools:skill:reverse-engineer-anything")
|
||||
self.assertEqual(entry["url"], skill["url"])
|
||||
self.assertNotIn("data", entry)
|
||||
ard = json.loads((site / ".well-known/ard.json").read_text())
|
||||
resource, = ard["entries"]
|
||||
self.assertEqual(resource["url"], skill["url"])
|
||||
self.assertTrue(2 <= len(resource["representativeQueries"]) <= 5)
|
||||
|
||||
def test_reference_changes_invalidate_digest_and_yaml_scalars_are_parsed(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
source = root / ".agents/skills/reverse-engineer-anything"
|
||||
source.mkdir(parents=True)
|
||||
(source / "SKILL.md").write_text(
|
||||
'---\nname: reverse-engineer-anything\ndescription: >-\n Inspect programs:\n preserve evidence.\n---\n# REA\n'
|
||||
)
|
||||
(source / "references").mkdir()
|
||||
reference = source / "references/evidence.md"
|
||||
reference.write_text("First revision")
|
||||
site = root / "site"
|
||||
def build():
|
||||
prepare_discovery(root, site)
|
||||
return json.loads((site / ".well-known/agent-skills/index.json").read_text())["skills"][0]
|
||||
before = build()
|
||||
self.assertEqual(before["description"], "Inspect programs: preserve evidence.")
|
||||
self.assertEqual(build(), before)
|
||||
reference.write_text("Second revision")
|
||||
self.assertNotEqual(build()["digest"], before["digest"])
|
||||
|
||||
def test_markdown_preserves_content_and_resources_without_publishing_excluded_pages(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory)
|
||||
page = site / "zh-CN/guides/test/index.html"
|
||||
page.parent.mkdir(parents=True)
|
||||
page.write_text('''<html><head><title>REA: test</title></head><body>
|
||||
<nav>Outside navigation</nav><main><aside>Shortened display paths.</aside>
|
||||
<h1>Inspect a program</h1><nav>Contents navigation</nav>
|
||||
<ol><li><span class="flow-label">Read</span><span class="flow-node"><code>input.exe</code></span><span class="flow-note">Keep evidence.</span></li></ol>
|
||||
<p><a href="../other/?q=a%2Fb#code">Another guide</a></p>
|
||||
<pre><code>const value = a < b ? "x_y" : "&";\n keepIndent();</code></pre>
|
||||
<details><summary>Evidence</summary><p>Hidden evidence remains available.</p></details>
|
||||
<img src="../../../assets/diagram.svg" alt="Investigation diagram">
|
||||
<button>Copy code</button><script>doNotPublish()</script></main></body></html>''')
|
||||
fixture = site / "examples/notes-web/index.html"
|
||||
fixture.parent.mkdir(parents=True)
|
||||
fixture.write_text("<main>Target application</main>")
|
||||
fallback = site / "zh-CN/fallback/index.html"
|
||||
fallback.parent.mkdir(parents=True)
|
||||
fallback.write_text('<head><meta name="robots" content="noindex, follow"></head><main>English fallback notice.</main>')
|
||||
prepare_markdown(site)
|
||||
markdown = page.with_suffix(".md").read_text()
|
||||
self.assertIn('# Inspect a program', markdown)
|
||||
self.assertIn('const value = a < b ? "x_y" : "&";\n keepIndent();', markdown)
|
||||
self.assertIn("Hidden evidence remains available.", markdown)
|
||||
self.assertIn("Shortened display paths.", markdown)
|
||||
self.assertNotIn("Read`input.exe`", markdown)
|
||||
self.assertIn("https://rea.tools/zh-CN/guides/other/?q=a%2Fb#code", markdown)
|
||||
self.assertIn("https://rea.tools/assets/diagram.svg", markdown)
|
||||
for text in ("Outside navigation", "Contents navigation", "Copy code", "doNotPublish"):
|
||||
self.assertNotIn(text, markdown)
|
||||
self.assertFalse(fixture.with_suffix(".md").exists())
|
||||
self.assertFalse(fallback.with_suffix(".md").exists())
|
||||
html = BeautifulSoup(page.read_text(), "html.parser")
|
||||
self.assertEqual(html.find("link", rel="alternate")["href"], "https://rea.tools/zh-CN/guides/test/index.md")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -38,15 +38,16 @@ if (
|
||||
!isAbsolute(strace ?? "")
|
||||
)
|
||||
throw new Error(
|
||||
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
|
||||
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the AArch64/i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
|
||||
);
|
||||
const run = createVerifierRun();
|
||||
const execute = promisify(execFile);
|
||||
const ELF32_TOOLCHAINS = [
|
||||
["i686-linux-gnu", "EM_386", "little"],
|
||||
["arm-linux-gnueabi", "EM_ARM", "little"],
|
||||
["mips-linux-gnu", "EM_MIPS", "big"],
|
||||
["mipsel-linux-gnu", "EM_MIPS", "little"],
|
||||
const CROSS_TOOLCHAINS = [
|
||||
["aarch64-linux-gnu", "EM_AARCH64", 64, "little"],
|
||||
["i686-linux-gnu", "EM_386", 32, "little"],
|
||||
["arm-linux-gnueabi", "EM_ARM", 32, "little"],
|
||||
["mips-linux-gnu", "EM_MIPS", 32, "big"],
|
||||
["mipsel-linux-gnu", "EM_MIPS", 32, "little"],
|
||||
];
|
||||
for (const command of [
|
||||
"gcc",
|
||||
@@ -54,7 +55,7 @@ for (const command of [
|
||||
"strip",
|
||||
"/usr/bin/prlimit",
|
||||
strace,
|
||||
...ELF32_TOOLCHAINS.flatMap(([prefix]) => [
|
||||
...CROSS_TOOLCHAINS.flatMap(([prefix]) => [
|
||||
`${prefix}-gcc`,
|
||||
`${prefix}-readelf`,
|
||||
]),
|
||||
@@ -347,14 +348,15 @@ try {
|
||||
cases++;
|
||||
}
|
||||
}
|
||||
// ELF32 profiles: cross-compiled, never executed, checked against GNU readelf.
|
||||
// Cross profiles: cross-compiled, never executed, checked against GNU readelf.
|
||||
for (const [
|
||||
prefix,
|
||||
machine,
|
||||
bits,
|
||||
byteOrder,
|
||||
kind,
|
||||
flags,
|
||||
] of ELF32_TOOLCHAINS.flatMap((toolchain) => [
|
||||
] of CROSS_TOOLCHAINS.flatMap((toolchain) => [
|
||||
[...toolchain, "executable", ["-fPIE", "-pie"]],
|
||||
[...toolchain, "relocatable", ["-c", "-fPIC"]],
|
||||
])) {
|
||||
@@ -423,16 +425,18 @@ try {
|
||||
`readelf relocation count: ${name}`,
|
||||
);
|
||||
for (const [entryIndex, [, offset, info]] of rows.entries()) {
|
||||
// ELF32 r_info uses the low byte for type and the upper 24 bits for
|
||||
// the symbol index. readelf has already decoded the file's byte order.
|
||||
// ELF32 r_info packs an 8-bit type under a 24-bit symbol index;
|
||||
// ELF64 packs a 32-bit type under a 32-bit index. readelf has already
|
||||
// decoded the file's byte order.
|
||||
const decodedInfo = BigInt(`0x${info}`);
|
||||
const typeBits = bits === 32 ? 8n : 32n;
|
||||
expectedRelocations.push({
|
||||
section_index: section.index,
|
||||
entry_index: entryIndex,
|
||||
reported_offset: BigInt(`0x${offset}`),
|
||||
type: Number(decodedInfo & 0xffn),
|
||||
type: Number(decodedInfo & ((1n << typeBits) - 1n)),
|
||||
symbol_table_index: section.link,
|
||||
symbol_index: Number(decodedInfo >> 8n),
|
||||
symbol_index: Number(decodedInfo >> typeBits),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -445,7 +449,7 @@ try {
|
||||
const value = await inspect(mode, path);
|
||||
assert.deepEqual(value.architecture, {
|
||||
machine,
|
||||
bits: 32,
|
||||
bits,
|
||||
byte_order: byteOrder,
|
||||
});
|
||||
assert.equal(
|
||||
@@ -1312,7 +1316,8 @@ try {
|
||||
}
|
||||
}
|
||||
}
|
||||
unsupported.writeUInt16LE(183, 18);
|
||||
// EM_RISCV: AArch64 (183) is now an admitted profile.
|
||||
unsupported.writeUInt16LE(243, 18);
|
||||
const core = Buffer.from(object);
|
||||
core.writeUInt16LE(4, 16);
|
||||
for (const [name, bytes, category] of [
|
||||
@@ -1337,7 +1342,7 @@ try {
|
||||
"invalid_input",
|
||||
],
|
||||
["undersized-program-header", undersizedProgramHeader, "invalid_input"],
|
||||
["arm64", unsupported, "unsupported_target"],
|
||||
["riscv64", unsupported, "unsupported_target"],
|
||||
["core", core, "unsupported_target"],
|
||||
]) {
|
||||
const path = join(root.path, name);
|
||||
|
||||
@@ -245,7 +245,13 @@ try {
|
||||
scope,
|
||||
{ environment: process.env },
|
||||
);
|
||||
if (mismatch.ok || !mismatch.error.message.includes("target-mismatch"))
|
||||
// A selection failure carries its helper code in the issue, not in its message.
|
||||
if (
|
||||
mismatch.ok ||
|
||||
!mismatch.error.issues?.some(({ message }) =>
|
||||
message.startsWith("target-mismatch:"),
|
||||
)
|
||||
)
|
||||
throw new Error("Real helper did not reject changed target bytes");
|
||||
report = {
|
||||
ok: true,
|
||||
|
||||
@@ -161,6 +161,38 @@ try {
|
||||
fromNumericOffset.normalized_result,
|
||||
fromOffset.normalized_result,
|
||||
);
|
||||
const missingProcedure = "rea_missing_procedure_fixture";
|
||||
const missingName = await client.callTool(
|
||||
{
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { procedure: missingProcedure },
|
||||
},
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.equal(missingName.isError, true);
|
||||
const missingNameError = JSON.parse(
|
||||
missingName.content.find((item) => item.type === "text").text,
|
||||
).error;
|
||||
assert.equal(missingNameError.code, "invalid_request");
|
||||
assert.match(missingNameError.message, /rea_missing_procedure_fixture/u);
|
||||
assert.match(missingNameError.remediation.action, /list_procedures/u);
|
||||
assert.match(missingNameError.remediation.action, /does not prove/u);
|
||||
assert.equal(missingNameError.recovery, undefined);
|
||||
const unmappedOffset = await client.callTool(
|
||||
{
|
||||
name: "procedure_pseudo_code",
|
||||
arguments: { procedure: "file-offset:0x1fffffffffffff" },
|
||||
},
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.equal(unmappedOffset.isError, true);
|
||||
const unmappedError = JSON.parse(
|
||||
unmappedOffset.content.find((item) => item.type === "text").text,
|
||||
).error;
|
||||
assert.equal(unmappedError.code, "invalid_request");
|
||||
assert.match(unmappedError.message, /no loaded source mapping/u);
|
||||
assert.match(unmappedError.remediation.action, /inspect_native_load_image/u);
|
||||
assert.equal(unmappedError.recovery, undefined);
|
||||
const missingAddress = "ram:0xffffffff";
|
||||
const rejected = await client.callTool(
|
||||
{ name: "procedure_pseudo_code", arguments: { procedure: missingAddress } },
|
||||
|
||||
@@ -276,6 +276,67 @@ try {
|
||||
assert.equal(updated.annotations.inline_comment, changes.inline_comment);
|
||||
assert.equal(updated.dossier.procedure.name, changes.name);
|
||||
assert.ok(updated.dossier.pseudocode.includes(changes.name));
|
||||
// Exercise the documented recovery through the public MCP catalog on real
|
||||
// Ghidra, rather than assuming the provider exposes direct comment methods.
|
||||
const contextCall = async (name, args) => {
|
||||
const definition = tools.find((tool) => tool.name === name);
|
||||
assert.ok(definition?.outputSchema, `${name} must be advertised`);
|
||||
const response = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 240000, toolDefinition: definition },
|
||||
);
|
||||
return requireMcpOutput(response, name);
|
||||
};
|
||||
const context = (
|
||||
await contextCall("inspect_address_context", {
|
||||
address: fixture.entry,
|
||||
})
|
||||
).result;
|
||||
assert.equal(context.procedure.state, "available");
|
||||
assert.equal(context.procedure.value.found, true);
|
||||
for (const facet of [context.comment, context.inline_comment]) {
|
||||
assert.equal(facet.state, "unavailable");
|
||||
assert.match(
|
||||
facet.remediation,
|
||||
/analyze_function.*parent_evidence_id.*inspect_analysis_view/u,
|
||||
);
|
||||
}
|
||||
assert.match(context.bookmarks.remediation, /binary_session/u);
|
||||
const summary = parseEvidence(
|
||||
await contextCall("analyze_function", {
|
||||
procedure: context.procedure.value.procedure.address,
|
||||
}),
|
||||
).normalized_result;
|
||||
const comments = parseEvidence(
|
||||
await contextCall("inspect_analysis_view", {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: summary.parent_evidence_id,
|
||||
},
|
||||
view: { kind: "native", facet: "comments" },
|
||||
}),
|
||||
).normalized_result;
|
||||
for (const [kind, text] of [
|
||||
["comment", changes.comment],
|
||||
["inline", changes.inline_comment],
|
||||
])
|
||||
assert.ok(
|
||||
comments.item.some(
|
||||
(item) =>
|
||||
item.address === context.procedure.value.query_address &&
|
||||
item.kind === kind &&
|
||||
item.text === text,
|
||||
),
|
||||
);
|
||||
const outside = (
|
||||
await contextCall("inspect_address_context", {
|
||||
address: unlistedAddress,
|
||||
})
|
||||
).result;
|
||||
assert.equal(outside.procedure.state, "available");
|
||||
assert.equal(outside.procedure.value.found, false);
|
||||
assert.equal(outside.comment.state, "unavailable");
|
||||
assert.match(outside.comment.remediation, /binary_session/u);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: changes.name }),
|
||||
fixture.entry,
|
||||
@@ -386,6 +447,7 @@ try {
|
||||
fresh_decompiler: true,
|
||||
clear_comments: true,
|
||||
cli_mcp_parity: true,
|
||||
address_context_comment_recovery: true,
|
||||
},
|
||||
context_input_errors_preserved: true,
|
||||
original_unchanged: true,
|
||||
|
||||
@@ -619,7 +619,35 @@ export async function verifyNativeTypeLayout(client, typeNames) {
|
||||
)
|
||||
)
|
||||
throw new Error("Nested union or enum layout drifted");
|
||||
return { layout, payload, state };
|
||||
const bitsSymbol = typeNames.find((item) =>
|
||||
matchesSymbol(item.value, "rea_ghidra_inventory_bits"),
|
||||
);
|
||||
if (bitsSymbol === undefined)
|
||||
throw new Error("Bit-field fixture symbol unavailable");
|
||||
const bits = await functionCall(client, "inspect_native_data_type", {
|
||||
document: null,
|
||||
address: bitsSymbol.address,
|
||||
});
|
||||
const low = bits.fields.find((field) => field.name === "low");
|
||||
if (
|
||||
bits.kind !== "struct" ||
|
||||
low?.bit_size !== 3 ||
|
||||
low.bit_field?.declared_bit_size !== 3 ||
|
||||
low.bit_field.component_type_lookup !== "not-found" ||
|
||||
low.bit_field.base_type_lookup !== "equivalent"
|
||||
)
|
||||
throw new Error(`Bit-field identity unavailable: ${JSON.stringify(bits)}`);
|
||||
const component = await functionCall(client, "inspect_native_data_type", {
|
||||
document: null,
|
||||
type: low.type_id,
|
||||
});
|
||||
const base = await functionCall(client, "inspect_native_data_type", {
|
||||
document: null,
|
||||
type: low.bit_field.base_type_id,
|
||||
});
|
||||
if (component.status !== "unavailable" || base.status !== "available")
|
||||
throw new Error("Bit-field component or base lookup drifted");
|
||||
return { layout, payload, state, bits, base };
|
||||
}
|
||||
|
||||
export async function verifyRelativeSwitch(client, procedures, entrySize) {
|
||||
|
||||
@@ -46,8 +46,40 @@ async function expectRemoteFailure(client, operation, parameters, code) {
|
||||
return { operation, code };
|
||||
}
|
||||
|
||||
async function verifyBridgeRejections(client) {
|
||||
async function verifyBridgeRejections(client, names) {
|
||||
const global = names.find((item) =>
|
||||
matchesSymbol(item.value, "rea_ghidra_inventory_global"),
|
||||
);
|
||||
if (global === undefined)
|
||||
throw new Error("Ghidra initialized global probe is unavailable");
|
||||
const mapping = await call(client, "address_to_file_offset", {
|
||||
document: null,
|
||||
address: global.address,
|
||||
});
|
||||
if (!Number.isSafeInteger(mapping.file_offset))
|
||||
throw new Error("Ghidra initialized global has no exact file offset");
|
||||
return {
|
||||
unknownProcedure: await expectRemoteFailure(
|
||||
client,
|
||||
"procedure_address",
|
||||
{ document: null, procedure: "rea_missing_procedure_fixture" },
|
||||
"procedure_not_found",
|
||||
),
|
||||
mappedDataOffset: await expectRemoteFailure(
|
||||
client,
|
||||
"procedure_pseudo_code",
|
||||
{
|
||||
document: null,
|
||||
procedure: `file-offset:0x${mapping.file_offset.toString(16)}`,
|
||||
},
|
||||
"procedure_not_found",
|
||||
),
|
||||
unmappedFileOffset: await expectRemoteFailure(
|
||||
client,
|
||||
"procedure_pseudo_code",
|
||||
{ document: null, procedure: "file-offset:0x1fffffffffffff" },
|
||||
"file_offset_unmapped",
|
||||
),
|
||||
unknownDocument: await expectRemoteFailure(
|
||||
client,
|
||||
"list_segments",
|
||||
@@ -86,7 +118,7 @@ export async function verifyInventoryOperations({
|
||||
});
|
||||
|
||||
const rejections =
|
||||
variant === "debug" ? await verifyBridgeRejections(client) : null;
|
||||
variant === "debug" ? await verifyBridgeRejections(client, names) : null;
|
||||
const stringSearch = await call(client, "search_strings", {
|
||||
pattern: String.raw`^REA_GHIDRA_(?:INVENTORY_ENTRY|LEAF_VALUE)$`,
|
||||
mode: "regex",
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
realpath,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { basename, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
@@ -41,8 +42,10 @@ import {
|
||||
import {
|
||||
requireMcpToolError,
|
||||
requireMcpOperationResult,
|
||||
requireMcpOutput,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { runOwnedCommand } from "../dist/process/OwnedCommand.js";
|
||||
|
||||
// Optional cross-target lane. No tool acquisition and no target execution.
|
||||
const mode = process.argv[2];
|
||||
@@ -90,6 +93,114 @@ for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) {
|
||||
if (process.env[setting] !== undefined) env[setting] = process.env[setting];
|
||||
}
|
||||
|
||||
/** Observe an existing project, and verify REA never alters the caller's copy. */
|
||||
async function verifyExistingMipsProject(fixture, byteOrder) {
|
||||
const projectDir = join(workspace, `${byteOrder}-existing-project`);
|
||||
await mkdir(projectDir);
|
||||
assert.ok(installation.analyzeHeadlessPath);
|
||||
await runOwnedCommand(
|
||||
{
|
||||
command: installation.analyzeHeadlessPath,
|
||||
arguments: [
|
||||
projectDir,
|
||||
"ExistingMips",
|
||||
"-import",
|
||||
fixture.path,
|
||||
"-noanalysis",
|
||||
],
|
||||
env,
|
||||
runId: run.run_id,
|
||||
expectedCommand: null,
|
||||
},
|
||||
{ timeoutMs: 180000, diagnosticBytes: 1024 * 1024 },
|
||||
);
|
||||
const projectPath = join(projectDir, "ExistingMips.gpr");
|
||||
const digestTree = async () => {
|
||||
const paths = [projectPath];
|
||||
const walk = async (dir) => {
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) await walk(path);
|
||||
else if (entry.isFile()) paths.push(path);
|
||||
}
|
||||
};
|
||||
await walk(join(projectDir, "ExistingMips.rep"));
|
||||
return Promise.all(
|
||||
paths.sort().map(async (path) => [
|
||||
relative(projectDir, path),
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex"),
|
||||
]),
|
||||
);
|
||||
};
|
||||
const before = await digestTree();
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: {
|
||||
...env,
|
||||
REA_GHIDRA_PROJECT_PATH: projectPath,
|
||||
REA_GHIDRA_PROGRAM_NAME: basename(fixture.path),
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-existing-mips-proof", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const opened = await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: fixture.path, provider_id: "ghidra" },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
requireMcpOperationResult(opened, "open_binary");
|
||||
const result = await client.callTool(
|
||||
{ name: "list_procedures", arguments: {} },
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
const observation = parseEvidence(
|
||||
requireMcpOutput(result, "list_procedures"),
|
||||
);
|
||||
assert.ok(Array.isArray(observation.normalized_result));
|
||||
const parameters = observation.analysis_profile?.parameters;
|
||||
assert.equal(parameters?.import_mode, "existing-project-private-copy-v1");
|
||||
assert.equal(parameters?.mips_elf?.byte_order, byteOrder);
|
||||
assert.equal(parameters?.mips_support_lane, undefined);
|
||||
assert.deepEqual(parameters?.original_analysis, {
|
||||
completeness: "unknown",
|
||||
settings: "unknown",
|
||||
});
|
||||
requireMcpOperationResult(
|
||||
await client.callTool({ name: "close_binary", arguments: {} }),
|
||||
"close_binary",
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
assert.deepEqual(
|
||||
await digestTree(),
|
||||
before,
|
||||
"REA changed the source project",
|
||||
);
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(fixture.path))
|
||||
.digest("hex"),
|
||||
fixture.sha256,
|
||||
);
|
||||
return {
|
||||
byte_order: byteOrder,
|
||||
project_unchanged: true,
|
||||
target_unchanged: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed float signatures must use soft-float o32 storage, and callers must see
|
||||
* the source constants through it rather than FPU inputs.
|
||||
@@ -320,6 +431,10 @@ try {
|
||||
);
|
||||
}
|
||||
assert.equal(resolved.value.sha256, fixture.sha256);
|
||||
const existingProject =
|
||||
!psp && floatAbi === "hard"
|
||||
? await verifyExistingMipsProject(fixture, byteOrder)
|
||||
: undefined;
|
||||
const snapshotPath = join(
|
||||
workspace,
|
||||
`${byteOrder}-${floatAbi}.snapshot.json`,
|
||||
@@ -682,6 +797,9 @@ try {
|
||||
}
|
||||
reports.push({
|
||||
...fixture,
|
||||
...(existingProject === undefined
|
||||
? {}
|
||||
: { existing_project: existingProject }),
|
||||
independent_reader: independent,
|
||||
effective_language: image.observations.language_id,
|
||||
effective_compiler_spec: image.observations.compiler_spec_id,
|
||||
|
||||
@@ -110,6 +110,39 @@ try {
|
||||
// the plain loader would have chosen e500 for this header.
|
||||
const image = await call("inspect_native_load_image");
|
||||
assertPowerPcLoadedImage(image.observations, fixture.sha256);
|
||||
const names = await call("list_names");
|
||||
const bitsName = names.find((item) => item.value === "rea_ppc_bits");
|
||||
assert.ok(bitsName, "Big-endian bit-field fixture is missing");
|
||||
const bitsBytes = await call("read_bytes", {
|
||||
address: bitsName.address,
|
||||
length: 4,
|
||||
});
|
||||
assert.equal(bitsBytes.bytes_hex, "22000000");
|
||||
const bits = await call("inspect_native_data_type", {
|
||||
address: bitsName.address,
|
||||
});
|
||||
assert.equal(bits.kind, "struct");
|
||||
const bitFields = bits.fields.filter((field) => field.bit_field);
|
||||
assert.deepEqual(
|
||||
bitFields.map((field) => [
|
||||
field.name,
|
||||
field.bit_size,
|
||||
field.bit_offset,
|
||||
]),
|
||||
[
|
||||
["first", 3, 5],
|
||||
["second", 5, 0],
|
||||
],
|
||||
);
|
||||
for (const field of bitFields) {
|
||||
assert.equal(field.bit_field.component_type_lookup, "not-found");
|
||||
assert.equal(field.bit_field.base_type_lookup, "equivalent");
|
||||
assert.equal(field.bit_field.declared_bit_size, field.bit_size);
|
||||
const base = await call("inspect_native_data_type", {
|
||||
type: field.bit_field.base_type_id,
|
||||
});
|
||||
assert.equal(base.status, "available");
|
||||
}
|
||||
const procedures = await call("list_procedures");
|
||||
const entry = procedures.find((p) => p.value === "rea_ppc_entry");
|
||||
const leaf = procedures.find((p) => p.value === "rea_ppc_leaf");
|
||||
@@ -245,6 +278,7 @@ try {
|
||||
"fixed instruction bytes and immediate",
|
||||
"conditional branch destination",
|
||||
"global word and marker bytes",
|
||||
"big-endian bit-field storage and base type handoff",
|
||||
"CLI/MCP and snapshot identity",
|
||||
],
|
||||
procedures: procedures.length,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -11,12 +18,18 @@ import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { cleanupOwnedProcessGroup } from "../dist/process/ProcessOwnership.js";
|
||||
import {
|
||||
ProviderProcessSupervisor,
|
||||
spawnOwnedProviderProcess,
|
||||
} from "../dist/process/ProviderProcess.js";
|
||||
import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs";
|
||||
import {
|
||||
requireMcpLifecycleResult,
|
||||
requireMcpOutput,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
|
||||
if (process.argv.length !== 2)
|
||||
throw new Error("Usage: node scripts/verify-real-ghidra-raw.mjs");
|
||||
@@ -39,6 +52,7 @@ assert.equal(installation.status, "available", JSON.stringify(installation));
|
||||
|
||||
const run = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-raw-proof-"));
|
||||
let fixtureCleaned = true;
|
||||
const runtime = join(workspace, "runtime");
|
||||
await mkdir(runtime);
|
||||
const bytes = Buffer.from("1eff2fe1", "hex"); // ARM little-endian bx lr.
|
||||
@@ -53,6 +67,17 @@ const env = {
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_PROCESS_RUN_ID: run.run_id,
|
||||
GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.REA_MCP_INPUT_SCHEMA_PROFILE === undefined
|
||||
? {}
|
||||
: {
|
||||
REA_MCP_INPUT_SCHEMA_PROFILE: process.env.REA_MCP_INPUT_SCHEMA_PROFILE,
|
||||
}),
|
||||
...(process.env.REA_MCP_OUTPUT_SCHEMA_PROFILE === undefined
|
||||
? {}
|
||||
: {
|
||||
REA_MCP_OUTPUT_SCHEMA_PROFILE:
|
||||
process.env.REA_MCP_OUTPUT_SCHEMA_PROFILE,
|
||||
}),
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
@@ -78,8 +103,13 @@ transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
let report;
|
||||
let searchDefinition;
|
||||
try {
|
||||
await client.connect(transport);
|
||||
searchDefinition = (await client.listTools()).tools.find(
|
||||
(tool) => tool.name === "search_bytes",
|
||||
);
|
||||
assert.ok(searchDefinition?.outputSchema);
|
||||
const profiles = [];
|
||||
const sessions = [];
|
||||
for (const entryPoint of ["0x08000000", "0x8000000", "0x09000000"]) {
|
||||
@@ -300,6 +330,198 @@ try {
|
||||
assert.equal(wrongCursor.isError, true);
|
||||
await call("close_binary");
|
||||
assert.deepEqual(await readFile(repeatedPath), repeatedBytes);
|
||||
|
||||
// Two adjacent virtual regions backed by disjoint file ranges. The same
|
||||
// signature crosses their boundary and overlaps at every possible start.
|
||||
const splitPath = join(workspace, "split.elf");
|
||||
const splitBytes = splitSearchElf(0);
|
||||
const splitSha256 = createHash("sha256").update(splitBytes).digest("hex");
|
||||
await writeFile(splitPath, splitBytes);
|
||||
await call("open_binary", { path: splitPath, provider_id: "ghidra" });
|
||||
let cursorReopened = false;
|
||||
for (const pattern of ["aa aa aa", "aa ?? aa"]) {
|
||||
let cursor;
|
||||
const addresses = [];
|
||||
const pages = [];
|
||||
do {
|
||||
const evidence = await call(
|
||||
"search_bytes",
|
||||
{
|
||||
pattern,
|
||||
start: "0x401000",
|
||||
end: "0x401007",
|
||||
limit: 2,
|
||||
...(cursor === undefined ? {} : { cursor }),
|
||||
},
|
||||
splitSha256,
|
||||
"elf",
|
||||
);
|
||||
const result = evidence.normalized_result;
|
||||
addresses.push(...result.matches.map((match) => match.address));
|
||||
pages.push({
|
||||
matches: result.matches.map(({ address, segment, file_offset }) => ({
|
||||
address,
|
||||
segment,
|
||||
file_offset,
|
||||
})),
|
||||
blocks: result.searched.blocks,
|
||||
});
|
||||
cursor = result.next_cursor ?? undefined;
|
||||
if (cursor !== undefined && !cursorReopened) {
|
||||
const snapshot = join(workspace, "split-snapshot.json");
|
||||
await call("close_binary", { snapshot_path: snapshot });
|
||||
await call("open_binary", {
|
||||
path: splitPath,
|
||||
provider_id: "ghidra",
|
||||
snapshot_path: snapshot,
|
||||
});
|
||||
cursorReopened = true;
|
||||
}
|
||||
} while (cursor !== undefined);
|
||||
assert.deepEqual(addresses, [
|
||||
"0x401000",
|
||||
"0x401001",
|
||||
"0x401002",
|
||||
"0x401003",
|
||||
"0x401004",
|
||||
"0x401005",
|
||||
]);
|
||||
assert.deepEqual(pages, [
|
||||
{
|
||||
matches: [
|
||||
{ address: "0x401000", segment: "segment_0", file_offset: 0x1000 },
|
||||
{ address: "0x401001", segment: "segment_0", file_offset: 0x1001 },
|
||||
],
|
||||
blocks: [
|
||||
{
|
||||
segment: "segment_0",
|
||||
start: "0x401000",
|
||||
end: "0x401003",
|
||||
bytes: 4,
|
||||
},
|
||||
{
|
||||
segment: "segment_1",
|
||||
start: "0x401004",
|
||||
end: "0x401004",
|
||||
bytes: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
matches: [
|
||||
{ address: "0x401002", segment: "segment_0", file_offset: 0x1002 },
|
||||
{ address: "0x401003", segment: "segment_0", file_offset: 0x1003 },
|
||||
],
|
||||
blocks: [
|
||||
{
|
||||
segment: "segment_0",
|
||||
start: "0x401002",
|
||||
end: "0x401003",
|
||||
bytes: 2,
|
||||
},
|
||||
{
|
||||
segment: "segment_1",
|
||||
start: "0x401004",
|
||||
end: "0x401006",
|
||||
bytes: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
matches: [
|
||||
{ address: "0x401004", segment: "segment_1", file_offset: 0x3000 },
|
||||
{ address: "0x401005", segment: "segment_1", file_offset: 0x3001 },
|
||||
],
|
||||
blocks: [
|
||||
{
|
||||
segment: "segment_1",
|
||||
start: "0x401004",
|
||||
end: "0x401007",
|
||||
bytes: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
const bounded = await call(
|
||||
"search_bytes",
|
||||
{
|
||||
pattern,
|
||||
start: "0x401000",
|
||||
end: "0x401005",
|
||||
limit: 20,
|
||||
},
|
||||
splitSha256,
|
||||
"elf",
|
||||
);
|
||||
assert.deepEqual(
|
||||
bounded.normalized_result.matches.map((match) => match.address),
|
||||
addresses.slice(0, 4),
|
||||
);
|
||||
}
|
||||
assert.equal(cursorReopened, true);
|
||||
const fileStart = await call(
|
||||
"address_to_file_offset",
|
||||
{ address: "0x401000" },
|
||||
splitSha256,
|
||||
"elf",
|
||||
);
|
||||
const fileSecond = await call(
|
||||
"address_to_file_offset",
|
||||
{ address: "0x401004" },
|
||||
splitSha256,
|
||||
"elf",
|
||||
);
|
||||
assert.equal(fileStart.normalized_result.file_offset, 0x1000);
|
||||
assert.equal(fileSecond.normalized_result.file_offset, 0x3000);
|
||||
await call("close_binary");
|
||||
assert.deepEqual(await readFile(splitPath), splitBytes);
|
||||
await verifyCrossBlockProject(splitPath, splitSha256);
|
||||
const gapPath = join(workspace, "gap.elf");
|
||||
const gapBytes = splitSearchElf(1);
|
||||
const gapSha256 = createHash("sha256").update(gapBytes).digest("hex");
|
||||
await writeFile(gapPath, gapBytes);
|
||||
await call("open_binary", { path: gapPath, provider_id: "ghidra" });
|
||||
const gap = await call(
|
||||
"search_bytes",
|
||||
{
|
||||
pattern: "aa aa aa aa ?? aa aa aa aa",
|
||||
start: "0x401000",
|
||||
end: "0x401008",
|
||||
},
|
||||
gapSha256,
|
||||
"elf",
|
||||
);
|
||||
assert.deepEqual(gap.normalized_result.matches, []);
|
||||
await call("close_binary");
|
||||
assert.deepEqual(await readFile(gapPath), gapBytes);
|
||||
const zeroFillPath = join(workspace, "initialized-zerofill.elf");
|
||||
const zeroFillBytes = splitSearchElf(1, true);
|
||||
const zeroFillSha256 = createHash("sha256")
|
||||
.update(zeroFillBytes)
|
||||
.digest("hex");
|
||||
await writeFile(zeroFillPath, zeroFillBytes);
|
||||
await call("open_binary", { path: zeroFillPath, provider_id: "ghidra" });
|
||||
const zeroFill = await call(
|
||||
"search_bytes",
|
||||
{
|
||||
pattern: "aa aa aa aa ?? aa aa aa aa",
|
||||
start: "0x401000",
|
||||
end: "0x401008",
|
||||
},
|
||||
zeroFillSha256,
|
||||
"elf",
|
||||
);
|
||||
// This Ghidra loader initializes the ELF zero-fill byte in this fixture.
|
||||
assert.deepEqual(
|
||||
zeroFill.normalized_result.matches.map((match) => match.address),
|
||||
["0x401000"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
zeroFill.normalized_result.searched.skipped_uninitialized,
|
||||
[],
|
||||
);
|
||||
await call("close_binary");
|
||||
assert.deepEqual(await readFile(zeroFillPath), zeroFillBytes);
|
||||
report = {
|
||||
ok: true,
|
||||
mocked: false,
|
||||
@@ -321,6 +543,7 @@ try {
|
||||
original_unchanged: true,
|
||||
x86_targets: x86Targets,
|
||||
byte_search: {
|
||||
cross_block_paging_and_gap: true,
|
||||
matches: 1005,
|
||||
page_size: 1000,
|
||||
first_search_elapsed_ms: searchElapsedMs,
|
||||
@@ -336,7 +559,7 @@ try {
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
if (fixtureCleaned) await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
const verifierRun = await completeVerifierRun(run);
|
||||
assert.equal(verifierRun.process_lineage.status, "verified");
|
||||
@@ -349,15 +572,211 @@ process.stdout.write(
|
||||
}) + "\n",
|
||||
);
|
||||
|
||||
async function call(name, args = {}, targetSha256 = sha256) {
|
||||
async function call(
|
||||
name,
|
||||
args = {},
|
||||
targetSha256 = sha256,
|
||||
targetFormat = "raw-binary",
|
||||
) {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{ timeout: 240000 },
|
||||
{
|
||||
timeout: 240000,
|
||||
...(name === "search_bytes" ? { toolDefinition: searchDefinition } : {}),
|
||||
},
|
||||
);
|
||||
if (["open_binary", "close_binary", "binary_session"].includes(name))
|
||||
return requireMcpLifecycleResult(result, name);
|
||||
const evidence = parseEvidence(requireMcpOutput(result, name));
|
||||
const output = requireMcpOutput(result, name);
|
||||
const evidence = parseEvidence(output);
|
||||
assert.equal(evidence.subject.digest.sha256, targetSha256);
|
||||
assert.equal(evidence.subject.format, "raw-binary");
|
||||
assert.equal(evidence.subject.format, targetFormat);
|
||||
return evidence;
|
||||
}
|
||||
|
||||
// Minimal static ELF64 with adjacent (or one-byte-separated) virtual segments
|
||||
// and noncontiguous file backing. The target is never executed.
|
||||
function splitSearchElf(gap, zeroFill = false) {
|
||||
const result = Buffer.alloc(0x3004);
|
||||
result.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0]);
|
||||
result.writeUInt16LE(2, 16); // ET_EXEC
|
||||
result.writeUInt16LE(62, 18); // EM_X86_64
|
||||
result.writeUInt32LE(1, 20);
|
||||
result.writeBigUInt64LE(0x401000n, 24);
|
||||
result.writeBigUInt64LE(64n, 32);
|
||||
result.writeUInt16LE(64, 52);
|
||||
result.writeUInt16LE(56, 54);
|
||||
result.writeUInt16LE(2, 56);
|
||||
const segment = (offset, fileOffset, address, memorySize = 4) => {
|
||||
result.writeUInt32LE(1, offset); // PT_LOAD
|
||||
result.writeUInt32LE(4, offset + 4); // PF_R
|
||||
result.writeBigUInt64LE(BigInt(fileOffset), offset + 8);
|
||||
result.writeBigUInt64LE(BigInt(address), offset + 16);
|
||||
result.writeBigUInt64LE(BigInt(address), offset + 24);
|
||||
result.writeBigUInt64LE(4n, offset + 32);
|
||||
result.writeBigUInt64LE(BigInt(memorySize), offset + 40);
|
||||
result.writeBigUInt64LE(1n, offset + 48);
|
||||
result.fill(0xaa, fileOffset, fileOffset + 4);
|
||||
};
|
||||
segment(64, 0x1000, 0x401000, zeroFill ? 5 : 4);
|
||||
segment(120, 0x3000, 0x401004 + gap);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Add test-only memory to an owned project, then let REA inspect its private copy.
|
||||
async function verifyCrossBlockProject(target, targetSha256) {
|
||||
const projectDirectory = join(workspace, "boundary-project");
|
||||
await mkdir(projectDirectory);
|
||||
const scriptDirectory = fileURLToPath(
|
||||
new URL("../tests/conformance/ghidra", import.meta.url),
|
||||
);
|
||||
const launch = await spawnOwnedProviderProcess({
|
||||
command: installation.analyzeHeadlessPath,
|
||||
arguments: [
|
||||
projectDirectory,
|
||||
"Boundary",
|
||||
"-import",
|
||||
target,
|
||||
"-noanalysis",
|
||||
"-scriptPath",
|
||||
scriptDirectory,
|
||||
"-postScript",
|
||||
"ReaCrossBlockFixture.java",
|
||||
],
|
||||
env,
|
||||
runId: run.run_id,
|
||||
expectedCommand: null,
|
||||
});
|
||||
fixtureCleaned = false;
|
||||
const supervisor = new ProviderProcessSupervisor({
|
||||
...launch,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup: () => cleanupOwnedProcessGroup(launch.ownership),
|
||||
});
|
||||
await withVerifierCleanup(async () => {
|
||||
assert.ok(await supervisor.waitForExit(180000), "Fixture setup timed out");
|
||||
assert.ok(
|
||||
await supervisor.waitForOutputClose(1000),
|
||||
"Fixture output did not close",
|
||||
);
|
||||
const snapshot = supervisor.snapshot();
|
||||
const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`;
|
||||
assert.equal(snapshot.exitCode, 0, output);
|
||||
assert.doesNotMatch(output, /REPORT SCRIPT ERROR/u);
|
||||
}, [
|
||||
async () => {
|
||||
const stopped = await supervisor.stop();
|
||||
fixtureCleaned = stopped.status === "verified-cleanup";
|
||||
assert.ok(
|
||||
fixtureCleaned,
|
||||
`Owned cleanup incomplete; retained ${workspace}: ${JSON.stringify(stopped)}`,
|
||||
);
|
||||
},
|
||||
]);
|
||||
const beforeProject = await fingerprintFiles(projectDirectory);
|
||||
const projectTransport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: {
|
||||
...env,
|
||||
REA_GHIDRA_PROJECT_PATH: join(projectDirectory, "Boundary.gpr"),
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
const projectClient = new Client({
|
||||
name: "rea-cross-block-project-proof",
|
||||
version: "1",
|
||||
});
|
||||
try {
|
||||
await projectClient.connect(projectTransport);
|
||||
const definition = (await projectClient.listTools()).tools.find(
|
||||
(tool) => tool.name === "search_bytes",
|
||||
);
|
||||
assert.ok(definition?.outputSchema);
|
||||
const projectCall = async (name, args) => {
|
||||
const reply = await projectClient.callTool(
|
||||
{ name, arguments: args },
|
||||
{
|
||||
timeout: 240000,
|
||||
...(name === "search_bytes" ? { toolDefinition: definition } : {}),
|
||||
},
|
||||
);
|
||||
if (name === "open_binary" || name === "close_binary")
|
||||
return requireMcpLifecycleResult(reply, name);
|
||||
const output = requireMcpOutput(reply, name);
|
||||
const evidence = parseEvidence(output);
|
||||
assert.equal(evidence.subject.digest.sha256, targetSha256);
|
||||
return evidence.normalized_result;
|
||||
};
|
||||
await projectCall("open_binary", { path: target, provider_id: "ghidra" });
|
||||
const pattern = "de ad ?? ?? be ef";
|
||||
const adjacent = await projectCall("search_bytes", {
|
||||
pattern,
|
||||
start: "0x60000000",
|
||||
end: "0x60000005",
|
||||
});
|
||||
assert.deepEqual(
|
||||
adjacent.matches.map((match) => match.address),
|
||||
["0x60000000"],
|
||||
);
|
||||
const gap = await projectCall("search_bytes", {
|
||||
pattern,
|
||||
start: "0x60000100",
|
||||
end: "0x60000105",
|
||||
});
|
||||
assert.deepEqual(gap.matches, []);
|
||||
assert.ok(
|
||||
gap.searched.skipped_uninitialized.some(
|
||||
(block) => block.start === "0x60000102" && block.end === "0x60000103",
|
||||
),
|
||||
);
|
||||
const all = await projectCall("search_bytes", { pattern });
|
||||
assert.ok(all.matches.some((match) => match.address === "0x60000000"));
|
||||
assert.ok(
|
||||
!all.matches.some((match) => match.address.startsWith("overlay_test:")),
|
||||
);
|
||||
assert.ok(
|
||||
all.searched.skipped_not_loaded.some(
|
||||
(block) => block.segment === "overlay_test",
|
||||
),
|
||||
);
|
||||
const overlay = await projectCall("search_bytes", {
|
||||
pattern,
|
||||
start: "overlay_test:0x60000000",
|
||||
end: "overlay_test:0x60000005",
|
||||
});
|
||||
assert.deepEqual(
|
||||
overlay.matches.map((match) => match.address),
|
||||
["overlay_test:0x60000000"],
|
||||
);
|
||||
await projectCall("close_binary", {});
|
||||
} finally {
|
||||
await projectClient.close();
|
||||
await projectTransport.close();
|
||||
}
|
||||
assert.deepEqual(await fingerprintFiles(projectDirectory), beforeProject);
|
||||
}
|
||||
|
||||
async function fingerprintFiles(directory) {
|
||||
const result = [];
|
||||
const visit = async (relative) => {
|
||||
const entries = await readdir(join(directory, relative), {
|
||||
withFileTypes: true,
|
||||
});
|
||||
entries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
for (const entry of entries) {
|
||||
const name = join(relative, entry.name);
|
||||
if (entry.isDirectory()) await visit(name);
|
||||
else if (entry.isFile())
|
||||
result.push([
|
||||
name,
|
||||
createHash("sha256")
|
||||
.update(await readFile(join(directory, name)))
|
||||
.digest("hex"),
|
||||
]);
|
||||
else throw new Error(`Unexpected project entry: ${name}`);
|
||||
}
|
||||
};
|
||||
await visit("");
|
||||
return result;
|
||||
}
|
||||
@@ -28,7 +28,10 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { inspectKunaInstallation } from "../dist/kuna/KunaInstallation.js";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js";
|
||||
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
|
||||
import {
|
||||
requireMcpOperationResult,
|
||||
requireMcpToolError,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { verifyKunaReferences } from "./lib/kuna-reference-verifier.mjs";
|
||||
import {
|
||||
nativeKunaStrings,
|
||||
@@ -172,7 +175,7 @@ const sampleTree = async (root) => {
|
||||
};
|
||||
|
||||
/** One MCP server with a selected provider, sampled while it runs. */
|
||||
const openServer = async (provider) => {
|
||||
const openServer = async (provider, kunaSettings = {}) => {
|
||||
const owner = Symbol(provider.id);
|
||||
pendingServers.add(owner);
|
||||
const transport = new StdioClientTransport({
|
||||
@@ -185,6 +188,7 @@ const openServer = async (provider) => {
|
||||
REA_PROCESS_RUN_ID: run.run_id,
|
||||
REA_KUNA_COMMAND: kuna,
|
||||
REA_KUNA_CACHE_DIR: cacheRoot,
|
||||
...kunaSettings,
|
||||
...(process.env.GHIDRA_INSTALL_DIR === undefined
|
||||
? {}
|
||||
: { GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR }),
|
||||
@@ -229,6 +233,13 @@ const openServer = async (provider) => {
|
||||
structured: result.structuredContent,
|
||||
};
|
||||
};
|
||||
const callError = async (name, arguments_ = {}) => {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 3_600_000 },
|
||||
);
|
||||
return requireMcpToolError(result);
|
||||
};
|
||||
const close = async (priorFailure) => {
|
||||
clearInterval(timer);
|
||||
await sample();
|
||||
@@ -270,7 +281,59 @@ const openServer = async (provider) => {
|
||||
// A rejected connection/target still owns its transport and workspace.
|
||||
await close(cause);
|
||||
}
|
||||
return { call, close };
|
||||
return { call, callError, close };
|
||||
};
|
||||
|
||||
/** Verify a forced inventory timeout can be retried without changing jobs. */
|
||||
const verifyProjectTimeoutRecovery = async (path, address) => {
|
||||
const before = createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
const settings = {
|
||||
REA_KUNA_JOBS: "2",
|
||||
REA_KUNA_TIMEOUT_MS: "60000",
|
||||
REA_KUNA_PROJECT_TIMEOUT_MS: "1",
|
||||
};
|
||||
const timed = await openServer({ path, id: "kuna" }, settings);
|
||||
let timedFailure;
|
||||
try {
|
||||
const failure = await timed.callError("procedure_assembly", {
|
||||
procedure: address,
|
||||
});
|
||||
assert.equal(failure.code, "capability_unavailable");
|
||||
assert.match(failure.message, /kuna functions/u);
|
||||
assert.match(failure.remediation.action, /REA_KUNA_PROJECT_TIMEOUT_MS/u);
|
||||
assert.doesNotMatch(failure.remediation.action, /REA_KUNA_JOBS/u);
|
||||
} catch (cause) {
|
||||
timedFailure = cause;
|
||||
} finally {
|
||||
await timed.close(timedFailure);
|
||||
}
|
||||
const retry = await openServer(
|
||||
{ path, id: "kuna" },
|
||||
{ ...settings, REA_KUNA_PROJECT_TIMEOUT_MS: "60000" },
|
||||
);
|
||||
let retryFailure;
|
||||
try {
|
||||
const assembly = await retry.call("procedure_assembly", {
|
||||
procedure: address,
|
||||
});
|
||||
assert.equal(typeof assembly.value, "string");
|
||||
assert.ok(assembly.value.length > 0);
|
||||
assert.equal(assembly.structured.analysis_profile.parameters.jobs, 2);
|
||||
} catch (cause) {
|
||||
retryFailure = cause;
|
||||
} finally {
|
||||
await retry.close(retryFailure);
|
||||
}
|
||||
assert.equal(
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex"),
|
||||
before,
|
||||
"Kuna timeout recovery changed the target",
|
||||
);
|
||||
return { forced_deadline_ms: 1, retry_deadline_ms: 60000, jobs: 2 };
|
||||
};
|
||||
|
||||
/** Count structuring and diagnostic markers in provider pseudocode. */
|
||||
@@ -448,6 +511,30 @@ const verifyKuna = async (
|
||||
references,
|
||||
});
|
||||
}
|
||||
if (functions.length > 0) {
|
||||
const trace = await cold.call("trace_native_values", {
|
||||
procedure: functions[0][1],
|
||||
max_depth: 0,
|
||||
max_functions: 1,
|
||||
});
|
||||
const bundle = await cold.call("get_evidence_bundle", {
|
||||
detail: "complete",
|
||||
});
|
||||
for (const source of [
|
||||
...trace.value.procedures,
|
||||
...trace.value.nodes,
|
||||
...trace.value.edges,
|
||||
]) {
|
||||
assert.ok(trace.structured.evidence_links.includes(source.evidence_id));
|
||||
assert.equal(
|
||||
bundle.value.records.find(
|
||||
(record) => record.evidence_id === source.evidence_id,
|
||||
)?.operation,
|
||||
"analyze_function",
|
||||
`Missing source Evidence ${source.evidence_id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (cause) {
|
||||
coldFailure = cause;
|
||||
} finally {
|
||||
@@ -535,6 +622,7 @@ const verifyVariant = async ({ flavor, directory, symbols }, variant) => {
|
||||
};
|
||||
|
||||
const results = [];
|
||||
let timeoutRecovery;
|
||||
const failures = [];
|
||||
let inspectionResourcesClean = true;
|
||||
try {
|
||||
@@ -596,6 +684,11 @@ try {
|
||||
}),
|
||||
});
|
||||
|
||||
timeoutRecovery = await verifyProjectTimeoutRecovery(
|
||||
built,
|
||||
fixtureFunctions.get("rea_checksum"),
|
||||
);
|
||||
|
||||
// Caller-built matrix.
|
||||
for (const pair of callerPairs)
|
||||
for (const variant of [pair.file, `${pair.file}.stripped`])
|
||||
@@ -650,6 +743,9 @@ const report = {
|
||||
platform: `${process.platform}/${process.arch}`,
|
||||
verifier_run: lifecycle,
|
||||
results,
|
||||
...(timeoutRecovery === undefined
|
||||
? {}
|
||||
: { timeout_recovery: timeoutRecovery }),
|
||||
limitations: [
|
||||
"Targets are analyzed statically; they are never executed.",
|
||||
"Memory is the sampled peak resident set of the MCP server process tree; CPU is its sampled cumulative time. A short-lived peak between 200 ms samples is not observed.",
|
||||
|
||||
+2
-1
@@ -327,8 +327,9 @@ def main():
|
||||
refresh_contributors(root)
|
||||
elif args.command == "check":
|
||||
sync_pages(root, check=True)
|
||||
for name in ("prepare-website.py", "verify-website.py", "test-website.py"):
|
||||
for name in ("prepare-website.py", "verify-website.py", "test-website.py", "test-website-agents.py"):
|
||||
run_script(root, name)
|
||||
subprocess.run(["node", "--test", "website/worker.test.mjs"], cwd=root, check=True)
|
||||
elif args.command == "links":
|
||||
run_script(root, "audit-website-links.py", ["--report", args.report] if args.report else [])
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Build public agent discovery and Markdown from their maintained sources."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from urllib.parse import urljoin
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from markdownify import MarkdownConverter
|
||||
import yaml
|
||||
|
||||
from website import NOTES_FIXTURES, PageMetadata, SITE_ORIGIN, page_url
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def prepare_discovery(root, site):
|
||||
"""Publish the complete skill, including its relative reference documents."""
|
||||
source = root / ".agents/skills/reverse-engineer-anything"
|
||||
skill = (source / "SKILL.md").read_text(encoding="utf-8")
|
||||
if not skill.startswith("---\n") or "\n---\n" not in skill[4:]:
|
||||
raise ValueError("Public REA skill needs YAML frontmatter")
|
||||
metadata = yaml.safe_load(skill[4:].split("\n---\n", 1)[0])
|
||||
if not isinstance(metadata, dict):
|
||||
raise ValueError("Public REA skill needs YAML metadata")
|
||||
name, description = metadata["name"], metadata["description"]
|
||||
if name != source.name or not isinstance(description, str) or not description.strip():
|
||||
raise ValueError("Invalid public REA skill metadata")
|
||||
discovery = site / ".well-known/agent-skills"
|
||||
discovery.mkdir(parents=True, exist_ok=True)
|
||||
artifact = discovery / f"{name}.zip"
|
||||
with ZipFile(artifact, "w") as archive:
|
||||
for path in [source / "SKILL.md", *sorted(source.glob("references/**/*.md"))]:
|
||||
relative = path.relative_to(source)
|
||||
entry = ZipInfo(f"{name}/{relative.as_posix()}", (1980, 1, 1, 0, 0, 0))
|
||||
entry.create_system = 3
|
||||
entry.external_attr = 0o100644 << 16
|
||||
archive.writestr(entry, path.read_bytes(), compress_type=ZIP_DEFLATED)
|
||||
artifact_url = f"{SITE_ORIGIN}/.well-known/agent-skills/{artifact.name}"
|
||||
write_json(discovery / "index.json", {
|
||||
"$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
|
||||
"skills": [{
|
||||
"name": name, "type": "archive", "description": description,
|
||||
"url": artifact_url,
|
||||
"digest": "sha256:" + hashlib.sha256(artifact.read_bytes()).hexdigest(),
|
||||
}],
|
||||
})
|
||||
entry = {
|
||||
"identifier": f"urn:air:rea.tools:skill:{name}",
|
||||
"displayName": "REA reverse engineering skill",
|
||||
"description": description,
|
||||
"type": "application/zip",
|
||||
"url": artifact_url,
|
||||
}
|
||||
write_json(site / ".well-known/ai-catalog.json", {
|
||||
"specVersion": "1.0",
|
||||
"host": {"displayName": "REA", "documentationUrl": f"{SITE_ORIGIN}/get-started/"},
|
||||
"entries": [entry],
|
||||
})
|
||||
write_json(site / ".well-known/ard.json", {"entries": [{
|
||||
**entry,
|
||||
"representativeQueries": [
|
||||
"Explain how a feature works in a shipped executable using REA.",
|
||||
"Trace an Electron application's behavior back to its packaged JavaScript.",
|
||||
"Build an AgentEnv bundle from a web application capture.",
|
||||
],
|
||||
}]})
|
||||
print("Prepared agent skill archive, discovery index, AI Catalog and ARD manifest.")
|
||||
|
||||
|
||||
def prepare_markdown(site):
|
||||
"""Convert rendered reading pages, keeping code and resolving resource URLs."""
|
||||
converter = MarkdownConverter(heading_style="ATX", bullets="-", escape_underscores=False)
|
||||
count = 0
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
if path.relative_to(site).as_posix() in NOTES_FIXTURES:
|
||||
continue
|
||||
source = path.read_text(encoding="utf-8")
|
||||
metadata = PageMetadata()
|
||||
metadata.feed(source)
|
||||
# Drafts and English fallbacks keep their HTML indexing policy intact.
|
||||
if metadata.noindex:
|
||||
continue
|
||||
soup = BeautifulSoup(source, "html.parser")
|
||||
main = soup.find("main")
|
||||
if main is None:
|
||||
continue
|
||||
# Preserve fallback notices and closed disclosures; remove interface chrome.
|
||||
for node in main.select('script, style, nav, button, svg, .home-page-nav, [aria-hidden="true"]'):
|
||||
if node.parent is not None:
|
||||
node.decompose()
|
||||
# These spans are separate lines in the shared flow component's CSS.
|
||||
for node in main.select("span.flow-label, span.flow-node, span.flow-note"):
|
||||
node.name = "div"
|
||||
canonical = page_url(site, path)
|
||||
for node in main.select("[href], [src]"):
|
||||
for attribute in ("href", "src"):
|
||||
if node.has_attr(attribute):
|
||||
node[attribute] = urljoin(canonical, node[attribute])
|
||||
title = metadata.titles[0] if metadata.titles else "REA"
|
||||
header = yaml.safe_dump({"title": title, "url": canonical}, allow_unicode=True, sort_keys=False)
|
||||
markdown = converter.convert_soup(main).strip()
|
||||
path.with_suffix(".md").write_text(f"---\n{header}---\n\n{markdown}\n", encoding="utf-8")
|
||||
alternate = page_url(site, path.with_suffix(".md"))
|
||||
links = (
|
||||
f'<link rel="alternate" type="text/markdown" href="{alternate}" />\n'
|
||||
f'<link rel="ard" type="application/json" href="{SITE_ORIGIN}/.well-known/ard.json" />\n'
|
||||
f'<link rel="ai-catalog" type="application/ai-catalog+json" href="{SITE_ORIGIN}/.well-known/ai-catalog.json" />\n'
|
||||
f'<link rel="related" type="application/json" title="Agent skills index" href="{SITE_ORIGIN}/.well-known/agent-skills/index.json" />\n'
|
||||
)
|
||||
path.write_text(source.replace("</head>", links + "</head>", 1), encoding="utf-8")
|
||||
count += 1
|
||||
print(f"Prepared Markdown for {count} reading pages.")
|
||||
Reference in new issue
Block a user