chore(release): harden package artifacts

This commit is contained in:
morluto 2026-07-12 08:45:28 +08:00
parent 2b7ef8610d
commit e32906afb7
5 changed files with 31 additions and 8 deletions

View File

@ -30,7 +30,7 @@
},
"files": [
"dist",
"bridge",
"bridge/hopper_bridge.py",
"scripts/rea.mjs",
"skills",
"README.md",

View File

@ -14,6 +14,8 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
// Exercise the packed artifact in isolated HOME and prefix directories so the
// verifier cannot mutate real MCP registrations or rely on checkout-only files.
const exec = promisify(execFile);
const root = process.cwd();
const workspace = await mkdtemp(join(tmpdir(), "rea-package-"));
@ -23,6 +25,16 @@ try {
tarball = (
await exec("npm", ["pack", "--silent"], { cwd: root })
).stdout.trim();
const packedFiles = (
await exec("tar", ["-tf", join(root, tarball)])
).stdout.split("\n");
if (
packedFiles.some(
(path) => path.includes("__pycache__") || path.endsWith(".pyc"),
)
) {
throw new Error("package contained generated Python bytecode");
}
const prefix = join(workspace, "prefix");
const home = join(workspace, "home");
const fakeBin = join(workspace, "bin");

View File

@ -10,9 +10,11 @@ import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
const execFileAsync = promisify(execFile);
const timeout = 180_000;
const hopperProcessPrefix = "/Applications/Hopper Disassembler.app/Contents/";
// Snapshot bundle processes so cleanup terminates only Hopper processes this
// verifier caused to appear, never an instance the user already had running.
const hopperProcessesBefore = await hopperProcessIds();
const sessionsBefore = new Set(
(await readdir("/tmp")).filter((name) => name.startsWith("bbm-")),
(await readdir("/tmp")).filter((name) => name.startsWith("rea-")),
);
const textValue = (result) => {
@ -163,7 +165,7 @@ try {
await new Promise((resolve) => setTimeout(resolve, 500));
const sessionsAfter = (await readdir("/tmp")).filter(
(name) => name.startsWith("bbm-") && !sessionsBefore.has(name),
(name) => name.startsWith("rea-") && !sessionsBefore.has(name),
);
if (sessionsAfter.length > 0) {
throw new Error("The MCP runtime leaked a bridge session directory");

View File

@ -23,6 +23,7 @@ import {
const MAX_LINE_BYTES = 10 * 1024 * 1024;
const SESSION_ROOT = process.platform === "darwin" ? "/tmp" : tmpdir();
/** Dependencies, deadlines, and redacted diagnostics for one bridge client. */
export interface HopperClientOptions {
readonly launcher: BridgeLauncher;
readonly requestTimeoutMs?: number;
@ -30,6 +31,7 @@ export interface HopperClientOptions {
readonly onDiagnostic?: (event: HopperDiagnostic) => void;
}
/** Safe launcher telemetry; stderr content is intentionally never exposed. */
export type HopperDiagnostic =
| { readonly type: "launcher-stderr"; readonly bytes: number }
| { readonly type: "launcher-exit"; readonly code: number | null };
@ -46,7 +48,14 @@ interface PendingRequest {
readonly onAbort: (() => void) | undefined;
}
/** Owned client for the repository's authenticated in-Hopper bridge. */
/**
* Owns one authenticated NDJSON-over-Unix-socket bridge session.
*
* Each instance creates a private directory and random bearer token, correlates
* concurrent requests by numeric id, and removes its artifacts on close. It
* only terminates launch processes explicitly marked as owned; the normal
* Hopper launcher does not confer ownership of the GUI application.
*/
export class HopperClient {
readonly #options: Required<
Pick<HopperClientOptions, "requestTimeoutMs" | "startupTimeoutMs">
@ -71,7 +80,7 @@ export class HopperClient {
};
}
/** Launch Hopper's owned bridge and complete its health handshake. */
/** Launch the bridge once and complete its authenticated health handshake. */
start(): Promise<Result<HopperServerInfo, HopperError>> {
this.#startPromise ??= this.#start();
return this.#startPromise;
@ -82,7 +91,7 @@ export class HopperClient {
return err(new HopperProtocolError("Hopper client is already started"));
}
try {
this.#directory = await mkdtemp(join(SESSION_ROOT, "bbm-"));
this.#directory = await mkdtemp(join(SESSION_ROOT, "rea-"));
} catch (cause: unknown) {
return err(new HopperStartError({ cause }));
}
@ -120,7 +129,7 @@ export class HopperClient {
return parsed;
}
/** Invoke one declared Hopper operation through the bridge. */
/** Invoke one declared operation, returning timeout and cancellation as values. */
async callTool(
name: string,
arguments_: Readonly<Record<string, JsonValue>> = {},

View File

@ -23,7 +23,7 @@ afterEach(async () => {
describe("target-free MCP lifecycle", () => {
it("reports no-target, opens, analyzes, switches, reports status, and closes", async () => {
directory = await mkdtemp(join(tmpdir(), "bb-mcp-session-"));
directory = await mkdtemp(join(tmpdir(), "rea-mcp-session-"));
const first = join(directory, "first.hop");
const second = join(directory, "second.hop");
await writeFile(first, "one");