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
5 changes: 4 additions & 1 deletion .github/workflows/smoke-copilot.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 44 additions & 1 deletion containers/agent/api-proxy-health-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,49 @@ if [ -n "$OPENAI_BASE_URL" ]; then
fi
fi

# Check GitHub Copilot configuration
if [ -n "$COPILOT_API_URL" ]; then
API_PROXY_CONFIGURED=true
echo "[health-check] Checking GitHub Copilot API proxy configuration..."
echo "[health-check] COPILOT_API_URL=$COPILOT_API_URL"

# Verify COPILOT_GITHUB_TOKEN is placeholder (protected by one-shot-token)
if [ -n "$COPILOT_GITHUB_TOKEN" ]; then
if [ "$COPILOT_GITHUB_TOKEN" != "placeholder-token-for-credential-isolation" ]; then
echo "[health-check][ERROR] COPILOT_GITHUB_TOKEN contains non-placeholder value!"
echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'"
exit 1
fi
echo "[health-check] ✓ COPILOT_GITHUB_TOKEN is placeholder value (correct)"
fi

# Verify COPILOT_TOKEN is placeholder (if present)
if [ -n "$COPILOT_TOKEN" ]; then
if [ "$COPILOT_TOKEN" != "placeholder-token-for-credential-isolation" ]; then
echo "[health-check][ERROR] COPILOT_TOKEN contains non-placeholder value!"
echo "[health-check][ERROR] Token should be 'placeholder-token-for-credential-isolation'"
exit 1
fi
echo "[health-check] ✓ COPILOT_TOKEN is placeholder value (correct)"
fi

# Perform health check using API URL
echo "[health-check] Testing connectivity to GitHub Copilot API proxy at $COPILOT_API_URL..."

# Extract host and port from API URL (format: http://IP:PORT)
PROXY_HOST=$(echo "$COPILOT_API_URL" | sed -E 's|^https?://([^:]+):.*|\1|')
PROXY_PORT=$(echo "$COPILOT_API_URL" | sed -E 's|^https?://[^:]+:([0-9]+).*|\1|')

# Test TCP connectivity with timeout
if timeout 5 bash -c "cat < /dev/null > /dev/tcp/$PROXY_HOST/$PROXY_PORT" 2>/dev/null; then
echo "[health-check] ✓ GitHub Copilot API proxy is reachable at $COPILOT_API_URL"
else
echo "[health-check][ERROR] Cannot connect to GitHub Copilot API proxy at $COPILOT_API_URL"
echo "[health-check][ERROR] Proxy may not be running or network is blocked"
exit 1
fi
fi

# Summary
if [ "$API_PROXY_CONFIGURED" = "true" ]; then
echo "[health-check] =========================================="
Expand All @@ -99,7 +142,7 @@ if [ "$API_PROXY_CONFIGURED" = "true" ]; then
echo "[health-check] ✓ Connectivity established"
echo "[health-check] =========================================="
else
echo "[health-check] No API proxy configured (ANTHROPIC_BASE_URL and OPENAI_BASE_URL not set)"
echo "[health-check] No API proxy configured (ANTHROPIC_BASE_URL, OPENAI_BASE_URL, and COPILOT_API_URL not set)"
echo "[health-check] Skipping health checks"
fi

Expand Down
31 changes: 29 additions & 2 deletions containers/api-proxy/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
// Read API keys from environment (set by docker-compose)
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
const COPILOT_GITHUB_TOKEN = process.env.COPILOT_GITHUB_TOKEN;

// Squid proxy configuration (set via HTTP_PROXY/HTTPS_PROXY in docker-compose)
const HTTPS_PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
Expand All @@ -57,6 +58,9 @@
if (ANTHROPIC_API_KEY) {
console.log('[API Proxy] Anthropic API key configured');
}
if (COPILOT_GITHUB_TOKEN) {
console.log('[API Proxy] GitHub Copilot token configured');
}

// Create proxy agent for routing through Squid
const proxyAgent = HTTPS_PROXY ? new HttpsProxyAgent(HTTPS_PROXY) : undefined;
Expand Down Expand Up @@ -169,7 +173,7 @@
status: 'healthy',
service: 'awf-api-proxy',
squid_proxy: HTTPS_PROXY || 'not configured',
providers: { openai: true, anthropic: !!ANTHROPIC_API_KEY },
providers: { openai: true, anthropic: !!ANTHROPIC_API_KEY, copilot: !!COPILOT_GITHUB_TOKEN },
}));
return;
}
Expand All @@ -193,7 +197,7 @@
status: 'healthy',
service: 'awf-api-proxy',
squid_proxy: HTTPS_PROXY || 'not configured',
providers: { openai: false, anthropic: !!ANTHROPIC_API_KEY },
providers: { openai: false, anthropic: !!ANTHROPIC_API_KEY, copilot: !!COPILOT_GITHUB_TOKEN },
}));
return;
}
Expand Down Expand Up @@ -231,6 +235,29 @@
});
}


// GitHub Copilot API proxy (port 10002)
if (COPILOT_GITHUB_TOKEN) {
const copilotServer = http.createServer((req, res) => {
// Health check endpoint
if (req.url === '/health' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'healthy', service: 'copilot-proxy' }));
return;
}

// Log and proxy the request
console.log(`[Copilot Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 5 days ago

To fix the problem in general, user-controlled values should only be written to logs after being passed through a clear, robust sanitization step that (a) strips or encodes control characters (especially \r and \n), (b) limits length, and (c) makes it obvious in the log line where user input begins and ends. The logger call should never insert raw, unsanitized request data.

In this specific file, most of that is already done via sanitizeForLog. The minimal, non‑functional change that addresses the concern is to: (1) ensure sanitizeForLog behaves safely even for non‑string inputs (e.g., undefined, objects) by converting them to strings before sanitizing, and (2) slightly adjust the log message on line 250 so that user-supplied fields are clearly marked (e.g., method="..." url="...") and continue to go through sanitizeForLog. This keeps all existing behavior (still logs method and URL, same truncation/character stripping) while making the sanitization more robust and explicit.

Concretely:

  • In containers/api-proxy/server.js, update sanitizeForLog so it stringifies non‑string inputs instead of returning an empty string, then strips control characters and truncates as before.
  • In the Copilot proxy server request handler, keep using sanitizeForLog(req.method) and sanitizeForLog(req.url) but change the template string on line 250 to clearly delineate user content, e.g., [Copilot Proxy] method="${...}" url="${...}". This preserves functionality while clarifying intent and improving readability/security posture.

No new imports or external methods are required; we reuse built‑in string manipulation.

Suggested changeset 1
containers/api-proxy/server.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/containers/api-proxy/server.js b/containers/api-proxy/server.js
--- a/containers/api-proxy/server.js
+++ b/containers/api-proxy/server.js
@@ -35,11 +35,15 @@
   return STRIPPED_HEADERS.has(lower) || lower.startsWith('x-forwarded-');
 }
 
-/** Sanitize a string for safe logging (strip control chars, limit length). */
+/** Sanitize a value for safe logging (stringify, strip control chars, limit length). */
 function sanitizeForLog(str) {
-  if (typeof str !== 'string') return '';
+  if (str === null || str === undefined) {
+    return '';
+  }
+  // Ensure we always operate on a string representation
+  const s = String(str);
   // eslint-disable-next-line no-control-regex
-  return str.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200);
+  return s.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200);
 }
 
 // Read API keys from environment (set by docker-compose)
@@ -247,7 +249,7 @@
     }
 
     // Log and proxy the request
-    console.log(`[Copilot Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`);
+    console.log(`[Copilot Proxy] method="${sanitizeForLog(req.method)}" url="${sanitizeForLog(req.url)}"`);
     console.log(`[Copilot Proxy] Injecting Authorization header with COPILOT_GITHUB_TOKEN`);
     proxyRequest(req, res, 'api.githubcopilot.com', {
       'Authorization': `Bearer ${COPILOT_GITHUB_TOKEN}`,
EOF
@@ -35,11 +35,15 @@
return STRIPPED_HEADERS.has(lower) || lower.startsWith('x-forwarded-');
}

/** Sanitize a string for safe logging (strip control chars, limit length). */
/** Sanitize a value for safe logging (stringify, strip control chars, limit length). */
function sanitizeForLog(str) {
if (typeof str !== 'string') return '';
if (str === null || str === undefined) {
return '';
}
// Ensure we always operate on a string representation
const s = String(str);
// eslint-disable-next-line no-control-regex
return str.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200);
return s.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200);
}

// Read API keys from environment (set by docker-compose)
@@ -247,7 +249,7 @@
}

// Log and proxy the request
console.log(`[Copilot Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`);
console.log(`[Copilot Proxy] method="${sanitizeForLog(req.method)}" url="${sanitizeForLog(req.url)}"`);
console.log(`[Copilot Proxy] Injecting Authorization header with COPILOT_GITHUB_TOKEN`);
proxyRequest(req, res, 'api.githubcopilot.com', {
'Authorization': `Bearer ${COPILOT_GITHUB_TOKEN}`,
Copilot is powered by AI and may make mistakes. Always verify output.
console.log(`[Copilot Proxy] Injecting Authorization header with COPILOT_GITHUB_TOKEN`);
proxyRequest(req, res, 'api.githubcopilot.com', {
'Authorization': `Bearer ${COPILOT_GITHUB_TOKEN}`,
});
});

copilotServer.listen(10002, '0.0.0.0', () => {
console.log('[API Proxy] GitHub Copilot proxy listening on port 10002');
});
}
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('[API Proxy] Received SIGTERM, shutting down gracefully...');
Expand Down
33 changes: 23 additions & 10 deletions examples/github-copilot.sh
Original file line number Diff line number Diff line change
@@ -1,39 +1,52 @@
#!/bin/bash
# Example: Using GitHub Copilot CLI with the firewall
# Example: Using GitHub Copilot CLI with the firewall and API proxy
#
# This example shows how to run GitHub Copilot CLI through the firewall.
# Copilot requires access to several GitHub domains.
# This example shows how to run GitHub Copilot CLI through the firewall
# with credential isolation via the API proxy sidecar.
#
# Prerequisites:
# - GitHub Copilot CLI installed: npm install -g @github/copilot
# - GITHUB_TOKEN environment variable set
# - COPILOT_API_KEY environment variable set (for API proxy)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states COPILOT_API_KEY should be set, but the actual environment variable used throughout the codebase is COPILOT_GITHUB_TOKEN. This should be changed to COPILOT_GITHUB_TOKEN to match the implementation in src/cli.ts:995, src/docker-manager.ts:965, and the test files.

This issue also appears in the following locations of the same file:

  • line 19
  • line 40

Copilot uses AI. Check for mistakes.
# - GITHUB_TOKEN environment variable set (for GitHub API access)
#
# Usage: sudo -E ./examples/github-copilot.sh

set -e

echo "=== AWF GitHub Copilot CLI Example ==="
echo "=== AWF GitHub Copilot CLI Example (with API Proxy) ==="
echo ""

# Check for COPILOT_API_KEY
if [ -z "$COPILOT_API_KEY" ]; then
echo "Error: COPILOT_API_KEY environment variable is not set"
echo "Set it with: export COPILOT_API_KEY='your_copilot_api_key'"
exit 1
fi

# Check for GITHUB_TOKEN
if [ -z "$GITHUB_TOKEN" ]; then
echo "Error: GITHUB_TOKEN environment variable is not set"
echo "Set it with: export GITHUB_TOKEN='your_token'"
exit 1
fi

echo "Running GitHub Copilot CLI through the firewall..."
# Enable one-shot-token debug logging
export AWF_ONE_SHOT_TOKEN_DEBUG=1

echo "Running GitHub Copilot CLI with API proxy and debug logging enabled..."
echo ""

# Run Copilot CLI with required domains
# Use sudo -E to preserve environment variables (especially GITHUB_TOKEN)
# Run Copilot CLI with API proxy enabled
# Use sudo -E to preserve environment variables (COPILOT_GITHUB_TOKEN, GITHUB_TOKEN, AWF_ONE_SHOT_TOKEN_DEBUG)
# Required domains:
# - api.githubcopilot.com: Copilot API endpoint (proxied via api-proxy)
# - github.com: GitHub API access
# - api.github.com: GitHub REST API
# - api.enterprise.githubcopilot.com: Copilot API endpoint
# - registry.npmjs.org: NPM package registry (for npx)
sudo -E awf \
--allow-domains github.com,api.github.com,api.enterprise.githubcopilot.com,registry.npmjs.org \
--enable-api-proxy \
--allow-domains api.githubcopilot.com,github.com,api.github.com,registry.npmjs.org \
--log-level debug \
-- 'npx @github/copilot --prompt "What is 2+2?" --no-mcp'

echo ""
Expand Down
23 changes: 18 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,12 +262,14 @@ export interface ApiProxyValidationResult {
* @param enableApiProxy - Whether --enable-api-proxy flag was provided
* @param hasOpenaiKey - Whether an OpenAI API key is present
* @param hasAnthropicKey - Whether an Anthropic API key is present
* @param hasCopilotKey - Whether a GitHub Copilot API key is present
* @returns ApiProxyValidationResult with warnings and debug messages
*/
export function validateApiProxyConfig(
enableApiProxy: boolean,
hasOpenaiKey?: boolean,
hasAnthropicKey?: boolean
hasAnthropicKey?: boolean,
hasCopilotKey?: boolean
): ApiProxyValidationResult {
if (!enableApiProxy) {
return { enabled: false, warnings: [], debugMessages: [] };
Expand All @@ -276,16 +278,19 @@ export function validateApiProxyConfig(
const warnings: string[] = [];
const debugMessages: string[] = [];

if (!hasOpenaiKey && !hasAnthropicKey) {
if (!hasOpenaiKey && !hasAnthropicKey && !hasCopilotKey) {
warnings.push('⚠️ API proxy enabled but no API keys found in environment');
warnings.push(' Set OPENAI_API_KEY or ANTHROPIC_API_KEY to use the proxy');
warnings.push(' Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or COPILOT_GITHUB_TOKEN to use the proxy');
}
if (hasOpenaiKey) {
debugMessages.push('OpenAI API key detected - will be held securely in sidecar');
}
if (hasAnthropicKey) {
debugMessages.push('Anthropic API key detected - will be held securely in sidecar');
}
if (hasCopilotKey) {
debugMessages.push('GitHub Copilot API key detected - will be held securely in sidecar');
}

return { enabled: true, warnings, debugMessages };
}
Expand Down Expand Up @@ -987,6 +992,7 @@ program
enableApiProxy: options.enableApiProxy,
openaiApiKey: process.env.OPENAI_API_KEY,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
copilotGithubToken: process.env.COPILOT_GITHUB_TOKEN,
};

// Warn if --env-all is used
Expand Down Expand Up @@ -1025,8 +1031,15 @@ program
const apiProxyValidation = validateApiProxyConfig(
config.enableApiProxy || false,
!!config.openaiApiKey,
!!config.anthropicApiKey
!!config.anthropicApiKey,
!!config.copilotGithubToken
);

// Log API proxy status at info level for visibility
if (config.enableApiProxy) {
logger.info(`API proxy enabled: OpenAI=${!!config.openaiApiKey}, Anthropic=${!!config.anthropicApiKey}, Copilot=${!!config.copilotGithubToken}`);
}

for (const warning of apiProxyValidation.warnings) {
logger.warn(warning);
}
Expand All @@ -1038,7 +1051,7 @@ program
// to prevent sensitive data from flowing to logger (CodeQL sensitive data logging)
const redactedConfig: Record<string, unknown> = {};
for (const [key, value] of Object.entries(config)) {
if (key === 'openaiApiKey' || key === 'anthropicApiKey') continue;
if (key === 'openaiApiKey' || key === 'anthropicApiKey' || key === 'copilotGithubToken') continue;
redactedConfig[key] = key === 'agentCommand' ? redactSecrets(value as string) : value;
}
logger.debug('Configuration:', JSON.stringify(redactedConfig, null, 2));
Expand Down
25 changes: 25 additions & 0 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ export function generateDockerCompose(
EXCLUDED_ENV_VARS.add('CODEX_API_KEY');
EXCLUDED_ENV_VARS.add('ANTHROPIC_API_KEY');
EXCLUDED_ENV_VARS.add('CLAUDE_API_KEY');
// COPILOT_GITHUB_TOKEN gets a placeholder (not excluded), protected by one-shot-token
}

// Start with required/overridden environment variables
Expand All @@ -346,8 +347,18 @@ export function generateDockerCompose(
SQUID_PROXY_PORT: SQUID_PORT.toString(),
HOME: homeDir,
PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
// Configure one-shot-token library with sensitive tokens to protect
// These tokens are cached on first access and unset from /proc/self/environ
AWF_ONE_SHOT_TOKENS: 'COPILOT_GITHUB_TOKEN,GITHUB_TOKEN,GH_TOKEN,GITHUB_API_TOKEN,GITHUB_PAT,GH_ACCESS_TOKEN,OPENAI_API_KEY,OPENAI_KEY,ANTHROPIC_API_KEY,CLAUDE_API_KEY,CODEX_API_KEY',
};

// When api-proxy is enabled with Copilot, set placeholder tokens early
// so --env-all won't override them with real values from host environment
if (config.enableApiProxy && config.copilotGithubToken) {
environment.COPILOT_GITHUB_TOKEN = 'placeholder-token-for-credential-isolation';
logger.debug('COPILOT_GITHUB_TOKEN set to placeholder value (early) to prevent --env-all override');
}

// When host access is enabled, bypass the proxy for the host gateway IPs.
// MCP Streamable HTTP (SSE) traffic through Squid crashes it (comm.cc:1583),
// so MCP gateway traffic must go directly to the host, not through Squid.
Expand Down Expand Up @@ -421,6 +432,7 @@ export function generateDockerCompose(
if (process.env.OPENAI_API_KEY && !config.enableApiProxy) environment.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (process.env.CODEX_API_KEY && !config.enableApiProxy) environment.CODEX_API_KEY = process.env.CODEX_API_KEY;
if (process.env.ANTHROPIC_API_KEY && !config.enableApiProxy) environment.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
// COPILOT_GITHUB_TOKEN is handled separately - gets placeholder when api-proxy enabled
if (process.env.USER) environment.USER = process.env.USER;
if (process.env.TERM) environment.TERM = process.env.TERM;
if (process.env.XDG_CONFIG_HOME) environment.XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
Expand Down Expand Up @@ -950,6 +962,7 @@ export function generateDockerCompose(
// Pass API keys securely to sidecar (not visible to agent)
...(config.openaiApiKey && { OPENAI_API_KEY: config.openaiApiKey }),
...(config.anthropicApiKey && { ANTHROPIC_API_KEY: config.anthropicApiKey }),
...(config.copilotGithubToken && { COPILOT_GITHUB_TOKEN: config.copilotGithubToken }),
// Route through Squid to respect domain whitelisting
HTTP_PROXY: `http://${networkConfig.squidIp}:${SQUID_PORT}`,
HTTPS_PROXY: `http://${networkConfig.squidIp}:${SQUID_PORT}`,
Expand Down Expand Up @@ -1013,6 +1026,18 @@ export function generateDockerCompose(
environment.CLAUDE_CODE_API_KEY_HELPER = '/usr/local/bin/get-claude-key.sh';
logger.debug('Claude Code API key helper configured: /usr/local/bin/get-claude-key.sh');
}
if (config.copilotGithubToken) {
environment.COPILOT_API_URL = `http://${networkConfig.proxyIp}:10002`;
logger.debug(`GitHub Copilot API will be proxied through sidecar at http://${networkConfig.proxyIp}:10002`);

// Set placeholder token for GitHub Copilot CLI compatibility
// Real authentication happens via COPILOT_API_URL pointing to api-proxy
environment.COPILOT_TOKEN = 'placeholder-token-for-credential-isolation';
logger.debug('COPILOT_TOKEN set to placeholder value for credential isolation');

// Note: COPILOT_GITHUB_TOKEN placeholder is set early (before --env-all)
// to prevent override by host environment variable
}

logger.info('API proxy sidecar enabled - API keys will be held securely in sidecar container');
logger.info('API proxy will route through Squid to respect domain whitelisting');
Expand Down
Loading
Loading