mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
fix: fail closed on unusable Codex auth during sandbox auth merge
When reusing a Codex sandbox, the workspace-restore step now performs an auth-merge check before adopting the sandbox's auth.json. The merge fails closed when: - host or sandbox Codex auth is unusable (missing token or account) - the subscription payload shapes differ between host and sandbox - subscription account identities do not match Additional hardening: node absence on PATH during the embedded decision script now exits non-zero instead of silently falling through. Co-change comments link parseAuth (the embedded script) to hasUsableAuthPayload (codex-home.ts) so a future auth-format change cannot silently diverge between the two implementations. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
98df214917
3422 files changed
+1614966
No files matched your search
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Dev-only: link the in-repo @paperclipai/plugin-sdk into plugin packages that
|
||||
// are excluded from the pnpm workspace (see pnpm-workspace.yaml). Workspace
|
||||
// members get their SDK link from pnpm automatically; excluded packages do not.
|
||||
//
|
||||
// Invoked from the root postinstall. Intentionally NOT referenced from any
|
||||
// plugin's own package.json so the published tarballs cannot carry a lifecycle
|
||||
// script that escapes their package directory at install time.
|
||||
|
||||
import { existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(__dirname, "..");
|
||||
const sdkDir = join(repoRoot, "packages", "plugins", "sdk");
|
||||
|
||||
// Plugin packages excluded from the workspace that still need @paperclipai/plugin-sdk
|
||||
// linked in for local dev. Keep in sync with pnpm-workspace.yaml exclusions.
|
||||
function excludedPluginDirs() {
|
||||
return [
|
||||
...readPluginsUnder(join(repoRoot, "packages", "plugins", "sandbox-providers")),
|
||||
join(repoRoot, "packages", "plugins", "examples", "plugin-orchestration-smoke-example"),
|
||||
];
|
||||
}
|
||||
|
||||
export function linkExcludedPlugins() {
|
||||
let linked = 0;
|
||||
let skipped = 0;
|
||||
for (const packageDir of excludedPluginDirs()) {
|
||||
if (!existsSync(join(packageDir, "package.json"))) continue;
|
||||
if (linkSdkInto(packageDir)) {
|
||||
linked += 1;
|
||||
} else {
|
||||
skipped += 1;
|
||||
}
|
||||
}
|
||||
return { linked, skipped };
|
||||
}
|
||||
|
||||
// Run as a CLI (root postinstall) when invoked directly, but stay importable so
|
||||
// repo-internal scripts (e.g. the standalone package builder) can relink a
|
||||
// single package after a fresh install.
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const { linked, skipped } = linkExcludedPlugins();
|
||||
console.log(` ✓ Linked @paperclipai/plugin-sdk into ${linked} excluded plugin(s) (skipped ${skipped})`);
|
||||
}
|
||||
|
||||
// Recursively collect package directories (those containing a package.json)
|
||||
// under parentDir. The matching pnpm-workspace.yaml exclusion uses a recursive
|
||||
// glob (e.g. "!packages/plugins/sandbox-providers/**"), so a provider nested
|
||||
// deeper than one level must still be discovered here.
|
||||
export function readPluginsUnder(parentDir) {
|
||||
if (!existsSync(parentDir)) return [];
|
||||
const found = [];
|
||||
for (const entry of readdirSync(parentDir, { withFileTypes: true })) {
|
||||
// Skip symlinked directories so discovery can't be steered outside the
|
||||
// intended plugin subtree, and skip node_modules.
|
||||
if (!entry.isDirectory() || entry.isSymbolicLink() || entry.name === "node_modules") continue;
|
||||
const childDir = join(parentDir, entry.name);
|
||||
if (existsSync(join(childDir, "package.json"))) {
|
||||
found.push(childDir);
|
||||
} else {
|
||||
found.push(...readPluginsUnder(childDir));
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export function linkSdkInto(packageDir) {
|
||||
const scopeDir = join(packageDir, "node_modules", "@paperclipai");
|
||||
const linkTarget = join(scopeDir, "plugin-sdk");
|
||||
const relativeSdkDir = relative(scopeDir, sdkDir);
|
||||
|
||||
mkdirSync(scopeDir, { recursive: true });
|
||||
|
||||
try {
|
||||
const stat = lstatSync(linkTarget);
|
||||
if (stat.isSymbolicLink()) {
|
||||
if (readlinkSync(linkTarget) === relativeSdkDir) {
|
||||
// Already linked to the in-repo SDK; nothing to do.
|
||||
return false;
|
||||
}
|
||||
rmSync(linkTarget, { force: true });
|
||||
} else {
|
||||
// A real install has already populated @paperclipai/plugin-sdk (e.g. the
|
||||
// plugin host did `npm install` of the published tarball). Leave it.
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
// A missing target is expected (nothing linked yet); surface anything else.
|
||||
if (error?.code !== "ENOENT") throw error;
|
||||
}
|
||||
|
||||
symlinkSync(relativeSdkDir, linkTarget, "dir");
|
||||
return true;
|
||||
}
|
||||
Reference in new issue
Block a user