feat: add OpenCode community provider#1372
Conversation
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
📝 WalkthroughWalkthroughIntroduces 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
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. Comment |
There was a problem hiding this comment.
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
testscript 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 themock.module()guideline), a smallscripts/run-tests.shor 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 validatingportrange.
typeof raw.port === 'number'acceptsNaN, 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
returnon the firstresultchunk 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 fallbackyieldat L74–83 unreachable in the normal path (the only way to reach it is when the SSE stream ends with nomessage.updatedand nosession.error— in which caseaccumulatedTokensis still zero because no result chunk was observed).If the intent is "sum across multiple partial results," remove the early
returnand 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
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (13)
docs/superpowers/specs/2026-04-23-opencode-provider-design.mdpackages/providers/package.jsonpackages/providers/src/community/opencode/capabilities.tspackages/providers/src/community/opencode/config.test.tspackages/providers/src/community/opencode/config.tspackages/providers/src/community/opencode/event-bridge.tspackages/providers/src/community/opencode/index.tspackages/providers/src/community/opencode/provider.test.tspackages/providers/src/community/opencode/provider.tspackages/providers/src/community/opencode/registration.tspackages/providers/src/community/opencode/server-manager.tspackages/providers/src/registry.test.tspackages/providers/src/registry.ts
| | 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 | |
There was a problem hiding this comment.
🧩 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:
- 1: https://opencode-tutorial.com/en/docs/server
- 2: https://frank.dev.opencode.ai/docs/server/
- 3: https://opencode.ai/docs/server/
- 4: https://open-code.ai/docs/en/server
- 5: https://opencode.ai/docs/sdk/
- 6: https://github.com/sst/opencode/blob/9ad6588f/packages/web/src/content/docs/sdk.mdx
- 7: https://opencode.ai/docs/sdk
- 8: fix: Plugin client returns 401 Unauthorized when OPENCODE_SERVER_PASSWORD is set (Desktop) anomalyco/opencode#8676
- 9: Expose other
createOpenCodeClientoptions ben-vargas/ai-sdk-provider-opencode-sdk#5 - 10: https://github.com/ben-vargas/ai-sdk-provider-opencode-sdk/blob/ad6e2bbb/src/opencode-client-manager.ts
🏁 Script executed:
fd -t f "provider.ts" | head -20Repository: coleam00/Archon
Length of output: 243
🏁 Script executed:
fd -t f "server-manager.ts" | head -20Repository: 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.
| 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, | ||
| }; | ||
| } |
There was a problem hiding this comment.
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.
| 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 } })); |
There was a problem hiding this comment.
🧩 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.jsonRepository: 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.tsRepository: 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
mockSessionStatusto be called, butsession.getis undefined. The provider catches the error and callscreateinstead. 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 callsstatus. 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.
| 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}`, | ||
| }); |
There was a problem hiding this comment.
🧩 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/opencodeRepository: 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.tsRepository: 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 fRepository: 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/opencodeRepository: 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.tsRepository: 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.tsRepository: 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 -100Repository: 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 -80Repository: 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 3Repository: 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 jsonRepository: 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:
- 1: https://opencode.ai/docs/sdk/
- 2: https://opencode.ai/docs/sdk
- 3: https://www.npmjs.com/package/@opencode-ai/sdk
- 4: https://www.mintlify.com/anomalyco/opencode/sdk/client
- 5: https://github.com/sst/opencode/blob/9ad6588f/packages/web/src/content/docs/sdk.mdx
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.
| 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 }; |
There was a problem hiding this comment.
Child process: add exit handler and drain stdout.
Two small but real reliability concerns:
- With
stdio: 'pipe',proc.stdoutis never consumed. Ifopencode servewrites enough to stdout, the OS pipe buffer can fill and block the child process. Either attach a listener (even a noopresume()) or switch stdout to'ignore'. - No
proc.on('exit', ...)/'close'handler. If the server dies mid-run,managedServerkeeps a stale reference until the nextensureServercall discovers it via the health check round-trip — meanwhile other concurrent callers also round-trip needlessly. ClearingmanagedServer(and logging exit code/signal) onexitmakes 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.
| /** | ||
| * 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)}`; | ||
| } |
There was a problem hiding this comment.
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.
|
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: Applycurl -L https://gist.github.com/rm-rf-etc/fcdb35b3d0fba3a2e126d75b30c07c4c/raw/opencode-fixes.patch | git applyOr 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-fixesSummary of fixes
TestingAll fixes pass |
|
Hi @choufeng — thanks for opening this PR. This repository uses a PR template at
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. |
|
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:
A smoke test workflow in Once you and @cropse align on a single PR and the evidence is in, happy to merge. |
Summary
Adds OpenCode as a community AI assistant provider for Archon, closing #1151.
What It Does
opencode serve) on first use with lazy lifecycle management@opencode-ai/sdkto bridge OpenCode's SSE events into Archon'sMessageChunkcontractSupported Capabilities
allowed_tools/denied_toolsmapped to SDKConfiguration
Files Changed
packages/providers/src/community/opencode/— New provider implementation (10 files)packages/providers/src/registry.ts— Register OpenCode providerpackages/providers/src/registry.test.ts— Registry tests for OpenCodepackages/providers/package.json— Add@opencode-ai/sdkdependency and exportsTesting
config.test.tsandprovider.test.tsCross-Cutting Impact
Per the Phase 2 community provider contract (#1195), changes are localized to:
packages/providers/src/community/opencode/packages/providers/src/registry.tspackages/providers/package.jsonexportspackages/providers/src/registry.test.tsNo entrypoint edits, no config-type edits.
Summary by CodeRabbit
Release Notes