diff --git a/.github/workflows/smoke-copilot-byok-aoai-apikey.lock.yml b/.github/workflows/smoke-copilot-byok-aoai-apikey.lock.yml index e0184001d..27229a04e 100644 --- a/.github/workflows/smoke-copilot-byok-aoai-apikey.lock.yml +++ b/.github/workflows/smoke-copilot-byok-aoai-apikey.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ca03dee0fcab8f11852e5efdfe428b40e17e0356688ce5611f2c9d47b4e29a1a","body_hash":"d03b433f93c7aa088412b66483b24af3c67f3ec8a92e635c7135653a695212dc","compiler_version":"v0.79.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"71b2deaf9af93cf8476c2bf95e1328b4b32057deb5be3feb3bca8df2041dec75","body_hash":"5d5a2fbac5357d7f705076d222c304db207738139975cb7d8b290c551bc90a62","compiler_version":"v0.79.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} # gh-aw-manifest: {"version":1,"secrets":["FOUNDRY_API_KEY","FOUNDRY_OPENAI_ENDPOINT","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"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":"9b1d730701c16b15673633e5696d01677fad9844","version":"v0.79.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.68"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.68"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.68"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -120,6 +120,45 @@ jobs: GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AWF_VERSION: "v0.25.68" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Pre-compute BYOK smoke test data + id: smoke-data + run: | + echo "::group::Verify BYOK configuration" + echo "COPILOT_API_TARGET=${COPILOT_API_TARGET:-derived from COPILOT_PROVIDER_BASE_URL}" + echo "::endgroup::" + + echo "::group::Fetching last 2 merged PRs" + PR_DATA=$(gh pr list --repo "$GITHUB_REPOSITORY" --state merged --limit 2 \ + --json number,title,author,mergedAt \ + --jq '.[] | "PR #\(.number): \(.title) (by @\(.author.login), merged \(.mergedAt))"' \ + || echo "(PR fetch failed)") + echo "$PR_DATA" + echo "::endgroup::" + + echo "::group::GitHub.com connectivity check" + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://github.com || echo "000") + echo "github.com returned HTTP $HTTP_CODE" + echo "::endgroup::" + + echo "::group::File write/read test" + TEST_DIR="/tmp/gh-aw/agent" + TEST_FILE="$TEST_DIR/smoke-test-copilot-byok-aoai-apikey-${GITHUB_RUN_ID}.txt" + mkdir -p "$TEST_DIR" + echo "BYOK AOAI api-key smoke test passed at $(date)" > "$TEST_FILE" + FILE_CONTENT=$(cat "$TEST_FILE") + echo "Wrote and read back: $FILE_CONTENT" + echo "::endgroup::" + + { + echo "SMOKE_PR_DATA<> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ github.token }} - name: Generate agentic run info id: generate_aw_info env: @@ -227,7 +266,6 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_2805DAC9: ${{ steps.smoke-data.outputs.SMOKE_FILE_PATH }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_7EA93000: ${{ steps.smoke-data.outputs.SMOKE_HTTP_CODE }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -298,7 +336,6 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_EXPR_EC16C26C: ${{ steps.smoke-data.outputs.SMOKE_FILE_CONTENT }} - GH_AW_EXPR_2805DAC9: ${{ steps.smoke-data.outputs.SMOKE_FILE_PATH }} GH_AW_EXPR_7EA93000: ${{ steps.smoke-data.outputs.SMOKE_HTTP_CODE }} GH_AW_EXPR_ABDF8D58: ${{ steps.smoke-data.outputs.SMOKE_PR_DATA }} with: @@ -312,7 +349,6 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} - GH_AW_EXPR_2805DAC9: ${{ steps.smoke-data.outputs.SMOKE_FILE_PATH }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_7EA93000: ${{ steps.smoke-data.outputs.SMOKE_HTTP_CODE }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} @@ -336,7 +372,6 @@ jobs: file: process.env.GH_AW_PROMPT, substitutions: { GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, - GH_AW_EXPR_2805DAC9: process.env.GH_AW_EXPR_2805DAC9, GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, GH_AW_EXPR_7EA93000: process.env.GH_AW_EXPR_7EA93000, GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, @@ -445,12 +480,6 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} - - env: - GH_TOKEN: ${{ github.token }} - id: smoke-data - name: Pre-compute BYOK smoke test data - run: "echo \"::group::Verify BYOK configuration\"\necho \"COPILOT_API_TARGET=${COPILOT_API_TARGET:-derived from COPILOT_PROVIDER_BASE_URL}\"\necho \"::endgroup::\"\n\necho \"::group::Fetching last 2 merged PRs\"\nPR_DATA=$(gh pr list --repo \"$GITHUB_REPOSITORY\" --state merged --limit 2 \\\n --json number,title,author,mergedAt \\\n --jq '.[] | \"PR #\\(.number): \\(.title) (by @\\(.author.login), merged \\(.mergedAt))\"' \\\n || echo \"(PR fetch failed)\")\necho \"$PR_DATA\"\necho \"::endgroup::\"\n\necho \"::group::GitHub.com connectivity check\"\nHTTP_CODE=$(curl -s -o /dev/null -w \"%{http_code}\" --max-time 10 https://github.com || echo \"000\")\necho \"github.com returned HTTP $HTTP_CODE\"\necho \"::endgroup::\"\n\necho \"::group::File write/read test\"\nTEST_DIR=\"/tmp/gh-aw/agent\"\nTEST_FILE=\"$TEST_DIR/smoke-test-copilot-byok-aoai-apikey-${GITHUB_RUN_ID}.txt\"\nmkdir -p \"$TEST_DIR\"\necho \"BYOK AOAI api-key smoke test passed at $(date)\" > \"$TEST_FILE\"\nFILE_CONTENT=$(cat \"$TEST_FILE\")\necho \"Wrote and read back: $FILE_CONTENT\"\necho \"::endgroup::\"\n\n{\n echo \"SMOKE_PR_DATA<> \"$GITHUB_OUTPUT\"\n" - - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} diff --git a/.github/workflows/smoke-copilot-byok-aoai-apikey.md b/.github/workflows/smoke-copilot-byok-aoai-apikey.md index 7a21d5fc9..321804a3a 100644 --- a/.github/workflows/smoke-copilot-byok-aoai-apikey.md +++ b/.github/workflows/smoke-copilot-byok-aoai-apikey.md @@ -56,46 +56,48 @@ sandbox: agent: id: awf strict: true -steps: - - name: Pre-compute BYOK smoke test data - id: smoke-data - run: | - echo "::group::Verify BYOK configuration" - echo "COPILOT_API_TARGET=${COPILOT_API_TARGET:-derived from COPILOT_PROVIDER_BASE_URL}" - echo "::endgroup::" - - echo "::group::Fetching last 2 merged PRs" - PR_DATA=$(gh pr list --repo "$GITHUB_REPOSITORY" --state merged --limit 2 \ - --json number,title,author,mergedAt \ - --jq '.[] | "PR #\(.number): \(.title) (by @\(.author.login), merged \(.mergedAt))"' \ - || echo "(PR fetch failed)") - echo "$PR_DATA" - echo "::endgroup::" - - echo "::group::GitHub.com connectivity check" - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://github.com || echo "000") - echo "github.com returned HTTP $HTTP_CODE" - echo "::endgroup::" - - echo "::group::File write/read test" - TEST_DIR="/tmp/gh-aw/agent" - TEST_FILE="$TEST_DIR/smoke-test-copilot-byok-aoai-apikey-${GITHUB_RUN_ID}.txt" - mkdir -p "$TEST_DIR" - echo "BYOK AOAI api-key smoke test passed at $(date)" > "$TEST_FILE" - FILE_CONTENT=$(cat "$TEST_FILE") - echo "Wrote and read back: $FILE_CONTENT" - echo "::endgroup::" - - { - echo "SMOKE_PR_DATA<> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ github.token }} +jobs: + activation: + pre-steps: + - name: Pre-compute BYOK smoke test data + id: smoke-data + run: | + echo "::group::Verify BYOK configuration" + echo "COPILOT_API_TARGET=${COPILOT_API_TARGET:-derived from COPILOT_PROVIDER_BASE_URL}" + echo "::endgroup::" + + echo "::group::Fetching last 2 merged PRs" + PR_DATA=$(gh pr list --repo "$GITHUB_REPOSITORY" --state merged --limit 2 \ + --json number,title,author,mergedAt \ + --jq '.[] | "PR #\(.number): \(.title) (by @\(.author.login), merged \(.mergedAt))"' \ + || echo "(PR fetch failed)") + echo "$PR_DATA" + echo "::endgroup::" + + echo "::group::GitHub.com connectivity check" + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://github.com || echo "000") + echo "github.com returned HTTP $HTTP_CODE" + echo "::endgroup::" + + echo "::group::File write/read test" + TEST_DIR="/tmp/gh-aw/agent" + TEST_FILE="$TEST_DIR/smoke-test-copilot-byok-aoai-apikey-${GITHUB_RUN_ID}.txt" + mkdir -p "$TEST_DIR" + echo "BYOK AOAI api-key smoke test passed at $(date)" > "$TEST_FILE" + FILE_CONTENT=$(cat "$TEST_FILE") + echo "Wrote and read back: $FILE_CONTENT" + echo "::endgroup::" + + { + echo "SMOKE_PR_DATA<> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ github.token }} post-steps: - name: Validate safe outputs were invoked run: | @@ -151,9 +153,7 @@ Pre-step result: HTTP ${{ steps.smoke-data.outputs.SMOKE_HTTP_CODE }} from githu ✅ if HTTP 200 or 301, ❌ otherwise. ### 3. File Write/Read Test -Pre-step wrote and read back: "${{ steps.smoke-data.outputs.SMOKE_FILE_CONTENT }}" -File path: ${{ steps.smoke-data.outputs.SMOKE_FILE_PATH }} -Verify by running `cat` on the file path using bash to confirm it exists. +The activation pre-step wrote and read back: "${{ steps.smoke-data.outputs.SMOKE_FILE_CONTENT }}" (that file lives on the activation runner, not here). To exercise agent-side file I/O in the sandbox, write a short string to `/tmp/gh-aw/agent/agent-write-test.txt` and `cat` it back with bash. ✅ if the read-back matches. ### 4. BYOK Inference Test You are running in direct BYOK mode against Azure OpenAI (Foundry) right now, using `o4-mini-aw` via an api-key. The fact that you can read this prompt and respond means the BYOK inference path (agent → api-proxy sidecar → Foundry endpoint) is working. Confirm ✅. diff --git a/src/commands/validators/config-assembly.test.ts b/src/commands/validators/config-assembly.test.ts index 87a2a149d..0bbfe8140 100644 --- a/src/commands/validators/config-assembly.test.ts +++ b/src/commands/validators/config-assembly.test.ts @@ -823,6 +823,53 @@ describe('config-assembly', () => { ); }); + it('should allow custom COPILOT_MODEL values in BYOK mode with a provider base URL', () => { + mockBuildConfigOnce({ + copilotProviderApiKey: 'byok-api-key-for-azure-foundry', + copilotProviderBaseUrl: 'https://example-resource.openai.azure.com/openai/deployments/o4-mini-aw', + additionalEnv: { COPILOT_MODEL: 'o4-mini-aw' }, + }); + + const agentOptions = createMinimalAgentOptions(); + agentOptions.additionalEnv = { COPILOT_MODEL: 'o4-mini-aw' }; + + const result = assembleAndValidateConfig( + {}, + 'echo test', + createMinimalLogAndLimits(), + createMinimalNetworkOptions(), + agentOptions, + ); + + expect(logger.error).not.toHaveBeenCalled(); + expect(result.additionalEnv?.COPILOT_MODEL).toBe('o4-mini-aw'); + }); + + it('should allow custom COPILOT_MODEL values when provider base URL is set via env file', () => { + const envFilePath = path.join(testDir, 'byok.env'); + fs.writeFileSync(envFilePath, 'COPILOT_PROVIDER_BASE_URL=https://example-resource.openai.azure.com/openai/deployments/o4-mini-aw\n'); + + mockBuildConfigOnce({ + copilotProviderApiKey: 'byok-api-key-for-azure-foundry', + envFile: envFilePath, + additionalEnv: { COPILOT_MODEL: 'o4-mini-aw' }, + }); + + const agentOptions = createMinimalAgentOptions(); + agentOptions.additionalEnv = { COPILOT_MODEL: 'o4-mini-aw' }; + + const result = assembleAndValidateConfig( + {}, + 'echo test', + createMinimalLogAndLimits(), + createMinimalNetworkOptions(), + agentOptions, + ); + + expect(logger.error).not.toHaveBeenCalled(); + expect(result.additionalEnv?.COPILOT_MODEL).toBe('o4-mini-aw'); + }); + it('should log normalization when COPILOT_MODEL casing is adjusted', () => { mockBuildConfigOnce({ copilotGithubToken: 'github_pat_testtoken', diff --git a/src/commands/validators/config-assembly.ts b/src/commands/validators/config-assembly.ts index b70609b14..d29f86167 100644 --- a/src/commands/validators/config-assembly.ts +++ b/src/commands/validators/config-assembly.ts @@ -44,9 +44,10 @@ export function assembleAndValidateConfig( networkOptions: NetworkOptionsResult, agentOptions: AgentOptionsResult, ): WrapperConfig { - const readCopilotModelFromEnvFiles = (envFile: unknown): string | undefined => { + const readEnvVarFromEnvFiles = (envFile: unknown, key: string): string | undefined => { const envFiles = Array.isArray(envFile) ? envFile : envFile ? [envFile] : []; let lastSeen: string | undefined; + const pattern = new RegExp(`^(?:export\\s+)?${key}\\s*=\\s*(.*)$`); for (const candidate of envFiles) { if (typeof candidate !== 'string' || candidate.trim() === '') continue; try { @@ -57,7 +58,7 @@ export function assembleAndValidateConfig( for (const line of envFileContents.split(/\r?\n/)) { const trimmedLine = line.trim(); if (!trimmedLine || trimmedLine.startsWith('#')) continue; - const match = trimmedLine.match(/^(?:export\s+)?COPILOT_MODEL\s*=\s*(.*)$/); + const match = trimmedLine.match(pattern); if (match) { lastSeen = match[1]?.trim() || ''; } @@ -69,6 +70,10 @@ export function assembleAndValidateConfig( return lastSeen; }; + const readCopilotModelFromEnvFiles = (envFile: unknown): string | undefined => { + return readEnvVarFromEnvFiles(envFile, 'COPILOT_MODEL'); + }; + // --- Config assembly ----------------------------------------------------- const config = buildConfig({ @@ -276,7 +281,17 @@ export function assembleAndValidateConfig( logger.warn.bind(logger), ); - if (copilotModel && (config.copilotGithubToken || config.copilotProviderApiKey)) { + const hasCustomCopilotProviderBaseUrl = !!( + config.copilotProviderBaseUrl || + config.additionalEnv?.COPILOT_PROVIDER_BASE_URL || + readEnvVarFromEnvFiles((config as { envFile?: unknown }).envFile, 'COPILOT_PROVIDER_BASE_URL') || + (config.envAll ? process.env.COPILOT_PROVIDER_BASE_URL : undefined) + ); + if ( + copilotModel && + !hasCustomCopilotProviderBaseUrl && + (config.copilotGithubToken || config.copilotProviderApiKey) + ) { const validation = validateCopilotModel(copilotModel); if (!validation.valid) { logger.error(validation.message);