Files
PaperClipAI/scripts/check-docker-deps-stage.mjs
T
Harold KimandPaperclip 98df214917 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>
2026-07-09 10:08:57 +00:00

177 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
import { existsSync, readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import process from "node:process";
const repoRoot = process.cwd();
const dockerfilePath = path.join(repoRoot, "Dockerfile");
const workspacePath = path.join(repoRoot, "pnpm-workspace.yaml");
function extractDepsStage(dockerfileText) {
const lines = dockerfileText.split("\n");
const captured = [];
let inDeps = false;
for (const line of lines) {
if (!inDeps) {
if (/^FROM .* AS deps$/i.test(line.trim())) inDeps = true;
continue;
}
if (/^FROM /i.test(line.trim())) break;
captured.push(line);
}
return captured.join("\n");
}
function parseWorkspaceRoots(workspaceText) {
return workspaceText
.split("\n")
.map((line) => line.match(/^\s*-\s+(.+)\s*$/)?.[1]?.trim() ?? null)
.map((entry) => {
if (!entry) return entry;
return entry.replace(/^(['"])(.*)\1$/, "$2");
})
.filter(Boolean)
.filter((entry) => !entry.startsWith("!"))
.map((entry) => entry.replace(/\*+$/, ""))
.filter((entry) => entry.length > 0)
.filter((entry) => !entry.includes("examples"))
.filter((entry) => !entry.includes("create-paperclip-plugin"));
}
function walkPackageJsonFiles(rootRelative, maxDepth) {
const results = [];
const rootAbsolute = path.join(repoRoot, rootRelative);
if (!existsSync(rootAbsolute)) return results;
function visit(currentAbsolute, depthFromRoot) {
const entries = readdirSync(currentAbsolute, { withFileTypes: true });
for (const entry of entries) {
if (entry.name === "node_modules") continue;
const absolute = path.join(currentAbsolute, entry.name);
const relative = path.relative(repoRoot, absolute).split(path.sep).join("/");
if (entry.isDirectory()) {
if (depthFromRoot < maxDepth) visit(absolute, depthFromRoot + 1);
continue;
}
if (
entry.name === "package.json" &&
!relative.includes("/examples/") &&
!relative.includes("/create-paperclip-plugin/")
) {
results.push(relative);
}
}
}
visit(rootAbsolute, 0);
return results;
}
function globToRegExp(pattern) {
const normalized = pattern.replace("/./", "/");
let regex = "";
for (let index = 0; index < normalized.length; index += 1) {
const char = normalized[index];
const next = normalized[index + 1];
if (char === "*" && next === "*") {
regex += ".*";
index += 1;
continue;
}
if (char === "*") {
regex += "[^/]*";
continue;
}
if (char === "?") {
regex += "[^/]";
continue;
}
regex += /[|\\{}()[\]^$+?.]/.test(char) ? `\\${char}` : char;
}
return new RegExp(`^${regex}$`);
}
function parseCopySources(depsStage) {
const sources = [];
for (const rawLine of depsStage.split("\n")) {
const line = rawLine.trim();
if (!line.startsWith("COPY ")) continue;
const tokens = line.split(/\s+/);
let index = 1;
while (tokens[index]?.startsWith("--")) index += 1;
const args = tokens.slice(index);
if (args.length < 2) continue;
const lineSources = args.slice(0, -1);
for (const source of lineSources) {
sources.push(source);
}
}
return sources;
}
function main() {
const depsStage = extractDepsStage(readFileSync(dockerfilePath, "utf8"));
if (!depsStage.trim()) {
console.error("Could not extract deps stage from Dockerfile (expected 'FROM ... AS deps').");
process.exit(1);
}
const workspaceRoots = parseWorkspaceRoots(readFileSync(workspacePath, "utf8"));
if (workspaceRoots.length === 0) {
console.error("Could not derive workspace roots from pnpm-workspace.yaml.");
process.exit(1);
}
const requiredPackageJsons = [...new Set(
workspaceRoots.flatMap((root) => walkPackageJsonFiles(root, 2)),
)].sort();
const copySources = parseCopySources(depsStage);
const copyMatchers = copySources.map((source) => ({
source,
regex: globToRegExp(source),
}));
let missing = 0;
for (const pkg of requiredPackageJsons) {
const covered = copyMatchers.some(({ regex }) => regex.test(pkg));
if (!covered) {
console.error(`Dockerfile deps stage missing package manifest coverage for: ${pkg}`);
missing = 1;
}
}
if (existsSync(path.join(repoRoot, "patches"))) {
const patchesCovered = copySources.includes("patches/");
if (!patchesCovered) {
console.error("Dockerfile deps stage missing: COPY patches/ patches/");
missing = 1;
}
}
if (missing) {
console.error("Dockerfile deps stage is out of sync. Update it to cover the missing files.");
process.exit(1);
}
console.log("PASS");
}
main();