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,