Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions plugins/codex-security/mcp-app/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,19 @@ node --test --test-concurrency=1 "tests/test_*.mjs"
```

Use the existing injected clocks for retry policy tests. Keep real timers, subprocesses, SQLite files, and isolated temporary directories in the lifecycle, locking, permissions, and stdio tests. Do not share writable fixtures between test files or rebuild the bundled plugin while another test is reading it.

## Build for the local host

For a local or CI plugin that only runs on the build host, install the MCP app dependencies and the toolchain declared in `../native/rust-toolchain.toml`. Windows also requires the MSVC C++ build tools. From this directory:

```sh
pnpm install --frozen-lockfile
pnpm run build:native
node scripts/build_mcp_app.mjs --output .preview/mcp --native host
```

`build:native` compiles the native TypeScript tools with this package's existing dependencies, runs the locked Cargo build, and prepares dependency notices. It does not require the SDK source tree. Native outputs stay in ignored `../native/dist` and `../native/target` directories; `CARGO_TARGET_DIR` can select another Cargo cache.

`--native host` copies the current OS, CPU, and Linux libc target from `native/dist`, together with all shared notices. It requires a fresh `build:native` run after native source changes and fails if the host binary is missing. Build again on each execution platform; a host build is not a portable release artifact.

The default, `--native universal`, still requires the complete verified `native/prebuilt` payload. Package and release validation keep checking every supported platform. For distributing GNU Linux binaries, retain the glibc 2.28 build environment and the compatibility checks described in the [native README](../native/README.md).
1 change: 1 addition & 0 deletions plugins/codex-security/mcp-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"private": true,
"scripts": {
"build": "tsc --noEmit",
"build:native": "node scripts/build_native.mjs",
"build:mcp": "node scripts/build_mcp_app.mjs --output .preview/mcp",
"test:mcp": "node --test --test-concurrency=2 --test-reporter=./scripts/test_reporter.mjs \"tests/test_*.mjs\"",
"typecheck": "tsc --noEmit"
Expand Down
41 changes: 30 additions & 11 deletions plugins/codex-security/mcp-app/scripts/build_mcp_app.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@ import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { brotliCompressSync, constants as zlibConstants } from "node:zlib";
import { execFileSync } from "node:child_process";
import { parseArgs } from "node:util";
import { build } from "esbuild";

const root = resolve(import.meta.dirname, "..");
const maxChunkBytes = 140_000;

export async function buildMcpApp({ output }) {
export async function buildMcpApp({ output, native = "universal" }) {
if (native !== "universal" && native !== "host") {
throw new Error("Native packaging must be universal or host.");
}
const mcpDir = resolve(output);
const nativeTarget = native === "host"
? (await import("../../native/platform.mjs")).nativeTarget
: undefined;

execFileSync(process.execPath, ["--run", "build"], {
cwd: root,
Expand All @@ -21,11 +28,18 @@ export async function buildMcpApp({ output }) {

await writeRuntime("server", "main.ts");
const contract = JSON.parse(await readFile(join(root, "../plugin-files.json"), "utf8"));
for (const file of contract.shippedExact.filter((path) => path.startsWith("mcp/native/"))) {
const nativeFiles = contract.shippedExact.filter((path) => path.startsWith("mcp/native/"));
if (nativeTarget !== undefined && !nativeFiles.some((path) => path.startsWith(`mcp/native/${nativeTarget}/`))) {
throw new Error(`Unsupported native target: ${nativeTarget}`);
}
for (const file of nativeFiles) {
if (nativeTarget !== undefined && file.endsWith(".node") && !file.startsWith(`mcp/native/${nativeTarget}/`)) {
continue;
}
const path = file.slice("mcp/native/".length);
const destination = join(mcpDir, "native", path);
await mkdir(dirname(destination), { recursive: true });
await copyFile(join(root, "../native/prebuilt", path), destination);
await copyFile(join(root, "../native", native === "host" ? "dist" : "prebuilt", path), destination);
}
await writeRuntime("helpers", "helpers-main.ts");

Expand Down Expand Up @@ -71,15 +85,20 @@ if (
invokedPath !== undefined
&& pathToFileURL(resolve(invokedPath)).href === import.meta.url
) {
const args = process.argv.slice(2);
if (args.length !== 2 || args[0] !== "--output") {
console.error("Usage: node scripts/build_mcp_app.mjs --output <directory>");
process.exitCode = 1;
} else {
buildMcpApp({ output: args[1] }).catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
try {
const { values } = parseArgs({
options: {
output: { type: "string" },
native: { type: "string", default: "universal" }
}
});
if (!values.output) {
throw new Error("Usage: node scripts/build_mcp_app.mjs --output <directory> [--native universal|host]");
}
await buildMcpApp(values);
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
}
}

Expand Down
27 changes: 27 additions & 0 deletions plugins/codex-security/mcp-app/scripts/build_native.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { join, resolve } from "node:path";

const app = resolve(import.meta.dirname, "..");
const native = join(app, "../native");

execFileSync(
process.execPath,
[
join(app, "node_modules/typescript/bin/tsc"),
"--project",
join(app, "tsconfig.native.json"),
],
{ cwd: app, stdio: "inherit" },
);

execFileSync(process.execPath, ["build.mjs"], {
cwd: native,
stdio: "inherit",
});
// Notices include every locked dependency, including those for other targets.
execFileSync("cargo", ["fetch", "--locked"], { cwd: native, stdio: "inherit" });
execFileSync(process.execPath, ["notices.mjs"], {
cwd: native,
stdio: "inherit",
});
164 changes: 164 additions & 0 deletions plugins/codex-security/mcp-app/tests/test_build_mcp_app.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import {
copyFile,
mkdir,
mkdtemp,
readFile,
readdir,
realpath,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { test } from "node:test";
import { fileURLToPath } from "node:url";
import { transform } from "esbuild";

const app = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const contract = JSON.parse(
await readFile(join(app, "../plugin-files.json"), "utf8"),
);
const nativeFiles = contract.shippedExact
.filter((path) => path.startsWith("mcp/native/"))
.map((path) => path.slice("mcp/native/".length));
const platform = await transform(
await readFile(join(app, "../native/platform.mts"), "utf8"),
{
loader: "ts",
format: "esm",
},
);
const { nativeTarget } = await import(
`data:text/javascript,${encodeURIComponent(platform.code)}`
);
const hostBinary = nativeFiles.find((path) =>
path.startsWith(`${nativeTarget}/`),
);
assert.ok(hostBinary);
const foreignBinary = nativeFiles.find(
(path) => path.endsWith(".node") && path !== hostBinary,
);
const notices = nativeFiles.filter((path) => !path.endsWith(".node"));

async function fixture(t) {
const root = await realpath(
await mkdtemp(join(tmpdir(), "native-package-test-")),
);
t.after(() => rm(root, { recursive: true, force: true }));
const mcp = join(root, "mcp-app");
await mkdir(join(mcp, "scripts"), { recursive: true });
await symlink(
join(app, "node_modules"),
join(mcp, "node_modules"),
"junction",
);
await copyFile(
join(app, "scripts/build_mcp_app.mjs"),
join(mcp, "scripts/build_mcp_app.mjs"),
);
await writeFile(
join(mcp, "package.json"),
JSON.stringify({
type: "module",
scripts: { build: "tsc --noEmit" },
}),
);
await writeFile(
join(mcp, "tsconfig.json"),
JSON.stringify({
compilerOptions: { skipLibCheck: true },
include: ["*.ts"],
}),
);
await writeFile(join(mcp, "main.ts"), 'console.log("server fixture");\n');
await writeFile(
join(mcp, "helpers-main.ts"),
'console.log("helper fixture");\n',
);
await writeFile(join(root, "plugin-files.json"), JSON.stringify(contract));
for (const [directory, files] of [
["prebuilt", nativeFiles],
["dist", [...notices, hostBinary]],
]) {
for (const path of files) {
const destination = join(root, "native", directory, path);
await mkdir(dirname(destination), { recursive: true });
await writeFile(destination, `${directory}:${path}`);
}
}
await writeFile(join(root, "native/platform.mjs"), platform.code);
const output = join(root, "output");
return {
root,
output,
build: (...args) =>
spawnSync(
process.execPath,
[join(mcp, "scripts/build_mcp_app.mjs"), "--output", output, ...args],
{ encoding: "utf8" },
),
};
}

test("host packaging uses only the source-built target and shared notices", async (t) => {
const { root, output, build } = await fixture(t);
await rm(join(root, "native/prebuilt"), { recursive: true });
const result = build("--native", "host");
assert.equal(result.status, 0, result.stderr);
const files = (
await readdir(join(output, "native"), {
recursive: true,
withFileTypes: true,
})
).filter((entry) => entry.isFile());
assert.equal(files.length, notices.length + 1);
for (const path of [...notices, hostBinary]) {
assert.equal(
await readFile(join(output, "native", path), "utf8"),
`dist:${path}`,
);
}
assert.equal(
spawnSync(process.execPath, [join(output, "server.mjs")], {
encoding: "utf8",
}).stdout,
"server fixture\n",
);
assert.equal(
spawnSync(process.execPath, [join(output, "helpers.mjs")], {
encoding: "utf8",
}).stdout,
"helper fixture\n",
);
});

test("default packaging retains every verified prebuilt target", async (t) => {
const { output, build } = await fixture(t);
const result = build();
assert.equal(result.status, 0, result.stderr);
for (const path of nativeFiles) {
assert.equal(
await readFile(join(output, "native", path), "utf8"),
`prebuilt:${path}`,
);
}
});

test("host packaging rejects a missing host build even when prebuilt artifacts exist", async (t) => {
const { root, build } = await fixture(t);
await rm(join(root, "native/dist", hostBinary));
const result = build("--native", "host");
assert.notEqual(result.status, 0);
assert.match(result.stderr, /ENOENT/);
});

test("universal packaging still rejects a missing foreign target", async (t) => {
const { root, build } = await fixture(t);
await rm(join(root, "native/prebuilt", foreignBinary));
const result = build();
assert.notEqual(result.status, 0);
assert.match(result.stderr, /ENOENT/);
});
12 changes: 12 additions & 0 deletions plugins/codex-security/mcp-app/tsconfig.native.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": false,
"noEmitOnError": true,
"rootDir": "../native",
"typeRoots": ["./node_modules/@types"]
},
"include": ["../native/*.mts"]
}
Loading