Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api-proxy-sidecar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`, `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) |
Expand Down
46 changes: 46 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,52 @@
sudo grep "TCP_DENIED" /tmp/squid-logs-<timestamp>/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
Expand Down
39 changes: 39 additions & 0 deletions src/services/credentials/copilot-credential-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof getConfigEnvValue>;

Expand All @@ -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 <host> (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 });
Expand Down
8 changes: 8 additions & 0 deletions src/services/credentials/copilot-credential-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <host> (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,
Expand Down
Loading