Merge branch 'main' into agent/process-zombie-settlement
This commit is contained in:
commit
95a228ffa7
|
|
@ -57,7 +57,7 @@ jobs:
|
|||
npm install --global --prefix "${prefix}" "./${tarball}"
|
||||
echo "REA_VERIFY_SERVER_COMMAND=${prefix}/bin/rea" >> "${GITHUB_ENV}"
|
||||
echo 'REA_VERIFY_SERVER_ARGS_JSON=["mcp"]' >> "${GITHUB_ENV}"
|
||||
- name: Verify two binaries through the in-process bridge
|
||||
- name: Verify two binaries through an alternate launcher symlink
|
||||
env:
|
||||
HOPPER_TARGET_PATH: ${{ inputs.first_target }}
|
||||
HOPPER_SECOND_TARGET_PATH: ${{ inputs.second_target }}
|
||||
|
|
|
|||
|
|
@ -170,72 +170,88 @@ try {
|
|||
throw new Error("packaged artifact inventory CLI failed");
|
||||
// prettier-ignore
|
||||
await verifyPackagedInvestigation({ cli, workspace, evidenceRoot, artifactArchive, environment });
|
||||
const overview = json(
|
||||
await run(cli, ["analyze", process.execPath, "--json"], environment),
|
||||
);
|
||||
if (
|
||||
overview.operation !== "binary_overview" ||
|
||||
overview.normalized_result?.procedure_count < 1
|
||||
)
|
||||
throw new Error(
|
||||
`packaged Hopper-backed analyze CLI failed: ${JSON.stringify(overview)}`,
|
||||
if (process.platform === "linux") {
|
||||
const unsupportedExecution = await runWithStatus(
|
||||
cli,
|
||||
["analyze", process.execPath, "--json"],
|
||||
environment,
|
||||
);
|
||||
const inspected = json(
|
||||
await run(
|
||||
cli,
|
||||
[
|
||||
"inspect",
|
||||
process.execPath,
|
||||
"--detail",
|
||||
"detailed",
|
||||
"--limit",
|
||||
"1",
|
||||
"--json",
|
||||
],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
inspected.operation !== "binary_overview" ||
|
||||
inspected.normalized_result?.detail !== "detailed"
|
||||
)
|
||||
throw new Error("packaged inspect CLI failed");
|
||||
const functionResult = json(
|
||||
await run(
|
||||
cli,
|
||||
["function", process.execPath, "0x1000", "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
functionResult.operation !== "analyze_function" ||
|
||||
functionResult.provider?.id !== "hopper" ||
|
||||
functionResult.normalized_result?.procedure?.address !== "0x1000"
|
||||
)
|
||||
throw new Error(
|
||||
`packaged function CLI failed: ${JSON.stringify(functionResult)}`,
|
||||
const unsupported = json(unsupportedExecution.stdout);
|
||||
if (
|
||||
unsupportedExecution.status !== 1 ||
|
||||
unsupported.details?.failure_code !== "unsupported_hopper_build"
|
||||
)
|
||||
throw new Error(
|
||||
`packaged Linux Hopper verification did not fail closed: ${JSON.stringify(unsupported)}`,
|
||||
);
|
||||
} else {
|
||||
const overview = json(
|
||||
await run(cli, ["analyze", process.execPath, "--json"], environment),
|
||||
);
|
||||
const xrefs = json(
|
||||
await run(
|
||||
cli,
|
||||
["xrefs", process.execPath, "0x1000", "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
xrefs.operation !== "xrefs" ||
|
||||
JSON.stringify(xrefs.normalized_result) !== JSON.stringify(["0x1000"])
|
||||
)
|
||||
throw new Error(`packaged xrefs CLI failed: ${JSON.stringify(xrefs)}`);
|
||||
const trace = json(
|
||||
await run(
|
||||
cli,
|
||||
["trace", process.execPath, "fixture", "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (trace.operation !== "trace_feature")
|
||||
throw new Error(`packaged trace CLI failed: ${JSON.stringify(trace)}`);
|
||||
if (
|
||||
overview.operation !== "binary_overview" ||
|
||||
overview.normalized_result?.procedure_count < 1
|
||||
)
|
||||
throw new Error(
|
||||
`packaged Hopper-backed analyze CLI failed: ${JSON.stringify(overview)}`,
|
||||
);
|
||||
const inspected = json(
|
||||
await run(
|
||||
cli,
|
||||
[
|
||||
"inspect",
|
||||
process.execPath,
|
||||
"--detail",
|
||||
"detailed",
|
||||
"--limit",
|
||||
"1",
|
||||
"--json",
|
||||
],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
inspected.operation !== "binary_overview" ||
|
||||
inspected.normalized_result?.detail !== "detailed"
|
||||
)
|
||||
throw new Error("packaged inspect CLI failed");
|
||||
const functionResult = json(
|
||||
await run(
|
||||
cli,
|
||||
["function", process.execPath, "0x1000", "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
functionResult.operation !== "analyze_function" ||
|
||||
functionResult.provider?.id !== "hopper" ||
|
||||
functionResult.normalized_result?.procedure?.address !== "0x1000"
|
||||
)
|
||||
throw new Error(
|
||||
`packaged function CLI failed: ${JSON.stringify(functionResult)}`,
|
||||
);
|
||||
const xrefs = json(
|
||||
await run(
|
||||
cli,
|
||||
["xrefs", process.execPath, "0x1000", "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
xrefs.operation !== "xrefs" ||
|
||||
JSON.stringify(xrefs.normalized_result) !== JSON.stringify(["0x1000"])
|
||||
)
|
||||
throw new Error(`packaged xrefs CLI failed: ${JSON.stringify(xrefs)}`);
|
||||
const trace = json(
|
||||
await run(
|
||||
cli,
|
||||
["trace", process.execPath, "fixture", "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (trace.operation !== "trace_feature")
|
||||
throw new Error(`packaged trace CLI failed: ${JSON.stringify(trace)}`);
|
||||
}
|
||||
const capabilities = json(
|
||||
await run(cli, ["capabilities", "--json"], environment),
|
||||
);
|
||||
|
|
@ -247,20 +263,22 @@ try {
|
|||
providers.providers.some(({ id }) => typeof id !== "string")
|
||||
)
|
||||
throw new Error("packaged providers CLI failed");
|
||||
const searchResult = json(
|
||||
await run(
|
||||
cli,
|
||||
["search", process.execPath, "fixture", "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
searchResult.operation !== "search_strings" ||
|
||||
searchResult.normalized_result?.items?.length !== 1
|
||||
)
|
||||
throw new Error(
|
||||
`packaged search CLI failed: ${JSON.stringify(searchResult)}`,
|
||||
if (process.platform !== "linux") {
|
||||
const searchResult = json(
|
||||
await run(
|
||||
cli,
|
||||
["search", process.execPath, "fixture", "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
searchResult.operation !== "search_strings" ||
|
||||
searchResult.normalized_result?.items?.length !== 1
|
||||
)
|
||||
throw new Error(
|
||||
`packaged search CLI failed: ${JSON.stringify(searchResult)}`,
|
||||
);
|
||||
}
|
||||
const referenceImport = json(
|
||||
await run(
|
||||
cli,
|
||||
|
|
@ -478,30 +496,41 @@ try {
|
|||
);
|
||||
if (opened.isError === true)
|
||||
throw new Error("packaged MCP could not open a binary");
|
||||
await prompts.verifyPromptCompletion(client, mcpOptions, true);
|
||||
await prompts.verifyPromptCompletion(
|
||||
client,
|
||||
mcpOptions,
|
||||
process.platform !== "linux",
|
||||
);
|
||||
const current = await client.callTool(
|
||||
{ name: "current_document", arguments: {} },
|
||||
mcpOptions,
|
||||
);
|
||||
if (
|
||||
current.isError === true ||
|
||||
json(prompts.mcpText(current)).normalized_result !== "fixture"
|
||||
)
|
||||
throw new Error("packaged MCP bridge call failed");
|
||||
const batch = await client.callTool(
|
||||
{ name: "batch_decompile", arguments: { addresses: ["0x1000"] } },
|
||||
mcpOptions,
|
||||
);
|
||||
const batchResult = json(prompts.mcpText(batch)).normalized_result;
|
||||
if (
|
||||
batch.isError === true ||
|
||||
batchResult?.total !== 1 ||
|
||||
batchResult?.succeeded !== 1 ||
|
||||
batchResult?.failed !== 0 ||
|
||||
batchResult?.items?.[0]?.status !== "ok" ||
|
||||
batchResult?.items?.[0]?.pseudocode !== "return 0;"
|
||||
)
|
||||
throw new Error("packaged MCP structured batch result failed");
|
||||
if (process.platform === "linux") {
|
||||
if (
|
||||
current.isError !== true ||
|
||||
!prompts
|
||||
.mcpText(current)
|
||||
.includes("not supported for unattended Linux analysis")
|
||||
)
|
||||
throw new Error("packaged Linux MCP did not reject an unpinned Hopper");
|
||||
} else {
|
||||
if (json(prompts.mcpText(current)).normalized_result !== "fixture")
|
||||
throw new Error("packaged MCP bridge call failed");
|
||||
const batch = await client.callTool(
|
||||
{ name: "batch_decompile", arguments: { addresses: ["0x1000"] } },
|
||||
mcpOptions,
|
||||
);
|
||||
const batchResult = json(prompts.mcpText(batch)).normalized_result;
|
||||
if (
|
||||
batch.isError === true ||
|
||||
batchResult?.total !== 1 ||
|
||||
batchResult?.succeeded !== 1 ||
|
||||
batchResult?.failed !== 0 ||
|
||||
batchResult?.items?.[0]?.status !== "ok" ||
|
||||
batchResult?.items?.[0]?.pseudocode !== "return 0;"
|
||||
)
|
||||
throw new Error("packaged MCP structured batch result failed");
|
||||
}
|
||||
const closed = await client.callTool(
|
||||
{ name: "close_binary", arguments: {} },
|
||||
mcpOptions,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { access } from "node:fs/promises";
|
||||
import { access, mkdtemp, rm, symlink } from "node:fs/promises";
|
||||
import { execFile } from "node:child_process";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
|
@ -14,5 +16,14 @@ await access("/usr/bin/python3");
|
|||
const linked = await execFileAsync("ldd", [launcher]);
|
||||
if (`${linked.stdout}\n${linked.stderr}`.includes("not found"))
|
||||
throw new Error("Hopper has unresolved shared-library dependencies");
|
||||
process.env.HOPPER_LAUNCHER_PATH = launcher;
|
||||
await import("./verify-real-hopper.mjs");
|
||||
const alternateDirectory = await mkdtemp(
|
||||
join(tmpdir(), "rea-real-hopper-launcher-"),
|
||||
);
|
||||
const alternateLauncher = join(alternateDirectory, "Hopper-alternate");
|
||||
try {
|
||||
await symlink(launcher, alternateLauncher);
|
||||
process.env.HOPPER_LAUNCHER_PATH = alternateLauncher;
|
||||
await import("./verify-real-hopper.mjs");
|
||||
} finally {
|
||||
await rm(alternateDirectory, { recursive: true, force: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,15 +40,25 @@ export interface BridgeLauncher {
|
|||
): Promise<Result<BridgeLaunch, HopperStartError | HopperCancelledError>>;
|
||||
}
|
||||
|
||||
export interface HopperApplicationLauncherOptions {
|
||||
interface SharedHopperApplicationLauncherOptions {
|
||||
readonly launcherPath: string;
|
||||
readonly targetPath: string;
|
||||
readonly targetKind: "executable" | "database";
|
||||
readonly loaderArgs: readonly string[];
|
||||
readonly bridgeScriptPath: string;
|
||||
readonly demoHelperPath?: string;
|
||||
}
|
||||
|
||||
/** Explicit launcher contract; the Linux adapter verifies its pinned Hopper build before execution. */
|
||||
export type HopperApplicationLauncherOptions =
|
||||
| (SharedHopperApplicationLauncherOptions & {
|
||||
readonly launchMode: "native";
|
||||
readonly demoHelperPath?: never;
|
||||
})
|
||||
| (SharedHopperApplicationLauncherOptions & {
|
||||
readonly launchMode: "verified_linux_demo";
|
||||
readonly demoHelperPath: string;
|
||||
});
|
||||
|
||||
/**
|
||||
* Launches Hopper through its documented CLI and injects only REA's owned bridge.
|
||||
*
|
||||
|
|
@ -186,10 +196,13 @@ const linuxDemoLaunch = (
|
|||
};
|
||||
};
|
||||
|
||||
const usesLinuxDemo = (options: HopperApplicationLauncherOptions): boolean =>
|
||||
process.platform === "linux" &&
|
||||
basename(options.launcherPath) === "Hopper" &&
|
||||
options.demoHelperPath !== undefined;
|
||||
/** Select the version-pinned adapter from the caller's explicit launch mode. */
|
||||
export const usesLinuxDemo = (
|
||||
options: HopperApplicationLauncherOptions,
|
||||
): options is HopperApplicationLauncherOptions & {
|
||||
readonly launchMode: "verified_linux_demo";
|
||||
readonly demoHelperPath: string;
|
||||
} => options.launchMode === "verified_linux_demo";
|
||||
|
||||
const spawnLauncher = async (
|
||||
command: string,
|
||||
|
|
|
|||
|
|
@ -114,9 +114,14 @@ export class HopperProvider implements AnalysisProvider {
|
|||
bridgeScriptPath: fileURLToPath(
|
||||
new URL("../../bridge/hopper_bridge.py", import.meta.url),
|
||||
),
|
||||
demoHelperPath: fileURLToPath(
|
||||
new URL("../../scripts/hopper-demo-x11.py", import.meta.url),
|
||||
),
|
||||
...(process.platform === "linux"
|
||||
? {
|
||||
launchMode: "verified_linux_demo" as const,
|
||||
demoHelperPath: fileURLToPath(
|
||||
new URL("../../scripts/hopper-demo-x11.py", import.meta.url),
|
||||
),
|
||||
}
|
||||
: { launchMode: "native" as const }),
|
||||
}),
|
||||
logger: this.logger.child({ layer: "bridge" }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { access, chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
type HopperApplicationLauncherOptions,
|
||||
usesLinuxDemo,
|
||||
} from "../src/hopper/BridgeLauncher.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const demoHelperPath = fileURLToPath(
|
||||
new URL("../scripts/hopper-demo-x11.py", import.meta.url),
|
||||
);
|
||||
|
||||
const options = (launcherPath: string): HopperApplicationLauncherOptions => ({
|
||||
launcherPath,
|
||||
targetPath: "/target",
|
||||
targetKind: "executable",
|
||||
loaderArgs: [],
|
||||
bridgeScriptPath: "/rea/hopper_bridge.py",
|
||||
launchMode: "verified_linux_demo",
|
||||
demoHelperPath: "/rea/hopper-demo-x11.py",
|
||||
});
|
||||
|
||||
describe("Hopper bridge launcher selection", () => {
|
||||
it.each([
|
||||
"/opt/hopper/bin/Hopper",
|
||||
"/usr/local/bin/hopper",
|
||||
"/workspace/bin/hopper-wrapper",
|
||||
])(
|
||||
"routes launcher spelling %s through the explicitly selected pinned adapter",
|
||||
(path) => {
|
||||
expect(usesLinuxDemo(options(path))).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not infer demo behavior from a native launcher's basename", () => {
|
||||
expect(
|
||||
usesLinuxDemo({
|
||||
launcherPath: "/opt/hopper/bin/Hopper",
|
||||
targetPath: "/target",
|
||||
targetKind: "executable",
|
||||
loaderArgs: [],
|
||||
bridgeScriptPath: "/rea/hopper_bridge.py",
|
||||
launchMode: "native",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects an unpinned wrapper before executing it", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "rea-hopper-wrapper-"));
|
||||
const wrapper = join(directory, "hopper-wrapper");
|
||||
const marker = join(directory, "executed");
|
||||
try {
|
||||
await writeFile(wrapper, `#!/bin/sh\ntouch ${JSON.stringify(marker)}\n`);
|
||||
await chmod(wrapper, 0o700);
|
||||
await expect(
|
||||
execFileAsync(
|
||||
"python3",
|
||||
[
|
||||
demoHelperPath,
|
||||
"--hopper",
|
||||
wrapper,
|
||||
"--socket",
|
||||
join(directory, "bridge.sock"),
|
||||
"--",
|
||||
wrapper,
|
||||
],
|
||||
{ timeout: 3_000 },
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
code: 72,
|
||||
stderr: expect.stringContaining("unsupported Hopper binary"),
|
||||
});
|
||||
await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -37,7 +37,7 @@ describe("production stdio runtime", () => {
|
|||
name: "current_document",
|
||||
arguments: {},
|
||||
});
|
||||
expect(result.isError).not.toBe(true);
|
||||
expect(result.isError === true).toBe(process.platform === "linux");
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
|
|
@ -53,7 +53,7 @@ describe("production stdio runtime", () => {
|
|||
mode: "mcp",
|
||||
layer: "server",
|
||||
tool: "current_document",
|
||||
status: "ok",
|
||||
status: process.platform === "linux" ? "error" : "ok",
|
||||
}),
|
||||
);
|
||||
expect(stderr).not.toContain("HOPPER_LOADER_ARGS_JSON");
|
||||
|
|
|
|||
Loading…
Reference in New Issue