Skip to content

Commit 83ccb65

Browse files
CopilotlpcoxCopilot
authored
fix: include Gemini in api-proxy validation, add 503 fallback (#1808)
* Initial plan * fix: include Gemini in api-proxy validation, add 503 fallback Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/a39432e2-e38c-4850-97ba-ae1596be856c * fix: address review feedback for Gemini api-proxy changes Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/a39432e2-e38c-4850-97ba-ae1596be856c * Update src/docker-manager.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Landon Cox <landon.cox@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent ffd2946 commit 83ccb65

File tree

4 files changed

+60
-9
lines changed

4 files changed

+60
-9
lines changed

containers/api-proxy/server.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,28 @@ if (require.main === module) {
911911
geminiServer.listen(10003, '0.0.0.0', () => {
912912
logRequest('info', 'server_start', { message: 'Google Gemini proxy listening on port 10003', target: GEMINI_API_TARGET });
913913
});
914+
} else {
915+
// No Gemini key — listen on port 10003 and return 503 so the Gemini CLI
916+
// gets an actionable error instead of a silent connection-refused.
917+
const geminiServer = http.createServer((req, res) => {
918+
if (req.url === '/health' && req.method === 'GET') {
919+
res.writeHead(503, { 'Content-Type': 'application/json' });
920+
res.end(JSON.stringify({ status: 'not_configured', service: 'gemini-proxy', error: 'GEMINI_API_KEY not configured in api-proxy sidecar' }));
921+
return;
922+
}
923+
924+
res.writeHead(503, { 'Content-Type': 'application/json' });
925+
res.end(JSON.stringify({ error: 'Gemini proxy not configured (no GEMINI_API_KEY). Set GEMINI_API_KEY in the AWF runner environment to enable credential isolation.' }));
926+
});
927+
928+
geminiServer.on('upgrade', (req, socket) => {
929+
socket.write('HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\n\r\n');
930+
socket.destroy();
931+
});
932+
933+
geminiServer.listen(10003, '0.0.0.0', () => {
934+
logRequest('info', 'server_start', { message: 'Gemini endpoint listening on port 10003 (Gemini not configured — returning 503)' });
935+
});
914936
}
915937

916938
// OpenCode API proxy (port 10004) — routes to Anthropic (default BYOK provider)

src/cli.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,7 @@ describe('cli', () => {
13971397
expect(result.enabled).toBe(true);
13981398
expect(result.warnings).toHaveLength(2);
13991399
expect(result.warnings[0]).toContain('no API keys found');
1400+
expect(result.warnings[1]).toContain('GEMINI_API_KEY');
14001401
expect(result.debugMessages).toEqual([]);
14011402
});
14021403

@@ -1430,14 +1431,23 @@ describe('cli', () => {
14301431
expect(result.debugMessages[0]).toContain('Copilot');
14311432
});
14321433

1433-
it('should detect all three keys', () => {
1434-
const result = validateApiProxyConfig(true, true, true, true);
1434+
it('should detect Gemini key', () => {
1435+
const result = validateApiProxyConfig(true, false, false, false, true);
14351436
expect(result.enabled).toBe(true);
14361437
expect(result.warnings).toEqual([]);
1437-
expect(result.debugMessages).toHaveLength(3);
1438+
expect(result.debugMessages).toHaveLength(1);
1439+
expect(result.debugMessages[0]).toContain('Gemini');
1440+
});
1441+
1442+
it('should detect all four keys', () => {
1443+
const result = validateApiProxyConfig(true, true, true, true, true);
1444+
expect(result.enabled).toBe(true);
1445+
expect(result.warnings).toEqual([]);
1446+
expect(result.debugMessages).toHaveLength(4);
14381447
expect(result.debugMessages[0]).toContain('OpenAI');
14391448
expect(result.debugMessages[1]).toContain('Anthropic');
14401449
expect(result.debugMessages[2]).toContain('Copilot');
1450+
expect(result.debugMessages[3]).toContain('Gemini');
14411451
});
14421452

14431453
it('should not warn when disabled even with keys', () => {
@@ -1446,6 +1456,15 @@ describe('cli', () => {
14461456
expect(result.warnings).toEqual([]);
14471457
expect(result.debugMessages).toEqual([]);
14481458
});
1459+
1460+
it('should detect mixed key combination (OpenAI + Gemini)', () => {
1461+
const result = validateApiProxyConfig(true, true, false, false, true);
1462+
expect(result.enabled).toBe(true);
1463+
expect(result.warnings).toEqual([]);
1464+
expect(result.debugMessages).toHaveLength(2);
1465+
expect(result.debugMessages[0]).toContain('OpenAI');
1466+
expect(result.debugMessages[1]).toContain('Gemini');
1467+
});
14491468
});
14501469

14511470
describe('buildRateLimitConfig', () => {

src/cli.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,15 @@ export interface ApiProxyValidationResult {
277277
* @param hasOpenaiKey - Whether an OpenAI API key is present
278278
* @param hasAnthropicKey - Whether an Anthropic API key is present
279279
* @param hasCopilotKey - Whether a GitHub Copilot API key is present
280+
* @param hasGeminiKey - Whether a Google Gemini API key is present
280281
* @returns ApiProxyValidationResult with warnings and debug messages
281282
*/
282283
export function validateApiProxyConfig(
283284
enableApiProxy: boolean,
284285
hasOpenaiKey?: boolean,
285286
hasAnthropicKey?: boolean,
286-
hasCopilotKey?: boolean
287+
hasCopilotKey?: boolean,
288+
hasGeminiKey?: boolean
287289
): ApiProxyValidationResult {
288290
if (!enableApiProxy) {
289291
return { enabled: false, warnings: [], debugMessages: [] };
@@ -292,9 +294,9 @@ export function validateApiProxyConfig(
292294
const warnings: string[] = [];
293295
const debugMessages: string[] = [];
294296

295-
if (!hasOpenaiKey && !hasAnthropicKey && !hasCopilotKey) {
297+
if (!hasOpenaiKey && !hasAnthropicKey && !hasCopilotKey && !hasGeminiKey) {
296298
warnings.push('⚠️ API proxy enabled but no API keys found in environment');
297-
warnings.push(' Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or COPILOT_GITHUB_TOKEN to use the proxy');
299+
warnings.push(' Set OPENAI_API_KEY, ANTHROPIC_API_KEY, COPILOT_GITHUB_TOKEN, or GEMINI_API_KEY to use the proxy');
298300
}
299301
if (hasOpenaiKey) {
300302
debugMessages.push('OpenAI API key detected - will be held securely in sidecar');
@@ -305,6 +307,9 @@ export function validateApiProxyConfig(
305307
if (hasCopilotKey) {
306308
debugMessages.push('GitHub Copilot API key detected - will be held securely in sidecar');
307309
}
310+
if (hasGeminiKey) {
311+
debugMessages.push('Google Gemini API key detected - will be held securely in sidecar');
312+
}
308313

309314
return { enabled: true, warnings, debugMessages };
310315
}
@@ -1912,12 +1917,13 @@ program
19121917
config.enableApiProxy || false,
19131918
!!config.openaiApiKey,
19141919
!!config.anthropicApiKey,
1915-
!!config.copilotGithubToken
1920+
!!config.copilotGithubToken,
1921+
!!config.geminiApiKey
19161922
);
19171923

19181924
// Log API proxy status at info level for visibility
19191925
if (config.enableApiProxy) {
1920-
logger.info(`API proxy enabled: OpenAI=${!!config.openaiApiKey}, Anthropic=${!!config.anthropicApiKey}, Copilot=${!!config.copilotGithubToken}`);
1926+
logger.info(`API proxy enabled: OpenAI=${!!config.openaiApiKey}, Anthropic=${!!config.anthropicApiKey}, Copilot=${!!config.copilotGithubToken}, Gemini=${!!config.geminiApiKey}`);
19211927
}
19221928

19231929
for (const warning of apiProxyValidation.warnings) {
@@ -1937,7 +1943,7 @@ program
19371943
// to prevent sensitive data from flowing to logger (CodeQL sensitive data logging)
19381944
const redactedConfig: Record<string, unknown> = {};
19391945
for (const [key, value] of Object.entries(config)) {
1940-
if (key === 'openaiApiKey' || key === 'anthropicApiKey' || key === 'copilotGithubToken') continue;
1946+
if (key === 'openaiApiKey' || key === 'anthropicApiKey' || key === 'copilotGithubToken' || key === 'geminiApiKey') continue;
19411947
redactedConfig[key] = key === 'agentCommand' ? redactSecrets(value as string) : value;
19421948
}
19431949
logger.debug('Configuration:', JSON.stringify(redactedConfig, null, 2));

src/docker-manager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,6 +1612,10 @@ export function generateDockerCompose(
16121612
// Real authentication happens via GEMINI_API_BASE_URL pointing to api-proxy.
16131613
environment.GEMINI_API_KEY = 'gemini-api-key-placeholder-for-credential-isolation';
16141614
logger.debug('GEMINI_API_KEY set to placeholder value for credential isolation');
1615+
} else {
1616+
logger.warn('--enable-api-proxy is active but GEMINI_API_KEY is not set.');
1617+
logger.warn(` The api-proxy Gemini listener (port ${API_PROXY_PORTS.GEMINI}) will start in fallback mode and return 503 responses until GEMINI_API_KEY is set.`);
1618+
logger.warn(' Set GEMINI_API_KEY in the AWF runner environment to enable Gemini credential isolation.');
16151619
}
16161620

16171621
logger.info('API proxy sidecar enabled - API keys will be held securely in sidecar container');

0 commit comments

Comments
 (0)