Skip to content

feat: add OpenCode community provider#1372

Open
choufeng wants to merge 1 commit intocoleam00:devfrom
choufeng:feat/opencode-provider
Open

feat: add OpenCode community provider#1372
choufeng wants to merge 1 commit intocoleam00:devfrom
choufeng:feat/opencode-provider

Conversation

@choufeng
Copy link
Copy Markdown

@choufeng choufeng commented Apr 23, 2026

Summary

Adds OpenCode as a community AI assistant provider for Archon, closing #1151.

What It Does

  • Auto-starts OpenCode Server (opencode serve) on first use with lazy lifecycle management
  • Connects via @opencode-ai/sdk to bridge OpenCode's SSE events into Archon's MessageChunk contract
  • Supports session resume — workflow nodes can resume previous OpenCode sessions
  • Honest capability declaration — only declares features that are actually wired up

Supported Capabilities

Capability Status Notes
sessionResume Session IDs persisted by OpenCode Server
mcp Native MCP support via OpenCode
skills Injected via systemPrompt
toolRestrictions allowed_tools / denied_tools mapped to SDK
structuredOutput SDK-native JSON Schema support
envInjection Per-request env vars
effortControl Via reasoning effort
thinkingControl Via reasoning toggle
hooks Archon hooks ≠ OpenCode plugins
agents No inline sub-agent definitions
costControl No cost limit API
fallbackModel No automatic fallback
sandbox No sandbox support

Configuration

# .archon/config.yaml
assistants:
  opencode:
    model: anthropic/claude-sonnet-4
    hostname: 127.0.0.1
    port: 4096
    autoStartServer: true

Files Changed

  • packages/providers/src/community/opencode/ — New provider implementation (10 files)
  • packages/providers/src/registry.ts — Register OpenCode provider
  • packages/providers/src/registry.test.ts — Registry tests for OpenCode
  • packages/providers/package.json — Add @opencode-ai/sdk dependency and exports

Testing

  • 23 new tests across config.test.ts and provider.test.ts
  • All existing provider tests continue to pass
  • TypeScript type-check passes

Cross-Cutting Impact

Per the Phase 2 community provider contract (#1195), changes are localized to:

  1. A new directory under packages/providers/src/community/opencode/
  2. One line in packages/providers/src/registry.ts
  3. One line in packages/providers/package.json exports
  4. Test additions in packages/providers/src/registry.test.ts

No entrypoint edits, no config-type edits.

Summary by CodeRabbit

Release Notes

  • New Features
    • Integrated OpenCode as a community provider with session resumption, tool execution, and structured output support
    • Configurable server parameters: hostname, port, password, and auto-start option
    • Automatic server health checks and session lifecycle management

Add OpenCode as a community AI assistant provider for Archon.

- Implements IAgentProvider interface via @opencode-ai/sdk
- Auto-starts opencode serve on first use (lazy server lifecycle)
- Bridges SSE events to Archon MessageChunk contract
- Supports: session resume, MCP, tool restrictions, structured output,
  skills (via systemPrompt), env injection, thinking/effort control
- Honest capability declaration with false for unsupported features
- Full test coverage with mocked SDK

Closes coleam00#1151
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Introduces a new OpenCode community provider for Archon with configuration parsing, server management, event bridging, and provider registration, extending the provider registry with lazy-loading SDK support and session lifecycle management capabilities.

Changes

Cohort / File(s) Summary
Specification & Design
docs/superpowers/specs/2026-04-23-opencode-provider-design.md
New design spec documenting OpenCode provider architecture, client/server lifecycle, event-to-chunk mapping, health-check workflow, and capability matrix comparison with Pi provider.
Package Configuration
packages/providers/package.json
Added ./community/opencode export path, new @opencode-ai/sdk runtime dependency, and extended test script with config/provider test targets.
Provider Capabilities & Configuration
packages/providers/src/community/opencode/capabilities.ts, config.ts, config.test.ts
Declares OPENCODE_CAPABILITIES object and introduces OpencodeProviderDefaults interface with parseOpencodeConfig() parser function to validate and extract typed configuration fields (model, hostname, port, serverPassword, autoStartServer).
Event Bridging & Server Management
packages/providers/src/community/opencode/event-bridge.ts, server-manager.ts
Exports bridgeEvents() async generator to convert OpenCode SSE event streams into Archon MessageChunk values; adds ensureServer() to manage lazy server spawning with health checks, 30s timeout, process caching, and generatePassword() utility.
Provider Implementation
packages/providers/src/community/opencode/provider.ts, provider.test.ts
Implements OpenCodeProvider class with sendQuery() async generator, session resumption/creation, prompt forwarding, and SDK client lifecycle; comprehensive test suite validates session handling, event chunk production, and tool transitions.
Registration & Exports
packages/providers/src/community/opencode/registration.ts, index.ts, packages/providers/src/registry.ts, registry.test.ts
Adds registerOpencodeProvider() for idempotent provider registration with model-compatibility matching; integrates into registerCommunityProviders() bootstrap; exports facade at index.ts; validates registration semantics and capability declarations in test suite.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/Consumer
    participant Provider as OpenCodeProvider
    participant ServerMgr as ServerManager
    participant OpenCodeSDK as OpenCode SDK/Server
    participant EventBridge as EventBridge

    Client->>Provider: sendQuery(prompt, resumeSessionId?)
    activate Provider
    
    Provider->>ServerMgr: ensureServer(config)
    activate ServerMgr
    ServerMgr->>OpenCodeSDK: Check health at /global/health
    alt Server not running & autoStart enabled
        ServerMgr->>OpenCodeSDK: Spawn "opencode serve"
        ServerMgr->>OpenCodeSDK: Poll health until ready
    end
    ServerMgr-->>Provider: return ServerInfo
    deactivate ServerMgr
    
    Provider->>OpenCodeSDK: createOpencodeClient(ServerInfo)
    
    alt resumeSessionId provided
        Provider->>OpenCodeSDK: resumeSession(resumeSessionId)
    else
        Provider->>OpenCodeSDK: createSession()
    end
    
    Provider->>OpenCodeSDK: session.promptAsync(prompt, config)
    activate OpenCodeSDK
    OpenCodeSDK-->>EventBridge: SSE event stream
    
    Provider->>EventBridge: bridgeEvents(client, sessionId, abortSignal)
    activate EventBridge
    loop Process OpenCode events
        EventBridge->>EventBridge: Map event type to MessageChunk
        note over EventBridge: text → assistant<br/>reasoning → thinking<br/>tool → tool/tool_result<br/>message.updated → result
        EventBridge-->>Provider: yield MessageChunk
    end
    deactivate EventBridge
    deactivate OpenCodeSDK
    
    Provider-->>Client: async iterate MessageChunk
    deactivate Provider
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • Wirasm

🐰 Hops excitedly around the new provider files

OpenCode joins the warren with a leap,
Events bridge into chunks, so neat and deep,
Sessions persist, servers auto-start with care,
A community provider beyond compare! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is comprehensive and addresses most key sections (Summary, What It Does, Supported Capabilities, Configuration, Files Changed, Testing, Cross-Cutting Impact) but is missing several required template sections (UX Journey flows, Architecture Diagram with connection inventory, Label Snapshot, Security Impact, Validation Evidence, Compatibility/Migration details, Human Verification, and Side Effects/Blast Radius). Add the missing template sections: UX Journey (before/after flows), Architecture Diagram with module connections, Label Snapshot (risk/size/scope/module labels), Validation Evidence (test/lint/typecheck results), Security Impact assessment, Compatibility details, Human Verification checklist, and Rollback Plan.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add OpenCode community provider' clearly and concisely summarizes the main change—adding OpenCode as a new community AI provider for Archon.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/opencode-provider

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
packages/providers/package.json (1)

22-22: Test script has grown unwieldy; consider extracting to a runner.

The test script is now a ~17-command && chain. With each new community provider this gets harder to maintain and diff-review. Since per-file invocations are required (per the mock.module() guideline), a small scripts/run-tests.sh or a JSON file listing test files would keep the manifest tidy. Non-blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/package.json` at line 22, The package.json "test" script
has become an unwieldy long && chain; extract the list of per-file test
invocations into a small runner and update the "test" script to call it. Create
a new script file (e.g. scripts/run-tests.sh or scripts/run-tests.js) or a JSON
array (e.g. testFiles) that enumerates the individual test paths referenced in
package.json, implement iteration to call the existing test command (bun test
<file>) for each entry, and then replace the long "test" value in package.json
with a single call to that runner so symbols to change are the "test" script in
package.json and the new runner script that executes the per-file bun test
invocations.
packages/providers/src/community/opencode/config.ts (1)

31-33: Consider validating port range.

typeof raw.port === 'number' accepts NaN, non-integers, negatives, and values > 65535. These will silently propagate to server startup and surface as opaque connection errors later. A cheap guard here keeps the "drop invalid silently" contract while preventing nonsense ports.

♻️ Proposed tightening
-  if (typeof raw.port === 'number') {
-    result.port = raw.port;
-  }
+  if (
+    typeof raw.port === 'number' &&
+    Number.isInteger(raw.port) &&
+    raw.port >= 1 &&
+    raw.port <= 65535
+  ) {
+    result.port = raw.port;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/community/opencode/config.ts` around lines 31 - 33,
The code currently assigns raw.port to result.port when typeof raw.port ===
'number', which allows NaN, non-integers, negatives, and >65535; change the
guard around raw.port so you only set result.port when raw.port is a finite
integer in the valid TCP port range (0–65535) — e.g., check
Number.isInteger(raw.port) && isFinite(raw.port) && raw.port >= 0 && raw.port <=
65535 before assigning to result.port to preserve the "drop invalid silently"
behavior while preventing nonsense ports from propagating.
packages/providers/src/community/opencode/event-bridge.ts (1)

26-83: Token/cost accumulator is effectively dead code.

You return on the first result chunk at L61–63, so the accumulator block at L50–56 can only ever run for the single final chunk (immediately before return). That makes the fallback yield at L74–83 unreachable in the normal path (the only way to reach it is when the SSE stream ends with no message.updated and no session.error — in which case accumulatedTokens is still zero because no result chunk was observed).

If the intent is "sum across multiple partial results," remove the early return and let the stream close naturally. If the intent is just a safety net, the accumulator can be dropped and the fallback can yield zero tokens. Either way, the current shape is misleading to future maintainers.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/specs/2026-04-23-opencode-provider-design.md`:
- Around line 24-30: provider.ts is creating the OpenCode SDK client with only
baseUrl and never passing the retrieved password (the variable from lines where
password is read), causing 401s; fix createOpencodeClient usage by constructing
HTTP Basic credentials from the static user "opencode" and the retrieved
password (base64 encode "opencode:password") and pass it as an Authorization
header in the client options (i.e. add headers: { Authorization: `Basic
${credentials}` }) when calling createOpencodeClient so all SDK requests include
the required auth.

In `@packages/providers/src/community/opencode/event-bridge.ts`:
- Around line 132-146: In the message.updated branch of event-bridge.ts (the
switch case building the return object from info and info.tokens), the computed
tokens.total can become NaN when any of tokens.input/output/reasoning are
missing; fix by computing total using the guarded values used for input/output
(e.g., use (tokens.input ?? 0) + (tokens.output ?? 0) + (tokens.reasoning ?? 0))
and only produce undefined when tokens itself is missing (keep the existing
tokens ? ... : undefined behavior); update the total expression where
tokens.total is set in that return object so it cannot produce NaN.

In `@packages/providers/src/community/opencode/provider.test.ts`:
- Line 23: The mock client defines session.status but the provider calls
client.session.get, so update the mock to expose session.get (replace references
to mockSessionStatus with a mockSessionGet async mock) and update any mock
implementations/usages (e.g., mockSessionStatus.mockImplementationOnce) to
target mockSessionGet; ensure tests that assert calls reference mockSessionGet
so the provider's calls to client.session.get exercise the mock instead of
triggering the catch/fallback to session.create.

In `@packages/providers/src/community/opencode/provider.ts`:
- Around line 50-60: The SDK client is created without forwarding the computed
password so API calls will fail under the server's HTTP Basic Auth; update the
createOpencodeClient call to pass a custom fetch (or SDK-supported auth option)
that injects an Authorization: Basic header built from the computed password
variable (the same password passed into ensureServer), e.g. wrap the default
fetch to set Authorization using Buffer/atob logic, and ensure
createOpencodeClient({ baseUrl:
`http://${serverInfo.hostname}:${serverInfo.port}`, fetch: /* custom fetch that
uses password */ }) is used so calls from the client (used later in this file)
include the server password.

In `@packages/providers/src/community/opencode/server-manager.ts`:
- Around line 63-95: The child process created by spawn (proc) currently sets
stdio: 'pipe' but never consumes stdout and lacks an exit handler; to fix,
either drain stdout (call proc.stdout?.resume() or attach a noop/data listener)
to avoid the OS pipe blocking the child, and add proc.on('exit' / 'close',
(code, signal) => { log the code/signal via getLog() and set managedServer =
undefined }) so the stale managedServer reference is cleared and failures are
observable; update ServerInfo/pid handling if needed but keep waitForReady usage
unchanged.
- Around line 122-127: The generatePassword function currently uses Math.random
which is not cryptographically secure; replace its implementation to generate a
strong secret using Node's crypto module (e.g., crypto.randomBytes or
crypto.randomUUID) inside generatePassword to produce a sufficiently long,
URL-safe password string, ensuring the function name generatePassword is updated
to import/require and use crypto and to return the securely generated string
instead of Math.random-based values.

---

Nitpick comments:
In `@packages/providers/package.json`:
- Line 22: The package.json "test" script has become an unwieldy long && chain;
extract the list of per-file test invocations into a small runner and update the
"test" script to call it. Create a new script file (e.g. scripts/run-tests.sh or
scripts/run-tests.js) or a JSON array (e.g. testFiles) that enumerates the
individual test paths referenced in package.json, implement iteration to call
the existing test command (bun test <file>) for each entry, and then replace the
long "test" value in package.json with a single call to that runner so symbols
to change are the "test" script in package.json and the new runner script that
executes the per-file bun test invocations.

In `@packages/providers/src/community/opencode/config.ts`:
- Around line 31-33: The code currently assigns raw.port to result.port when
typeof raw.port === 'number', which allows NaN, non-integers, negatives, and
>65535; change the guard around raw.port so you only set result.port when
raw.port is a finite integer in the valid TCP port range (0–65535) — e.g., check
Number.isInteger(raw.port) && isFinite(raw.port) && raw.port >= 0 && raw.port <=
65535 before assigning to result.port to preserve the "drop invalid silently"
behavior while preventing nonsense ports from propagating.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a85c4b92-f476-4516-b4d1-a2e325db8da4

📥 Commits

Reviewing files that changed from the base of the PR and between b99cee4 and 1d9286f.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • docs/superpowers/specs/2026-04-23-opencode-provider-design.md
  • packages/providers/package.json
  • packages/providers/src/community/opencode/capabilities.ts
  • packages/providers/src/community/opencode/config.test.ts
  • packages/providers/src/community/opencode/config.ts
  • packages/providers/src/community/opencode/event-bridge.ts
  • packages/providers/src/community/opencode/index.ts
  • packages/providers/src/community/opencode/provider.test.ts
  • packages/providers/src/community/opencode/provider.ts
  • packages/providers/src/community/opencode/registration.ts
  • packages/providers/src/community/opencode/server-manager.ts
  • packages/providers/src/registry.test.ts
  • packages/providers/src/registry.ts

Comment on lines +24 to +30
| SDK loading | Dynamic import with `PI_PACKAGE_DIR` shim | Clean dynamic import |
| Auth | OAuth + API key file (`~/.pi/agent/auth.json`) | HTTP Basic Auth (`OPENCODE_SERVER_PASSWORD`) |
| Model refs | `<pi-provider>/<model-id>` | `<provider-id>/<model-id>` |
| Session storage | `~/.pi/agent/sessions/` (filesystem) | OpenCode Server internal |
| Structured output | Prompt engineering (best-effort) | SDK native support |
| MCP | Not supported | Native support |
| Server management | None (library call) | Must manage `opencode serve` lifecycle |
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does opencode server spawned via opencode serveenforce the OPENCODE_SERVER_PASSWORD env variable on HTTP requests, and how is it passed by@opencode-ai/sdk createOpencodeClient?

💡 Result:

Yes, the opencode server spawned via opencode serve enforces the OPENCODE_SERVER_PASSWORD environment variable on HTTP requests using HTTP Basic Authentication. When set, the server requires clients to provide the correct username (default "opencode", override with OPENCODE_SERVER_USERNAME) and password via the Authorization header. Without valid credentials, requests return 401 Unauthorized. The @opencode-ai/sdk's createOpencodeClient does not automatically read or pass the OPENCODE_SERVER_PASSWORD. It supports custom fetch implementations and headers (via options like fetch or headers in wrappers), allowing manual addition of Basic Auth. Internal OpenCode clients (TUI, plugins, run mode) add the header when the env var is set, but plain SDK clients require explicit configuration, such as: const credentials = btoa(opencode:${process.env.OPENCODE_SERVER_PASSWORD}); const client = createOpencodeClient({ baseUrl: "http://localhost:4096", headers: { Authorization: Basic ${credentials} } }); SDK docs list options like baseUrl, fetch, etc., but not auth directly; Basic Auth is handled via headers or custom fetch interceptors. Provider auth (e.g., client.auth.set for Anthropic API keys) is separate from server auth.

Citations:


🏁 Script executed:

fd -t f "provider.ts" | head -20

Repository: coleam00/Archon

Length of output: 243


🏁 Script executed:

fd -t f "server-manager.ts" | head -20

Repository: coleam00/Archon

Length of output: 118


🏁 Script executed:

cat -n "packages/providers/src/community/opencode/provider.ts"

Repository: coleam00/Archon

Length of output: 6643


🏁 Script executed:

cat -n "packages/providers/src/community/opencode/server-manager.ts"

Repository: coleam00/Archon

Length of output: 4874


Wire HTTP Basic Auth into the OpenCode SDK client.

The password is correctly spawned into OPENCODE_SERVER_PASSWORD in server-manager.ts (line 70), and the web search confirms the server enforces HTTP Basic Auth. However, provider.ts (lines 58–60) creates the SDK client with only baseUrl, omitting any Authorization header. The password retrieved on lines 50–51 is never passed to createOpencodeClient(), so every request will fail with 401 Unauthorized.

Update the client creation to include the auth header:

Example fix
const credentials = btoa(`opencode:${password}`);
const client = createOpencodeClient({
  baseUrl: `http://${serverInfo.hostname}:${serverInfo.port}`,
  headers: { Authorization: `Basic ${credentials}` },
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-23-opencode-provider-design.md` around lines
24 - 30, provider.ts is creating the OpenCode SDK client with only baseUrl and
never passing the retrieved password (the variable from lines where password is
read), causing 401s; fix createOpencodeClient usage by constructing HTTP Basic
credentials from the static user "opencode" and the retrieved password (base64
encode "opencode:password") and pass it as an Authorization header in the client
options (i.e. add headers: { Authorization: `Basic ${credentials}` }) when
calling createOpencodeClient so all SDK requests include the required auth.

Comment on lines +132 to +146
case 'message.updated': {
const info = event.properties.info;
if (info.role === 'assistant') {
const tokens = info.tokens;
return {
type: 'result',
sessionId: info.sessionID,
tokens: {
input: tokens?.input ?? 0,
output: tokens?.output ?? 0,
total: tokens ? tokens.input + tokens.output + tokens.reasoning : undefined,
},
cost: info.cost > 0 ? info.cost : undefined,
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

total can become NaN when any token field is missing.

input/output below are already guarded with ?? 0, but total directly sums the three fields without the same guard. If the SDK ever omits input/output/reasoning (plausible for errored or cached responses — note the surrounding code even treats tokens itself as possibly undefined), the addition yields NaN, which then gets propagated downstream in MessageChunk.tokens.total.

🛡️ Proposed fix
-          tokens: {
-            input: tokens?.input ?? 0,
-            output: tokens?.output ?? 0,
-            total: tokens ? tokens.input + tokens.output + tokens.reasoning : undefined,
-          },
+          tokens: {
+            input: tokens?.input ?? 0,
+            output: tokens?.output ?? 0,
+            total: tokens
+              ? (tokens.input ?? 0) + (tokens.output ?? 0) + (tokens.reasoning ?? 0)
+              : undefined,
+          },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'message.updated': {
const info = event.properties.info;
if (info.role === 'assistant') {
const tokens = info.tokens;
return {
type: 'result',
sessionId: info.sessionID,
tokens: {
input: tokens?.input ?? 0,
output: tokens?.output ?? 0,
total: tokens ? tokens.input + tokens.output + tokens.reasoning : undefined,
},
cost: info.cost > 0 ? info.cost : undefined,
};
}
case 'message.updated': {
const info = event.properties.info;
if (info.role === 'assistant') {
const tokens = info.tokens;
return {
type: 'result',
sessionId: info.sessionID,
tokens: {
input: tokens?.input ?? 0,
output: tokens?.output ?? 0,
total: tokens
? (tokens.input ?? 0) + (tokens.output ?? 0) + (tokens.reasoning ?? 0)
: undefined,
},
cost: info.cost > 0 ? info.cost : undefined,
};
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/community/opencode/event-bridge.ts` around lines 132 -
146, In the message.updated branch of event-bridge.ts (the switch case building
the return object from info and info.tokens), the computed tokens.total can
become NaN when any of tokens.input/output/reasoning are missing; fix by
computing total using the guarded values used for input/output (e.g., use
(tokens.input ?? 0) + (tokens.output ?? 0) + (tokens.reasoning ?? 0)) and only
produce undefined when tokens itself is missing (keep the existing tokens ? ...
: undefined behavior); update the total expression where tokens.total is set in
that return object so it cannot produce NaN.

data: { id: mockSessionId },
}));

const mockSessionStatus = mock(async () => ({ data: { id: mockSessionId } }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm provider uses session.get, not session.status, and that no session.get mock exists in the test.
rg -nP '\bclient\.session\.(get|status)\b' packages/providers/src/community/opencode
rg -nP 'session\s*:\s*\{' -A 10 packages/providers/src/community/opencode/provider.test.ts
# Confirm the test script actually runs this file
rg -nP '"test.*":' packages/providers/package.json

Repository: coleam00/Archon

Length of output: 1234


🏁 Script executed:

cat -n packages/providers/src/community/opencode/provider.test.ts | sed -n '1,50p'

Repository: coleam00/Archon

Length of output: 1778


🏁 Script executed:

cat -n packages/providers/src/community/opencode/provider.test.ts | sed -n '140,220p'

Repository: coleam00/Archon

Length of output: 3340


🏁 Script executed:

cat -n packages/providers/src/community/opencode/provider.ts | sed -n '70,100p'

Repository: coleam00/Archon

Length of output: 1208


🏁 Script executed:

cat -n packages/providers/src/community/opencode/provider.ts | sed -n '1,30p'

Repository: coleam00/Archon

Length of output: 1298


🏁 Script executed:

rg -n 'mockSessionStatus' packages/providers/src/community/opencode/provider.test.ts

Repository: coleam00/Archon

Length of output: 426


Fix mock client to use session.get instead of session.status — both test cases will fail or pass for wrong reasons.

The provider (line 81) calls client.session.get({ path: { id } }), but the mock client defines session.status instead. When the provider attempts to call the non-existent session.get, it throws a TypeError, which the provider's catch block handles by falling back to session.create. This breaks both resume tests:

  • Test "resumes existing session when resumeSessionId is provided" (L147–179): expects mockSessionStatus to be called, but session.get is undefined. The provider catches the error and calls create instead. Assertions at L177–L178 will fail.
  • Test "falls back to new session when resumeSessionId is invalid" (L181–218): mocks mockSessionStatus.mockImplementationOnce() but the provider never calls status. The test passes for the wrong reason—the error is caught, but not from the mock's rejection.
🔧 Suggested fix
-const mockSessionStatus = mock(async () => ({ data: { id: mockSessionId } }));
+const mockSessionGet = mock(async () => ({ data: { id: mockSessionId } }));
@@
 const mockClient = {
   session: {
     create: mockCreateSession,
-    status: mockSessionStatus,
+    get: mockSessionGet,
     promptAsync: mockPromptAsync,
     abort: mockSessionAbort,
     list: mock(async () => ({ data: mockSessionList })),
   },
@@
-    mockSessionStatus.mockClear();
+    mockSessionGet.mockClear();
@@
-    expect(mockSessionStatus).toHaveBeenCalledWith({ path: { id: 'existing-session-id' } });
+    expect(mockSessionGet).toHaveBeenCalledWith({ path: { id: 'existing-session-id' } });
@@
-    mockSessionStatus.mockImplementationOnce(async () => {
+    mockSessionGet.mockImplementationOnce(async () => {
       throw new Error('Session not found');
     });
@@
-    expect(mockSessionStatus).toHaveBeenCalled();
+    expect(mockSessionGet).toHaveBeenCalled();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/community/opencode/provider.test.ts` at line 23, The
mock client defines session.status but the provider calls client.session.get, so
update the mock to expose session.get (replace references to mockSessionStatus
with a mockSessionGet async mock) and update any mock implementations/usages
(e.g., mockSessionStatus.mockImplementationOnce) to target mockSessionGet;
ensure tests that assert calls reference mockSessionGet so the provider's calls
to client.session.get exercise the mock instead of triggering the catch/fallback
to session.create.

Comment on lines +50 to +60
const password =
config.serverPassword ?? process.env.OPENCODE_SERVER_PASSWORD ?? generatePassword();
const autoStart = config.autoStartServer !== false;

// 1. Ensure server is running
const serverInfo = await ensureServer({ hostname, port, cwd, password }, autoStart);

// 2. Create SDK client
const client = createOpencodeClient({
baseUrl: `http://${serverInfo.hostname}:${serverInfo.port}`,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# See whether any opencode client config (fetch/headers/auth) is plumbed anywhere.
rg -nP '(createOpencodeClient|OPENCODE_SERVER_PASSWORD|Authorization)' packages/providers/src/community/opencode

Repository: coleam00/Archon

Length of output: 657


🏁 Script executed:

#!/bin/bash
# Read the full provider.ts to see complete createOpencodeClient usage
cat -n packages/providers/src/community/opencode/provider.ts

Repository: coleam00/Archon

Length of output: 6643


🏁 Script executed:

#!/bin/bash
# Check if there's any SDK documentation or type definitions available
fd -t f 'opencode' packages/providers/src/community/opencode --type f

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Look for any auth-related code in the entire community/opencode directory
rg -n 'auth|Auth|fetch|Fetch|header|Header' packages/providers/src/community/opencode

Repository: coleam00/Archon

Length of output: 332


🏁 Script executed:

#!/bin/bash
# Read server-manager.ts to see how password is used
cat -n packages/providers/src/community/opencode/server-manager.ts

Repository: coleam00/Archon

Length of output: 4874


🏁 Script executed:

#!/bin/bash
# Read config.ts to understand auth requirements
cat -n packages/providers/src/community/opencode/config.ts

Repository: coleam00/Archon

Length of output: 1665


🏁 Script executed:

#!/bin/bash
# Read the test file to see how createOpencodeClient is mocked/used
cat -n packages/providers/src/community/opencode/provider.test.ts | head -100

Repository: coleam00/Archon

Length of output: 3580


🏁 Script executed:

#!/bin/bash
# Search for any examples or documentation about `@opencode-ai/sdk`
rg -n '@opencode-ai/sdk|createOpencodeClient' packages/ --type ts --type tsx -B 2 -A 3 | head -80

Repository: coleam00/Archon

Length of output: 86


🏁 Script executed:

#!/bin/bash
# Search for `@opencode-ai/sdk` usage across the repo
rg '@opencode-ai/sdk' packages/ -g '*.ts' -B 2 -A 3

Repository: coleam00/Archon

Length of output: 8729


🏁 Script executed:

#!/bin/bash
# Check if there's any package.json that might show SDK version
rg -n '"@opencode-ai/sdk"' --type json

Repository: coleam00/Archon

Length of output: 129


🌐 Web query:

@opencode-ai/sdk createOpencodeClient authentication API documentation

💡 Result:

To use @opencode-ai/sdk with createOpencodeClient for authentication in OpenCode: 1. Install the SDK: npm install @opencode-ai/sdk 2. Create the client (connects to a running OpenCode server at default http://localhost:4096): import { createOpencodeClient } from "@opencode-ai/sdk" const client = createOpencodeClient({ baseUrl: "http://localhost:4096", // Optional: URL of the OpenCode server // Other options: fetch, parseAs, responseStyle, throwOnError, headers, directory }) 3. Set authentication credentials using the client's auth.set method: await client.auth.set({ path: { id: "anthropic" }, // Provider ID, e.g., "anthropic", "openai", etc. body: { type: "api", key: "your-api-key" } // Or other auth types like OAuth }) This returns a boolean on success. Authentication is handled per-provider. Providers and API keys are typically configured via OpenCode CLI (/connect command, stored in ~/.local/share/opencode/auth.json) or opencode.json(c). The SDK client sets or overrides credentials for specific providers when calling OpenCode server APIs like sessions, projects, etc. The client assumes an OpenCode server is running (start via opencode CLI). Use createOpencode instead if you want the SDK to start the server automatically. Official documentation: https://opencode.ai/docs/sdk/

Citations:


Client is constructed without forwarding the server password.

password is computed from config/env/generator (L50–51) and handed to ensureServer, which injects it as OPENCODE_SERVER_PASSWORD into the spawned opencode serve process. However, the SDK client created at L58–60 only receives baseUrl — no custom fetch, no auth header, no authentication mechanism.

The config (line 10) explicitly documents the server expects "HTTP Basic Auth (OPENCODE_SERVER_PASSWORD)". The password is computed but never forwarded to SDK client calls (lines 81, 87, 94, 141). If the OpenCode server enforces HTTP Basic Auth on API endpoints, all SDK calls will fail with 401.

Fix: Pass a custom fetch to createOpencodeClient that adds the Authorization: Basic header with the password, or handle auth via another SDK-supported mechanism.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/community/opencode/provider.ts` around lines 50 - 60,
The SDK client is created without forwarding the computed password so API calls
will fail under the server's HTTP Basic Auth; update the createOpencodeClient
call to pass a custom fetch (or SDK-supported auth option) that injects an
Authorization: Basic header built from the computed password variable (the same
password passed into ensureServer), e.g. wrap the default fetch to set
Authorization using Buffer/atob logic, and ensure createOpencodeClient({
baseUrl: `http://${serverInfo.hostname}:${serverInfo.port}`, fetch: /* custom
fetch that uses password */ }) is used so calls from the client (used later in
this file) include the server password.

Comment on lines +63 to +95
const proc = spawn(
'opencode',
['serve', '--port', String(config.port), '--hostname', config.hostname],
{
cwd: config.cwd,
env: {
...process.env,
OPENCODE_SERVER_PASSWORD: config.password,
},
detached: false,
stdio: 'pipe',
}
);

proc.on('error', err => {
getLog().error({ err }, 'opencode.server.process_error');
});

proc.stderr?.on('data', (data: Buffer) => {
getLog().debug({ msg: data.toString().trim() }, 'opencode.server.stderr');
});

// 4. Wait for readiness
await waitForReady(config.hostname, config.port, 30000);

const info: ServerInfo = {
hostname: config.hostname,
port: config.port,
password: config.password,
pid: proc.pid ?? undefined,
};

managedServer = { proc, info };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Child process: add exit handler and drain stdout.

Two small but real reliability concerns:

  1. With stdio: 'pipe', proc.stdout is never consumed. If opencode serve writes enough to stdout, the OS pipe buffer can fill and block the child process. Either attach a listener (even a noop resume()) or switch stdout to 'ignore'.
  2. No proc.on('exit', ...) / 'close' handler. If the server dies mid-run, managedServer keeps a stale reference until the next ensureServer call discovers it via the health check round-trip — meanwhile other concurrent callers also round-trip needlessly. Clearing managedServer (and logging exit code/signal) on exit makes failures observable and recovery cheap.
🔧 Proposed fix
   proc.on('error', err => {
     getLog().error({ err }, 'opencode.server.process_error');
   });

+  proc.on('exit', (code, signal) => {
+    getLog().warn({ code, signal, pid: proc.pid }, 'opencode.server.exited');
+    if (managedServer?.proc === proc) managedServer = undefined;
+  });
+
+  // Prevent the child from blocking on a full stdout pipe.
+  proc.stdout?.resume();
+
   proc.stderr?.on('data', (data: Buffer) => {
     getLog().debug({ msg: data.toString().trim() }, 'opencode.server.stderr');
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/community/opencode/server-manager.ts` around lines 63
- 95, The child process created by spawn (proc) currently sets stdio: 'pipe' but
never consumes stdout and lacks an exit handler; to fix, either drain stdout
(call proc.stdout?.resume() or attach a noop/data listener) to avoid the OS pipe
blocking the child, and add proc.on('exit' / 'close', (code, signal) => { log
the code/signal via getLog() and set managedServer = undefined }) so the stale
managedServer reference is cleared and failures are observable; update
ServerInfo/pid handling if needed but keep waitForReady usage unchanged.

Comment on lines +122 to +127
/**
* Generate a random password for the OpenCode Server.
*/
export function generatePassword(): string {
return `archon-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't use Math.random() for an auth credential.

Math.random() is not cryptographically secure — it's seeded from predictable sources and is explicitly called out as unsuitable for secrets in the Node docs. Even if this password is usually only exposed on 127.0.0.1, it's still a shared secret between Archon and the opencode serve child, and there's no reason to pick a weak source when node:crypto is already available.

🔒 Proposed fix
-import { spawn } from 'node:child_process';
+import { spawn } from 'node:child_process';
+import { randomBytes } from 'node:crypto';
@@
 export function generatePassword(): string {
-  return `archon-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
+  return `archon-${randomBytes(24).toString('base64url')}`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/community/opencode/server-manager.ts` around lines 122
- 127, The generatePassword function currently uses Math.random which is not
cryptographically secure; replace its implementation to generate a strong secret
using Node's crypto module (e.g., crypto.randomBytes or crypto.randomUUID)
inside generatePassword to produce a sufficiently long, URL-safe password
string, ensuring the function name generatePassword is updated to import/require
and use crypto and to return the securely generated string instead of
Math.random-based values.

@rm-rf-etc
Copy link
Copy Markdown

Hi @choufeng! Great PR — exactly what we need. The following patch and comment were generated by GLM-5.1.

Tested in our environment and applied fixes for all the CodeRabbit review items. Here's a patch you can apply directly:

Apply

curl -L https://gist.github.com/rm-rf-etc/fcdb35b3d0fba3a2e126d75b30c07c4c/raw/opencode-fixes.patch | git apply

Or if you prefer, add our branch as a remote and merge:

git remote add fixes https://github.com/obra/Archon fix/opencode-provider-review-fixes
git fetch fixes
git merge fixes/fix/opencode-provider-review-fixes

Summary of fixes

File Fix Source
provider.ts Pass server password via HTTP Basic Auth in createOpencodeClient — prevents 401s CodeRabbit 🟠 Major
server-manager.ts Use crypto.randomBytes instead of Math.random for password generation CodeRabbit 🟠 Major
server-manager.ts Add proc.stdout?.resume() to prevent pipe buffer deadlock CodeRabbit 🟡 Minor
server-manager.ts Add proc.on('exit', ...) handler to clear stale managedServer reference CodeRabbit 🟡 Minor
server-manager.ts Support OPENCODE_BIN_PATH env var for compiled binary spawn resolution Our testing
event-bridge.ts Guard tokens.total with ?? 0 to prevent NaN when fields are missing CodeRabbit 🟡 Minor
event-bridge.ts Remove dead token/cost accumulator (unreachable after early return) CodeRabbit Nitpick
config.ts Validate port range (1–65535, integer) in parseOpencodeConfig CodeRabbit Nitpick
provider.test.ts Fix mock: session.statussession.get to match provider code CodeRabbit 🔴 Critical

Testing

All fixes pass bun run type-check with zero errors. Provider successfully connects to an existing OpenCode server (autoStartServer: false) and creates sessions with model refs like zai/glm-5.1.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

Hi @choufeng — thanks for opening this PR.

This repository uses a PR template at .github/pull_request_template.md with several required sections. A few of them appear to be empty or placeholder here:

  • UX Journey
  • Architecture Diagram
  • Label Snapshot
  • Change Metadata
  • Linked Issue
  • Human Verification
  • Side Effects / Blast Radius
  • Rollback Plan
  • Risks and Mitigations

Could you fill those out (even briefly)? The template helps reviewers understand scope, risk, and rollback — it speeds up review significantly.

If a section genuinely doesn't apply, just write "N/A" in it rather than leaving it blank.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

@choufeng related to #1422 — overlapping area or partial fix.

@Wirasm
Copy link
Copy Markdown
Collaborator

Wirasm commented Apr 27, 2026

Thanks for putting this together, @choufeng!

Heads up: there's another OpenCode community provider PR open at #1384 (@cropse). Could you two coordinate on which one to land? Only one OpenCode community provider should merge.

Since this is a community provider, I won't review or run it deeply myself. For evidence-to-merge, please share:

  • Models tested — which OpenCode-routed models you exercised.
  • Configs used — auth + extension config.
  • Results — scenarios tested (chat, tool use, structured output, etc.) and what worked.
  • Video evidence if possible — a short end-to-end recording.

A smoke test workflow in .archon/workflows/ (like e2e-pi-smoke.yaml) with run output captured would be ideal — it doubles as adoption docs.

Once you and @cropse align on a single PR and the evidence is in, happy to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants