mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-15 12:28:10 +02:00
155 lines
14 KiB
TypeScript
155 lines
14 KiB
TypeScript
/** Optional published-install lane. No production hook or admission override. */
|
|
import { constants } from "node:fs";
|
|
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, 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)) ||
|
|
(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");
|
|
}
|
|
async function readOwned(path: string, maximum: number): Promise<Buffer> {
|
|
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
try {
|
|
const before = await handle.stat({ bigint: true });
|
|
if (!before.isFile() || before.size < 1n || before.size > BigInt(maximum)) throw new Error("Installed CLI proof requires a bounded regular file");
|
|
const bytes = await handle.readFile(); const after = await handle.stat({ bigint: true }); const named = await lstat(path, { bigint: true });
|
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink() || bytes.length !== Number(before.size)) throw new Error("Installed CLI proof file changed during admission");
|
|
return bytes;
|
|
} finally { await handle.close(); }
|
|
}
|
|
export async function verifyInstalledCli(env: NodeJS.ProcessEnv, executions: readonly MatrixExecution[]) {
|
|
if (!usesInstalledCli(env)) return undefined;
|
|
assertInstalledCliSelection(env, executions);
|
|
const entry = env.PAPERCLIP_RUNNER_E2E_INSTALLED_CLI!;
|
|
const serverRoot = env.PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_ROOT!;
|
|
for (const path of [entry, serverRoot]) if (!isAbsolute(path) || resolve(path) !== path || await realpath(path) !== path) throw new Error("Installed CLI proof paths must be canonical and unlinked");
|
|
if (!entry.endsWith("/dist/index.js")) throw new Error("Installed CLI proof requires published dist/index.js");
|
|
const cliRoot = resolve(dirname(entry), "..");
|
|
const cli = JSON.parse((await readOwned(join(cliRoot, "package.json"), 65536)).toString());
|
|
const manifest = findPackageJSON("@paperclipai/server", pathToFileURL(entry));
|
|
if (!manifest || await realpath(manifest) !== join(serverRoot, "package.json")) throw new Error("Installed CLI resolved a foreign server dependency");
|
|
const server = JSON.parse((await readOwned(join(serverRoot, "package.json"), 65536)).toString());
|
|
if (cli.name !== "paperclipai" || cli.bin?.paperclipai !== "./dist/index.js" || server.name !== "@paperclipai/server" || typeof cli.version !== "string" || cli.version !== server.version || server.exports?.["."]?.import !== "./dist/index.js") throw new Error("Installed CLI/server public package identity differs");
|
|
const serverEntry = join(serverRoot, "dist/index.js");
|
|
if (await realpath(serverEntry) !== serverEntry) throw new Error("Installed server entrypoint escapes its package");
|
|
const digest = (bytes: Buffer) => createHash("sha256").update(bytes).digest("hex");
|
|
const cliSha256 = digest(await readOwned(entry, 64 * 1024 * 1024));
|
|
const serverSha256 = digest(await readOwned(serverEntry, 16 * 1024 * 1024));
|
|
if (cliSha256 !== env.PAPERCLIP_RUNNER_E2E_INSTALLED_CLI_SHA256 || serverSha256 !== env.PAPERCLIP_RUNNER_E2E_INSTALLED_SERVER_SHA256) throw new Error("Installed CLI/server bytes differ from their reviewed pins");
|
|
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;
|
|
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 };
|
|
} finally {
|
|
clearTimeout(timer);
|
|
if (session) await session.close({ reason: "installed startup cleanup" });
|
|
else if (bundle && !cleanupConfirmed) await bundle.transport.close("installed startup cleanup");
|
|
if (!bundle || bundle.evidence().runnerExited) await rm(directory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
/** 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);
|
|
if (!usesInstalledCli(env) || executions.length !== 1 || executions[0]?.id !== "extended-harnesses.runner-acpx-pi.local.hello-complete" || options.ids.length !== 1 || options.ids[0] !== executions[0]?.id || options.maxParallel !== 1 || options.maxAutomaticRetries !== 0 || options.headed || options.ui || options.debug || options.list || options.matrixJson) throw new Error("Installed startup-only requires one explicit local Pi hello, no retries or interactive mode");
|
|
if (CREDENTIAL_NAMES.some(key => env[key] !== undefined) || Object.keys(env).some(key => /^(?:OPENAI|ANTHROPIC|OPENROUTER|DAYTONA|XAI|GROK|CURSOR|COPILOT|GITHUB|GH)(?:_|$)/.test(key) && env[key] !== undefined)) throw new Error("Installed startup-only forbids provider credential inputs");
|
|
}
|