Skip to content

Commit 335adfa

Browse files
CopilotlpcoxCopilot
authored
feat: warn on classic PAT + COPILOT_MODEL incompatibility (Copilot CLI 1.0.21+) (#1907)
* Initial plan * feat: warn when classic PAT is used with COPILOT_MODEL Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/99b813df-d6d8-41a5-9288-a8516b85733d * fix: clarify variable name and test assertion for PAT warning * Update src/cli.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/cli.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update docs/environment.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Landon Cox <landon.cox@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 4da2ae8 commit 335adfa

File tree

3 files changed

+124
-1
lines changed

3 files changed

+124
-1
lines changed

docs/environment.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,32 @@ sudo -E awf --allow-domains github.com 'copilot --prompt "..."'
8888
- Working with untrusted code
8989
- In production/CI environments
9090

91+
## `COPILOT_GITHUB_TOKEN` and Classic PAT Compatibility
92+
93+
When `COPILOT_GITHUB_TOKEN` is set in the host environment, AWF injects it into the agent container so the Copilot CLI can authenticate against the GitHub Copilot API.
94+
95+
### ⚠️ Classic PAT + `COPILOT_MODEL` Incompatibility (Copilot CLI 1.0.21+)
96+
97+
Copilot CLI 1.0.21 introduced a startup model validation step: when `COPILOT_MODEL` is set, the CLI calls `GET /models` before executing any task. **This endpoint does not accept classic PATs** (`ghp_*` tokens), causing the agent to fail at startup with exit code 1 — before any useful work begins.
98+
99+
**Affected combination:**
100+
- `COPILOT_GITHUB_TOKEN` is a classic PAT (prefixed with `ghp_`)
101+
- `COPILOT_MODEL` is set in the agent environment (e.g., via `--env COPILOT_MODEL=...`, `--env-file`, or `--env-all`)
102+
103+
**Unaffected:** Workflows that do not set `COPILOT_MODEL` are not affected — the `/models` validation is only triggered when `COPILOT_MODEL` is set.
104+
105+
**AWF detects this combination at startup** and emits a `[WARN]` message:
106+
```
107+
⚠️ COPILOT_MODEL is set with a classic PAT (ghp_* token)
108+
Copilot CLI 1.0.21+ validates COPILOT_MODEL via GET /models at startup.
109+
Classic PATs are rejected by this endpoint — the agent will likely fail with exit code 1.
110+
Use a fine-grained PAT or OAuth token, or unset COPILOT_MODEL to skip model validation.
111+
```
112+
113+
**Remediation options:**
114+
1. Replace the classic PAT with a **fine-grained PAT** or **OAuth token** (these are accepted by the `/models` endpoint).
115+
2. Remove `COPILOT_MODEL` from the agent environment to skip model validation entirely.
116+
91117
## Internal Environment Variables
92118

93119
The following environment variables are set internally by the firewall and used by container scripts:

src/cli.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Command } from 'commander';
2-
import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, parseDnsOverHttps, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, validateAllowHostServicePorts, applyHostServicePortsConfig, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, hasRateLimitOptions, collectRulesetFile, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, DEFAULT_GEMINI_API_TARGET, emitApiProxyTargetWarnings, emitCliProxyStatusLogs, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction, resolveApiTargetsToAllowedDomains, extractGhesDomainsFromEngineApiTarget, extractGhecDomainsFromServerUrl } from './cli';
2+
import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, parseDnsOverHttps, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, validateAllowHostServicePorts, applyHostServicePortsConfig, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, hasRateLimitOptions, collectRulesetFile, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, DEFAULT_GEMINI_API_TARGET, emitApiProxyTargetWarnings, emitCliProxyStatusLogs, warnClassicPATWithCopilotModel, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction, resolveApiTargetsToAllowedDomains, extractGhesDomainsFromEngineApiTarget, extractGhecDomainsFromServerUrl } from './cli';
33
import { redactSecrets } from './redact-secrets';
44
import * as fs from 'fs';
55
import * as path from 'path';
@@ -2112,6 +2112,46 @@ describe('cli', () => {
21122112
});
21132113
});
21142114

2115+
describe('warnClassicPATWithCopilotModel', () => {
2116+
it('should emit warnings when classic PAT and COPILOT_MODEL are both set', () => {
2117+
const warns: string[] = [];
2118+
warnClassicPATWithCopilotModel(true, true, (msg) => warns.push(msg));
2119+
expect(warns.length).toBeGreaterThan(0);
2120+
expect(warns[0]).toContain('COPILOT_MODEL');
2121+
expect(warns.some(w => w.includes('classic PAT'))).toBe(true);
2122+
});
2123+
2124+
it('should not warn when token is not a classic PAT', () => {
2125+
const warns: string[] = [];
2126+
warnClassicPATWithCopilotModel(false, true, (msg) => warns.push(msg));
2127+
expect(warns).toHaveLength(0);
2128+
});
2129+
2130+
it('should not warn when COPILOT_MODEL is not set', () => {
2131+
const warns: string[] = [];
2132+
warnClassicPATWithCopilotModel(true, false, (msg) => warns.push(msg));
2133+
expect(warns).toHaveLength(0);
2134+
});
2135+
2136+
it('should not warn when neither condition holds', () => {
2137+
const warns: string[] = [];
2138+
warnClassicPATWithCopilotModel(false, false, (msg) => warns.push(msg));
2139+
expect(warns).toHaveLength(0);
2140+
});
2141+
2142+
it('should mention /models endpoint in warning', () => {
2143+
const warns: string[] = [];
2144+
warnClassicPATWithCopilotModel(true, true, (msg) => warns.push(msg));
2145+
expect(warns.some(w => w.includes('/models'))).toBe(true);
2146+
});
2147+
2148+
it('should mention exit code 1 in warning', () => {
2149+
const warns: string[] = [];
2150+
warnClassicPATWithCopilotModel(true, true, (msg) => warns.push(msg));
2151+
expect(warns.some(w => w.includes('exit code 1'))).toBe(true);
2152+
});
2153+
});
2154+
21152155
describe('resolveApiTargetsToAllowedDomains', () => {
21162156
it('should add copilot-api-target option to allowed domains', () => {
21172157
const domains: string[] = ['github.com'];

src/cli.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,30 @@ export function emitCliProxyStatusLogs(
420420
}
421421
}
422422

423+
/**
424+
* Warns when a classic GitHub PAT (ghp_* prefix) is used alongside COPILOT_MODEL.
425+
* Copilot CLI 1.0.21+ performs a GET /models validation at startup when COPILOT_MODEL
426+
* is set. This endpoint rejects classic PATs, causing the agent to fail with exit code 1
427+
* before any useful work begins.
428+
* Accepts booleans (not actual tokens/values) to prevent sensitive data from flowing
429+
* through to log output (CodeQL: clear-text logging of sensitive information).
430+
* @param isClassicPAT - Whether COPILOT_GITHUB_TOKEN starts with 'ghp_' (classic PAT)
431+
* @param hasCopilotModel - Whether COPILOT_MODEL is set in the agent environment
432+
* @param warn - Function to emit a warning message
433+
*/
434+
export function warnClassicPATWithCopilotModel(
435+
isClassicPAT: boolean,
436+
hasCopilotModel: boolean,
437+
warn: (msg: string) => void,
438+
): void {
439+
if (!isClassicPAT || !hasCopilotModel) return;
440+
441+
warn('⚠️ COPILOT_MODEL is set with a classic PAT (ghp_* token)');
442+
warn(' Copilot CLI 1.0.21+ validates COPILOT_MODEL via GET /models at startup.');
443+
warn(' Classic PATs are rejected by this endpoint — the agent will likely fail with exit code 1.');
444+
warn(' Use a fine-grained PAT or OAuth token, or unset COPILOT_MODEL to skip model validation.');
445+
}
446+
423447
/**
424448
* Extracts GHEC domains from GITHUB_SERVER_URL and GITHUB_API_URL environment variables.
425449
* When GITHUB_SERVER_URL points to a GHEC tenant (*.ghe.com), returns the tenant hostname,
@@ -1935,6 +1959,39 @@ program
19351959
// Log CLI proxy status
19361960
emitCliProxyStatusLogs(config, logger.info.bind(logger), logger.warn.bind(logger));
19371961

1962+
// Warn if a classic PAT is combined with COPILOT_MODEL (Copilot CLI 1.0.21+ incompatibility)
1963+
const hasCopilotModelInEnvFiles = (envFile: unknown): boolean => {
1964+
const envFiles = Array.isArray(envFile) ? envFile : envFile ? [envFile] : [];
1965+
for (const candidate of envFiles) {
1966+
if (typeof candidate !== 'string' || candidate.trim() === '') continue;
1967+
try {
1968+
const envFilePath = path.isAbsolute(candidate) ? candidate : path.resolve(process.cwd(), candidate);
1969+
const envFileContents = fs.readFileSync(envFilePath, 'utf8');
1970+
for (const line of envFileContents.split(/\r?\n/)) {
1971+
const trimmedLine = line.trim();
1972+
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
1973+
if (/^(?:export\s+)?COPILOT_MODEL\s*=/.test(trimmedLine)) {
1974+
return true;
1975+
}
1976+
}
1977+
} catch {
1978+
// Ignore unreadable env files here; this check is only for a pre-flight warning.
1979+
}
1980+
}
1981+
return false;
1982+
};
1983+
1984+
// Warn if a classic PAT is combined with COPILOT_MODEL (Copilot CLI 1.0.21+ incompatibility)
1985+
// Check if COPILOT_MODEL is set via --env/-e flags, host env (when --env-all is active), or --env-file
1986+
const copilotModelFromFlags = !!(additionalEnv['COPILOT_MODEL']);
1987+
const copilotModelInHostEnv = !!(config.envAll && process.env.COPILOT_MODEL);
1988+
const copilotModelInEnvFile = hasCopilotModelInEnvFiles((config as { envFile?: unknown }).envFile);
1989+
warnClassicPATWithCopilotModel(
1990+
config.copilotGithubToken?.startsWith('ghp_') ?? false,
1991+
copilotModelFromFlags || copilotModelInHostEnv || copilotModelInEnvFile,
1992+
logger.warn.bind(logger)
1993+
);
1994+
19381995
// Log config with redacted secrets - remove API keys entirely
19391996
// to prevent sensitive data from flowing to logger (CodeQL sensitive data logging)
19401997
const redactedConfig: Record<string, unknown> = {};

0 commit comments

Comments
 (0)