mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
<!-- entire-trail-link-start --> https://entire.com/gh/paperclipai/-/trails/26/changes/paperclip/26 <!-- entire-trail-link-end --> ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agents run on coding harnesses. Each adapter has a model catalog, and sandboxes contain pinned CLIs. > - Upstream providers released new CLI versions and new models. OpenAI retired some Codex models for ChatGPT sign-in. > - A stale catalog hides valid models. A misleading error makes operators upgrade a CLI that is already current. > - This pull request adds verified OpenCode model IDs, names Codex ChatGPT retirements in the environment Test, and refreshes the sandbox CLI pins. > - The benefit is that operators can select current models and get the correct reason when a retired model fails. ## Linked Issues or Issue Description Refs #16708 (adds Claude Haiku 5.5 to `claude_local`; this PR does not duplicate it). Refs #17245 (moves the Codex runtime to 0.163.0; this PR does not change Codex pins). **What happened?** OpenAI retired `gpt-5.5` (October 14, 2026) and `gpt-5.4` / `gpt-5.4-mini` (August 31, 2026) from Codex with ChatGPT sign-in. A saved agent with one of these models and ChatGPT sign-in now fails. The legacy environment Test says that the plan, the rollout, or an old CLI can be the cause. The Paperclip Runner Test gives generic account and runtime advice. The real cause is the retirement. **Expected behavior** Both Tests give the retirement date and the documented replacement. API-key users keep the models. **Proposed change** Record the verified retirements. Add the new verified OpenCode IDs. Refresh the sandbox CLI pins with verified digests. Record the audit. ## What Changed - Codex: add a table of models retired from Codex with ChatGPT sign-in (`gpt-5.5` → `gpt-6.1-sol`, or `gpt-6-luna` on Free and Go; `gpt-5.4` → `gpt-6-sol`; `gpt-5.4-mini` → `gpt-6-luna`). The shared `codexChatGptRetirementHint` builds the hint. The legacy CLI Test (`codexHelloProbeModelRejectionCheck`) and the Paperclip Runner Test (`testNativeRunnerAuthentication`) both use it when the backend rejects one of these models. For other failures, the Paperclip Runner Test hint stays the same. The picker keeps the models for API-key users. - OpenCode: add `anthropic/claude-haiku-5-5` and `google/gemini-4-argon` to the remote fallback catalog. The Paperclip Runner OpenCode picker also uses this catalog. - Evaluation image sandbox layer (`docker/daytona-runner`): | CLI | Before | After | | --- | --- | --- | | Grok CLI | 1.0.46 | 1.0.70 | | Gemini CLI | 0.62.0 | 0.65.0 | | Kimi Code | 2.1.1 | 2.3.3 | | Cursor CLI | 2026.10.01-e373342 | 2026.10.22-27dcf5f | | GitHub CLI | 2.102.0 | 2.103.3 | - Add `doc/adapter-model-audit-2026-10-23.md` with sources, compatibility findings, and deferred items. No model in this PR needs a newer Codex, Claude, or OpenCode runtime than the current pins. Claude Haiku 5.5 needs Claude Code 2.1.293 or later; #16708 records that floor, and this PR does not add it. ## Verification - `vitest run src/index.test.ts src/server/test.remote.test.ts src/server/acp.cli-version.test.ts` in `packages/adapters/codex-local`: 41 passed. - `vitest run src/services/native-runtime/setup-readiness.test.ts` in `server`: 98 passed. These include the two new Paperclip Runner Test cases: a retired model shows the retirement hint, and a model that is not retired keeps the generic hint. The retirement case fails on the earlier code. Run with the host's empty `ANTHROPIC_BASE_URL` unset; see the baseline note in the audit doc. - `vitest run` in `packages/adapters/opencode-local`: 87 passed. - `vitest run src/__tests__/adapter-models.test.ts` in `server`: 27 passed. - `tsc --noEmit` in `packages/adapters/codex-local` and `packages/adapters/opencode-local`: passed. - Cursor archive SHA-256 `85296944e4600a83c4b764f578d1502bf9c416db200d33a593e8f3ecabfdc294` is computed from the versioned Linux x64 tarball. The extracted binary reports `2026.10.22-27dcf5f`. On October 26, 2026, `https://cursor.com/install` still resolves this version. - GitHub CLI SHA-256 `4c806978ffce1cec70853302a56d70ac6da4f19bae7adca4e1a8ab00633dd91d` matches `gh_2.103.3_checksums.txt`. - Grok CLI 1.0.70 and Kimi Code 2.3.3 installed from npm report those versions. The flags that Paperclip uses (`-m`, `--effort`, `grok models`; `-m`, `-p`, `--output-format`, `acp`) are present in `--help`. - `git merge-tree` with #16708 (head `3a06740e9`) and #17245 (head `41f9c611f`): this PR shares no files with either PR. The two PRs can merge in any order. - Not run in the authoring environment: the full `pnpm -r typecheck` and `pnpm build` (no Rust toolchain), the full server `tsc` (memory limit), and the image build. CI runs these. ## Risks - Low risk for catalogs: the IDs are additive suggestions. No saved selection, default, or spending setting changes. - The Paperclip Runner Test changes only the hint text, and only for a ChatGPT rejection of a retired model. Check codes and pass/fail results do not change. - Sandbox CLI pins: - Kimi Code 2.2.0 and 2.3.0 remove the `kaos` MCP executor and require qualified plugin skill names. Paperclip uses neither. Kimi Code 2.3.2 stops automatic swarms in `kimi -p` and ACP sessions. - Gemini CLI 0.65.0 makes workspace settings read-only in untrusted folders. Paperclip writes home settings and sets `GEMINI_CLI_TRUST_WORKSPACE` for remote runs. - xAI publishes no Grok CLI changelog. Grok 1.0.70 behavior changes are verified only against the CLI reference and `--help`. - The pins apply only after a new image is built and promoted. The fleet sandbox image definition must get the same pins. - Rollback: revert this PR. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use (shell, web research, repository search). ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Paperclip <noreply@paperclip.ing>
268 lines
19 KiB
TypeScript
268 lines
19 KiB
TypeScript
import { constants } from "node:fs";
|
|
import { lstat, mkdir, mkdtemp, open, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join, posix } from "node:path";
|
|
import { CODEX_CHATGPT_MODEL_REJECTION_RE, codexChatGptRetirementHint } from "@paperclipai/adapter-codex-local";
|
|
import { copyBackCodexAuth } from "@paperclipai/adapter-codex-local/server";
|
|
import type { AdapterEnvironmentTestContext, AdapterEnvironmentTestResult } from "@paperclipai/adapter-utils";
|
|
import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "@paperclipai/shared";
|
|
import { probeNativeRunnerEnvironment, resolveSourceCodexHome } from "../../vendor/paperclip-runner/index.js";
|
|
import { resolvePaperclipRunnerBinary, resolveRemotePaperclipRunnerBinary } from "./native-codex-runner.js";
|
|
import { readLocalAiCredentialFile } from "../local-ai-credential-file.js";
|
|
import { createNativeSshCommandRunner } from "./native-ssh-command-runner.js";
|
|
import { resolvePaperclipRunnerTransport } from "@paperclipai/adapter-utils/runner-connectivity";
|
|
import { createNativeRunnerExecutionDriver } from "./native-runner-execution-driver.js";
|
|
|
|
|
|
const PROBE_TRANSPORT_ENV_KEYS = ["PATH", "LANG", "LANGUAGE", "TZ", "TMPDIR", "TEMP", "TMP", "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "no_proxy", "all_proxy"];
|
|
|
|
function redactNativeProbeMessage(message: string, environment: Record<string, string>): string {
|
|
const redact = (value: unknown): void => {
|
|
if (typeof value === "string" && value) message = message.replaceAll(value, "[REDACTED]");
|
|
else if (value && typeof value === "object") for (const item of Object.values(value)) redact(item);
|
|
};
|
|
for (const [name, value] of Object.entries(environment)) {
|
|
if (!/key|token|secret|password/i.test(name) || !value) continue;
|
|
redact(value);
|
|
try { redact(JSON.parse(value)); } catch { /* Most credentials are plain strings. */ }
|
|
}
|
|
return message.slice(0, 2000);
|
|
}
|
|
|
|
async function prepareProbeCodexSource(runtimeDirectory: string, sourceCodexHome: string, authJson?: string): Promise<string> {
|
|
if (authJson === undefined) return sourceCodexHome;
|
|
let config: Buffer | undefined;
|
|
const source = await open(join(sourceCodexHome, "config.toml"), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK)
|
|
.catch((error: NodeJS.ErrnoException) => { if (error.code === "ENOENT" || error.code === "ELOOP") return null; throw error; });
|
|
if (source) {
|
|
try {
|
|
const metadata = await source.stat();
|
|
if (metadata.isFile()) {
|
|
const bytes = Buffer.alloc(1024 * 1024 + 1);
|
|
let size = 0;
|
|
while (size < bytes.length) {
|
|
const result = await source.read(bytes, size, bytes.length - size, size);
|
|
if (!result.bytesRead) break;
|
|
size += result.bytesRead;
|
|
}
|
|
if (size > 1024 * 1024) throw new Error("The selected Codex configuration exceeds its size limit.");
|
|
config = bytes.subarray(0, size);
|
|
}
|
|
} finally { await source.close(); }
|
|
}
|
|
const credentialSource = join(runtimeDirectory, "codex-credential-source");
|
|
let created = false;
|
|
try {
|
|
await mkdir(credentialSource, { mode: 0o700 });
|
|
created = true;
|
|
await writeFile(join(credentialSource, "auth.json"), authJson, { mode: 0o600, flag: "wx" });
|
|
if (config) await writeFile(join(credentialSource, "config.toml"), config, { mode: 0o600, flag: "wx" });
|
|
return credentialSource;
|
|
} catch (error) {
|
|
if (created) await rm(credentialSource, { recursive: true, force: true });
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function probePreparedNativeRuntime(runtimeDirectory: string, prepare: () => Promise<Parameters<typeof probeNativeRunnerEnvironment>[0]>) {
|
|
let handedOff = false;
|
|
try {
|
|
const options = await prepare();
|
|
handedOff = true;
|
|
return await probeNativeRunnerEnvironment(options);
|
|
} catch (error) {
|
|
if (!handedOff) {
|
|
try { await rm(runtimeDirectory, { recursive: true, force: true }); }
|
|
catch (cleanupError) {
|
|
const message = error instanceof Error ? error.message : "Native setup preparation failed.";
|
|
throw new AggregateError([error, cleanupError], `${message}; Native setup preparation cleanup is incomplete.`, { cause: error });
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function probeRemoteNativeCodex(options: {
|
|
context: AdapterEnvironmentTestContext;
|
|
model: string | null;
|
|
reasoningEffort?: string;
|
|
environment: Record<string, string>;
|
|
sourceCodexHome: string;
|
|
credentialRefreshAuthPath?: string;
|
|
sourceAuthJson?: string;
|
|
timeoutMs: number;
|
|
}) {
|
|
const target = options.context.executionTarget;
|
|
if (target?.kind !== "remote") throw new Error("Native setup requires the selected remote execution target.");
|
|
const { createRemoteNativeArtifactPreparation, createRemoteRunnerProcessLauncher, stageRemoteRunnerFile, readRemoteRunnerState } = await import("./native-session-executor.js");
|
|
const runner = target.transport === "ssh" ? createNativeSshCommandRunner({ spec: target.spec, defaultCwd: target.remoteCwd }) : target.runner;
|
|
if (!runner) throw new Error("runner_transport_ineligible: remote process runner is unavailable");
|
|
const id = crypto.randomUUID();
|
|
const runnerInstanceId = `setup-${id}`;
|
|
const ingress = target.transport === "sandbox" && target.effectiveCapabilities?.runnerWebSocketIngress === true;
|
|
const outbound = !ingress ? await resolvePaperclipRunnerTransport({ target, runId: id, localConnectUrl: "ws://127.0.0.1/unused",
|
|
runnerPublicUrl: process.env.PAPERCLIP_RUNNER_PUBLIC_URL?.trim() || null,
|
|
runnerCaBundlePath: process.env.PAPERCLIP_RUNNER_CA_BUNDLE_PATH?.trim() || null, runnerIngressAuthorized: true }) : null;
|
|
if (outbound && outbound.mode !== "direct_outbound") throw new Error("runner_transport_ineligible: expected outbound native transport");
|
|
if (outbound?.mode === "direct_outbound" && outbound.caBundlePath) await stat(outbound.caBundlePath).catch(() => { throw new Error("runner_direct_wss_failed: configured runner CA bundle is unavailable"); });
|
|
const remoteRoot = posix.join(target.remoteCwd, ".paperclip-runtime", `paperclip-native-setup-${id}`);
|
|
const remoteBinary = posix.join(remoteRoot, "bin", "paperclip-runnerd");
|
|
const remoteStateDirectory = posix.join(remoteRoot, "runner");
|
|
const remoteFilesystemRoot = posix.join(remoteRoot, "filesystem");
|
|
const remoteWorkspace = posix.join(remoteFilesystemRoot, "workspace");
|
|
const remoteCodexNpmSpec = process.env.PAPERCLIP_RUNNER_REMOTE_CODEX_NPM_SPEC?.trim() || null;
|
|
const remoteCodexBinary = remoteCodexNpmSpec ? posix.join(remoteRoot, "harnesses", "codex", "node_modules", ".bin", "codex") : posix.join(remoteRoot, "bin", "codex");
|
|
const explicitRunner = process.env.PAPERCLIP_RUNNER_REMOTE_BINARY_PATH?.trim() || null;
|
|
const controllerRunnerBinary = await resolveRemotePaperclipRunnerBinary({ runner, remoteCwd: target.remoteCwd, configuredPath: explicitRunner });
|
|
const artifacts = createRemoteNativeArtifactPreparation({ target, runner, remoteBinary, controllerRunnerBinary, remoteRuntimeRoot: remoteRoot, remoteCodexBinary,
|
|
targetPlatformArtifactVerified: true,
|
|
runnerRemoteBinaryPath: explicitRunner, runnerRemoteCodexPath: process.env.PAPERCLIP_RUNNER_REMOTE_CODEX_PATH?.trim(), runnerRemoteCodexNpmSpec: remoteCodexNpmSpec, model: options.model });
|
|
const requireSuccess = (result: { timedOut: boolean; exitCode: number | null }, message: string) => {
|
|
if (result.timedOut || result.exitCode !== 0) throw new Error(message);
|
|
};
|
|
const runtimeDirectory = await realpath(await mkdtemp(join(tmpdir(), "paperclip-native-setup-")));
|
|
return probePreparedNativeRuntime(runtimeDirectory, async () => {
|
|
const sourceCodexHome = await prepareProbeCodexSource(runtimeDirectory, options.sourceCodexHome, options.sourceAuthJson);
|
|
let remoteDirectoryCreated = false;
|
|
let signalSpawned!: () => void;
|
|
const spawned = new Promise<void>(resolve => { signalSpawned = resolve; });
|
|
return { runtimeDirectory: runtimeDirectory, provider: "codex", model: options.model, reasoningEffort: options.reasoningEffort,
|
|
workingDirectory: remoteWorkspace, environment: options.environment, timeoutMs: options.timeoutMs,
|
|
transportOptions: {
|
|
externallySandboxed: target.transport === "sandbox",
|
|
runnerBinary: controllerRunnerBinary, codexCommand: remoteCodexBinary, sourceCodexHome,
|
|
runnerFilesystemRoot: remoteFilesystemRoot,
|
|
prpIdentity: { runnerInstanceId, runId: id, environmentLeaseId: `setup-${id}`, normalizedSessionId: `setup-${id}`, turnId: `turn-${id}`, itemId: `item-${id}` },
|
|
...createNativeRunnerExecutionDriver({
|
|
target, binding: { companyId: options.context.companyId, runId: id },
|
|
runnerIngressAuthorized: true,
|
|
runnerPublicUrl: process.env.PAPERCLIP_RUNNER_PUBLIC_URL?.trim() || null,
|
|
runnerCaBundlePath: process.env.PAPERCLIP_RUNNER_CA_BUNDLE_PATH?.trim() || null,
|
|
processStarted: spawned,
|
|
prepareRemote: async mode => {
|
|
const created = await runner.execute({ command: "sh", args: ["-c", 'set -eu; umask 077; mkdir -p -- "$1"; test -d "$1" && test ! -L "$1"; mkdir -- "$2"; install -d -m 0700 "$3" "$4" "$5"',
|
|
"paperclip-native-setup", posix.dirname(remoteRoot), remoteRoot, remoteFilesystemRoot, remoteWorkspace, posix.join(remoteFilesystemRoot, "probe-home")], cwd: target.remoteCwd, bypassSession: true, timeoutMs: 10_000 });
|
|
requireSuccess(created, "Native setup could not claim a private runtime directory.");
|
|
remoteDirectoryCreated = true;
|
|
await artifacts.prepare(mode);
|
|
},
|
|
mapCaBundle: async sourcePath => {
|
|
const targetPath = posix.join(remoteRoot, "bin", "runner-ca-bundle.pem");
|
|
await stageRemoteRunnerFile({ target, runner, sourcePath, targetPath, mode: 0o600 });
|
|
return targetPath;
|
|
},
|
|
process: {
|
|
runnerStateDirectory: remoteStateDirectory,
|
|
readRunnerState: () => readRemoteRunnerState({ runner, stateDirectory: remoteStateDirectory }),
|
|
runnerProcessLauncher: createRemoteRunnerProcessLauncher({ target, runner, remoteBinary, stateDirectory: remoteStateDirectory,
|
|
onRunnerProcessSpawned: signalSpawned, processIdentityPath: posix.join(remoteStateDirectory, "runner-process.identity"), diagnosticsDirectory: posix.join(remoteRoot, "diagnostics"), runnerInstanceId,
|
|
ensureArtifact: async () => {
|
|
for (const name of ["auth.json", "config.toml"] as const) {
|
|
const sourcePath = join(runtimeDirectory, "codex-home", name);
|
|
try { await stat(sourcePath); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") continue; throw error; }
|
|
await stageRemoteRunnerFile({ target, runner, sourcePath, targetPath: posix.join(remoteFilesystemRoot, "codex-home", name), mode: 0o600 });
|
|
}
|
|
} }),
|
|
},
|
|
}),
|
|
},
|
|
...(options.credentialRefreshAuthPath ? { onCodexCredentialRefresh: async (filename: string) => {
|
|
if (filename !== posix.join(remoteFilesystemRoot, "codex-home", "auth.json")) throw new Error("The native Codex credential refresh handoff is invalid.");
|
|
await copyBackCodexAuth({ hostAuthPath: options.credentialRefreshAuthPath!, log: () => {}, readSandboxAuth: async () => {
|
|
const read = await runner.execute({ command: "sh", args: ["-c", 'set -eu; file=$1; test -f "$file" || exit 66; test ! -L "$file"; parent=$(dirname -- "$file"); while test "$parent" != /; do test -d "$parent" && test ! -L "$parent"; parent=$(dirname -- "$parent"); done; metadata=$(stat -c "%u %a %s" "$file" 2>/dev/null || stat -f "%u %Lp %z" "$file"); set -- $metadata; test "$1" = "$(id -u)" && test "$2" = 600 && test "$3" -le 65536; head -c 65537 -- "$file" | base64', "paperclip-native-refresh", filename], bypassSession: true, timeoutMs: 10_000 });
|
|
if (read.timedOut || read.exitCode !== 0) throw Object.assign(new Error("Native Codex credential refresh handoff unavailable."), { code: read.exitCode === 66 ? "ENOENT" : "INVALID_CREDENTIAL" });
|
|
const body = Buffer.from(read.stdout, "base64");
|
|
if (body.length > 65_536) throw new Error("Native Codex credential refresh handoff exceeds the credential size limit.");
|
|
return body;
|
|
} });
|
|
} } : {}),
|
|
onCleanupConfirmed: async () => {
|
|
if (remoteDirectoryCreated) {
|
|
const cleaned = await runner.execute({ command: "rm", args: ["-rf", "--", remoteRoot], bypassSession: true, timeoutMs: 10_000 });
|
|
requireSuccess(cleaned, "Native setup cleanup is incomplete; private recovery state was retained.");
|
|
}
|
|
await rm(runtimeDirectory, { recursive: true, force: true });
|
|
},
|
|
};
|
|
});
|
|
}
|
|
|
|
export async function testNativeRunnerAuthentication(context: AdapterEnvironmentTestContext, provider: "codex", model: string | null): Promise<AdapterEnvironmentTestResult> {
|
|
const remote = context.executionTarget?.kind === "remote";
|
|
const configured = context.config.env;
|
|
const environment: Record<string, string> = Object.fromEntries([
|
|
...(remote ? [] : PROBE_TRANSPORT_ENV_KEYS.flatMap(key => typeof process.env[key] === "string" ? [[key, process.env[key]!]] : [])),
|
|
...Object.entries(configured && typeof configured === "object" && !Array.isArray(configured) ? configured : {})
|
|
.filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
|
]);
|
|
const authKey = "_PAPERCLIP_NATIVE_SETUP_CODEX_AUTH_JSON_SECRET";
|
|
const configKey = "_PAPERCLIP_NATIVE_SETUP_CODEX_CONFIG_TOML_SECRET";
|
|
delete environment[authKey];
|
|
delete environment[configKey];
|
|
let sourceCodexHome: string | null | undefined;
|
|
const effort = context.config.modelReasoningEffort ?? context.config.reasoningEffort ?? context.config.effort;
|
|
const timeoutMs = remote ? 90_000 : 45_000;
|
|
let runtimeDirectory: string | undefined;
|
|
let sourceAuthJson: string | undefined;
|
|
try {
|
|
if (provider === "codex") {
|
|
const { buildNativeProviderEnvironment } = await import("./native-session-executor.js");
|
|
sourceCodexHome = context.managedAiCredentialHome ?? resolveSourceCodexHome(buildNativeProviderEnvironment(environment));
|
|
}
|
|
let credentialRefreshAuthPath: string | undefined;
|
|
if (sourceCodexHome && !environment.CODEX_API_KEY?.trim() && !environment.OPENAI_API_KEY?.trim() && !environment.PAPERCLIP_AI_PROVIDER_KEY?.trim()) {
|
|
let linked = false;
|
|
try {
|
|
const authPath = join(sourceCodexHome, "auth.json");
|
|
linked = (await lstat(authPath)).isSymbolicLink();
|
|
if (linked && !context.managedAiCredentialHome?.trim()) {
|
|
const field = environment.CODEX_HOME?.trim() ? "env.CODEX_HOME" : "operator CODEX_HOME";
|
|
throw new Error(`${field} uses a linked auth.json that Paperclip Runner tasks cannot read. Choose Legacy runner in Advanced or use a managed Codex Connection.`);
|
|
}
|
|
const resolvedAuthPath = await realpath(authPath);
|
|
const source = await stat(resolvedAuthPath);
|
|
if (!source.isFile() || source.uid !== process.getuid?.()) throw new Error("Invalid Codex source credential owner.");
|
|
if (linked) sourceAuthJson = await readLocalAiCredentialFile(resolvedAuthPath);
|
|
credentialRefreshAuthPath = resolvedAuthPath;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
if (linked) throw new Error("Codex authentication required: the selected credential link is unavailable.");
|
|
}
|
|
}
|
|
let receipt: Awaited<ReturnType<typeof probeNativeRunnerEnvironment>>;
|
|
if (context.executionTarget?.kind === "remote") {
|
|
receipt = await probeRemoteNativeCodex({ context, model, environment, sourceCodexHome: sourceCodexHome ?? "", credentialRefreshAuthPath, sourceAuthJson, timeoutMs,
|
|
...(typeof effort === "string" && effort ? { reasoningEffort: effort } : {}) });
|
|
} else {
|
|
const runnerBinary = resolvePaperclipRunnerBinary();
|
|
runtimeDirectory = await realpath(await mkdtemp(join(tmpdir(), "paperclip-native-setup-")));
|
|
receipt = await probePreparedNativeRuntime(runtimeDirectory, async () => {
|
|
const probeSourceCodexHome = await prepareProbeCodexSource(runtimeDirectory!, sourceCodexHome ?? "", sourceAuthJson);
|
|
return { runtimeDirectory: runtimeDirectory!, provider, model, environment, timeoutMs,
|
|
...(provider === "codex" && typeof effort === "string" && effort ? { reasoningEffort: effort } : {}),
|
|
onCleanupConfirmed: async () => { await rm(runtimeDirectory!, { recursive: true, force: true }); },
|
|
transportOptions: { runnerBinary, sourceCodexHome: probeSourceCodexHome, externallySandboxed: false },
|
|
...(credentialRefreshAuthPath ? { onCodexCredentialRefresh: async (filename: string) => {
|
|
if (filename !== join(runtimeDirectory!, "codex-home", "auth.json")) throw new Error("The native Codex credential refresh handoff is invalid.");
|
|
await copyBackCodexAuth({ hostAuthPath: credentialRefreshAuthPath!, log: () => {}, readSandboxAuth: async () => Buffer.from(await readLocalAiCredentialFile(filename)) });
|
|
} } : {}),
|
|
};
|
|
});
|
|
}
|
|
if (receipt.helloProbePassed !== true || receipt.provider !== provider || typeof receipt.effectiveModel !== "string" || !receipt.effectiveModel.trim() || receipt.effectiveModel === "unknown" || (model !== null && receipt.effectiveModel !== model)
|
|
|| receipt.providerDriver !== "codex_app_server") throw new Error("The selected native runtime returned an incompatible account or model verification receipt.");
|
|
return { adapterType: "paperclip_runner", status: "pass", testedAt: new Date().toISOString(), checks: [{ code: `${provider}_hello_probe_passed`, level: "info",
|
|
message: `The native ${provider} runtime verified the selected account and model in ${remote ? "the selected environment" : "the Paperclip host"}.` }] };
|
|
} catch (error) {
|
|
const message = redactNativeProbeMessage(error instanceof Error ? error.message : "The selected native runtime could not verify this account.", { ...environment, ...(sourceAuthJson ? { [authKey]: sourceAuthJson } : {}) });
|
|
const authentication = /(?:invalid (?:api[- ]?key|auth(?:entication)? token)|authentication (?:failed|required)|unauthenticated|unauthorized|not authenticated|please (?:log|sign) in|not logged in|\b(?:401|403)\b)/i.test(message);
|
|
const rejectedModel = provider === "codex" ? CODEX_CHATGPT_MODEL_REJECTION_RE.exec(message)?.[1] : undefined;
|
|
const retirementHint = rejectedModel ? codexChatGptRetirementHint(rejectedModel) : null;
|
|
return { adapterType: "paperclip_runner", status: "fail", testedAt: new Date().toISOString(), checks: [{ code: `${provider}_hello_probe_${authentication ? "auth_required" : /timed out/i.test(message) ? "timeout" : "failed"}`, level: "error", message,
|
|
hint: retirementHint ?? "Check the selected account, model access, and native runtime prerequisites, then retry. Legacy runner is available explicitly in Advanced." },
|
|
...(authentication && provider === "codex" ? [{ code: ADAPTER_AUTH_MISSING_CHECK_CODE, level: "error" as const,
|
|
message: "The selected Codex account needs authentication.",
|
|
hint: "Sign in to Codex or choose an authenticated account, then test again." }] : [])] };
|
|
}
|
|
}
|