mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
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:
@@ -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;
|
||||
};
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(", ")}`,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user