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
3 changes: 2 additions & 1 deletion containers/api-proxy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ USER apiproxy
# 10000 - OpenAI API proxy (also serves as health check endpoint)
# 10001 - Anthropic API proxy
# 10002 - GitHub Copilot API proxy
EXPOSE 10000 10001 10002
# 10004 - OpenCode API proxy (routes to Anthropic)
EXPOSE 10000 10001 10002 10004

# Redirect stdout/stderr to log file for persistence
# Use shell form to enable redirection and tee for both file and console
Expand Down
29 changes: 29 additions & 0 deletions containers/api-proxy/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,35 @@
console.log('[API Proxy] GitHub Copilot proxy listening on port 10002');
});
}

// OpenCode API proxy (port 10004) — routes to Anthropic (default BYOK provider)
// OpenCode gets a separate port from Claude (10001) for per-engine rate limiting,
// metrics isolation, and future provider routing (OpenCode is BYOK and may route
// to different providers in the future based on model prefix).
if (ANTHROPIC_API_KEY) {
const opencodeServer = http.createServer((req, res) => {
if (req.url === '/health' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'healthy', service: 'opencode-proxy' }));
return;
}

const logMethod = sanitizeForLog(req.method);
const logUrl = sanitizeForLog(req.url);
console.log(`[OpenCode Proxy] ${logMethod} ${logUrl}`);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.

Copilot Autofix

AI 8 months ago

In general, to fix log injection, all user-controlled data should be sanitized before logging: remove or normalize line breaks and control characters, optionally collapse or escape other whitespace that could visually obscure boundaries, and clearly delimit user input in log messages so operators can see what part is untrusted.

For this specific code, the best low-impact fix is:

  1. Enhance sanitizeForLog to also strip Unicode line/paragraph separators and normalize tabs to spaces, while still removing ASCII control chars and limiting length to 200 characters.
  2. Keep using sanitizeForLog on req.method and req.url as already done.
  3. Slightly adjust the log format to clearly bracket user-controlled fields, e.g. "[OpenCode Proxy] method=%s url=%s" where the method and URL are enclosed in quotes. This doesn’t change functionality but makes the boundary between fixed text and user data explicit.

Concretely in containers/api-proxy/server.js:

  • Modify the sanitizeForLog implementation around lines 38–43 to:
    • Convert input to string safely.
    • Remove ASCII control chars as before.
    • Remove Unicode line (\u2028) and paragraph (\u2029) separators.
    • Replace tab characters with a single space.
  • Update the logging line around 276 to include clear delimiters around the sanitized values, e.g.:
    • From: console.log(`[OpenCode Proxy] ${logMethod} ${logUrl}`);
    • To: console.log(`[OpenCode Proxy] method="${logMethod}" url="${logUrl}"`);

No new imports are needed; all changes are inside the already-present helper and log call.


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
@@ -37,9 +37,16 @@
 
 /** Sanitize a string for safe logging (strip control chars, limit length). */
 function sanitizeForLog(str) {
-  if (typeof str !== 'string') return '';
+  if (str == null) return '';
+  const s = String(str);
+  // Remove ASCII control chars (including \n, \r, \t, etc.) and DEL
   // eslint-disable-next-line no-control-regex
-  return str.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200);
+  const noAsciiControls = s.replace(/[\x00-\x1f\x7f]/g, '');
+  // Also remove Unicode line and paragraph separators to prevent multi-line logs
+  const noUnicodeSeparators = noAsciiControls.replace(/[\u2028\u2029]/g, '');
+  // Normalize any remaining tabs to a single space for readability
+  const normalizedWhitespace = noUnicodeSeparators.replace(/\t+/g, ' ');
+  return normalizedWhitespace.slice(0, 200);
 }
 
 // Read API keys from environment (set by docker-compose)
@@ -273,7 +279,7 @@
 
     const logMethod = sanitizeForLog(req.method);
     const logUrl = sanitizeForLog(req.url);
-    console.log(`[OpenCode Proxy] ${logMethod} ${logUrl}`);
+    console.log(`[OpenCode Proxy] method="${logMethod}" url="${logUrl}"`);
     console.log('[OpenCode Proxy] Injecting x-api-key header with ANTHROPIC_API_KEY');
     const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
     if (!req.headers['anthropic-version']) {
EOF
@@ -37,9 +37,16 @@

/** Sanitize a string for safe logging (strip control chars, limit length). */
function sanitizeForLog(str) {
if (typeof str !== 'string') return '';
if (str == null) return '';
const s = String(str);
// Remove ASCII control chars (including \n, \r, \t, etc.) and DEL
// eslint-disable-next-line no-control-regex
return str.replace(/[\x00-\x1f\x7f]/g, '').slice(0, 200);
const noAsciiControls = s.replace(/[\x00-\x1f\x7f]/g, '');
// Also remove Unicode line and paragraph separators to prevent multi-line logs
const noUnicodeSeparators = noAsciiControls.replace(/[\u2028\u2029]/g, '');
// Normalize any remaining tabs to a single space for readability
const normalizedWhitespace = noUnicodeSeparators.replace(/\t+/g, ' ');
return normalizedWhitespace.slice(0, 200);
}

// Read API keys from environment (set by docker-compose)
@@ -273,7 +279,7 @@

const logMethod = sanitizeForLog(req.method);
const logUrl = sanitizeForLog(req.url);
console.log(`[OpenCode Proxy] ${logMethod} ${logUrl}`);
console.log(`[OpenCode Proxy] method="${logMethod}" url="${logUrl}"`);
console.log('[OpenCode Proxy] Injecting x-api-key header with ANTHROPIC_API_KEY');
const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
if (!req.headers['anthropic-version']) {
Copilot is powered by AI and may make mistakes. Always verify output.
console.log('[OpenCode Proxy] Injecting x-api-key header with ANTHROPIC_API_KEY');
const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
if (!req.headers['anthropic-version']) {
anthropicHeaders['anthropic-version'] = '2023-06-01';
}
proxyRequest(req, res, 'api.anthropic.com', anthropicHeaders);
});

opencodeServer.listen(10004, '0.0.0.0', () => {
console.log('[API Proxy] OpenCode proxy listening on port 10004 (-> Anthropic)');
});
}
Comment on lines +262 to +288

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

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

The OpenCode proxy (port 10004) is functionally identical to the Claude proxy (port 10001) - both route to api.anthropic.com with the same ANTHROPIC_API_KEY and apply the same header transformations. Consider documenting why two separate proxies are needed for the same endpoint, or if there are planned differences in behavior, consider implementing them now or adding TODO comments explaining the future differentiation. This will help maintainers understand the architecture decision.

Copilot uses AI. Check for mistakes.
Comment on lines +262 to +288

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

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

The OpenCode proxy server is set up on port 10004, but there's no corresponding environment variable configuration in docker-manager.ts to tell OpenCode agents how to use this proxy. Other proxies set environment variables like OPENAI_BASE_URL (line 1041), ANTHROPIC_BASE_URL (line 1045), and COPILOT_API_URL (line 1059). Consider adding similar configuration for OpenCode, such as setting an OPENCODE_BASE_URL or OPENCODE_API_URL environment variable when anthropicApiKey is provided, to make the proxy automatically discoverable by OpenCode agents. If this configuration will be added in the companion PR, consider adding a TODO comment noting this.

Copilot uses AI. Check for mistakes.

// Graceful shutdown
process.on('SIGTERM', () => {
console.log('[API Proxy] Received SIGTERM, shutting down gracefully...');
Expand Down
7 changes: 4 additions & 3 deletions src/host-iptables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,11 +443,12 @@ export async function setupHostIptables(squidIp: string, squidPort: number, dnsS
]);

// 5b. Allow traffic to API proxy sidecar (when enabled)
// Allow all API proxy ports (OpenAI, Anthropic, GitHub Copilot).
// Allow all API proxy ports (OpenAI, Anthropic, GitHub Copilot, OpenCode).
// The sidecar itself routes through Squid, so domain whitelisting is still enforced.
if (apiProxyIp) {
const minPort = Math.min(API_PROXY_PORTS.OPENAI, API_PROXY_PORTS.ANTHROPIC, API_PROXY_PORTS.COPILOT);
const maxPort = Math.max(API_PROXY_PORTS.OPENAI, API_PROXY_PORTS.ANTHROPIC, API_PROXY_PORTS.COPILOT);
const allPorts = Object.values(API_PROXY_PORTS);
const minPort = Math.min(...allPorts);
const maxPort = Math.max(...allPorts);
logger.debug(`Allowing traffic to API proxy sidecar at ${apiProxyIp}:${minPort}-${maxPort}`);
await execa('iptables', [
'-t', 'filter', '-A', CHAIN_NAME,
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export const API_PROXY_PORTS = {
* @see containers/api-proxy/server.js
*/
COPILOT: 10002,

/**
* OpenCode API proxy port (routes to Anthropic by default)
* OpenCode is BYOK — defaults to Anthropic as the primary provider
* @see containers/api-proxy/server.js
*/
OPENCODE: 10004,
} as const;

/**
Expand Down Expand Up @@ -432,6 +439,7 @@ export interface WrapperConfig {
* - http://api-proxy:10000 - OpenAI API proxy (for Codex) {@link API_PROXY_PORTS.OPENAI}
* - http://api-proxy:10001 - Anthropic API proxy (for Claude) {@link API_PROXY_PORTS.ANTHROPIC}
* - http://api-proxy:10002 - GitHub Copilot API proxy {@link API_PROXY_PORTS.COPILOT}
* - http://api-proxy:10004 - OpenCode API proxy (routes to Anthropic) {@link API_PROXY_PORTS.OPENCODE}
*
* When the corresponding API key is provided, the following environment
* variables are set in the agent container:
Expand Down
Loading