mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix: harden Codex auth sync-back errors
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2 files changed
+120
-3
No files matched your search
@@ -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);
|
||||
|
||||
@@ -597,7 +597,6 @@ async function writeFileAtomic0600(targetPath: string, bytes: Buffer): Promise<v
|
||||
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;
|
||||
@@ -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)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user