mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
feat(runner): qualify native Codex platforms and cache hosted builds
Extract the tested runtime and installed-package prerequisite without changing agent creation defaults or experimental gates. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7d9b93aee2
commit
ddcd746eb6
40 files changed
+3474
-565
No files matched your search
@@ -1,21 +1,33 @@
|
||||
/** Optional published-install lane. No production hook or admission override. */
|
||||
import { constants } from "node:fs";
|
||||
import { lstat, open, realpath } from "node:fs/promises";
|
||||
import { lstat, mkdir, mkdtemp, open, realpath, rm } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { findPackageJSON } from "node:module";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { CREDENTIAL_NAMES, type MatrixExecution } from "./types.js";
|
||||
|
||||
export const installedCliKeys = ["PAPERCLIP_RUNNER_E2E_INSTALLED_CLI", "PAPERCLIP_RUNNER_E2E_INSTALLED_CLI_SHA256", "PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_ROOT", "PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_SHA256"] as const;
|
||||
const overrideKeys = ["PAPERCLIP_RUNNER_BINARY", "PAPERCLIP_RUNNER_REMOTE_BINARY_PATH", "PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH", "PAPERCLIP_RUNNER_ACPX_QUALIFICATION", "PAPERCLIP_ACPX_BUILTIN_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"] as const;
|
||||
const suites = new Set(["extended-harnesses", "pi-native", "pi-controls", "rich-acp-warm-continuity"]);
|
||||
// Reuse the production-onboarding, independent file/revision, and restart
|
||||
// journeys. Installed proof does not add cases or relax their existing graders.
|
||||
const installedCodexCells = new Set([
|
||||
"first-task.runner-codex.local.task-card-accept",
|
||||
"everyday-workflows.runner-codex.local.build-revise",
|
||||
"agent-chat.runner-codex.local.continuity-restart",
|
||||
]);
|
||||
export function usesInstalledCli(env: NodeJS.ProcessEnv): boolean {
|
||||
return installedCliKeys.some(key => env[key] !== undefined);
|
||||
}
|
||||
export function assertInstalledCliSelection(env: NodeJS.ProcessEnv, executions: readonly MatrixExecution[]): void {
|
||||
if (!usesInstalledCli(env)) return;
|
||||
if (!installedCliKeys.every(key => Boolean(env[key])) || executions.length === 0 || executions.some(e => e.profile.id !== "runner-acpx-pi" || e.profile.qualificationCandidate !== "pi" || !e.suite.manualOnly || !suites.has(e.suite.id))) throw new Error("Installed CLI proof requires complete pins and explicit supported Pi cases");
|
||||
if (!installedCliKeys.every(key => Boolean(env[key])) || executions.length === 0 || executions.some(e => !(
|
||||
(e.profile.id === "runner-acpx-pi" && e.profile.qualificationCandidate === "pi" && e.suite.manualOnly && suites.has(e.suite.id)) ||
|
||||
(e.profile.id === "runner-codex" && e.profile.provider === "codex" && e.environment.id === "local" && installedCodexCells.has(e.id))
|
||||
))) throw new Error("Installed CLI proof requires complete pins and explicit supported Pi or Codex cases");
|
||||
for (const key of overrideKeys) if (env[key] !== undefined) throw new Error(`Installed CLI proof forbids ${key}`);
|
||||
if (env.NODE_OPTIONS !== undefined || env.NODE_PATH !== undefined) throw new Error("Installed CLI proof forbids ambient Node injection");
|
||||
}
|
||||
@@ -51,6 +63,115 @@ export async function verifyInstalledCli(env: NodeJS.ProcessEnv, executions: rea
|
||||
return { schema: "paperclip.e2e.installed-cli-admission/v1", entry, cliRoot, cliSha256, serverRoot, serverEntry, serverSha256, version: cli.version, defaultRuntimeResolution: true, qualificationOverride: false };
|
||||
}
|
||||
|
||||
/** Clean-install protocol proof only: no credentials, task, or provider turn. */
|
||||
export async function probeInstalledCodexStartup(input: {
|
||||
consumerRoot: string;
|
||||
serverRoot: string;
|
||||
expectedRunnerSha256: string;
|
||||
timeoutMs?: number;
|
||||
}) {
|
||||
for (const key of [...overrideKeys, "NODE_OPTIONS", "NODE_PATH"]) {
|
||||
if (process.env[key] !== undefined) throw new Error(`Installed Codex startup forbids ${key}`);
|
||||
}
|
||||
const inside = (root: string, filename: string) => {
|
||||
const name = relative(root, filename);
|
||||
return name !== "" && name !== ".." && !name.startsWith(`..${sep}`) && !isAbsolute(name);
|
||||
};
|
||||
for (const filename of [input.consumerRoot, input.serverRoot]) {
|
||||
if (!isAbsolute(filename) || resolve(filename) !== filename || await realpath(filename) !== filename) throw new Error("Installed Codex startup paths must be canonical and unlinked");
|
||||
}
|
||||
if (!inside(join(input.consumerRoot, "node_modules"), input.serverRoot) || !/^[a-f0-9]{64}$/.test(input.expectedRunnerSha256)) throw new Error("Installed Codex startup requires a contained server and reviewed daemon digest");
|
||||
const manifest = JSON.parse((await readOwned(join(input.serverRoot, "package.json"), 65536)).toString());
|
||||
if (manifest.name !== "@paperclipai/server" || manifest.exports?.["."]?.import !== "./dist/index.js") throw new Error("Installed Codex startup requires the published server");
|
||||
const vendorRoot = join(input.serverRoot, "dist/vendor/paperclip-runner");
|
||||
const entries = [join(vendorRoot, "index.js"), join(input.serverRoot, "dist/services/native-runtime/native-codex-runner.js")];
|
||||
for (const filename of entries) if (await realpath(filename) !== filename) throw new Error("Installed Codex startup runtime escapes its package");
|
||||
const [runtime, production] = await Promise.all(entries.map(filename => import(pathToFileURL(filename).href)));
|
||||
const runnerBinary = await realpath(production.resolvePaperclipRunnerBinary());
|
||||
if (runnerBinary !== join(vendorRoot, "bin", `${process.platform}-${process.arch}`, "paperclip-runnerd")) throw new Error("Installed Codex startup requires the normal platform daemon; no source or flat fallback");
|
||||
const digest = (bytes: Buffer) => createHash("sha256").update(bytes).digest("hex");
|
||||
const runnerSha256 = digest(await readOwned(runnerBinary, 512 * 1024 * 1024));
|
||||
if (runnerSha256 !== input.expectedRunnerSha256) throw new Error("Installed Codex startup daemon differs from its reviewed pin");
|
||||
const directory = await realpath(await mkdtemp(join(tmpdir(), "pc-codex-startup-")));
|
||||
const home = join(directory, "home"), codexHome = join(directory, "codex-home"), workspace = join(directory, "workspace");
|
||||
for (const filename of [home, codexHome, workspace]) await mkdir(filename, { mode: 0o700 });
|
||||
const environment = { PATH: `${dirname(process.execPath)}:/usr/bin:/bin:/usr/sbin`, HOME: home, CODEX_HOME: codexHome,
|
||||
TMPDIR: directory, LC_ALL: "C", TZ: "UTC", PAPERCLIP_RUNNER_NETWORK_ACCESS: "disabled" };
|
||||
let session: any;
|
||||
let bundle: any;
|
||||
let cleanupConfirmed = false;
|
||||
let startupFailed = false;
|
||||
let startupFailure: unknown;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(new Error("Installed Codex startup timed out")), Math.max(1, Math.min(input.timeoutMs ?? 45000, 120000)));
|
||||
try {
|
||||
const codexCommand = await realpath(runtime.resolveCodexCommand(undefined, environment, workspace));
|
||||
if (!inside(join(input.consumerRoot, "node_modules"), codexCommand)) throw new Error("Installed Codex startup resolved an ambient Codex executable");
|
||||
const codexSha256 = digest(await readOwned(codexCommand, 512 * 1024 * 1024));
|
||||
const codexVersion = execFileSync(codexCommand, ["--version"], { cwd: workspace, env: environment, encoding: "utf8", timeout: 10000, maxBuffer: 16384 }).trim();
|
||||
if (!codexVersion || codexVersion.length > 200) throw new Error("Installed Codex startup returned no bounded version identity");
|
||||
// Keep the production security arguments; pin file-only auth for this
|
||||
// credential-free fixture so macOS keychain accounts cannot be consulted.
|
||||
const args = runtime.createIsolatedCodexAppServerArgs(environment);
|
||||
args.splice(args.indexOf("app-server"), 0, "-c", 'cli_auth_credentials_store="file"');
|
||||
bundle = runtime.createRunnerdCodexTransport({ provider: "codex", stateDirectory: directory, environment,
|
||||
sourceCodexHome: "", runnerBinary, codexCommand, codexArgs: args, closeGraceMs: 10000,
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } });
|
||||
const driver = new runtime.CodexAppServerDriver({
|
||||
taskEnvelope: { schema: "paperclip.skillless_task.v1", objective: "Credential-free installed startup proof.",
|
||||
completionContract: { revision: "installed-startup-v1", criteria: [] }, constraints: [], expectedResultSchema: "paperclip.run_result.v1" },
|
||||
model: "gpt-5.6-sol", conversationMode: "direct", requestedCollaborationMode: "plan", approvalPolicy: "never",
|
||||
includeCollaborationModeInstructions: false, dynamicTools: [], environment,
|
||||
transportFactory: () => bundle.transport, requireProviderSessionIdentity: true,
|
||||
});
|
||||
session = await driver.openSession({ runId: "installed-startup", normalizedSessionId: "installed-startup", workingDirectory: workspace, signal: controller.signal });
|
||||
const event = await session.events()[Symbol.asyncIterator]().next();
|
||||
const identity = session.ids();
|
||||
const evidence = bundle.evidence();
|
||||
if (event.value?.eventType !== "session.started" || event.value.payload?.context?.model !== "gpt-5.6-sol" ||
|
||||
event.value.payload?.context?.sandbox?.permissionProfile?.id !== "paperclip-runner-workspace-read-only" ||
|
||||
!identity.driverSessionId || !identity.providerSessionId || evidence.providerDriver !== "codex_app_server" ||
|
||||
!(evidence.runnerPid > 0) || !(evidence.codexPid > 0)) throw new Error("Installed Codex startup returned no matching native identity or read-only model context");
|
||||
await session.close({ reason: "credential-free installed startup complete" });
|
||||
session = undefined;
|
||||
if (!bundle.evidence().runnerExited) throw new Error("Installed Codex startup cleanup is incomplete");
|
||||
cleanupConfirmed = true;
|
||||
return { schema: "paperclip.e2e.installed-codex-startup/v1", platform: `${process.platform}-${process.arch}`,
|
||||
runnerSha256, codexSha256, codexVersion, providerDriver: "codex_app_server", model: "gpt-5.6-sol",
|
||||
nativeSessionStarted: true, cleanupConfirmed: true, providerTurns: 0, credentialsUsed: false,
|
||||
defaultRuntimeResolution: true, reasoningEffortExercised: false };
|
||||
} catch (error) {
|
||||
startupFailed = true;
|
||||
startupFailure = error;
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
const cleanupFailures: unknown[] = [];
|
||||
try {
|
||||
if (session) await session.close({ reason: "installed startup cleanup" });
|
||||
else if (bundle && !cleanupConfirmed) await bundle.transport.close("installed startup cleanup");
|
||||
} catch (error) { cleanupFailures.push(error); }
|
||||
try {
|
||||
if (!bundle || bundle.evidence().runnerExited) await rm(directory, { recursive: true, force: true });
|
||||
} catch (error) { cleanupFailures.push(error); }
|
||||
if (startupFailed) {
|
||||
const evidence = bundle?.evidence();
|
||||
// This zero-turn fixture has empty homes and no provider credentials.
|
||||
// Report only bounded failure text and lifecycle state, never identities
|
||||
// or raw durable state. A close failure cannot hide the startup cause.
|
||||
const bounded = (value: unknown) => String(value).replaceAll(directory, "[private-startup]").slice(0, 2000);
|
||||
const primaryMessage = bounded(startupFailure instanceof Error ? startupFailure.message : startupFailure);
|
||||
const diagnostic = { schema: "paperclip.e2e.installed-codex-startup-failure/v1", platform: `${process.platform}-${process.arch}`,
|
||||
runnerExited: evidence?.runnerExited ?? null, runnerExitCode: evidence?.runnerExitCode ?? null,
|
||||
runnerSignal: evidence?.runnerSignal ?? null,
|
||||
diagnostics: (Array.isArray(evidence?.diagnostics) ? evidence.diagnostics : []).slice(-16).map(bounded),
|
||||
cleanupFailures: cleanupFailures.map(error => bounded(error instanceof Error ? error.message : error)) };
|
||||
throw new AggregateError([startupFailure, ...cleanupFailures], `Installed Codex startup failed: ${primaryMessage}\n${JSON.stringify(diagnostic)}`, { cause: startupFailure });
|
||||
}
|
||||
if (cleanupFailures.length) throw new AggregateError(cleanupFailures, "Installed Codex startup cleanup failed", { cause: cleanupFailures[0] });
|
||||
}
|
||||
}
|
||||
|
||||
/** This prep lane cannot create agents or read the normal local credential file. */
|
||||
export function assertInstalledStartupOnly(env: NodeJS.ProcessEnv, executions: readonly MatrixExecution[], options: { ids: string[]; maxParallel: number; maxAutomaticRetries: number; headed: boolean; ui: boolean; debug: boolean; list: boolean; matrixJson: boolean }) {
|
||||
assertInstalledCliSelection(env, executions);
|
||||
|
||||
Reference in new issue
Block a user