diff --git a/.github/workflows/smoke-copilot-byok.lock.yml b/.github/workflows/smoke-copilot-byok.lock.yml index f1c844b49..a6caf3369 100644 --- a/.github/workflows/smoke-copilot-byok.lock.yml +++ b/.github/workflows/smoke-copilot-byok.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7916559212a99bf8893059836e4153d4f5e2778d511071f45165c715b7ab3078","body_hash":"83a37ed47106648e7506e9fc56af4158a503c2c9727d3fc1bf26d6ae46e72817","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7984988f7ef609f174e12c21ea1878b4a108861a4efd5c5210112f39a3a474c8","body_hash":"a79a4327270c1ff585874cbc620b23603f319bf43ca84c694cfb554c817e300c","compiler_version":"v0.77.5","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"3ea13c02d765410340d533515cb31a7eef2baaf0","version":"v0.77.5"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -217,20 +217,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_dfee62657b260be6_EOF' + cat << 'GH_AW_PROMPT_829c76c09524d329_EOF' - GH_AW_PROMPT_dfee62657b260be6_EOF + GH_AW_PROMPT_829c76c09524d329_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_dfee62657b260be6_EOF' + cat << 'GH_AW_PROMPT_829c76c09524d329_EOF' Tools: add_comment, add_labels, missing_tool, missing_data, noop - GH_AW_PROMPT_dfee62657b260be6_EOF + GH_AW_PROMPT_829c76c09524d329_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_dfee62657b260be6_EOF' + cat << 'GH_AW_PROMPT_829c76c09524d329_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -259,12 +259,12 @@ jobs: {{/if}} - GH_AW_PROMPT_dfee62657b260be6_EOF + GH_AW_PROMPT_829c76c09524d329_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_dfee62657b260be6_EOF' + cat << 'GH_AW_PROMPT_829c76c09524d329_EOF' {{#runtime-import .github/workflows/smoke-copilot-byok.md}} - GH_AW_PROMPT_dfee62657b260be6_EOF + GH_AW_PROMPT_829c76c09524d329_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -516,9 +516,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_454ca8164c4a31ac_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_99602e5aac9426bf_EOF' {"add_comment":{"hide_older_comments":true,"max":1},"add_labels":{"allowed":["smoke-copilot-byok"]},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_454ca8164c4a31ac_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_99602e5aac9426bf_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -732,7 +732,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_ccd3f0d2f7270137_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_6e1b117ae6debd46_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -773,7 +773,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_ccd3f0d2f7270137_EOF + GH_AW_MCP_CONFIG_6e1b117ae6debd46_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true diff --git a/containers/api-proxy/providers/cloud-oidc-init.js b/containers/api-proxy/providers/cloud-oidc-init.js new file mode 100644 index 000000000..d364d0915 --- /dev/null +++ b/containers/api-proxy/providers/cloud-oidc-init.js @@ -0,0 +1,80 @@ +'use strict'; + +const { OidcTokenProvider } = require('../oidc-token-provider'); + +/** + * Resolve cloud OIDC providers (Azure/AWS/GCP) from environment variables. + * + * @param {Record} env + * @param {{ skipWhen?: boolean }} [options] + * @returns {{ authProvider: string, oidcProvider: any, awsOidcProvider: any, oidcConfigured: boolean }} + */ +function resolveCloudOidcProviders(env, options = {}) { + const { skipWhen = false } = options; + const authType = (env.AWF_AUTH_TYPE || '').trim().toLowerCase(); + const authProvider = (env.AWF_AUTH_PROVIDER || 'azure').trim().toLowerCase(); + let oidcProvider = null; + let awsOidcProvider = null; + + if (authType === 'github-oidc' && !skipWhen) { + const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL; + const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; + + if (requestUrl && requestToken) { + if (authProvider === 'aws') { + const roleArn = env.AWF_AUTH_AWS_ROLE_ARN; + const region = env.AWF_AUTH_AWS_REGION; + if (roleArn && region) { + const { AwsOidcTokenProvider } = require('../aws-oidc-token-provider'); + awsOidcProvider = new AwsOidcTokenProvider({ + requestUrl, + requestToken, + roleArn, + region, + roleSessionName: env.AWF_AUTH_AWS_ROLE_SESSION_NAME, + oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE, + }); + } + } else if (authProvider === 'gcp') { + const workloadIdentityProvider = env.AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER; + if (workloadIdentityProvider) { + const { GcpOidcTokenProvider } = require('../gcp-oidc-token-provider'); + oidcProvider = new GcpOidcTokenProvider({ + requestUrl, + requestToken, + workloadIdentityProvider, + serviceAccount: env.AWF_AUTH_GCP_SERVICE_ACCOUNT, + oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE, + scope: env.AWF_AUTH_GCP_SCOPE, + }); + } + } else { + // Azure (default) + const tenantId = env.AWF_AUTH_AZURE_TENANT_ID; + const clientId = env.AWF_AUTH_AZURE_CLIENT_ID; + if (tenantId && clientId) { + oidcProvider = new OidcTokenProvider({ + requestUrl, + requestToken, + tenantId, + clientId, + oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE || 'api://AzureADTokenExchange', + azureScope: env.AWF_AUTH_AZURE_SCOPE || 'https://cognitiveservices.azure.com/.default', + azureCloud: env.AWF_AUTH_AZURE_CLOUD, + }); + } + } + } + } + + return { + authProvider, + oidcProvider, + awsOidcProvider, + oidcConfigured: !!(oidcProvider || awsOidcProvider), + }; +} + +module.exports = { + resolveCloudOidcProviders, +}; diff --git a/containers/api-proxy/providers/cloud-oidc-init.test.js b/containers/api-proxy/providers/cloud-oidc-init.test.js new file mode 100644 index 000000000..8f5677451 --- /dev/null +++ b/containers/api-proxy/providers/cloud-oidc-init.test.js @@ -0,0 +1,79 @@ +'use strict'; + +const { resolveCloudOidcProviders } = require('./cloud-oidc-init'); + +describe('resolveCloudOidcProviders', () => { + it('returns no providers when github-oidc is not configured', () => { + const result = resolveCloudOidcProviders({}); + expect(result.authProvider).toBe('azure'); + expect(result.oidcProvider).toBeNull(); + expect(result.awsOidcProvider).toBeNull(); + expect(result.oidcConfigured).toBe(false); + }); + + it('supports skipping provider initialization when skipWhen=true', () => { + const result = resolveCloudOidcProviders({ + AWF_AUTH_TYPE: 'github-oidc', + ACTIONS_ID_TOKEN_REQUEST_URL: 'http://localhost/token', + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'test-token', + AWF_AUTH_AZURE_TENANT_ID: 'tenant-uuid', + AWF_AUTH_AZURE_CLIENT_ID: 'client-uuid', + }, { skipWhen: true }); + + expect(result.oidcProvider).toBeNull(); + expect(result.awsOidcProvider).toBeNull(); + expect(result.oidcConfigured).toBe(false); + }); + + it('creates Azure provider by default when configured', () => { + const result = resolveCloudOidcProviders({ + AWF_AUTH_TYPE: 'github-oidc', + ACTIONS_ID_TOKEN_REQUEST_URL: 'http://localhost/token', + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'test-token', + AWF_AUTH_AZURE_TENANT_ID: 'tenant-uuid', + AWF_AUTH_AZURE_CLIENT_ID: 'client-uuid', + }); + + expect(result.authProvider).toBe('azure'); + expect(result.oidcProvider).toBeTruthy(); + expect(result.awsOidcProvider).toBeNull(); + expect(result.oidcConfigured).toBe(true); + + result.oidcProvider.shutdown(); + }); + + it('creates AWS provider when configured', () => { + const result = resolveCloudOidcProviders({ + AWF_AUTH_TYPE: 'github-oidc', + AWF_AUTH_PROVIDER: 'aws', + ACTIONS_ID_TOKEN_REQUEST_URL: 'http://localhost/token', + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'test-token', + AWF_AUTH_AWS_ROLE_ARN: 'arn:aws:iam::123456789012:role/my-role', + AWF_AUTH_AWS_REGION: 'us-east-1', + }); + + expect(result.authProvider).toBe('aws'); + expect(result.oidcProvider).toBeNull(); + expect(result.awsOidcProvider).toBeTruthy(); + expect(result.oidcConfigured).toBe(true); + + result.awsOidcProvider.shutdown(); + }); + + it('creates GCP provider when configured', () => { + const result = resolveCloudOidcProviders({ + AWF_AUTH_TYPE: 'github-oidc', + AWF_AUTH_PROVIDER: 'gcp', + ACTIONS_ID_TOKEN_REQUEST_URL: 'http://localhost/token', + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'test-token', + AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER: 'projects/123/locations/global/workloadIdentityPools/pool/providers/provider', + }); + + expect(result.authProvider).toBe('gcp'); + expect(result.oidcProvider).toBeTruthy(); + expect(result.awsOidcProvider).toBeNull(); + expect(result.oidcConfigured).toBe(true); + + result.oidcProvider.shutdown(); + }); +}); diff --git a/containers/api-proxy/providers/copilot.js b/containers/api-proxy/providers/copilot.js index e8a7d5e21..4db7c26de 100644 --- a/containers/api-proxy/providers/copilot.js +++ b/containers/api-proxy/providers/copilot.js @@ -24,7 +24,6 @@ const { composeBodyTransforms, } = require('../proxy-utils'); const { sanitizeNullToolCallTypes } = require('../body-transform'); -const { OidcTokenProvider } = require('../oidc-token-provider'); const { parseByokExtraHeaders, parseByokExtraBodyFields, @@ -36,6 +35,7 @@ const { resolveCopilotAuthToken, deriveCopilotApiTarget, } = require('./copilot-auth'); +const { resolveCloudOidcProviders } = require('./cloud-oidc-init'); const { URL } = require('url'); /** @@ -59,60 +59,12 @@ function createCopilotAdapter(env, deps = {}) { // adapter's OIDC plumbing so the Copilot CLI's direct-BYOK path can exchange a // GitHub Actions OIDC JWT for an upstream cloud token instead of requiring a // static COPILOT_PROVIDER_API_KEY. - const authType = (env.AWF_AUTH_TYPE || '').trim().toLowerCase(); - const authProvider = (env.AWF_AUTH_PROVIDER || 'azure').trim().toLowerCase(); - let oidcProvider = null; - let awsOidcProvider = null; - if (authType === 'github-oidc' && !staticAuthToken) { - const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL; - const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; - if (requestUrl && requestToken) { - if (authProvider === 'aws') { - const roleArn = env.AWF_AUTH_AWS_ROLE_ARN; - const region = env.AWF_AUTH_AWS_REGION; - if (roleArn && region) { - const { AwsOidcTokenProvider } = require('../aws-oidc-token-provider'); - awsOidcProvider = new AwsOidcTokenProvider({ - requestUrl, - requestToken, - roleArn, - region, - roleSessionName: env.AWF_AUTH_AWS_ROLE_SESSION_NAME, - oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE, - }); - } - } else if (authProvider === 'gcp') { - const workloadIdentityProvider = env.AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER; - if (workloadIdentityProvider) { - const { GcpOidcTokenProvider } = require('../gcp-oidc-token-provider'); - oidcProvider = new GcpOidcTokenProvider({ - requestUrl, - requestToken, - workloadIdentityProvider, - serviceAccount: env.AWF_AUTH_GCP_SERVICE_ACCOUNT, - oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE, - scope: env.AWF_AUTH_GCP_SCOPE, - }); - } - } else { - // Azure (default) - const tenantId = env.AWF_AUTH_AZURE_TENANT_ID; - const clientId = env.AWF_AUTH_AZURE_CLIENT_ID; - if (tenantId && clientId) { - oidcProvider = new OidcTokenProvider({ - requestUrl, - requestToken, - tenantId, - clientId, - oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE || 'api://AzureADTokenExchange', - azureScope: env.AWF_AUTH_AZURE_SCOPE || 'https://cognitiveservices.azure.com/.default', - azureCloud: env.AWF_AUTH_AZURE_CLOUD, - }); - } - } - } - } - const oidcConfigured = !!(oidcProvider || awsOidcProvider); + const { + authProvider, + oidcProvider, + awsOidcProvider, + oidcConfigured, + } = resolveCloudOidcProviders(env, { skipWhen: !!staticAuthToken }); // authToken is consumed by the existing validation/models-fetch/auth-header paths. // For OIDC mode the token isn't available synchronously at construction time, so diff --git a/containers/api-proxy/providers/openai.js b/containers/api-proxy/providers/openai.js index ffec8e0cf..50cc8f7ee 100644 --- a/containers/api-proxy/providers/openai.js +++ b/containers/api-proxy/providers/openai.js @@ -15,7 +15,7 @@ const { createAdapterMethods, normalizeBasePath, } = require('../proxy-utils'); -const { OidcTokenProvider } = require('../oidc-token-provider'); +const { resolveCloudOidcProviders } = require('./cloud-oidc-init'); function parseByokBaseUrl(baseUrl) { if (!baseUrl) return { target: undefined, basePath: '' }; @@ -83,60 +83,7 @@ function createOpenAIAdapter(env, deps = {}) { const bodyTransform = deps.bodyTransform || null; // OIDC auth strategy (Azure OpenAI, AWS Bedrock, GCP Vertex AI) - const authType = (env.AWF_AUTH_TYPE || '').trim().toLowerCase(); - const authProvider = (env.AWF_AUTH_PROVIDER || 'azure').trim().toLowerCase(); - let oidcProvider = null; - let awsOidcProvider = null; - if (authType === 'github-oidc') { - const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL; - const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN; - - if (requestUrl && requestToken) { - if (authProvider === 'aws') { - const roleArn = env.AWF_AUTH_AWS_ROLE_ARN; - const region = env.AWF_AUTH_AWS_REGION; - if (roleArn && region) { - const { AwsOidcTokenProvider } = require('../aws-oidc-token-provider'); - awsOidcProvider = new AwsOidcTokenProvider({ - requestUrl, - requestToken, - roleArn, - region, - roleSessionName: env.AWF_AUTH_AWS_ROLE_SESSION_NAME, - oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE, - }); - } - } else if (authProvider === 'gcp') { - const workloadIdentityProvider = env.AWF_AUTH_GCP_WORKLOAD_IDENTITY_PROVIDER; - if (workloadIdentityProvider) { - const { GcpOidcTokenProvider } = require('../gcp-oidc-token-provider'); - oidcProvider = new GcpOidcTokenProvider({ - requestUrl, - requestToken, - workloadIdentityProvider, - serviceAccount: env.AWF_AUTH_GCP_SERVICE_ACCOUNT, - oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE, - scope: env.AWF_AUTH_GCP_SCOPE, - }); - } - } else { - // Azure (default) - const tenantId = env.AWF_AUTH_AZURE_TENANT_ID; - const clientId = env.AWF_AUTH_AZURE_CLIENT_ID; - if (tenantId && clientId) { - oidcProvider = new OidcTokenProvider({ - requestUrl, - requestToken, - tenantId, - clientId, - oidcAudience: env.AWF_AUTH_OIDC_AUDIENCE || 'api://AzureADTokenExchange', - azureScope: env.AWF_AUTH_AZURE_SCOPE || 'https://cognitiveservices.azure.com/.default', - azureCloud: env.AWF_AUTH_AZURE_CLOUD, - }); - } - } - } - } + const { authProvider, oidcProvider, awsOidcProvider, oidcConfigured } = resolveCloudOidcProviders(env); /** * Build a static-key auth header object. * When AWF_OPENAI_AUTH_HEADER is set, uses that header name with the raw key. @@ -149,7 +96,6 @@ function createOpenAIAdapter(env, deps = {}) { return { 'Authorization': `Bearer ${key}` }; } - const oidcConfigured = !!(oidcProvider || awsOidcProvider); const adapterMethods = createAdapterMethods({ apiKey, rawTarget,