feat(plugins): make Claude Code and Codex installs self-contained (#7514)

Ship Claude Code and Codex from one release-pinned npm artifact with bundled runtime dependencies, extraction-only MCP qualification, plugin-aware recovery, and synchronized release metadata.

Integrates #2815 and the original proposal #1977. The original commits remain available on the PR branch; retain their author and assistant credit in this squash.

Validation: required CI passed, real Codex/Claude loading verified, and reviewed packaging/recovery findings addressed.

Co-authored-by: GamerDubz <themastergamerplays24.7@gmail.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: OpenAI Codex <openai-codex@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gensokyo-dev
2026-10-28 01:13:17 +08:00
committed by GitHub
parent 269630f4e1
commit 0cd7ef456b
29 changed files with 1591 additions and 132 deletions
+35
View File
@@ -0,0 +1,35 @@
interface PackageManifest {
name: string;
version: string;
devDependencies?: Record<string, string>;
dependencies?: Record<string, string>;
overrides?: Record<string, unknown>;
// npm manifests are extensible; preserve metadata outside this projection.
[key: string]: unknown;
}
interface LockedPackage {
version?: string;
dev?: boolean;
// Keep unmodified integrity, resolution, platform, and other lock-entry facts.
[key: string]: unknown;
}
interface PackageLock {
name: string;
version: string;
lockfileVersion: number;
packages: Record<string, LockedPackage>;
// Preserve other npm lock metadata rather than dropping unfamiliar fields.
[key: string]: unknown;
}
export function pluginPackageMetadata<Manifest extends PackageManifest>(
manifest: Manifest,
lock: PackageLock,
): {
packageJson: Omit<Manifest, "devDependencies" | "overrides"> & {
bundleDependencies: string[];
};
shrinkwrap: PackageLock;
};
+41
View File
@@ -0,0 +1,41 @@
/** Project the development manifest and lock into the shared runtime artifact. */
export function pluginPackageMetadata(manifest, lock) {
if (
lock.lockfileVersion !== 3 ||
lock.name !== manifest.name ||
lock.version !== manifest.version ||
lock.packages?.[""]?.version !== manifest.version
)
throw new Error("Plugin packaging requires the matching v3 package lock");
// Claude skips dependency installation when a published manifest has
// overrides. Preserve development security overrides in the source owner,
// and reject any future runtime override rather than silently discarding it.
for (const name of Object.keys(manifest.overrides ?? {})) {
const matches = Object.entries(lock.packages).filter(([path]) =>
path.endsWith(`node_modules/${name}`),
);
if (matches.length === 0 || matches.some(([, entry]) => entry.dev !== true))
throw new Error(`Cannot omit a runtime or unresolved override: ${name}`);
}
const packageJson = { ...manifest };
delete packageJson.devDependencies;
delete packageJson.overrides;
// Codex's npm source fetches the archive without installing dependencies.
// Keep optional Frida outside the bundle so ordinary npm installs can still
// run its host-specific installer. PTY binaries are transitive optionals of
// a runtime dependency and are included for every supported platform by the
// archive producer.
packageJson.bundleDependencies = Object.keys(manifest.dependencies ?? {});
const root = { ...lock.packages[""] };
delete root.devDependencies;
const shrinkwrap = {
...lock,
packages: Object.fromEntries(
Object.entries(lock.packages).filter(([, entry]) => entry.dev !== true),
),
};
shrinkwrap.packages[""] = root;
return { packageJson, shrinkwrap };
}
+59
View File
@@ -4,6 +4,37 @@ import { z } from "zod";
const shaSchema = z.string().regex(/^[a-f0-9]{40}$/u);
const versionSchema = z.object({ version: z.string() });
const manifestSchema = z.object({ ".": z.string() });
const PLUGIN_MANIFEST_PATHS = [
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
];
const inlinePluginManifestSchema = versionSchema.extend({
mcpServers: z.object({
rea: z.object({
command: z.literal("node"),
args: z.tuple([
z.literal("${CLAUDE_PLUGIN_ROOT}/scripts/plugin-mcp-launcher.mjs"),
z.string(),
]),
}),
}),
});
const codexPluginManifestSchema = versionSchema.extend({
mcpServers: z.literal("./.codex-plugin/mcp.json"),
});
const codexServerConfigSchema = z.object({
mcpServers: z.object({
rea: z.object({
command: z.literal("node"),
args: z.tuple([
z.literal("./scripts/plugin-mcp-launcher.mjs"),
z.string(),
]),
cwd: z.literal("."),
startup_timeout_sec: z.number().int().positive(),
}),
}),
});
const sectionsSchema = z.object({
versioning: z.string().optional(),
"changelog-sections": z.array(
@@ -189,6 +220,26 @@ async function validateCandidate(git, sha, version, commits) {
["server.json", server.version],
["server.json rea-agents package", npmPackage.version],
);
// A stale manifest keeps the previous plugin cache key or launches the
// previous server, so both are version authority like server.json.
const launches = [];
for (const path of PLUGIN_MANIFEST_PATHS) {
const manifest =
path === ".codex-plugin/plugin.json"
? await readJson(git, sha, path, codexPluginManifestSchema)
: await readJson(git, sha, path, inlinePluginManifestSchema);
versions.push([path, manifest.version]);
const server =
path === ".codex-plugin/plugin.json"
? await readJson(
git,
sha,
".codex-plugin/mcp.json",
codexServerConfigSchema,
)
: manifest;
launches.push([path, server.mcpServers.rea.args[1]]);
}
// Catalogs are generated and checked from this candidate by the release job;
// they are not version authority stored in the immutable source tree.
const generated = await git([
@@ -210,6 +261,14 @@ async function validateCandidate(git, sha, version, commits) {
);
}
}
// The launcher checks the scalar pin against the runtime in its own package.
for (const [location, launched] of launches) {
if (launched !== version) {
throw new Error(
`${location}@${sha} launches version ${launched ?? "no version"}; release branch requires ${version}.`,
);
}
}
const notes = releaseNotes(
await git(["show", `${sha}:CHANGELOG.md`]),
version,
+64 -1
View File
@@ -369,6 +369,60 @@ export function verifyReleaseNotes(raw, text, body) {
};
}
const VERSION_ONLY_PLUGIN_METADATA = new Set([
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
".codex-plugin/mcp.json",
".claude-plugin/marketplace.json",
".agents/plugins/marketplace.json",
]);
/** Serialize plugin metadata with only its release-owned version fields blanked. */
function pluginMetadataWithoutVersions(path, text) {
const manifest = JSON.parse(text);
if (manifest === null || typeof manifest !== "object") return text;
if (path.endsWith("marketplace.json")) {
if (Array.isArray(manifest.plugins))
for (const plugin of manifest.plugins)
if (
plugin?.name === "rea" &&
plugin?.source?.source === "npm" &&
plugin.source.package === "rea-agents" &&
typeof plugin.source.version === "string"
)
plugin.source.version = "";
return JSON.stringify(manifest);
}
if ("version" in manifest) manifest.version = "";
const launch = manifest.mcpServers?.rea?.args;
// Only the scalar pinned version is release-owned; the launcher owns the
// fixed package name and the executable/path remain product behavior.
if (Array.isArray(launch) && typeof launch[1] === "string") launch[1] = "";
return JSON.stringify(manifest);
}
/**
* Plugin metadata also carries behavior, so a release may change only
* its version, pinned server launch, and exact REA marketplace version. Any other difference, an unreadable
* side, or a manifest added by the release counts as a product change.
*/
async function changesOnlyPluginVersions(run, path, from, to) {
try {
return (
pluginMetadataWithoutVersions(
path,
await run("git", ["show", `${from}:${path}`]),
) ===
pluginMetadataWithoutVersions(
path,
await run("git", ["show", `${to}:${path}`]),
)
);
} catch {
return false;
}
}
/** Reject stale inventories and product changes made after the editorial target. */
export async function verifyInventoryHistory(run, raw, source) {
const inventory = releaseInventorySchema.parse(raw);
@@ -437,7 +491,16 @@ export async function verifyInventoryHistory(run, raw, source) {
)
.split("\0")
.filter(Boolean);
const product = paths.filter((path) => !allowed.has(path));
const product = [];
for (const path of paths) {
if (allowed.has(path)) continue;
if (
VERSION_ONLY_PLUGIN_METADATA.has(path) &&
(await changesOnlyPluginVersions(run, path, inventory.target, resolved))
)
continue;
product.push(path);
}
if (product.length)
throw new Error(
`Product changed after notes target; regenerate inventory: ${product.join(", ")}`,
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env node
import { access, readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
// Release Please owns the scalar pin in each client declaration. The runtime
// and skill come from the same installed artifact, without another npx child.
const [version, ...extra] = process.argv.slice(2);
try {
if (
extra.length !== 0 ||
!/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/u.test(version ?? "")
)
throw new Error("requires one exact rea-agents@VERSION pin");
const bundledPackage = JSON.parse(
await readFile(new URL("../package.json", import.meta.url), "utf8"),
);
if (
bundledPackage.name !== "rea-agents" ||
bundledPackage.version !== version
)
throw new Error(
"version pin does not match its installed rea-agents package; reinstall the plugin",
);
await access(new URL("../dist/mcp/main.js", import.meta.url)).catch(
(cause) => {
if (cause?.code !== "ENOENT") throw cause;
throw new Error(
"runtime is missing; reinstall the plugin, or run npm ci && npm run build:cached in a source checkout",
{ cause },
);
},
);
} catch (cause) {
process.stderr.write(`REA plugin ${cause.message}.\n`);
process.exitCode = 1;
}
if (process.exitCode !== 1) {
process.argv = [
process.execPath,
fileURLToPath(new URL("./rea.mjs", import.meta.url)),
"mcp",
];
await import("./rea.mjs");
}
+11 -6
View File
@@ -5,11 +5,12 @@ import {
requireMcpToolError,
} from "./lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { join } from "node:path";
import { readFile, rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import { readFile, realpath, rm } from "node:fs/promises";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { PRODUCT_IDENTITY } from "../dist/identity.js";
import { resolveMcpStartupBudgets } from "../dist/config/mcpStartupBudgets.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import * as prompts from "./verify-package-prompts.mjs";
@@ -239,7 +240,7 @@ const verifyMcpInlineArtifactEvidence = async (
}
};
/** Connect to the packaged MCP server and verify catalog and Evidence composition. */
/** Check CLI diagnostics, then verify catalog and Evidence through the installed plugin. */
export async function verifyPackageMcp({
commands,
cli,
@@ -249,7 +250,7 @@ export async function verifyPackageMcp({
}) {
const diagnosed = JSON.parse(
(
await commands.exec(cli, ["mcp", "doctor", "--json"], {
await commands.exec(process.execPath, [cli, "mcp", "doctor", "--json"], {
env: environment,
timeout: resolveMcpStartupBudgets(environment).doctor.milliseconds,
})
@@ -261,9 +262,13 @@ export async function verifyPackageMcp({
diagnosed.inventory?.tools?.expected
)
throw new Error("packaged production MCP doctor failed");
const scriptsDirectory = dirname(await realpath(cli));
const transport = new StdioClientTransport({
command: cli,
args: ["mcp"],
command: process.execPath,
args: [
join(scriptsDirectory, "plugin-mcp-launcher.mjs"),
PRODUCT_IDENTITY.packageVersion,
],
env: {
...environment,
},
+200 -10
View File
@@ -1,11 +1,12 @@
import { importVerifierSource } from "./lib/verifier-source-loader.mjs";
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { FULL_CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
import { parseTarListing } from "./lib/verify-package-listing.mjs";
import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
import { pluginPackageMetadata } from "./lib/plugin-package.mjs";
const { parseSkillManifestMetadata } = await importVerifierSource(
new URL("../src/domain/skillManifest.ts", import.meta.url).href,
@@ -16,13 +17,108 @@ const { parseSkillManifestMetadata } = await importVerifierSource(
export async function verifyPackagePack({ root, workspace, commands }) {
const packDirectory = join(workspace, "pack");
await mkdir(packDirectory);
const stagingDirectory = join(workspace, "plugin-package");
await mkdir(stagingDirectory);
const [{ files }] = JSON.parse(
(await commands.execNpm(["pack", "--dry-run", "--json"], { cwd: root }))
.stdout,
);
// Reuse npm's file selection, then project metadata in an owned directory.
// Source files and the development lock are never rewritten by packaging.
for (const { path } of files) {
const source = resolve(root, path);
const contained = relative(root, source);
if (
contained === ".." ||
contained.startsWith(`..${sep}`) ||
isAbsolute(contained)
)
throw new Error(`npm selected a path outside the package: ${path}`);
const destination = join(stagingDirectory, contained);
await mkdir(dirname(destination), { recursive: true });
await copyFile(source, destination);
}
const { packageJson, shrinkwrap } = pluginPackageMetadata(
JSON.parse(await readFile(join(root, "package.json"), "utf8")),
JSON.parse(await readFile(join(root, "package-lock.json"), "utf8")),
);
await writeFile(
join(stagingDirectory, "package.json"),
`${JSON.stringify(packageJson, null, 2)}\n`,
);
await writeFile(
join(stagingDirectory, "npm-shrinkwrap.json"),
`${JSON.stringify(shrinkwrap, null, 2)}\n`,
);
// Materialize exactly the locked runtime tree without executing installers.
await commands.execNpm(
["ci", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"],
{ cwd: stagingDirectory },
);
if (packageJson.dependencies?.["@lydell/node-pty"] !== undefined) {
// npm ci filters foreign-platform optional packages. Include the exact locked
// PTY archives as well, without re-resolving the installed dependency tree.
const ptyManifest = JSON.parse(
await readFile(
join(stagingDirectory, "node_modules/@lydell/node-pty/package.json"),
"utf8",
),
);
const platformDirectory = join(workspace, "plugin-platform-packages");
await mkdir(platformDirectory);
for (const dependency of Object.keys(ptyManifest.optionalDependencies)) {
const destination = join(stagingDirectory, "node_modules", dependency);
if (
await access(destination).then(
() => true,
(cause) => {
if (cause.code !== "ENOENT") throw cause;
return false;
},
)
)
continue;
const locked = shrinkwrap.packages[`node_modules/${dependency}`];
if (locked?.version === undefined || locked.integrity === undefined)
throw new Error(
`Plugin platform dependency is not locked: ${dependency}`,
);
const [archive] = JSON.parse(
(
await commands.execNpm(
[
"pack",
"--ignore-scripts",
"--json",
"--pack-destination",
platformDirectory,
`${dependency}@${locked.version}`,
],
{ cwd: platformDirectory },
)
).stdout,
);
if (archive.integrity !== locked.integrity)
throw new Error(
`Plugin platform archive differs from the lock: ${dependency}`,
);
await mkdir(destination, { recursive: true });
await commands.exec("tar", [
"-xzf",
join(platformDirectory, archive.filename),
"-C",
destination,
"--strip-components=1",
]);
}
}
const tarball = join(
packDirectory,
(
await commands.execNpm(
["pack", "--silent", "--pack-destination", packDirectory],
{
cwd: root,
cwd: stagingDirectory,
},
)
).stdout.trim(),
@@ -34,6 +130,54 @@ export async function verifyPackagePack({ root, workspace, commands }) {
(await commands.exec("tar", ["-xOf", tarball, "package/package.json"]))
.stdout,
);
if (packedManifest.mcpName !== "io.github.morluto/rea")
throw new Error("package did not retain its MCP Registry ownership marker");
const packedLock = JSON.parse(
(
await commands.exec("tar", [
"-xOf",
tarball,
"package/npm-shrinkwrap.json",
])
).stdout,
);
if (
packedManifest.overrides !== undefined ||
packedManifest.devDependencies !== undefined ||
![2, 3].includes(packedLock.lockfileVersion) ||
packedLock.version !== packedManifest.version ||
packedLock.packages?.[""].version !== packedManifest.version ||
JSON.stringify(packedLock.packages?.[""].dependencies) !==
JSON.stringify(packedManifest.dependencies) ||
JSON.stringify(packedLock.packages?.[""].optionalDependencies) !==
JSON.stringify(packedManifest.optionalDependencies)
)
throw new Error(
"package is not installable under the Claude plugin dependency policy",
);
for (const dependency of Object.keys(packedManifest.dependencies))
if (
!packedFiles.includes(`package/node_modules/${dependency}/package.json`)
)
throw new Error(
`Plugin runtime dependency is not bundled: ${dependency}`,
);
const pty = JSON.parse(
(
await commands.exec("tar", [
"-xOf",
tarball,
"package/node_modules/@lydell/node-pty/package.json",
])
).stdout,
);
for (const dependency of Object.keys(pty.optionalDependencies))
if (
!packedFiles.includes(`package/node_modules/${dependency}/package.json`)
)
throw new Error(
`Plugin PTY platform dependency is missing: ${dependency}`,
);
if (
packedManifest.scripts?.postinstall !== undefined ||
packedManifest.dependencies?.["node-pty"] !== undefined ||
@@ -45,14 +189,16 @@ export async function verifyPackagePack({ root, workspace, commands }) {
packedManifest.bin?.["rea-agents"] !== "scripts/rea.mjs"
)
throw new Error("package did not expose both rea command entry points");
if (packedManifest.mcpName !== "io.github.morluto/rea")
throw new Error("package did not retain its MCP Registry ownership marker");
if (!packedFiles.includes("package/scripts/lib/cli-process-environment.mjs"))
throw new Error("package omitted the CLI process environment owner");
if (packedFiles.some((path) => path.startsWith("package/.agents/")))
throw new Error(
"package included authored skill sources instead of only the generated bundle",
);
if (packedFiles.some((path) => path.startsWith("package/src/")))
throw new Error(
"Packaged runtime must not masquerade as a source checkout",
);
if (packedFiles.some((path) => path.startsWith("package/.cache/")))
throw new Error("package included development cache or test metadata");
const skill = (
@@ -71,13 +217,57 @@ export async function verifyPackagePack({ root, workspace, commands }) {
throw new Error(
"packaged skill metadata did not match the catalog shipped in the package",
);
if (
packedFiles.some(
(path) => path.includes("__pycache__") || path.endsWith(".pyc"),
for (const path of [
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
]) {
const manifest = JSON.parse(
(await commands.exec("tar", ["-xOf", tarball, `package/${path}`])).stdout,
);
const serverConfig = path.startsWith(".codex")
? JSON.parse(
(
await commands.exec("tar", [
"-xOf",
tarball,
"package/.codex-plugin/mcp.json",
])
).stdout,
)
: manifest;
const expectedArgs = [
`${path.startsWith(".claude") ? "${CLAUDE_PLUGIN_ROOT}/" : "./"}scripts/plugin-mcp-launcher.mjs`,
packedManifest.version,
];
if (
manifest.version !== packedManifest.version ||
manifest.skills !== "./skills" ||
(path.startsWith(".codex")
? manifest.mcpServers !== "./.codex-plugin/mcp.json" ||
serverConfig.mcpServers?.rea?.cwd !== "." ||
serverConfig.mcpServers?.rea?.startup_timeout_sec !== 30
: false) ||
serverConfig.mcpServers?.rea?.command !== "node" ||
JSON.stringify(serverConfig.mcpServers?.rea?.args) !==
JSON.stringify(expectedArgs)
)
) {
throw new Error("package contained generated Python bytecode");
throw new Error(
`Packaged plugin ${path} does not match the exact runtime and canonical skill release`,
);
}
if (packedFiles.includes("package/.mcp.json"))
throw new Error("Plugin metadata must not register a project MCP server");
if (!packedFiles.includes("package/.codex-plugin/mcp.json"))
throw new Error("Packaged Codex MCP server declaration is missing");
if (!packedFiles.includes("package/scripts/plugin-mcp-launcher.mjs"))
throw new Error("Packaged plugin launcher is missing");
if (
skill.includes(`${packedManifest.name}@latest`) ||
!skill.includes(`${packedManifest.name}@${packedManifest.version}`)
)
throw new Error(
"Packaged plugin skill recovery is not pinned to the shipped runtime",
);
await verifyPackedBridge({ tarball, packedFiles, commands });
return { tarball };
}
+8 -2
View File
@@ -220,10 +220,15 @@ await withVerifierCleanup(async () => {
root,
}),
);
await runStage("MCP tools and evidence", () =>
const extractedDirectory = join(workspace, "extracted-plugin");
await mkdir(extractedDirectory);
await commands.exec("tar", ["-xzf", tarball, "-C", extractedDirectory]);
// Codex extracts npm plugins without installing dependencies. Run the real
// catalog/Evidence workflow from that isolated tree so npm cannot repair it.
await runStage("extracted plugin MCP tools and evidence", () =>
verifyPackageMcp({
commands,
cli,
cli: join(extractedDirectory, "package", "scripts", "rea.mjs"),
environment: environmentData.environment,
evidenceRoot,
artifactArchive,
@@ -258,6 +263,7 @@ await withVerifierCleanup(async () => {
promptCompletion: true,
promptCompletionLifecycle: true,
evidenceMcp: true,
extractedPluginMcp: true,
targetFree: true,
targetLifecycle: true,
boundedRegexBridge: true,
+7
View File
@@ -5,6 +5,13 @@ const packedCutterBridge = "package/bridge/cutter/rea_bridge.py";
/** Check shipped bridge files; real-provider lanes verify runtime behavior. */
export async function verifyPackedBridge({ tarball, packedFiles, commands }) {
if (
packedFiles.some(
(path) => path.includes("__pycache__") || path.endsWith(".pyc"),
)
) {
throw new Error("package contained generated Python bytecode");
}
for (const path of [
packedHopperBridge,
packedCutterBridge,