diff --git a/packages/adapters/gemini-local/src/server/execute.remote.test.ts b/packages/adapters/gemini-local/src/server/execute.remote.test.ts index 2e266603..e050427b 100644 --- a/packages/adapters/gemini-local/src/server/execute.remote.test.ts +++ b/packages/adapters/gemini-local/src/server/execute.remote.test.ts @@ -126,6 +126,7 @@ describe("gemini remote execution", () => { }, config: { command: "gemini", + env: { GEMINI_API_KEY: "test-key" }, }, context: { paperclipWorkspace: { @@ -184,6 +185,19 @@ describe("gemini remote execution", () => { expect.stringContaining(".gemini/skills"), expect.anything(), ); + // The headless-auth settings.json write is scoped to managed HOMEs (sandbox + // transport). SSH targets keep the user's real home, where existing settings + // stay visible and the adapter must not create files. + expect(runSshCommand).not.toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining(".gemini/settings.json"), + expect.anything(), + ); + expect(runSshCommand).not.toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining("gemini-api-key"), + expect.anything(), + ); const call = runChildProcess.mock.calls[0] as unknown as | [string, string, string[], { env: Record; remoteExecution?: { remoteCwd: string } | null }] | undefined; @@ -209,6 +223,77 @@ describe("gemini remote execution", () => { expect(restoreWorkspaceFromSshExecution).toHaveBeenCalledTimes(1); }); + it("pre-selects gemini-api-key auth in the managed HOME for sandbox execution", async () => { + const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-gemini-sandbox-")); + cleanupDirs.push(rootDir); + const workspaceDir = path.join(rootDir, "workspace"); + await mkdir(workspaceDir, { recursive: true }); + + const geminiOutput = [ + JSON.stringify({ type: "system", subtype: "init", session_id: "gemini-session-2", model: "gemini-2.5-pro" }), + JSON.stringify({ type: "message", role: "assistant", content: "hello" }), + JSON.stringify({ + type: "result", + status: "success", + session_id: "gemini-session-2", + stats: { input_tokens: 1, cached_input_tokens: 0, output_tokens: 1 }, + }), + ].join("\n"); + const runnerExecute = vi.fn(async (input: { command: string; args?: string[] }) => ({ + exitCode: 0, + signal: null, + timedOut: false, + stdout: input.command === "gemini" ? geminiOutput : "", + stderr: "", + pid: 321, + startedAt: new Date().toISOString(), + })); + + await execute({ + runId: "run-sandbox-1", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Gemini Builder", + adapterType: "gemini_local", + adapterConfig: {}, + }, + runtime: { + sessionId: null, + sessionParams: null, + sessionDisplayId: null, + taskKey: null, + }, + config: { + command: "gemini", + env: { GEMINI_API_KEY: "test-key" }, + }, + context: { + paperclipWorkspace: { + cwd: workspaceDir, + source: "project_primary", + }, + }, + executionTarget: { + kind: "remote", + transport: "sandbox", + providerKey: "kubernetes", + remoteCwd: "/remote/workspace", + runner: { execute: runnerExecute }, + }, + onLog: async () => {}, + }); + + const runnerScripts = runnerExecute.mock.calls.map( + (call) => `${call[0].command} ${(call[0].args ?? []).join(" ")}`, + ); + const settingsWrite = runnerScripts.find((script) => script.includes(".gemini/settings.json")); + expect(settingsWrite).toBeDefined(); + expect(settingsWrite).toContain("gemini-api-key"); + // The managed HOME lives under the per-run runtime root, never a real home. + expect(settingsWrite).toContain(".paperclip-runtime"); + }); + it("resumes saved Gemini sessions for remote SSH execution only when the identity matches", async () => { const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-gemini-remote-resume-")); cleanupDirs.push(rootDir); diff --git a/packages/adapters/gemini-local/src/server/execute.ts b/packages/adapters/gemini-local/src/server/execute.ts index 12ec7ad0..5431eca5 100644 --- a/packages/adapters/gemini-local/src/server/execute.ts +++ b/packages/adapters/gemini-local/src/server/execute.ts @@ -356,18 +356,22 @@ export async function execute(ctx: AdapterExecutionContext): Promise ${JSON.stringify(remoteSettingsPath)}; }`, + { cwd, env, timeoutSec, graceSec, onLog }, + ); + } } catch (error) { await Promise.allSettled([ restoreRemoteWorkspace?.(),