From 91f076725786ca5fa34b32f4c863471a366ec81e Mon Sep 17 00:00:00 2001 From: Shakker Date: Mon, 15 Jun 2026 19:54:16 +0100 Subject: [PATCH] fix: keep CLI prepare credentials private --- src/agents/cli-runner/prepare.test.ts | 64 +++++++++++++++++++++++++++ src/agents/cli-runner/prepare.ts | 22 ++++++--- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/agents/cli-runner/prepare.test.ts b/src/agents/cli-runner/prepare.test.ts index 3218469eb238..73c8e748e8dd 100644 --- a/src/agents/cli-runner/prepare.test.ts +++ b/src/agents/cli-runner/prepare.test.ts @@ -412,6 +412,70 @@ describe("shouldSkipLocalCliCredentialEpoch", () => { } }); + it("does not expose auth profile credentials to non-Gemini CLI prepare hooks", async () => { + const { dir, sessionFile } = createSessionFile(); + const agentDir = path.join(dir, "agents", "main", "agent"); + const authProfileId = "test-cli:secret"; + const prepareExecution = vi.fn(async () => undefined); + fs.mkdirSync(agentDir, { recursive: true }); + saveAuthProfileStore( + { + version: 1, + profiles: { + [authProfileId]: { + type: "api_key", + provider: "test-cli", + key: "secret-key", + }, + }, + }, + agentDir, + ); + cliBackendsTesting.setDepsForTest({ + resolvePluginSetupCliBackend: () => undefined, + resolveRuntimeCliBackends: () => [ + { + id: "test-cli", + pluginId: "test-plugin", + bundleMcp: false, + prepareExecution, + config: { + command: "test-cli", + args: ["--prompt", "{prompt}"], + output: "json", + input: "arg", + sessionMode: "existing", + }, + }, + ], + }); + + try { + await prepareCliRunContext({ + sessionId: "session-test", + sessionKey: "agent:main:main", + sessionFile, + workspaceDir: dir, + prompt: "latest ask", + provider: "test-cli", + model: "test-model", + timeoutMs: 1_000, + runId: "run-test-non-gemini-credential-boundary", + authProfileId, + config: {}, + }); + + expect(prepareExecution).toHaveBeenCalledWith( + expect.objectContaining({ + authProfileId, + }), + ); + expect(prepareExecution.mock.calls[0]?.[0]).not.toHaveProperty("authCredential"); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + it("prepares side questions without agent-turn context, tools, hooks, or reusable sessions", async () => { const { dir, sessionFile } = createSessionFile(); appendTranscriptEntry(sessionFile, { diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts index 41870f3a80eb..bd53674f2664 100644 --- a/src/agents/cli-runner/prepare.ts +++ b/src/agents/cli-runner/prepare.ts @@ -463,20 +463,28 @@ export async function prepareCliRunContext( : undefined, warn: (message) => cliBackendLog.warn(message), }); - const preparedExecution = await backendResolved.prepareExecution?.({ + const prepareExecutionContext = { config: params.config, workspaceDir, agentDir, provider: params.provider, modelId, authProfileId: effectiveAuthProfileId, - // Private bridge for bundled Gemini CLI. This is intentionally not part - // of the public Plugin SDK until a credential-forwarding contract exists. - authCredential, executionMode, - } as Parameters>[0] & { - authCredential?: AuthProfileCredential; - }); + } as Parameters>[0]; + const preparedExecution = await backendResolved.prepareExecution?.( + (backendResolved.id === "google-gemini-cli" + ? { + ...prepareExecutionContext, + // Private bridge for bundled Gemini CLI. This is intentionally not + // part of the public Plugin SDK until a credential-forwarding + // contract exists. + authCredential, + } + : prepareExecutionContext) as typeof prepareExecutionContext & { + authCredential?: AuthProfileCredential; + }, + ); const skipLocalCredentialEpoch = shouldSkipLocalCliCredentialEpoch({ authEpochMode: backendResolved.authEpochMode, authProfileId: effectiveAuthProfileId,