chore(release): harden package artifacts
This commit is contained in:
parent
2b7ef8610d
commit
e32906afb7
|
|
@ -30,7 +30,7 @@
|
|||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"bridge",
|
||||
"bridge/hopper_bridge.py",
|
||||
"scripts/rea.mjs",
|
||||
"skills",
|
||||
"README.md",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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>> = {},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue