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:
authored and GitHub committed 2026-10-27 17:13:17 +00:00
1 parent 269630f4e1
commit 0cd7ef456b
29 files changed
+1591 -132

No files matched your search

+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(", ")}`,