Skip to content

feat: add API proxy port 10004 for OpenCode engine#1055

Merged
Mossaka merged 2 commits intomainfrom
feat/opencode-api-proxy
Feb 25, 2026
Merged

feat: add API proxy port 10004 for OpenCode engine#1055
Mossaka merged 2 commits intomainfrom
feat/opencode-api-proxy

Conversation

@Mossaka
Copy link
Collaborator

@Mossaka Mossaka commented Feb 25, 2026

Summary

  • Add OpenCode API proxy on port 10004, routing to Anthropic API (OpenCode's default BYOK provider)
  • Dynamic port range calculation in host-iptables.ts using Object.values(API_PROXY_PORTS) — future-proof for new engines
  • Expose port 10004 in api-proxy Dockerfile

Files changed

File Change
src/types.ts Add OPENCODE: 10004 to API_PROXY_PORTS, update JSDoc
containers/api-proxy/server.js Add OpenCode proxy listener (port 10004 → api.anthropic.com)
containers/api-proxy/Dockerfile Expose port 10004
src/host-iptables.ts Dynamic port range via Object.values() instead of hardcoded port names

Context

OpenCode is a provider-agnostic (BYOK) AI coding agent being added to gh-aw. It defaults to Anthropic as its LLM provider, so port 10004 routes to api.anthropic.com with x-api-key auth — same pattern as the existing Anthropic proxy on port 10001.

Companion PR: github/gh-aw#18403

Test plan

  • TypeScript compiles clean (npx tsc --noEmit)
  • Lint passes (0 errors, pre-existing warnings only)
  • Existing tests pass
  • Verify port 10004 is accessible from agent container in integration test

🤖 Generated with Claude Code

Add OpenCode API proxy support on port 10004, routing to Anthropic API
(OpenCode's default BYOK provider). Dynamic port range calculation in
host-iptables ensures future ports are auto-included.

- src/types.ts: Add OPENCODE port 10004 to API_PROXY_PORTS
- containers/api-proxy/server.js: Add OpenCode proxy listener (-> Anthropic)
- containers/api-proxy/Dockerfile: Expose port 10004
- src/host-iptables.ts: Use Object.values() for dynamic port range

Companion to github/gh-aw#18403 (OpenCode engine integration)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 25, 2026 20:46
@github-actions
Copy link
Contributor

github-actions bot commented Feb 25, 2026

✅ Coverage Check Passed

Overall Coverage

Metric Base PR Delta
Lines 82.39% 82.50% 📈 +0.11%
Statements 82.32% 82.42% 📈 +0.10%
Functions 82.74% 82.74% ➡️ +0.00%
Branches 74.55% 74.65% 📈 +0.10%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/host-iptables.ts 77.9% → 77.5% (-0.38%) 78.1% → 77.7% (-0.38%)
src/docker-manager.ts 83.6% → 84.1% (+0.56%) 82.8% → 83.4% (+0.54%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@github-actions
Copy link
Contributor

Build Test: Bun ✅

Project Install Tests Status
elysia 1/1 PASS
hono 1/1 PASS

Overall: PASS

Tested with Bun v1.3.9

Generated by Build Test Bun for issue #1055

@github-actions
Copy link
Contributor

Go Build Test Results

Project Download Tests Status
color PASS ✅ PASS
env PASS ✅ PASS
uuid PASS ✅ PASS

Overall: ✅ PASS

Generated by Build Test Go for issue #1055

@github-actions
Copy link
Contributor

Deno Build Test Results

Project Tests Status
oak 1/1 ✅ PASS
std 1/1 ✅ PASS

Overall: ✅ PASS

Generated by Build Test Deno for issue #1055

@github-actions
Copy link
Contributor

Rust Build Test Results

Project Build Tests Status
fd 1/1 PASS
zoxide 1/1 PASS

Overall: ✅ PASS

Generated by Build Test Rust for issue #1055

@github-actions
Copy link
Contributor

Smoke Test Results — Copilot Engine

Test Status
GitHub MCP: last 2 merged PRs #1036 docs: add integration test coverage guide with gap analysis, #1035 feat: group --help flags by category, hide dev-only options
Playwright: github.com title contains "GitHub"
File write: smoke-test-copilot-22415250280.txt
Bash: file read-back

Overall: PASS@Mossaka

📰 BREAKING: Report filed by Smoke Copilot for issue #1055

@github-actions
Copy link
Contributor

🟢 Build Test: Node.js — PASS

Project Install Tests Status
clsx PASS ✅ PASS
execa PASS ✅ PASS
p-limit PASS ✅ PASS

Overall: ✅ PASS

Generated by Build Test Node.js for issue #1055

@github-actions
Copy link
Contributor

.NET Build Test Results

Project Restore Build Run Status
hello-world PASS
json-parse PASS

Overall: PASS

Run output

hello-world:

Hello, World!

json-parse:

{
  "Name": "AWF Test",
  "Version": 1,
  "Success": true
}
Name: AWF Test, Success: True

Generated by Build Test .NET for issue #1055

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds infrastructure for an OpenCode API proxy on port 10004 that routes to Anthropic's API, supporting OpenCode as a provider-agnostic (BYOK) AI coding agent. The PR improves maintainability by making the iptables port range calculation dynamic instead of hardcoded.

Changes:

  • Added OPENCODE port (10004) to API_PROXY_PORTS constant with documentation
  • Implemented OpenCode proxy server on port 10004 that reuses ANTHROPIC_API_KEY credentials
  • Made host-level iptables firewall rules calculate port ranges dynamically using Object.values()
  • Exposed port 10004 in api-proxy Dockerfile

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/types.ts Adds OPENCODE: 10004 to API_PROXY_PORTS constant and updates WrapperConfig documentation
containers/api-proxy/server.js Implements OpenCode proxy listener on port 10004 routing to api.anthropic.com
containers/api-proxy/Dockerfile Exposes port 10004 for OpenCode proxy
src/host-iptables.ts Refactors iptables rules to use dynamic port range calculation from API_PROXY_PORTS

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 262 to 283
// OpenCode API proxy (port 10004) — routes to Anthropic (default BYOK provider)
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;
}

console.log(`[OpenCode Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`);
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)');
});
}
Copy link

Copilot AI Feb 25, 2026

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 283
// OpenCode API proxy (port 10004) — routes to Anthropic (default BYOK provider)
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;
}

console.log(`[OpenCode Proxy] ${sanitizeForLog(req.method)} ${sanitizeForLog(req.url)}`);
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)');
});
}
Copy link

Copilot AI Feb 25, 2026

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.
@github-actions
Copy link
Contributor

☕ Java Build Test Results

Project Compile Tests Status
gson 1/1 PASS
caffeine 1/1 PASS

Overall: ✅ PASS

All Java projects compiled and tested successfully via Maven with Squid proxy (172.30.0.10:3128).

Generated by Build Test Java for issue #1055

@github-actions
Copy link
Contributor

Smoke test results (overall: FAIL)

  1. GitHub MCP merged PRs ✅ - docs: add integration test coverage guide with gap analysis; feat: group --help flags by category, hide dev-only options
  2. safeinputs-gh PR list ✅ - Remove --allow-full-filesystem-access flag; feat: add API proxy port 10004 for OpenCode engine
  3. Playwright title check ✅
  4. Tavily web search ❌ (tool unavailable)
  5. File write ✅
  6. File read ✅
  7. Discussion comment ✅
  8. Build (npm ci && npm run build) ✅

🔮 The oracle has spoken through Smoke Codex for issue #1055

@github-actions
Copy link
Contributor

Smoke test results for Claude (run 22415250328):

Test Result
GitHub MCP: #1036 docs: add integration test coverage guide with gap analysis
GitHub MCP: #1035 feat: group --help flags by category, hide dev-only options
Playwright: github.com title contains "GitHub"
File write/read

Overall: PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1055

@github-actions
Copy link
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.12 Python 3.12.3 ❌ No
Node.js v24.13.1 v20.20.0 ❌ No
Go go1.22.12 go1.22.12 ✅ Yes

Overall: ❌ FAILED — Python and Node.js versions differ between host and chroot environment.

Tested by Smoke Chroot for issue #1055

@github-actions
Copy link
Contributor

C++ Build Test Results

Project CMake Build Status
fmt PASS
json PASS

Overall: PASS

Generated by Build Test C++ for issue #1055

- Fix log injection: extract sanitized values before template literal
- Add comment explaining why OpenCode gets a separate port from Claude
  (rate limiting isolation, metrics, future multi-provider routing)
- docker-manager.ts env var not needed: gh-aw passes ANTHROPIC_BASE_URL
  via --env-all at the GitHub Actions level

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

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 about 23 hours 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.
@github-actions
Copy link
Contributor

Bun Build Test Results

Project Install Tests Status
elysia 1/1 PASS
hono 1/1 PASS

Overall: PASS

Bun version: 1.3.9

Generated by Build Test Bun for issue #1055

@github-actions
Copy link
Contributor

Smoke Test Results — PASS

💥 [THE END] — Illustrated by Smoke Claude for issue #1055

@github-actions
Copy link
Contributor

C++ Build Test Results

Project CMake Build Status
fmt PASS
json PASS

Overall: PASS

Generated by Build Test C++ for issue #1055

@github-actions
Copy link
Contributor

🤖 Smoke test results for @Mossaka (no assignees):

✅ GitHub MCP — Last 2 merged PRs: #1036 "docs: add integration test coverage guide with gap analysis", #1035 "feat: group --help flags by category, hide dev-only options"
✅ Playwright — github.com title contains "GitHub"
✅ File Write — /tmp/gh-aw/agent/smoke-test-copilot-22416501482.txt created
✅ Bash — file verified via cat

Overall: PASS

📰 BREAKING: Report filed by Smoke Copilot for issue #1055

@github-actions
Copy link
Contributor

Deno Build Test Results

Project Tests Status
oak 1/1 ✅ PASS
std 1/1 ✅ PASS

Overall: ✅ PASS

Generated by Build Test Deno for issue #1055

@github-actions
Copy link
Contributor

🦀 Rust Build Test Results

Project Build Tests Status
fd 1/1 PASS
zoxide 1/1 PASS

Overall: ✅ PASS

Generated by Build Test Rust for issue #1055

@github-actions
Copy link
Contributor

Go Build Test Results ✅

Project Download Tests Status
color PASS ✅ PASS
env PASS ✅ PASS
uuid PASS ✅ PASS

Overall: PASS

Generated by Build Test Go for issue #1055

@github-actions
Copy link
Contributor

.NET Build Test Results

Project Restore Build Run Status
hello-world PASS
json-parse PASS

Overall: PASS

Run output

hello-world:

Hello, World!
```

**json-parse:**
```
{
  "Name": "AWF Test",
  "Version": 1,
  "Success": true
}
Name: AWF Test, Success: True

Generated by Build Test .NET for issue #1055

@github-actions
Copy link
Contributor

Smoke Test Results
✅ GitHub MCP merged PRs: docs: add integration test coverage guide with gap analysis; feat: group --help flags by category, hide dev-only options
✅ safeinputs-gh pr list
✅ Playwright title contains "GitHub"
❌ Tavily web search (tool unavailable)
✅ File write
✅ Bash cat
✅ Discussion query + comment
✅ npm ci && npm run build
Overall: FAIL

🔮 The oracle has spoken through Smoke Codex for issue #1055

@github-actions
Copy link
Contributor

Node.js Build Test Results

Project Install Tests Status
clsx PASS ✅ PASS
execa PASS ✅ PASS
p-limit PASS ✅ PASS

Overall: ✅ PASS

Generated by Build Test Node.js for issue #1055

@github-actions
Copy link
Contributor

☕ Java Build Test Results

Project Compile Tests Status
gson 1/1 PASS
caffeine 1/1 PASS

Overall: ✅ PASS

All Java projects compiled and tested successfully via Maven with the AWF proxy.

Generated by Build Test Java for issue #1055

@github-actions
Copy link
Contributor

Chroot Version Comparison Results

Runtime Host Version Chroot Version Match?
Python Python 3.12.12 Python 3.12.3
Node.js v24.13.1 v20.20.0
Go go1.22.12 go1.22.12

Result: Not all tests passed. Go versions match, but Python and Node.js versions differ between host and chroot environments.

Tested by Smoke Chroot for issue #1055

@Mossaka Mossaka merged commit 62e4d90 into main Feb 25, 2026
92 checks passed
@Mossaka Mossaka deleted the feat/opencode-api-proxy branch February 25, 2026 22:48
Copilot AI added a commit that referenced this pull request Feb 25, 2026
…r docs

Update documentation to reflect the addition of OpenCode engine support
on port 10004, as introduced in PR #1055:

- Update overview to list all four supported providers
- Add OpenCode to architecture diagram and traffic flow
- Add OpenCode usage example with note about separate port rationale
- Document OpenCode header injection (same as Anthropic)
- Add port 10004 to container configuration
- Update limitations to reflect all supported providers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants