Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1,445 changes: 1,445 additions & 0 deletions .github/workflows/smoke-opencode.lock.yml

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions .github/workflows/smoke-opencode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
description: Smoke test workflow that validates OpenCode engine functionality twice daily
on:
schedule: every 12h
workflow_dispatch:
pull_request:
types: [labeled]
names: ["smoke"]
reaction: "eyes"
status-comment: true
permissions:
contents: read
issues: read
pull-requests: read
name: Smoke OpenCode
engine:
id: opencode
model: anthropic/claude-sonnet-4-20250514
strict: true
imports:
- shared/gh.md
- shared/reporting.md
network:
allowed:
- defaults
- github
tools:
cache-memory: true
github:
toolsets: [repos, pull_requests]
edit:
bash:
- "*"
web-fetch:
safe-outputs:
add-comment:
hide-older-comments: true
max: 2
create-issue:
expires: 2h
close-older-issues: true
labels: [automation, testing]
add-labels:
allowed: [smoke-opencode]
messages:
footer: "> ⚡ *[{workflow_name}]({run_url}) — Powered by OpenCode*"
run-started: "⚡ OpenCode initializing... [{workflow_name}]({run_url}) begins on this {event_type}..."
run-success: "🎯 [{workflow_name}]({run_url}) **MISSION COMPLETE!** OpenCode has delivered. ⚡"
run-failure: "⚠️ [{workflow_name}]({run_url}) {status}. OpenCode encountered unexpected challenges..."
timeout-minutes: 15
---

# Smoke Test: OpenCode Engine Validation

**CRITICAL EFFICIENCY REQUIREMENTS:**
- Keep ALL outputs extremely short and concise. Use single-line responses.
- NO verbose explanations or unnecessary context.
- Minimize file reading - only read what is absolutely necessary for the task.

## Test Requirements

1. **GitHub MCP Testing**: Use GitHub MCP tools to fetch details of exactly 2 merged pull requests from ${{ github.repository }} (title and number only)
2. **Web Fetch Testing**: Use the web-fetch MCP tool to fetch https://github.com and verify the response contains "GitHub" (do NOT use bash or playwright for this test - use the web-fetch MCP tool directly)
3. **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)
4. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back)
5. **Build gh-aw**: Run `GOCACHE=/tmp/go-cache GOMODCACHE=/tmp/go-mod make build` to verify the agent can successfully build the gh-aw project. If the command fails, mark this test as ❌ and report the failure.

## Output

Add a **very brief** comment (max 5-10 lines) to the current pull request with:
- ✅ 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.

**Important**: If no action is needed after completing your analysis, you **MUST** call the `noop` safe-output tool with a brief explanation. Failing to call any safe-output tool is the most common cause of safe-output workflow failures.

```json
{"noop": {"message": "No action needed: [brief explanation of what was analyzed and why]"}}
```
114 changes: 114 additions & 0 deletions actions/setup/sh/convert_gateway_config_opencode.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/usr/bin/env bash
# Convert MCP Gateway Configuration to OpenCode Format
# This script converts the gateway's standard HTTP-based MCP configuration
# to the JSON format expected by OpenCode (opencode.jsonc)
#
# OpenCode reads MCP server configuration from opencode.jsonc:
# - Project: ./opencode.jsonc (used here)
# - Global: ~/.config/opencode/opencode.json
#
# See: https://opencode.ai/docs/mcp-servers/

set -e

# Required environment variables:
# - MCP_GATEWAY_OUTPUT: Path to gateway output configuration file
# - MCP_GATEWAY_DOMAIN: Domain to use for MCP server URLs (e.g., host.docker.internal)
# - MCP_GATEWAY_PORT: Port for MCP gateway (e.g., 80)
# - GITHUB_WORKSPACE: Workspace directory for project-level config
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

This converter writes ${GITHUB_WORKSPACE}/opencode.jsonc, which includes Authorization headers for MCP servers. Unlike convert_gateway_config_gemini.sh/start_mcp_gateway.sh, it doesn’t set a restrictive umask or chmod the output, so the config may be created with overly-permissive mode (e.g., 0644). Consider adding umask 077, set -euo pipefail, and chmod 600 on opencode.jsonc (and any temp files) after writing/merging.

Copilot uses AI. Check for mistakes.

if [ -z "$MCP_GATEWAY_OUTPUT" ]; then
echo "ERROR: MCP_GATEWAY_OUTPUT environment variable is required"
exit 1
fi

if [ ! -f "$MCP_GATEWAY_OUTPUT" ]; then
echo "ERROR: Gateway output file not found: $MCP_GATEWAY_OUTPUT"
exit 1
fi

if [ -z "$MCP_GATEWAY_DOMAIN" ]; then
echo "ERROR: MCP_GATEWAY_DOMAIN environment variable is required"
exit 1
fi

if [ -z "$MCP_GATEWAY_PORT" ]; then
echo "ERROR: MCP_GATEWAY_PORT environment variable is required"
exit 1
fi

if [ -z "$GITHUB_WORKSPACE" ]; then
echo "ERROR: GITHUB_WORKSPACE environment variable is required"
exit 1
fi

echo "Converting gateway configuration to OpenCode format..."
echo "Input: $MCP_GATEWAY_OUTPUT"
echo "Target domain: $MCP_GATEWAY_DOMAIN:$MCP_GATEWAY_PORT"

# Convert gateway output to OpenCode opencode.jsonc format
# Gateway format:
# {
# "mcpServers": {
# "server-name": {
# "type": "http",
# "url": "http://domain:port/mcp/server-name",
# "headers": {
# "Authorization": "apiKey"
# }
# }
# }
# }
#
# OpenCode format:
# {
# "mcp": {
# "server-name": {
# "type": "remote",
# "enabled": true,
# "url": "http://domain:port/mcp/server-name",
# "headers": {
# "Authorization": "apiKey"
# }
# }
# }
# }
#
# The main differences:
# 1. Top-level key is "mcp" not "mcpServers"
# 2. Server type is "remote" not "http"
# 3. Has "enabled": true field
# 4. Remove "tools" field (Copilot-specific)
# 5. URLs must use the correct domain (host.docker.internal) for container access

# Build the correct URL prefix using the configured domain and port
URL_PREFIX="http://${MCP_GATEWAY_DOMAIN}:${MCP_GATEWAY_PORT}"

OPENCODE_CONFIG_FILE="${GITHUB_WORKSPACE}/opencode.jsonc"

# Build the MCP section from gateway output
MCP_SECTION=$(jq --arg urlPrefix "$URL_PREFIX" '
.mcpServers | with_entries(
.value |= {
"type": "remote",
"enabled": true,
"url": (.url | sub("^http://[^/]+/mcp/"; $urlPrefix + "/mcp/")),
"headers": .headers
}
)
' "$MCP_GATEWAY_OUTPUT")

# Merge into existing opencode.jsonc or create new one
if [ -f "$OPENCODE_CONFIG_FILE" ]; then
echo "Merging MCP config into existing opencode.jsonc..."
jq --argjson mcpSection "$MCP_SECTION" '.mcp = (.mcp // {}) * $mcpSection' "$OPENCODE_CONFIG_FILE" > "${OPENCODE_CONFIG_FILE}.tmp"
mv "${OPENCODE_CONFIG_FILE}.tmp" "$OPENCODE_CONFIG_FILE"
else
echo "Creating new opencode.jsonc..."
jq -n --argjson mcpSection "$MCP_SECTION" '{"mcp": $mcpSection}' > "$OPENCODE_CONFIG_FILE"
fi

echo "OpenCode configuration written to $OPENCODE_CONFIG_FILE"
echo ""
echo "Converted configuration:"
cat "$OPENCODE_CONFIG_FILE"
4 changes: 4 additions & 0 deletions actions/setup/sh/start_mcp_gateway.sh
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ case "$ENGINE_TYPE" in
echo "Using Gemini converter..."
bash ${RUNNER_TEMP}/gh-aw/actions/convert_gateway_config_gemini.sh
;;
opencode)
echo "Using OpenCode converter..."
bash ${RUNNER_TEMP}/gh-aw/actions/convert_gateway_config_opencode.sh
;;
*)
echo "No agent-specific converter found for engine: $ENGINE_TYPE"
echo "Using gateway output directly"
Expand Down
3 changes: 3 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ const (

// GeminiLLMGatewayPort is the port for the Gemini LLM gateway
GeminiLLMGatewayPort = 10003

// OpenCodeLLMGatewayPort is the port for the OpenCode LLM gateway
OpenCodeLLMGatewayPort = 10004
)

// DefaultGitHubLockdown is the default value for the GitHub MCP server lockdown setting.
Expand Down
2 changes: 1 addition & 1 deletion pkg/constants/constants_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestAgenticEngines(t *testing.T) {
t.Error("AgenticEngines should not be empty")
}

expectedEngines := []string{"claude", "codex", "copilot", "gemini"}
expectedEngines := []string{"claude", "codex", "copilot", "gemini", "opencode"}
if len(AgenticEngines) != len(expectedEngines) {
t.Errorf("AgenticEngines length = %d, want %d", len(AgenticEngines), len(expectedEngines))
}
Expand Down
21 changes: 20 additions & 1 deletion pkg/constants/engine_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const (
CodexEngine EngineName = "codex"
// GeminiEngine is the Google Gemini engine identifier
GeminiEngine EngineName = "gemini"
// OpenCodeEngine is the OpenCode engine identifier
OpenCodeEngine EngineName = "opencode"

// DefaultEngine is the default agentic engine used when no engine is explicitly specified.
// Currently defaults to CopilotEngine.
Expand All @@ -30,7 +32,7 @@ const (
// Deprecated: Use workflow.NewEngineCatalog(workflow.NewEngineRegistry()).IDs() for a
// catalog-derived list. This slice is maintained for backward compatibility and must
// stay in sync with the built-in engines registered in NewEngineCatalog.
var AgenticEngines = []string{string(ClaudeEngine), string(CodexEngine), string(CopilotEngine), string(GeminiEngine)}
var AgenticEngines = []string{string(ClaudeEngine), string(CodexEngine), string(CopilotEngine), string(GeminiEngine), string(OpenCodeEngine)}

// EngineOption represents a selectable AI engine with its display metadata and secret configuration
type EngineOption struct {
Expand Down Expand Up @@ -83,6 +85,15 @@ var EngineOptions = []EngineOption{
KeyURL: "https://aistudio.google.com/app/apikey",
WhenNeeded: "Gemini engine workflows",
},
{
Value: string(OpenCodeEngine),
Label: "OpenCode",
Description: "OpenCode multi-provider AI coding agent (BYOK)",
SecretName: "ANTHROPIC_API_KEY",
AlternativeSecrets: []string{"OPENAI_API_KEY", "GOOGLE_API_KEY"},
KeyURL: "https://opencode.ai/docs/get-started/",
WhenNeeded: "OpenCode engine workflows (default: Anthropic provider)",
},
}

// SystemSecretSpec describes a system-level secret that is not engine-specific
Expand Down Expand Up @@ -177,6 +188,10 @@ const (
EnvVarModelDetectionCodex = "GH_AW_MODEL_DETECTION_CODEX"
// EnvVarModelDetectionGemini configures the default Gemini model for detection
EnvVarModelDetectionGemini = "GH_AW_MODEL_DETECTION_GEMINI"
// EnvVarModelAgentOpenCode configures the default OpenCode model for agent execution
EnvVarModelAgentOpenCode = "GH_AW_MODEL_AGENT_OPENCODE"
// EnvVarModelDetectionOpenCode configures the default OpenCode model for detection
EnvVarModelDetectionOpenCode = "GH_AW_MODEL_DETECTION_OPENCODE"

// CopilotCLIModelEnvVar is the native environment variable name supported by the Copilot CLI
// for selecting the model. Setting this env var is equivalent to passing --model to the CLI.
Expand All @@ -198,6 +213,10 @@ const (
// for selecting the model. Setting this env var is equivalent to passing --model to the CLI.
GeminiCLIModelEnvVar = "GEMINI_MODEL"

// OpenCodeCLIModelEnvVar is the native environment variable name for OpenCode model selection.
// OpenCode uses provider/model format (e.g., "anthropic/claude-sonnet-4-20250514").
OpenCodeCLIModelEnvVar = "OPENCODE_MODEL"

// Common environment variable names used across all engines

// EnvVarPrompt is the path to the workflow prompt file
Expand Down
3 changes: 3 additions & 0 deletions pkg/constants/version_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const DefaultCodexVersion Version = "0.118.0"
// DefaultGeminiVersion is the default version of the Google Gemini CLI
const DefaultGeminiVersion Version = "0.37.1"

// DefaultOpenCodeVersion is the default version of the OpenCode CLI
const DefaultOpenCodeVersion Version = "1.2.14"

// DefaultGitHubMCPServerVersion is the default version of the GitHub MCP server Docker image
const DefaultGitHubMCPServerVersion Version = "v0.32.0"

Expand Down
8 changes: 4 additions & 4 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9116,15 +9116,15 @@
"oneOf": [
{
"type": "string",
"description": "Engine name: built-in ('claude', 'codex', 'copilot', 'gemini') or a named catalog entry"
"description": "Engine name: built-in ('claude', 'codex', 'copilot', 'gemini', 'opencode') or a named catalog entry"
},
{
"type": "object",
"description": "Extended engine configuration object with advanced options for model selection, turn limiting, environment variables, and custom steps",
"properties": {
"id": {
"type": "string",
"description": "AI engine identifier: built-in ('claude', 'codex', 'copilot', 'gemini') or a named catalog entry"
"description": "AI engine identifier: built-in ('claude', 'codex', 'copilot', 'gemini', 'opencode') or a named catalog entry"
},
"version": {
"type": ["string", "number"],
Expand Down Expand Up @@ -9285,8 +9285,8 @@
"properties": {
"id": {
"type": "string",
"description": "Runtime adapter identifier (e.g. 'codex', 'claude', 'copilot', 'gemini')",
"examples": ["codex", "claude", "copilot", "gemini"]
"description": "Runtime adapter identifier (e.g. 'codex', 'claude', 'copilot', 'gemini', 'opencode')",
"examples": ["codex", "claude", "copilot", "gemini", "opencode"]
},
"version": {
"type": ["string", "number"],
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/agentic_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ func NewEngineRegistry() *EngineRegistry {
registry.Register(NewCodexEngine())
registry.Register(NewCopilotEngine())
registry.Register(NewGeminiEngine())
registry.Register(NewOpenCodeEngine())

agenticEngineLog.Printf("Registered %d engines", len(registry.engines))
return registry
Expand Down
16 changes: 16 additions & 0 deletions pkg/workflow/data/engines/opencode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
engine:
id: opencode
display-name: OpenCode
description: OpenCode CLI with headless mode and multi-provider LLM support
runtime-id: opencode
provider:
name: opencode
auth:
- role: api-key
secret: ANTHROPIC_API_KEY
---

<!-- # OpenCode CLI
Shared engine configuration for OpenCode multi-provider AI coding agent (BYOK). -->
Loading
Loading