From 1450518bf5869bfa59c383081b7022a8850d8c8f Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Wed, 15 Jul 2026 18:02:03 +0000 Subject: [PATCH] fix: harden Codex auth sync-back errors Co-authored-by: Paperclip --- .../src/sandbox-managed-runtime.test.ts | 118 ++++++++++++++++++ .../src/sandbox-managed-runtime.ts | 5 +- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/packages/adapter-utils/src/sandbox-managed-runtime.test.ts b/packages/adapter-utils/src/sandbox-managed-runtime.test.ts index 00bf4e5321d..80c47b5a311 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.test.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.test.ts @@ -341,6 +341,124 @@ describe("sandbox managed runtime", () => { await expect(readFile(path.join(nextPrepared.assetDirs.home, "auth.json"), "utf8")).resolves.toBe(expectedSyncedAuth); }); + it("preserves Codex auth sync-back error cause", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-auth-cause-")); + 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:00:00.000Z", + refreshToken: "refresh-host-before-cause", + }); + const sandboxAuth = codexSubscriptionAuth({ + lastRefresh: "2026-01-01T00:10:00.000Z", + refreshToken: "refresh-sandbox-cause", + }); + await writeFile(hostAuthSource, hostAuth, { mode: 0o600 }); + await symlink(hostAuthSource, path.join(localCodexHome, "auth.json")); + + 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 }], + }); + await writeFile(path.join(prepared.assetDirs.home, "auth.json"), sandboxAuth, "utf8"); + await rm(hostAuthSource, { force: true }); + await mkdir(hostAuthSource, { recursive: true }); + + let thrown: unknown = null; + try { + await prepared.restoreWorkspace(); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(Error); + const error = thrown as Error & { cause?: unknown }; + expect(error.message).toContain("Failed to sync Codex auth.json from sandbox to host"); + expect(error.message).toContain("EISDIR"); + expect(error.cause).toBeInstanceOf(Error); + expect((error.cause as { code?: unknown }).code).toBe("EISDIR"); + }); + + it("reports failed Codex auth sync-back when suppressing it after restore failure", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-auth-suppressed-")); + 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:00:00.000Z", + refreshToken: "refresh-host-before-suppressed", + }); + const sandboxAuth = codexSubscriptionAuth({ + lastRefresh: "2026-01-01T00:10:00.000Z", + refreshToken: "refresh-sandbox-suppressed", + }); + await writeFile(hostAuthSource, hostAuth, { mode: 0o600 }); + await symlink(hostAuthSource, path.join(localCodexHome, "auth.json")); + + const baseClient = createLocalSandboxClient(); + const client: SandboxManagedRuntimeClient = { + ...baseClient, + run: async (command, options) => { + if (command.includes("workspace-download.tar")) { + throw new Error("forced workspace restore failure"); + } + await baseClient.run(command, options); + }, + }; + 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 }], + onRuntimeProgress: (status) => { + runtimeStatuses.push(status.phase + ":" + status.message); + }, + }); + await writeFile(path.join(prepared.assetDirs.home, "auth.json"), sandboxAuth, "utf8"); + await rm(hostAuthSource, { force: true }); + await mkdir(hostAuthSource, { recursive: true }); + + await expect(prepared.restoreWorkspace()).rejects.toThrow("forced workspace restore failure"); + expect(runtimeStatuses).toContain( + "restore:Codex auth.json sync-back failed (suppressed after workspace restore failure)", + ); + expect(runtimeStatuses).not.toContain("restore:Codex auth.json sync-back skipped after workspace restore failure"); + }); + it("does not sync oversized sandbox Codex auth.json back to host", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-codex-auth-too-large-")); cleanupDirs.push(rootDir); diff --git a/packages/adapter-utils/src/sandbox-managed-runtime.ts b/packages/adapter-utils/src/sandbox-managed-runtime.ts index 849369aca8d..38411e43097 100644 --- a/packages/adapter-utils/src/sandbox-managed-runtime.ts +++ b/packages/adapter-utils/src/sandbox-managed-runtime.ts @@ -597,7 +597,6 @@ async function writeFileAtomic0600(targetPath: string, bytes: Buffer): Promise undefined); throw error; @@ -649,7 +648,7 @@ async function syncCodexHomeAuthJsonBackToHost(input: { } } } catch (error) { - throw new Error(`Failed to sync Codex auth.json from sandbox to host${errorCodeSuffix(error)}.`); + throw new Error(`Failed to sync Codex auth.json from sandbox to host${errorCodeSuffix(error)}.`, { cause: error }); } await emitCodexAuthSyncBackOutcome({ @@ -1072,7 +1071,7 @@ export async function prepareSandboxManagedRuntime(input: { await emitRuntimeStatus( input.onRuntimeProgress, "restore", - "Codex auth.json sync-back skipped after workspace restore failure", + "Codex auth.json sync-back failed (suppressed after workspace restore failure)", ); } }