From 7a03a0202ac43035961b006f6688a4dc3886b5a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 16:54:43 +0000 Subject: [PATCH 1/3] Initial plan From 43db74ce34aba7e104cbfb3913fed54ca7bcdece Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:02:53 +0000 Subject: [PATCH 2/3] fix: warn when Copilot offline mode disables native web tools Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- docs/api-proxy-sidecar.md | 2 +- docs/troubleshooting.md | 46 +++++++++++++++++++ .../copilot-credential-env.test.ts | 39 ++++++++++++++++ .../credentials/copilot-credential-env.ts | 8 ++++ 4 files changed, 94 insertions(+), 1 deletion(-) diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index b0c421022..44b5d08ce 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -204,7 +204,7 @@ The agent container receives **redacted placeholders** and proxy URLs: | `COPILOT_API_URL` | `http://172.30.0.30:10002` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | Redirects Copilot CLI to sidecar | | `COPILOT_TOKEN` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | Placeholder token (real auth via API_URL) | | `COPILOT_GITHUB_TOKEN` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_GITHUB_TOKEN` provided to host | Placeholder token protected by one-shot-token (real token in sidecar) | -| `COPILOT_OFFLINE` | `true` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | Enables offline+BYOK mode (skips GitHub OAuth handshake) | +| `COPILOT_OFFLINE` | `true` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | Enables offline+BYOK mode (skips GitHub OAuth handshake); disables native `web_fetch` / `web_search`. See the [curl workaround](troubleshooting.md#copilot-web-tools-unavailable-in-api-proxy-mode). | | `COPILOT_PROVIDER_BASE_URL` | `http://172.30.0.30:10002` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | Points Copilot CLI BYOK provider at sidecar (real upstream URL, if any, held in sidecar) | | `COPILOT_PROVIDER_API_KEY` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | BYOK provider API key placeholder (real key in sidecar) | | `GOOGLE_GEMINI_BASE_URL` | `http://172.30.0.30:10003` | `GEMINI_API_KEY` or GCP OIDC configured | Redirects Gemini CLI to proxy (primary var read by Gemini CLI) | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 0ac04ff1a..14e2373b7 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -26,6 +26,52 @@ sudo grep "TCP_DENIED" /tmp/squid-logs-/access.log ``` +### Copilot Web Tools Unavailable in API Proxy Mode + +**Problem:** With `engine: copilot`, `tools: web-fetch:` or `tools: web-search:` +compiles successfully, but Copilot never exposes `web_fetch` or `web_search`. +Squid sees no requests to the expected domains, even when they are allowlisted. + +**Cause:** AWF's Copilot API proxy flow sets `COPILOT_OFFLINE=true` and points +`COPILOT_PROVIDER_BASE_URL` at the sidecar (by default, `http://172.30.0.30:10002`). +Offline mode skips GitHub authentication, keeping real credentials exclusively in +the sidecar, but Copilot CLI also disables its native web tools. `--allow-tool +web_fetch` / `--allow-tool web_search` cannot enable tools absent from the catalog. +AWF warns about this limitation when Copilot sidecar routing is active and the +agent domain allowlist is non-empty. + +**Workaround:** Ask the agent to fetch content using `curl` from bash instead. +Allow both the shell tool and the destination URL in Copilot CLI, as well as the +domain in AWF. In a gh-aw workflow, use: + +```yaml +engine: + id: copilot + args: ["--allow-url", "osv.dev"] +network: + allowed: ["osv.dev"] +tools: + bash: ["curl"] +``` + +Prompt the agent to run `curl --fail --location https://osv.dev/`. Merge these +settings with the workflow's existing tools and network allowlist, and allow any +redirect destinations that are needed. Under `--no-ask-user`, shell permission +alone is insufficient: without URL permission, Copilot reports +`Permission denied and could not request permission from user`. + +Alternatively, use `engine.args: ["--allow-all-urls"]` to approve URLs at the +Copilot CLI layer. Neither `--allow-url` nor `--allow-all-urls` bypasses AWF: +Squid still enforces `network.allowed` (or `--allow-domains` for direct AWF use). +This workaround fetches known URLs; it does not restore native web search. +Do not disable offline mode or expose real credentials to the agent to work +around this limitation. + +Compiler-side warning or an MCP fetch fallback is tracked in +[github/gh-aw#65043](https://github.com/github/gh-aw/issues/65043). +AWF receives the command and domain allowlist, not gh-aw's `tools` declarations, +so its startup warning cannot identify which native web tools were requested. + ## Container Issues ### Container Won't Start diff --git a/src/services/credentials/copilot-credential-env.test.ts b/src/services/credentials/copilot-credential-env.test.ts index ca8b88f0c..2af1ebfdf 100644 --- a/src/services/credentials/copilot-credential-env.test.ts +++ b/src/services/credentials/copilot-credential-env.test.ts @@ -11,6 +11,7 @@ import { buildCopilotCredentialEnv } from './copilot-credential-env'; import type { WrapperConfig } from '../../types'; import { getConfigEnvValue } from '../../env-utils'; import { COPILOT_PLACEHOLDER_TOKEN } from '../../constants/placeholders'; +import { logger } from '../../logger'; const mockGetConfigEnvValue = getConfigEnvValue as jest.MockedFunction; @@ -31,6 +32,44 @@ describe('buildCopilotCredentialEnv', () => { expect(result).toEqual({}); }); + it.each([ + { copilotGithubToken: 'ghu_token' }, + { copilotProviderApiKey: 'provider-key' }, + { copilotProviderBaseUrl: 'https://provider.example.com/v1' }, + { additionalEnv: { COPILOT_PROVIDER_API_KEY: 'provider-key' } }, + { additionalEnv: { COPILOT_PROVIDER_BASE_URL: 'https://provider.example.com/v1' } }, + ])('warns about offline web tools for a domain allowlist with %p', (copilotConfig) => { + const config = { ...baseConfig, ...copilotConfig, allowedDomains: ['osv.dev'] } as WrapperConfig; + mockGetConfigEnvValue.mockImplementation((config, key) => { + if (key === 'COPILOT_PROVIDER_API_KEY') return config.additionalEnv?.COPILOT_PROVIDER_API_KEY; + if (key === 'COPILOT_PROVIDER_BASE_URL') return config.additionalEnv?.COPILOT_PROVIDER_BASE_URL; + return undefined; + }); + + const result = buildCopilotCredentialEnv({ config, proxyIp }); + + expect(result.COPILOT_OFFLINE).toBe('true'); + expect(result.COPILOT_PROVIDER_BASE_URL).toBe(`http://${proxyIp}:10002`); + expect(logger.warn).toHaveBeenCalledTimes(1); + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('web_fetch and web_search tools are unavailable')); + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('--allow-url (or --allow-all-urls)')); + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('AWF still enforces the domain allowlist')); + expect(Object.values(result)).not.toContain('provider-key'); + expect(Object.values(result)).not.toContain('ghu_token'); + }); + + it('does not warn about offline web tools when Copilot is not routed through the proxy', () => { + const config = { ...baseConfig, allowedDomains: ['osv.dev'] } as WrapperConfig; + expect(buildCopilotCredentialEnv({ config, proxyIp })).toEqual({}); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it('does not warn about offline web tools when no domains are allowlisted', () => { + const config = { ...baseConfig, copilotGithubToken: 'ghu_token', allowedDomains: [] } as WrapperConfig; + expect(buildCopilotCredentialEnv({ config, proxyIp }).COPILOT_OFFLINE).toBe('true'); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('returns env additions when copilotGithubToken is set', () => { const config = { ...baseConfig, copilotGithubToken: 'ghu_token' } as WrapperConfig; const result = buildCopilotCredentialEnv({ config, proxyIp }); diff --git a/src/services/credentials/copilot-credential-env.ts b/src/services/credentials/copilot-credential-env.ts index 6dbeec9f6..1eda5b989 100644 --- a/src/services/credentials/copilot-credential-env.ts +++ b/src/services/credentials/copilot-credential-env.ts @@ -71,6 +71,14 @@ export function buildCopilotCredentialEnv(params: CopilotCredentialEnvParams): R return env; } + if (config.allowedDomains?.length) { + logger.warn( + 'Copilot API proxy mode sets COPILOT_OFFLINE=true: native web_fetch and web_search tools are unavailable, ' + + 'even for allowlisted domains. Use shell curl with --allow-url (or --allow-all-urls); ' + + 'AWF still enforces the domain allowlist. See docs/troubleshooting.md#copilot-web-tools-unavailable-in-api-proxy-mode' + ); + } + // Credential-isolation placeholders for the BYOK auth variables. These MUST be // set here (in agentEnvAdditions, applied last in compose-generator) rather than // only in tool-specific-environment.ts, because `Object.assign(environment, From d6c118de1caa8774174b5aebcbdc1d9993ecf8dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:40:11 +0000 Subject: [PATCH 3/3] docs: clarify Copilot offline mode trigger --- docs/api-proxy-sidecar.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-proxy-sidecar.md b/docs/api-proxy-sidecar.md index 44b5d08ce..512de079c 100644 --- a/docs/api-proxy-sidecar.md +++ b/docs/api-proxy-sidecar.md @@ -204,7 +204,7 @@ The agent container receives **redacted placeholders** and proxy URLs: | `COPILOT_API_URL` | `http://172.30.0.30:10002` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | Redirects Copilot CLI to sidecar | | `COPILOT_TOKEN` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | Placeholder token (real auth via API_URL) | | `COPILOT_GITHUB_TOKEN` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_GITHUB_TOKEN` provided to host | Placeholder token protected by one-shot-token (real token in sidecar) | -| `COPILOT_OFFLINE` | `true` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | Enables offline+BYOK mode (skips GitHub OAuth handshake); disables native `web_fetch` / `web_search`. See the [curl workaround](troubleshooting.md#copilot-web-tools-unavailable-in-api-proxy-mode). | +| `COPILOT_OFFLINE` | `true` | `COPILOT_GITHUB_TOKEN`, `COPILOT_PROVIDER_API_KEY`, or `COPILOT_PROVIDER_BASE_URL` provided to host | Enables offline+BYOK mode (skips GitHub OAuth handshake); disables native `web_fetch` / `web_search`. See the [curl workaround](troubleshooting.md#copilot-web-tools-unavailable-in-api-proxy-mode). | | `COPILOT_PROVIDER_BASE_URL` | `http://172.30.0.30:10002` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | Points Copilot CLI BYOK provider at sidecar (real upstream URL, if any, held in sidecar) | | `COPILOT_PROVIDER_API_KEY` | `ghu_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` | `COPILOT_GITHUB_TOKEN` or `COPILOT_PROVIDER_API_KEY` provided to host | BYOK provider API key placeholder (real key in sidecar) | | `GOOGLE_GEMINI_BASE_URL` | `http://172.30.0.30:10003` | `GEMINI_API_KEY` or GCP OIDC configured | Redirects Gemini CLI to proxy (primary var read by Gemini CLI) |