diff --git a/packages/adapter-utils/src/codex-auth-merge-decision.cjs b/packages/adapter-utils/src/codex-auth-merge-decision.cjs index fa08fee82ae..abeb393f7ae 100644 --- a/packages/adapter-utils/src/codex-auth-merge-decision.cjs +++ b/packages/adapter-utils/src/codex-auth-merge-decision.cjs @@ -1,8 +1,10 @@ const fs = require("fs"); // Co-change notice: parseAuth below mirrors hasUsableAuthPayload in -// packages/adapters/codex-local/src/server/codex-home.ts. If the auth format -// changes (new shape, renamed field), update both sites together. +// packages/adapters/codex-local/src/server/codex-home.ts and +// parseCodexAuthPayload in packages/adapter-utils/src/sandbox-managed-runtime.ts. +// If the auth format changes (new shape, renamed field), update all sites +// together. function parseAuth(filePath) { let parsed; try { diff --git a/packages/adapter-utils/src/sandbox-managed-runtime.test.ts b/packages/adapter-utils/src/sandbox-managed-runtime.test.ts index a101fc817cf..99c816fb15b 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.test.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.test.ts @@ -1,4 +1,4 @@ -import { lstat, mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from "node:fs/promises"; +import { lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { execFile as execFileCallback } from "node:child_process"; @@ -28,6 +28,52 @@ async function listTarMembers(rootDir: string, name: string, bytes: Buffer): Pro return stdout.split("\n").map((line) => line.trim()).filter(Boolean); } +function createLocalSandboxClient(): SandboxManagedRuntimeClient { + return { + makeDir: async (remotePath) => { + await mkdir(remotePath, { recursive: true }); + }, + writeFile: async (remotePath, bytes) => { + await mkdir(path.dirname(remotePath), { recursive: true }); + await writeFile(remotePath, Buffer.from(bytes)); + }, + readFile: async (remotePath) => await readFile(remotePath), + listFiles: async (remotePath) => { + const entries = await readdir(remotePath, { withFileTypes: true }).catch(() => []); + return entries + .filter((entry) => entry.isFile()) + .map((entry) => entry.name) + .sort((left, right) => left.localeCompare(right)); + }, + remove: async (remotePath) => { + await rm(remotePath, { recursive: true, force: true }); + }, + run: async (command) => { + await execFile("sh", ["-c", command], { maxBuffer: 32 * 1024 * 1024 }); + }, + }; +} + +function codexSubscriptionAuth(input: { + accountId?: string; + lastRefresh: string; + refreshToken: string; + accessToken?: string; + idToken?: string; + padding?: string; +}): string { + return `${JSON.stringify({ + tokens: { + account_id: input.accountId ?? "acct-1", + id_token: input.idToken ?? "id-token", + access_token: input.accessToken ?? "access-token", + refresh_token: input.refreshToken, + }, + last_refresh: input.lastRefresh, + ...(input.padding ? { padding: input.padding } : {}), + })}\n`; +} + describe("sandbox managed runtime", () => { const cleanupDirs: string[] = []; @@ -162,6 +208,261 @@ describe("sandbox managed runtime", () => { expect(runtimeStatuses.at(-1)).toBe("finalize:Finalizing sandbox workspace"); }); + it("syncs newer sandbox Codex auth.json back to the host symlink source and seeds the next run", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-auth-")); + cleanupDirs.push(rootDir); + const localWorkspaceDir = path.join(rootDir, "local-workspace"); + const remoteWorkspaceDir = path.join(rootDir, "remote-workspace"); + const nextRemoteWorkspaceDir = path.join(rootDir, "remote-workspace-next"); + const localCodexHome = path.join(rootDir, "local-codex-home"); + const hostCodexHome = path.join(rootDir, "host-codex-home"); + const hostAuthSource = path.join(hostCodexHome, "auth.json"); + await mkdir(localWorkspaceDir, { recursive: true }); + await mkdir(localCodexHome, { recursive: true }); + await mkdir(hostCodexHome, { recursive: true }); + + const oldAuth = codexSubscriptionAuth({ + lastRefresh: "2026-01-01T00:00:00.000Z", + refreshToken: "refresh-old", + accessToken: "access-old", + idToken: "id-old", + }); + const newAuth = codexSubscriptionAuth({ + lastRefresh: "2026-01-01T00:01:00.000Z", + refreshToken: "refresh-new-secret", + accessToken: "access-new-secret", + idToken: "id-new-secret", + padding: "x".repeat(1024 * 1024), + }); + await writeFile(hostAuthSource, oldAuth, { mode: 0o600 }); + await symlink(hostAuthSource, path.join(localCodexHome, "auth.json")); + + const client = createLocalSandboxClient(); + const lines: string[] = []; + const runtimeStatuses: string[] = []; + const prepared = await prepareSandboxManagedRuntime({ + spec: { + transport: "sandbox", + provider: "test", + sandboxId: "sandbox-1", + remoteCwd: remoteWorkspaceDir, + timeoutMs: 30_000, + apiKey: null, + }, + adapterKey: "codex", + client, + workspaceLocalDir: localWorkspaceDir, + assets: [{ key: "home", localDir: localCodexHome, followSymlinks: true }], + onProgress: (line) => { + lines.push(line); + }, + onRuntimeProgress: (status) => { + runtimeStatuses.push(`${status.phase}:${status.message}`); + }, + }); + + await writeFile(path.join(prepared.assetDirs.home, "auth.json"), newAuth, "utf8"); + + const invalidReads: string[] = []; + let polling = true; + const pollPromise = (async () => { + while (polling) { + const raw = await readFile(hostAuthSource, "utf8"); + try { + JSON.parse(raw); + } catch { + invalidReads.push(raw.slice(0, 64)); + } + await new Promise((resolve) => setImmediate(resolve)); + } + })(); + + try { + await prepared.restoreWorkspace(); + } finally { + polling = false; + await pollPromise; + } + + expect(invalidReads).toEqual([]); + await expect(readFile(hostAuthSource, "utf8")).resolves.toBe(newAuth); + expect((await lstat(path.join(localCodexHome, "auth.json"))).isSymbolicLink()).toBe(true); + expect(await realpath(path.join(localCodexHome, "auth.json"))).toBe(await realpath(hostAuthSource)); + expect((await lstat(hostAuthSource)).mode & 0o777).toBe(0o600); + expect((await readdir(hostCodexHome)).filter((name) => name.includes(".auth.json.paperclip"))).toEqual([]); + + const combinedTelemetry = `${lines.join("")}\n${runtimeStatuses.join("\n")}`; + expect(combinedTelemetry).toContain("updated host auth source"); + expect(combinedTelemetry).not.toContain("refresh-new-secret"); + expect(combinedTelemetry).not.toContain("access-new-secret"); + expect(combinedTelemetry).not.toContain("id-new-secret"); + + const nextPrepared = await prepareSandboxManagedRuntime({ + spec: { + transport: "sandbox", + provider: "test", + sandboxId: "sandbox-2", + remoteCwd: nextRemoteWorkspaceDir, + timeoutMs: 30_000, + apiKey: null, + }, + adapterKey: "codex", + client, + workspaceLocalDir: localWorkspaceDir, + assets: [{ key: "home", localDir: localCodexHome, followSymlinks: true }], + }); + await expect(readFile(path.join(nextPrepared.assetDirs.home, "auth.json"), "utf8")).resolves.toBe(newAuth); + }); + + it("does not replace newer host Codex auth.json with older sandbox auth", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-auth-older-")); + cleanupDirs.push(rootDir); + const localWorkspaceDir = path.join(rootDir, "local-workspace"); + const remoteWorkspaceDir = path.join(rootDir, "remote-workspace"); + const localCodexHome = path.join(rootDir, "local-codex-home"); + const hostCodexHome = path.join(rootDir, "host-codex-home"); + const hostAuthSource = path.join(hostCodexHome, "auth.json"); + await mkdir(localWorkspaceDir, { recursive: true }); + await mkdir(localCodexHome, { recursive: true }); + await mkdir(hostCodexHome, { recursive: true }); + + const hostAuth = codexSubscriptionAuth({ + lastRefresh: "2026-01-01T00:10:00.000Z", + refreshToken: "refresh-host-newer", + }); + const sandboxAuth = codexSubscriptionAuth({ + lastRefresh: "2026-01-01T00:05:00.000Z", + refreshToken: "refresh-sandbox-older", + }); + await writeFile(hostAuthSource, hostAuth, { mode: 0o600 }); + await symlink(hostAuthSource, path.join(localCodexHome, "auth.json")); + + const lines: string[] = []; + const prepared = await prepareSandboxManagedRuntime({ + spec: { + transport: "sandbox", + provider: "test", + sandboxId: "sandbox-1", + remoteCwd: remoteWorkspaceDir, + timeoutMs: 30_000, + apiKey: null, + }, + adapterKey: "codex", + client: createLocalSandboxClient(), + workspaceLocalDir: localWorkspaceDir, + assets: [{ key: "home", localDir: localCodexHome, followSymlinks: true }], + onProgress: (line) => { + lines.push(line); + }, + }); + await writeFile(path.join(prepared.assetDirs.home, "auth.json"), sandboxAuth, "utf8"); + + await prepared.restoreWorkspace(); + + await expect(readFile(hostAuthSource, "utf8")).resolves.toBe(hostAuth); + expect(lines.join("")).toContain("sandbox auth is not newer"); + expect(lines.join("")).not.toContain("refresh-sandbox-older"); + }); + + it("does not sync sandbox Codex auth.json when account ids differ", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-auth-mismatch-")); + cleanupDirs.push(rootDir); + const localWorkspaceDir = path.join(rootDir, "local-workspace"); + const remoteWorkspaceDir = path.join(rootDir, "remote-workspace"); + const localCodexHome = path.join(rootDir, "local-codex-home"); + const hostCodexHome = path.join(rootDir, "host-codex-home"); + const hostAuthSource = path.join(hostCodexHome, "auth.json"); + await mkdir(localWorkspaceDir, { recursive: true }); + await mkdir(localCodexHome, { recursive: true }); + await mkdir(hostCodexHome, { recursive: true }); + + const hostAuth = codexSubscriptionAuth({ + accountId: "acct-host", + lastRefresh: "2026-01-01T00:00:00.000Z", + refreshToken: "refresh-host", + }); + const sandboxAuth = codexSubscriptionAuth({ + accountId: "acct-sandbox", + lastRefresh: "2026-01-01T00:10:00.000Z", + refreshToken: "refresh-sandbox-mismatch", + }); + await writeFile(hostAuthSource, hostAuth, { mode: 0o600 }); + await symlink(hostAuthSource, path.join(localCodexHome, "auth.json")); + + const lines: string[] = []; + const prepared = await prepareSandboxManagedRuntime({ + spec: { + transport: "sandbox", + provider: "test", + sandboxId: "sandbox-1", + remoteCwd: remoteWorkspaceDir, + timeoutMs: 30_000, + apiKey: null, + }, + adapterKey: "codex", + client: createLocalSandboxClient(), + workspaceLocalDir: localWorkspaceDir, + assets: [{ key: "home", localDir: localCodexHome, followSymlinks: true }], + onProgress: (line) => { + lines.push(line); + }, + }); + await writeFile(path.join(prepared.assetDirs.home, "auth.json"), sandboxAuth, "utf8"); + + await prepared.restoreWorkspace(); + + await expect(readFile(hostAuthSource, "utf8")).resolves.toBe(hostAuth); + expect(lines.join("")).toContain("account ids do not match"); + expect(lines.join("")).not.toContain("refresh-sandbox-mismatch"); + expect(lines.join("")).not.toContain("acct-sandbox"); + }); + + it("does not sync Codex auth.json back through a regular host auth file", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-auth-regular-")); + cleanupDirs.push(rootDir); + const localWorkspaceDir = path.join(rootDir, "local-workspace"); + const remoteWorkspaceDir = path.join(rootDir, "remote-workspace"); + const localCodexHome = path.join(rootDir, "local-codex-home"); + await mkdir(localWorkspaceDir, { recursive: true }); + await mkdir(localCodexHome, { recursive: true }); + + const hostAuth = codexSubscriptionAuth({ + lastRefresh: "2026-01-01T00:00:00.000Z", + refreshToken: "refresh-regular-host", + }); + const sandboxAuth = codexSubscriptionAuth({ + lastRefresh: "2026-01-01T00:10:00.000Z", + refreshToken: "refresh-regular-sandbox", + }); + await writeFile(path.join(localCodexHome, "auth.json"), hostAuth, { mode: 0o600 }); + + const lines: string[] = []; + const prepared = await prepareSandboxManagedRuntime({ + spec: { + transport: "sandbox", + provider: "test", + sandboxId: "sandbox-1", + remoteCwd: remoteWorkspaceDir, + timeoutMs: 30_000, + apiKey: null, + }, + adapterKey: "codex", + client: createLocalSandboxClient(), + workspaceLocalDir: localWorkspaceDir, + assets: [{ key: "home", localDir: localCodexHome, followSymlinks: true }], + onProgress: (line) => { + lines.push(line); + }, + }); + await writeFile(path.join(prepared.assetDirs.home, "auth.json"), sandboxAuth, "utf8"); + + await prepared.restoreWorkspace(); + + await expect(readFile(path.join(localCodexHome, "auth.json"), "utf8")).resolves.toBe(hostAuth); + expect(lines.join("")).toContain("host auth.json is not a symlink"); + expect(lines.join("")).not.toContain("refresh-regular-sandbox"); + }); + it("syncs git-backed workspaces through a shallow standalone clone and keeps .git out of archives", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-git-")); cleanupDirs.push(rootDir); diff --git a/packages/adapter-utils/src/sandbox-managed-runtime.ts b/packages/adapter-utils/src/sandbox-managed-runtime.ts index 10dc8ab40a5..3efafc7e116 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.ts @@ -52,6 +52,7 @@ const SANDBOX_WORKSPACE_HEAVY_DIR_EXCLUDES = SANDBOX_WORKSPACE_HEAVY_DIR_NAMES.f `*/${entry}`, `*/${entry}/*`, ]); +const CODEX_AUTH_JSON_NAME = "auth.json"; export interface SandboxRemoteExecutionSpec { transport: "sandbox"; @@ -100,6 +101,20 @@ export interface PreparedSandboxManagedRuntime { restoreWorkspace(onProgress?: RuntimeProgressSink): Promise; } +type ParsedCodexAuth = + | { kind: "unusable" } + | { kind: "apikey" } + | { kind: "subscription"; accountId: string; lastRefresh: number | null }; + +type CodexAuthSyncBackOutcome = + | "updated" + | "skipped_no_host_symlink" + | "skipped_sandbox_auth_missing" + | "skipped_unusable_auth" + | "skipped_api_key_auth" + | "skipped_account_mismatch" + | "skipped_not_newer"; + function asObject(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) @@ -349,6 +364,209 @@ function toBuffer(bytes: Buffer | Uint8Array | ArrayBuffer): Buffer { return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength); } +// Co-change notice: this parser mirrors hasUsableAuthPayload in +// packages/adapters/codex-local/src/server/codex-home.ts and parseAuth in +// packages/adapter-utils/src/codex-auth-merge-decision.cjs. If the auth format +// changes (new shape, renamed field), update all sites together. +function parseCodexAuthPayload(authPayload: unknown): ParsedCodexAuth { + if (authPayload === null || typeof authPayload !== "object" || Array.isArray(authPayload)) { + return { kind: "unusable" }; + } + + const parsedPayload = authPayload as Record; + const apiKey = parsedPayload.OPENAI_API_KEY; + if (typeof apiKey === "string" && apiKey.trim().length > 0) { + return { kind: "apikey" }; + } + + const tokens = parsedPayload.tokens; + if (tokens === null || typeof tokens !== "object" || Array.isArray(tokens)) { + return { kind: "unusable" }; + } + + const parsedTokens = tokens as Record; + const accountId = typeof parsedTokens.account_id === "string" ? parsedTokens.account_id.trim() : ""; + const hasTokenMaterial = ["id_token", "access_token", "refresh_token"].some((key) => { + const value = parsedTokens[key]; + return typeof value === "string" && value.trim().length > 0; + }); + if (!accountId || !hasTokenMaterial) { + return { kind: "unusable" }; + } + + const lastRefresh = typeof parsedPayload.last_refresh === "string" + ? Date.parse(parsedPayload.last_refresh) + : NaN; + return { + kind: "subscription", + accountId, + lastRefresh: Number.isFinite(lastRefresh) ? lastRefresh : null, + }; +} + +function parseCodexAuthBytes(bytes: Buffer): ParsedCodexAuth { + try { + return parseCodexAuthPayload(JSON.parse(bytes.toString("utf8"))); + } catch { + return { kind: "unusable" }; + } +} + +function isMissingFileError(error: unknown): boolean { + const code = typeof error === "object" && error !== null + ? (error as { code?: unknown }).code + : null; + return code === "ENOENT" || code === "ENOTDIR" || code === "NotFound"; +} + +function errorCodeSuffix(error: unknown): string { + const code = typeof error === "object" && error !== null + ? (error as { code?: unknown }).code + : null; + return typeof code === "string" && code.trim().length > 0 ? ` (${code.trim()})` : ""; +} + +function codexAuthSyncBackMessage(outcome: CodexAuthSyncBackOutcome): string { + switch (outcome) { + case "updated": + return "updated host auth source with newer sandbox refresh"; + case "skipped_no_host_symlink": + return "skipped because host auth.json is not a symlink"; + case "skipped_sandbox_auth_missing": + return "skipped because sandbox auth.json is missing"; + case "skipped_unusable_auth": + return "skipped because sandbox or host auth.json is not usable subscription auth"; + case "skipped_api_key_auth": + return "skipped because API-key auth is not synced back"; + case "skipped_account_mismatch": + return "skipped because account ids do not match"; + case "skipped_not_newer": + return "skipped because sandbox auth is not newer"; + } +} + +async function emitCodexAuthSyncBackOutcome(input: { + onProgress?: RuntimeProgressSink; + onRuntimeProgress?: RuntimeStatusSink; + outcome: CodexAuthSyncBackOutcome; +}): Promise { + const message = `Codex auth.json sync-back: ${codexAuthSyncBackMessage(input.outcome)}`; + await input.onProgress?.(`[paperclip] ${message}\n`); + await emitRuntimeStatus(input.onRuntimeProgress, "restore", message); +} + +async function resolveHostCodexAuthSymlinkSource(localHomeDir: string): Promise { + const localAuthPath = path.join(localHomeDir, CODEX_AUTH_JSON_NAME); + const stats = await fs.lstat(localAuthPath).catch(() => null); + if (!stats?.isSymbolicLink()) return null; + + const linkTarget = await fs.readlink(localAuthPath); + const resolved = path.resolve(path.dirname(localAuthPath), linkTarget); + return await fs.realpath(resolved).catch(() => resolved); +} + +function decideCodexAuthSyncBack(input: { + sandboxAuth: ParsedCodexAuth; + hostAuth: ParsedCodexAuth; +}): CodexAuthSyncBackOutcome { + if (input.sandboxAuth.kind === "unusable" || input.hostAuth.kind === "unusable") { + return "skipped_unusable_auth"; + } + + if (input.sandboxAuth.kind === "apikey" || input.hostAuth.kind === "apikey") { + return "skipped_api_key_auth"; + } + + if (input.sandboxAuth.accountId !== input.hostAuth.accountId) { + return "skipped_account_mismatch"; + } + + if ( + input.sandboxAuth.lastRefresh === null || + input.hostAuth.lastRefresh === null || + input.sandboxAuth.lastRefresh <= input.hostAuth.lastRefresh + ) { + return "skipped_not_newer"; + } + + return "updated"; +} + +async function writeFileAtomic0600(targetPath: string, bytes: Buffer): Promise { + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + const tempPath = path.join( + path.dirname(targetPath), + `.${path.basename(targetPath)}.paperclip-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`, + ); + + try { + await fs.writeFile(tempPath, bytes, { mode: 0o600 }); + await fs.chmod(tempPath, 0o600); + await fs.rename(tempPath, targetPath); + await fs.chmod(targetPath, 0o600); + } catch (error) { + await fs.rm(tempPath, { force: true }).catch(() => undefined); + throw error; + } +} + +async function syncCodexHomeAuthJsonBackToHost(input: { + adapterKey: string; + assets?: SandboxManagedRuntimeAsset[]; + assetDirs: Record; + client: SandboxManagedRuntimeClient; + onProgress?: RuntimeProgressSink; + onRuntimeProgress?: RuntimeStatusSink; +}): Promise { + const homeAsset = input.adapterKey === "codex" + ? input.assets?.find((asset) => asset.key === "home") + : undefined; + if (!homeAsset) return; + + let outcome: CodexAuthSyncBackOutcome; + try { + const hostAuthSource = await resolveHostCodexAuthSymlinkSource(homeAsset.localDir); + if (!hostAuthSource) { + outcome = "skipped_no_host_symlink"; + } else { + const remoteHomeDir = input.assetDirs[homeAsset.key]; + const sandboxAuthBytes = remoteHomeDir + ? await input.client.readFile(path.posix.join(remoteHomeDir, CODEX_AUTH_JSON_NAME)) + .then((bytes) => toBuffer(bytes)) + .catch((error: unknown) => { + if (isMissingFileError(error)) return null; + throw error; + }) + : null; + + if (!sandboxAuthBytes) { + outcome = "skipped_sandbox_auth_missing"; + } else { + const hostAuthBytes = await fs.readFile(hostAuthSource) + .catch((error: unknown) => { + if (isMissingFileError(error)) return null; + throw error; + }); + outcome = decideCodexAuthSyncBack({ + sandboxAuth: parseCodexAuthBytes(sandboxAuthBytes), + hostAuth: hostAuthBytes ? parseCodexAuthBytes(hostAuthBytes) : { kind: "unusable" }, + }); + if (outcome === "updated") { + await writeFileAtomic0600(hostAuthSource, sandboxAuthBytes); + } + } + } + } catch (error) { + throw new Error(`Failed to sync Codex auth.json from sandbox to host${errorCodeSuffix(error)}.`); + } + + await emitCodexAuthSyncBackOutcome({ + onProgress: input.onProgress, + onRuntimeProgress: input.onRuntimeProgress, + outcome, + }); +} + function tarExcludeFlags(exclude: string[] | undefined): string { return ["._*", ...(exclude ?? [])].map((entry) => `--exclude ${shellQuote(entry)}`).join(" "); } @@ -647,6 +865,7 @@ export async function prepareSandboxManagedRuntime(input: { let importedRef: string | null = null; let importedHead: string | null = null; let remoteWorkspaceStatus = "dirty"; + let restoreError: unknown = null; try { if (gitSnapshot) { await emitRuntimeStatus(input.onRuntimeProgress, "export", "Exporting git changes from sandbox"); @@ -741,11 +960,38 @@ export async function prepareSandboxManagedRuntime(input: { } : undefined, }); + } catch (error) { + restoreError = error; + throw error; } finally { + let syncBackError: unknown = null; + try { + await syncCodexHomeAuthJsonBackToHost({ + adapterKey: input.adapterKey, + assets: input.assets, + assetDirs, + client: input.client, + onProgress: restoreSink, + onRuntimeProgress: input.onRuntimeProgress, + }); + } catch (error) { + syncBackError = error; + if (restoreError) { + await emitRuntimeStatus( + input.onRuntimeProgress, + "restore", + "Codex auth.json sync-back skipped after workspace restore failure", + ); + } + } + await emitRuntimeStatus(input.onRuntimeProgress, "finalize", "Finalizing sandbox workspace"); if (importedRef) { await deleteLocalGitRef({ localDir: input.workspaceLocalDir, ref: importedRef }); } + if (!restoreError && syncBackError) { + throw syncBackError; + } } }); }, diff --git a/packages/adapters/codex-local/src/server/codex-home.ts b/packages/adapters/codex-local/src/server/codex-home.ts index 0739066a76e..b74d3402f2b 100644 --- a/packages/adapters/codex-local/src/server/codex-home.ts +++ b/packages/adapters/codex-local/src/server/codex-home.ts @@ -39,8 +39,9 @@ export async function pathExists(candidate: string): Promise { } // Co-change notice: this function's logic is mirrored by parseAuth in -// packages/adapter-utils/src/sandbox-managed-runtime.ts (buildCodexAuthMergeDecisionScript). -// If the auth format changes (new shape, renamed field), update both sites together. +// packages/adapter-utils/src/codex-auth-merge-decision.cjs and +// parseCodexAuthPayload in packages/adapter-utils/src/sandbox-managed-runtime.ts. +// If the auth format changes (new shape, renamed field), update all sites together. function hasUsableAuthPayload(authPayload: unknown): boolean { if (authPayload === null || typeof authPayload !== "object" || Array.isArray(authPayload)) { return false;