From 0a2935dbe181249bf190a2b590bafcdaf9c60cec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Aug 2026 15:53:35 +0000 Subject: [PATCH 1/3] Initial plan From 58dbd7fd7fcc2e86afe57eb28251aef840144f03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:06:27 +0000 Subject: [PATCH 2/3] refactor(api-proxy): spec-drive Google provider adapters --- containers/api-proxy/providers/gemini.js | 29 ++------- .../api-proxy/providers/google-adapter.js | 36 ++++++++++- .../providers/google-adapter.test.js | 63 +++++++++++++++++++ .../providers/google-provider-specs.js | 58 +++++++++++++++++ containers/api-proxy/providers/vertex.js | 17 ++--- docs/authentication-architecture.md | 2 +- 6 files changed, 165 insertions(+), 40 deletions(-) create mode 100644 containers/api-proxy/providers/google-adapter.test.js create mode 100644 containers/api-proxy/providers/google-provider-specs.js diff --git a/containers/api-proxy/providers/gemini.js b/containers/api-proxy/providers/gemini.js index cebbdba11..3398d39b7 100644 --- a/containers/api-proxy/providers/gemini.js +++ b/containers/api-proxy/providers/gemini.js @@ -11,11 +11,11 @@ * * URL transform: strips ?key=, ?apiKey=, ?api_key= query params that some * Gemini SDK versions append alongside the header. + * + * All configuration lives in GOOGLE_PROVIDER_SPECS.gemini (google-provider-specs.js). */ -const { stripGeminiKeyParam } = require('../proxy-utils'); -const { GEMINI_ENV } = require('../provider-env-constants'); -const { createGoogleApiKeyAdapter } = require('./google-adapter'); +const { createGoogleProviderAdapter } = require('./google-adapter'); /** * Create the Google Gemini provider adapter. @@ -25,28 +25,7 @@ const { createGoogleApiKeyAdapter } = require('./google-adapter'); * @returns {import('./index').ProviderAdapter} */ function createGeminiAdapter(env, deps = {}) { - return createGoogleApiKeyAdapter(env, deps, { - name: 'gemini', - port: 10003, - envConstants: GEMINI_ENV, - defaultTarget: 'generativelanguage.googleapis.com', - validationPath: '/v1beta/models', - modelsPath: '/v1beta/models', - healthServiceName: 'awf-api-proxy-gemini', - unconfiguredErrorMessage: 'Gemini proxy not configured (no GEMINI_API_KEY). Set GEMINI_API_KEY in the AWF runner environment to enable credential isolation.', - healthErrorMessage: 'GEMINI_API_KEY not configured in api-proxy sidecar', - /** - * Strip Gemini SDK auth query parameters before forwarding. - * The SDK injects ?key= (or ?apiKey=, ?api_key=) alongside the header; - * forwarding both causes API_KEY_INVALID errors on the upstream. - * - * @param {string} url - * @returns {string} - */ - transformRequestUrl(url) { - return stripGeminiKeyParam(url); - }, - }); + return createGoogleProviderAdapter('gemini', env, deps); } module.exports = { createGeminiAdapter }; diff --git a/containers/api-proxy/providers/google-adapter.js b/containers/api-proxy/providers/google-adapter.js index 272a424a9..d82a51465 100644 --- a/containers/api-proxy/providers/google-adapter.js +++ b/containers/api-proxy/providers/google-adapter.js @@ -13,6 +13,7 @@ const { makeUnconfiguredHealthResponse } = require('../proxy-utils'); const { createProviderAuthScaffold, createAdapterMethods, buildProviderAdapter } = require('../adapter-factory'); const { providerKeyHeaders } = require('./auth-headers'); +const { GOOGLE_PROVIDER_SPECS } = require('./google-provider-specs'); /** * Create a Google API-key–based provider adapter. @@ -92,4 +93,37 @@ function createGoogleApiKeyAdapter(env, deps = {}, opts) { }); } -module.exports = { createGoogleApiKeyAdapter }; +/** + * Create a Google provider adapter from its declarative spec. + * + * Error/health messaging is derived from the spec so each Google-family + * provider only needs a config entry in GOOGLE_PROVIDER_SPECS. + * + * @param {string} providerKey - Key into GOOGLE_PROVIDER_SPECS (e.g. 'gemini') + * @param {Record} env - Environment variables + * @param {{ bodyTransform?: ((body: Buffer) => (Buffer | null | Promise))|null }} [deps={}] - Injected dependencies + * @returns {import('./index').ProviderAdapter} + */ +function createGoogleProviderAdapter(providerKey, env, deps = {}) { + const spec = GOOGLE_PROVIDER_SPECS[providerKey]; + if (!spec) { + throw new Error(`Unknown Google provider spec: ${providerKey}`); + } + + const keyEnvVar = spec.envConstants.KEY; + + return createGoogleApiKeyAdapter(env, deps, { + name: spec.name, + port: spec.port, + envConstants: spec.envConstants, + defaultTarget: spec.defaultTarget, + validationPath: spec.validationPath, + modelsPath: spec.modelsPath, + healthServiceName: `awf-api-proxy-${spec.name}`, + unconfiguredErrorMessage: `${spec.label} proxy not configured (no ${keyEnvVar}). Set ${keyEnvVar} in the AWF runner environment to enable credential isolation.`, + healthErrorMessage: `${keyEnvVar} not configured in api-proxy sidecar`, + ...(spec.transformRequestUrl !== undefined ? { transformRequestUrl: spec.transformRequestUrl } : {}), + }); +} + +module.exports = { createGoogleApiKeyAdapter, createGoogleProviderAdapter }; diff --git a/containers/api-proxy/providers/google-adapter.test.js b/containers/api-proxy/providers/google-adapter.test.js new file mode 100644 index 000000000..7406ed86d --- /dev/null +++ b/containers/api-proxy/providers/google-adapter.test.js @@ -0,0 +1,63 @@ +'use strict'; + +const { createGoogleProviderAdapter } = require('./google-adapter'); +const { GOOGLE_PROVIDER_SPECS } = require('./google-provider-specs'); +const { createGeminiAdapter } = require('./gemini'); +const { createVertexAdapter } = require('./vertex'); + +describe('createGoogleProviderAdapter', () => { + it('throws for an unknown provider key', () => { + expect(() => createGoogleProviderAdapter('bogus', {})).toThrow(/Unknown Google provider spec: bogus/); + }); + + it('derives gemini ports, targets and messages from the spec', () => { + const adapter = createGoogleProviderAdapter('gemini', {}); + expect(adapter.name).toBe('gemini'); + expect(adapter.port).toBe(10003); + expect(adapter.isEnabled()).toBe(false); + expect(adapter.getUnconfiguredResponse()).toEqual({ + statusCode: 503, + body: { error: 'Gemini proxy not configured (no GEMINI_API_KEY). Set GEMINI_API_KEY in the AWF runner environment to enable credential isolation.' }, + }); + expect(adapter.getUnconfiguredHealthResponse().body).toMatchObject({ + service: 'awf-api-proxy-gemini', + error: 'GEMINI_API_KEY not configured in api-proxy sidecar', + }); + }); + + it('derives vertex ports, targets and messages from the spec', () => { + const adapter = createGoogleProviderAdapter('vertex', {}); + expect(adapter.name).toBe('vertex'); + expect(adapter.port).toBe(10004); + expect(adapter.getUnconfiguredResponse()).toEqual({ + statusCode: 503, + body: { error: 'Vertex AI proxy not configured (no GOOGLE_API_KEY). Set GOOGLE_API_KEY in the AWF runner environment to enable credential isolation.' }, + }); + expect(adapter.getUnconfiguredHealthResponse().body).toMatchObject({ + service: 'awf-api-proxy-vertex', + error: 'GOOGLE_API_KEY not configured in api-proxy sidecar', + }); + }); + + it('applies the gemini URL transform and omits it for vertex', () => { + const gemini = createGeminiAdapter({ GEMINI_API_KEY: 'k' }); + const vertex = createVertexAdapter({ GOOGLE_API_KEY: 'k' }); + expect(gemini.transformRequestUrl('/v1beta/models?key=secret')).toBe('/v1beta/models'); + expect(vertex.transformRequestUrl).toBeUndefined(); + }); + + it('exposes a models fetch config only when the spec defines a models path', () => { + const gemini = createGeminiAdapter({ GEMINI_API_KEY: 'k' }); + const vertex = createVertexAdapter({ GOOGLE_API_KEY: 'k' }); + expect(gemini.getModelsFetchConfig()).toMatchObject({ + url: 'https://generativelanguage.googleapis.com/v1beta/models', + }); + expect(vertex.getModelsFetchConfig()).toBeNull(); + expect(GOOGLE_PROVIDER_SPECS.vertex.modelsPath).toBeNull(); + }); + + it('authenticates both providers with the x-goog-api-key header', () => { + expect(createGeminiAdapter({ GEMINI_API_KEY: 'g' }).getAuthHeaders()).toEqual({ 'x-goog-api-key': 'g' }); + expect(createVertexAdapter({ GOOGLE_API_KEY: 'v' }).getAuthHeaders()).toEqual({ 'x-goog-api-key': 'v' }); + }); +}); diff --git a/containers/api-proxy/providers/google-provider-specs.js b/containers/api-proxy/providers/google-provider-specs.js new file mode 100644 index 000000000..c6b7ecbb3 --- /dev/null +++ b/containers/api-proxy/providers/google-provider-specs.js @@ -0,0 +1,58 @@ +'use strict'; + +/** + * Declarative specs for the Google API-key–based providers (Gemini, Vertex AI). + * + * Adding another Google-backed provider should be a single entry here plus a + * one-line wrapper module, instead of another near-clone adapter wrapper. + */ + +const { stripGeminiKeyParam } = require('../proxy-utils'); +const { GEMINI_ENV, VERTEX_ENV } = require('../provider-env-constants'); + +/** + * @typedef {Object} GoogleProviderSpec + * @property {string} name - Provider slug (e.g. 'gemini') + * @property {string} label - Human-readable name used in error messages + * @property {number} port - Proxy port + * @property {{ KEY: string, TARGET: string, BASE_PATH: string }} envConstants - Env var name constants + * @property {string} defaultTarget - Default upstream hostname + * @property {string} validationPath - URL path for the health/validation probe + * @property {string|null} modelsPath - URL path for models fetch, or null if unsupported + * @property {((url: string) => string)} [transformRequestUrl] - Optional URL transformer + */ + +/** @type {Record} */ +const GOOGLE_PROVIDER_SPECS = { + gemini: { + name: 'gemini', + label: 'Gemini', + port: 10003, + envConstants: GEMINI_ENV, + defaultTarget: 'generativelanguage.googleapis.com', + validationPath: '/v1beta/models', + modelsPath: '/v1beta/models', + /** + * Strip Gemini SDK auth query parameters before forwarding. + * The SDK injects ?key= (or ?apiKey=, ?api_key=) alongside the header; + * forwarding both causes API_KEY_INVALID errors on the upstream. + * + * @param {string} url + * @returns {string} + */ + transformRequestUrl(url) { + return stripGeminiKeyParam(url); + }, + }, + vertex: { + name: 'vertex', + label: 'Vertex AI', + port: 10004, + envConstants: VERTEX_ENV, + defaultTarget: 'aiplatform.googleapis.com', + validationPath: '/v1/projects', + modelsPath: null, + }, +}; + +module.exports = { GOOGLE_PROVIDER_SPECS }; diff --git a/containers/api-proxy/providers/vertex.js b/containers/api-proxy/providers/vertex.js index eb0f47d9e..ad6d8ee70 100644 --- a/containers/api-proxy/providers/vertex.js +++ b/containers/api-proxy/providers/vertex.js @@ -13,10 +13,11 @@ * (i.e. GOOGLE_GENAI_USE_VERTEXAI=true). Setting GOOGLE_VERTEX_BASE_URL routes * all Vertex AI traffic through the api-proxy sidecar instead of calling * aiplatform.googleapis.com directly, enabling credential isolation. + * + * All configuration lives in GOOGLE_PROVIDER_SPECS.vertex (google-provider-specs.js). */ -const { VERTEX_ENV } = require('../provider-env-constants'); -const { createGoogleApiKeyAdapter } = require('./google-adapter'); +const { createGoogleProviderAdapter } = require('./google-adapter'); /** * Create the Google Vertex AI provider adapter. @@ -26,17 +27,7 @@ const { createGoogleApiKeyAdapter } = require('./google-adapter'); * @returns {import('./index').ProviderAdapter} */ function createVertexAdapter(env, deps = {}) { - return createGoogleApiKeyAdapter(env, deps, { - name: 'vertex', - port: 10004, - envConstants: VERTEX_ENV, - defaultTarget: 'aiplatform.googleapis.com', - validationPath: '/v1/projects', - modelsPath: null, - healthServiceName: 'awf-api-proxy-vertex', - unconfiguredErrorMessage: 'Vertex AI proxy not configured (no GOOGLE_API_KEY). Set GOOGLE_API_KEY in the AWF runner environment to enable credential isolation.', - healthErrorMessage: 'GOOGLE_API_KEY not configured in api-proxy sidecar', - }); + return createGoogleProviderAdapter('vertex', env, deps); } module.exports = { createVertexAdapter }; diff --git a/docs/authentication-architecture.md b/docs/authentication-architecture.md index 05a3700b9..7d96519d6 100644 --- a/docs/authentication-architecture.md +++ b/docs/authentication-architecture.md @@ -790,7 +790,7 @@ OIDC authentication is configured via `apiProxy.auth` in the AWF config file or | `containers/api-proxy/providers/openai.js` | OpenAI adapter — selects OIDC provider based on `AWF_AUTH_PROVIDER` | | `containers/api-proxy/providers/anthropic.js` | Anthropic adapter — static `x-api-key` or WIF `Authorization: Bearer` | | `containers/api-proxy/providers/copilot.js`, `copilot-auth.js`, `copilot-byok.js` | Copilot adapter — GitHub token, BYOK, and OIDC handling, `token`/`Bearer` prefix logic | -| `containers/api-proxy/providers/gemini.js`, `vertex.js`, `google-adapter.js` | Gemini and Vertex AI adapters — static `x-goog-api-key` only, no OIDC | +| `containers/api-proxy/providers/gemini.js`, `vertex.js`, `google-adapter.js`, `google-provider-specs.js` | Gemini and Vertex AI adapters (declarative specs) — static `x-goog-api-key` only, no OIDC | | `containers/agent/setup-iptables.sh` | iptables rules for api-proxy routing | | `containers/agent/entrypoint.sh` | Entrypoint token cleanup, capability drop | | `containers/agent/api-proxy-health-check.sh` | Pre-flight credential isolation verification | From 61e1a868bc08e181f797f38562a16efb7bc7ee30 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Sun, 9 Aug 2026 09:40:57 -0700 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- containers/api-proxy/providers/google-provider-specs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/containers/api-proxy/providers/google-provider-specs.js b/containers/api-proxy/providers/google-provider-specs.js index c6b7ecbb3..3691956b7 100644 --- a/containers/api-proxy/providers/google-provider-specs.js +++ b/containers/api-proxy/providers/google-provider-specs.js @@ -3,8 +3,8 @@ /** * Declarative specs for the Google API-key–based providers (Gemini, Vertex AI). * - * Adding another Google-backed provider should be a single entry here plus a - * one-line wrapper module, instead of another near-clone adapter wrapper. + * Centralize Google-backed provider adapter settings here and keep each wrapper + * thin. New providers must also follow the registration and wiring checklist in ADDING-A-PROVIDER.md. */ const { stripGeminiKeyParam } = require('../proxy-utils');