Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
89 changes: 89 additions & 0 deletions .github/workflows/smoke-opencode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
description: Smoke test workflow that validates OpenCode engine functionality by testing AWF firewall capabilities
on:
roles: all
schedule: every 12h
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened]
reaction: "rocket"
permissions:
contents: read
issues: read
pull-requests: read
discussions: read
name: Smoke OpenCode
engine: opencode
strict: true
Comment on lines +1 to +17

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

This adds a new agentic workflow source file under .github/workflows/, but there is no corresponding compiled .lock.yml workflow checked in (unlike the other smoke workflows, e.g. smoke-copilot.md → smoke-copilot.lock.yml). As-is, this won't run in GitHub Actions and may be easy to overlook; consider either including the compiled lock file once supported, or adding an explicit note in the frontmatter/body that compilation is pending and the workflow is not yet active.

Copilot uses AI. Check for mistakes.
imports:
- shared/gh.md
- shared/reporting.md
network:
allowed:
- defaults
- github
tools:
cache-memory: true
github:
toolsets: [repos, pull_requests]
edit:
bash:
- "*"
safe-outputs:
threat-detection:
enabled: false
add-comment:
hide-older-comments: true
max: 2
create-issue:
expires: 2h
close-older-issues: true
add-labels:
allowed: [smoke-opencode]
hide-comment:
messages:
footer: "> 🌐 *Transmitted by [{workflow_name}]({run_url})*"
run-started: "🌐 [{workflow_name}]({run_url}) is initializing on this {event_type}..."
run-success: "✅ [{workflow_name}]({run_url}) completed successfully. All systems nominal. 🚀"
run-failure: "❌ [{workflow_name}]({run_url}) {status}. Investigation required..."
timeout-minutes: 15
post-steps:
- name: Validate safe outputs were invoked
run: |
OUTPUTS_FILE="${GH_AW_SAFE_OUTPUTS:-${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl}"
if [ ! -s "$OUTPUTS_FILE" ]; then
echo "::error::No safe outputs were invoked. Smoke tests require the agent to call safe output tools."
exit 1
fi
echo "Safe output entries found: $(wc -l < "$OUTPUTS_FILE")"
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
if ! grep -q '"add_comment"' "$OUTPUTS_FILE"; then
echo "::error::Agent did not call add_comment on a pull_request trigger."
exit 1
fi
echo "add_comment verified for PR trigger"
fi
echo "Safe output validation passed"
---

# Smoke Test: OpenCode Engine Validation

**IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.**

## Test Requirements

1. **GitHub MCP Testing**: Review the last 2 merged pull requests in `__GH_AW_GITHUB_REPOSITORY__`
2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-opencode-${{ github.run_id }}.txt` with content "Smoke test passed for OpenCode at $(date)" (create the directory if it doesn't exist)
3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back)
4. **Build AWF**: Run `npm ci && npm run build` to verify the agent can successfully build the AWF project. If the command fails, mark this test as ❌ and report the failure.
5. **Add Comment**: Use the `add_comment` tool to post a brief summary comment on the current pull request

## Output

**REQUIRED**: Call `add_comment` to post a brief comment (max 5-10 lines) on the current pull request (this is validated by the post-step check) containing:
- PR titles only (no descriptions)
- ✅ or ❌ for each test result
- Overall status: PASS or FAIL

If all tests pass:
- Use the `add_labels` safe-output tool to add the label `smoke-opencode` to the pull request
75 changes: 58 additions & 17 deletions containers/api-proxy/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -1027,11 +1027,16 @@ if (require.main === module) {
});
}

// 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) {
// OpenCode API proxy (port 10004) — dynamic provider routing
// Defaults to Copilot/OpenAI routing (OPENAI_API_KEY), with Anthropic as a BYOK fallback.
// OpenCode gets a separate port from Claude (10001) and Codex (10000) for per-engine
// rate limiting and metrics isolation.
//
// Credential priority (first available wins):
// 1. OPENAI_API_KEY → OpenAI/Copilot-compatible route (OPENAI_API_TARGET)
// 2. ANTHROPIC_API_KEY → Anthropic BYOK route (ANTHROPIC_API_TARGET)
// 3. COPILOT_AUTH_TOKEN → Copilot route (COPILOT_API_TARGET)
if (OPENAI_API_KEY || ANTHROPIC_API_KEY || COPILOT_AUTH_TOKEN) {
const opencodeServer = http.createServer((req, res) => {
if (req.url === '/health' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
Expand All @@ -1046,26 +1051,62 @@ if (require.main === module) {
method: logMethod,
url: logUrl,
});
logRequest('info', 'opencode_proxy_header_injection', {
message: '[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';

Copilot AI Apr 14, 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 (10004) HTTP handler doesn't apply rate limiting (unlike the other HTTP proxies that call checkRateLimit() before proxyRequest()). This makes the "per-engine rate limiting" comment inaccurate and allows unlimited HTTP traffic on 10004 even when AWF rate limiting is enabled. Consider computing content-length and calling checkRateLimit(req, res, 'opencode', contentLength) before routing/proxyRequest().

Suggested change
const parsedContentLength = Number(req.headers['content-length']);
const contentLength = Number.isFinite(parsedContentLength) && parsedContentLength > 0 ? parsedContentLength : 0;
if (!checkRateLimit(req, res, 'opencode', contentLength)) {
return;
}

Copilot uses AI. Check for mistakes.
if (OPENAI_API_KEY) {
logRequest('info', 'opencode_proxy_header_injection', {
message: '[OpenCode Proxy] Routing to OpenAI/Copilot via OPENAI_API_KEY',
target: OPENAI_API_TARGET,
});
proxyRequest(req, res, OPENAI_API_TARGET, {
'Authorization': `Bearer ${OPENAI_API_KEY}`,
}, 'opencode', OPENAI_API_BASE_PATH);
} else if (ANTHROPIC_API_KEY) {
logRequest('info', 'opencode_proxy_header_injection', {
message: '[OpenCode Proxy] Routing to Anthropic via ANTHROPIC_API_KEY',
target: ANTHROPIC_API_TARGET,
});
const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
if (!req.headers['anthropic-version']) {
anthropicHeaders['anthropic-version'] = '2023-06-01';
}
proxyRequest(req, res, ANTHROPIC_API_TARGET, anthropicHeaders, 'opencode', ANTHROPIC_API_BASE_PATH);
} else {
// COPILOT_AUTH_TOKEN only — route to Copilot API target
logRequest('info', 'opencode_proxy_header_injection', {
message: '[OpenCode Proxy] Routing to Copilot via COPILOT_AUTH_TOKEN',
target: COPILOT_API_TARGET,
});
proxyRequest(req, res, COPILOT_API_TARGET, {
'Authorization': `Bearer ${COPILOT_AUTH_TOKEN}`,
}, 'opencode');
}

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

New provider-routing behavior for the OpenCode proxy (OPENAI_API_KEY vs ANTHROPIC_API_KEY vs COPILOT_AUTH_TOKEN) isn't covered by tests. Since this repo already has containers/api-proxy/server.test.js, please add unit/integration coverage for the 10004 routing priority and header injection for each credential scenario (and WebSocket upgrade routing if applicable).

Copilot uses AI. Check for mistakes.
proxyRequest(req, res, ANTHROPIC_API_TARGET, anthropicHeaders);
});

opencodeServer.on('upgrade', (req, socket, head) => {
const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
if (!req.headers['anthropic-version']) {
anthropicHeaders['anthropic-version'] = '2023-06-01';
if (OPENAI_API_KEY) {
proxyWebSocket(req, socket, head, OPENAI_API_TARGET, {
'Authorization': `Bearer ${OPENAI_API_KEY}`,
}, 'opencode', OPENAI_API_BASE_PATH);
} else if (ANTHROPIC_API_KEY) {
const anthropicHeaders = { 'x-api-key': ANTHROPIC_API_KEY };
if (!req.headers['anthropic-version']) {
anthropicHeaders['anthropic-version'] = '2023-06-01';
}
proxyWebSocket(req, socket, head, ANTHROPIC_API_TARGET, anthropicHeaders, 'opencode', ANTHROPIC_API_BASE_PATH);
} else {
proxyWebSocket(req, socket, head, COPILOT_API_TARGET, {
'Authorization': `Bearer ${COPILOT_AUTH_TOKEN}`,
}, 'opencode');
}
proxyWebSocket(req, socket, head, ANTHROPIC_API_TARGET, anthropicHeaders, 'opencode');
});

opencodeServer.listen(10004, '0.0.0.0', () => {
console.log(`[API Proxy] OpenCode proxy listening on port 10004 (-> Anthropic at ${ANTHROPIC_API_TARGET})`);
const routingTarget = OPENAI_API_KEY
? `OpenAI/Copilot at ${OPENAI_API_TARGET}`
: ANTHROPIC_API_KEY
? `Anthropic at ${ANTHROPIC_API_TARGET}`
: `Copilot at ${COPILOT_API_TARGET}`;
logRequest('info', 'server_start', { message: `OpenCode proxy listening on port 10004 (-> ${routingTarget})` });
});
}

Expand Down
6 changes: 3 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export const API_PROXY_PORTS = {
GEMINI: 10003,

/**
* OpenCode API proxy port (routes to Anthropic by default)
* OpenCode is BYOK — defaults to Anthropic as the primary provider
* OpenCode API proxy port (defaults to Copilot/OpenAI routing; falls back to Anthropic)
* OpenCode is BYOK — credential priority: OPENAI_API_KEY > ANTHROPIC_API_KEY > COPILOT_AUTH_TOKEN

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

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

The API proxy docs mention COPILOT_AUTH_TOKEN in the OpenCode credential priority, but COPILOT_AUTH_TOKEN is an internal server.js-derived value (resolved from COPILOT_GITHUB_TOKEN/COPILOT_API_KEY) and isn't something users configure directly. Consider updating this comment to reference the actual env vars/config fields users can set, so the docs remain actionable.

Suggested change
* OpenCode is BYOK credential priority: OPENAI_API_KEY > ANTHROPIC_API_KEY > COPILOT_AUTH_TOKEN
* OpenCode is BYOK credential priority: OPENAI_API_KEY > ANTHROPIC_API_KEY > COPILOT_GITHUB_TOKEN/COPILOT_API_KEY

Copilot uses AI. Check for mistakes.
* @see containers/api-proxy/server.js
*/
OPENCODE: 10004,
Expand Down Expand Up @@ -611,7 +611,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}
* - http://api-proxy:10004 - OpenCode API proxy (defaults to Copilot/OpenAI routing) {@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